Skip to content

Commit cfaaa91

Browse files
committed
Complete mobile playback repairs and Android player UI
1 parent 47e2352 commit cfaaa91

36 files changed

Lines changed: 2123 additions & 128 deletions

‎ffmpeg/DECISIONS.md‎

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,12 @@ Patch the OHCodec surface path to expose an opaque `AVOHCodecBuffer` through
221221
installed `libavcodec/ohcodec_surface.h`. The public functions
222222
`av_ohcodec_release_buffer()` and `av_ohcodec_render_buffer_at_time()` make one
223223
native render/drop decision. An atomic flag guarantees that the native output
224-
is decided at most once. If the last frame reference is released without an
225-
explicit decision, the fallback unconditionally frees/drops the output rather
226-
than rendering it. The opaque token retains FFmpeg's decoder reference until
227-
the frame buffer is released.
224+
is decided at most once. A repeated decision through another retained frame
225+
view returns `AVERROR(EALREADY)` rather than reporting a second successful
226+
queue operation. If the last frame reference is released without an explicit
227+
decision, the fallback unconditionally frees/drops the output rather than
228+
rendering it. The opaque token retains FFmpeg's decoder reference until the
229+
frame buffer is released.
228230

229231
The policy is implemented by
230232
[`0055-ohcodec-explicit-surface-release.patch`](ports/ffmpeg/0055-ohcodec-explicit-surface-release.patch).
@@ -246,13 +248,18 @@ The installed-package verifier requires the header and both symbols.
246248
The OHOS package carries a small public overlay ABI that must stay paired with
247249
the QtAVCore OHCodec backend. This API controls direct surface output only and
248250
makes no native-buffer interop or zero-copy texture claim. Android and Windows
249-
packages are unchanged.
251+
packages are unchanged. QtAVCore maps `AVERROR(EALREADY)` to a stale import;
252+
the interop must not install a new pending producer association because no
253+
second native buffer or frame-available callback will be produced.
250254

251255
Validation requires `scripts/build-ohos.ps1`, `cmake/verify-install.cmake`,
252256
QtAVCore OHOS shared/static target compilation, an installed
253257
`QtAV::HWOHCodec` consumer, and connected-device HAP coverage that observes
254258
successful timed presentations and explicit drops. The 2026-08-05 Mate 60 Pro
255-
run completed 30 timed H.264 presentations and three explicit drops.
259+
run completed 30 timed H.264 presentations and three explicit drops. The
260+
2026-08-13 player regression additionally held `legend.mkv` at 0.5x through
261+
repeated full-screen swapchain recreation with balanced native release and
262+
callback counts.
256263

257264
### Retirement condition
258265

@@ -535,3 +542,64 @@ Revisit the target-specific override when FFmpeg changes the semantics of
535542
`--enable-small`, when binary-size limits require a separate dependency
536543
profile, or when repeatable device measurements show that `-O2` or another
537544
portable optimization level provides better sustained throughput.
545+
546+
## FD-009: Report a suppressed Android MediaCodec output release as stale
547+
548+
- Date: 2026-08-13
549+
- Status: Accepted
550+
- Scope: Android FFmpeg MediaCodec output ownership and the installed public
551+
release-result contract
552+
553+
### Context
554+
555+
FFmpeg 8.1.2 shares one atomic release flag across all retained
556+
`AVMediaCodecBuffer` views. Its MediaCodec helpers correctly invoke the native
557+
release only once, and also suppress a release after decoder flush changes the
558+
buffer serial. Both suppressed cases nevertheless returned success. A caller
559+
could therefore register an AImageReader timestamp association and wait for a
560+
frame-available callback even though no native output had been queued.
561+
562+
This becomes reachable when Android republishes the same Surface during a
563+
size/fullscreen refresh. QtAVCore invalidates the old presentation epoch, then
564+
may redraw the retained latest frame in the new epoch. Epoch/timestamp tracking
565+
cannot distinguish a real new producer release from FFmpeg's successful no-op;
566+
the missing callback is an ownership fact, not a slow-producer timeout or an
567+
unknown coded-size problem.
568+
569+
### Decision
570+
571+
Patch both `av_mediacodec_release_buffer()` and
572+
`av_mediacodec_render_buffer_at_time()` so zero means the current call actually
573+
invoked the native output release. Return `AVERROR(EALREADY)` when another view
574+
already released the shared buffer or decoder flush retired it. Keep all other
575+
native error values unchanged.
576+
577+
Document the result in installed `libavcodec/mediacodec.h` and require the
578+
contract from the Android branch of `cmake/verify-install.cmake`. QtAVCore owns
579+
the higher-level mapping to applied, stale, and failed; the dependency layer
580+
does not know about renderer epochs or presentation policy.
581+
582+
### Rejected alternatives
583+
584+
- Suppressing redraws by timestamp/frame identity would be an application
585+
workaround and could discard a valid frame after a real generation change.
586+
- Waiting less or treating a timeout as success would retain the false pending
587+
association and hide the ownership violation.
588+
- Reopening MediaCodec or probing the stream dimensions on every surface-size
589+
change does not make an already-released output emit another callback.
590+
- Keeping the result only inside QtAVCore is impossible because the public
591+
FFmpeg helper previously erased whether it performed the native release.
592+
593+
### Consequences and validation
594+
595+
Existing callers that check only for negative failure now observe a truthful
596+
stale result. QtAVCore's bool convenience functions remain source compatible
597+
but return true only for a real applied release; result-returning APIs preserve
598+
the three-way distinction.
599+
600+
Any change to this contract must run the directly affected Android dependency
601+
build and `cmake/verify-install.cmake`, followed by QtAVCore Android
602+
shared/static builds and installed-package consumers. Connected-device
603+
validation must exercise a Surface/fullscreen refresh while MediaCodec
604+
hardware output remains active and confirm that presentation continues without
605+
an unbounded pending image wait.

‎ffmpeg/README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,24 @@ vcpkg's current platform model.
7979
The OHOS FFmpeg overlay installs `libavcodec/ohcodec_surface.h`. Its opaque
8080
token permits exactly one immediate render, monotonic timed render, or drop of
8181
an OHCodec surface output without exposing FFmpeg's private decoder structure.
82+
Repeating a decision through another retained view returns
83+
`AVERROR(EALREADY)`; callers can therefore reject an old-frame redraw without
84+
waiting for a surface callback that the already-consumed output cannot emit.
8285
Releasing the last frame reference without an explicit decision unconditionally
8386
drops the output; it never implicitly renders an abandoned output.
8487
It does not expose `OH_AVBuffer`/`OH_NativeBuffer` texture interop. The API and
8588
retirement criteria are recorded in [FD-004](DECISIONS.md).
8689

90+
The Android FFmpeg overlay also makes MediaCodec output release observable.
91+
`av_mediacodec_release_buffer()` and
92+
`av_mediacodec_render_buffer_at_time()` return `0` only when that call actually
93+
releases the native output. A repeated retained-frame decision, or the first
94+
decision after decoder flush already retired the output, returns
95+
`AVERROR(EALREADY)`. This prevents a surface redraw from waiting for an
96+
`AImageReader` callback that MediaCodec cannot emit. The Android package
97+
verifier requires this header contract; rationale is recorded in
98+
[FD-009](DECISIONS.md).
99+
87100
The OHOS overlay also registers `vvc_ohcodec`, maps `AV_CODEC_ID_VVC` to the
88101
platform VVC MIME, and selects `vvc_mp4toannexb` for MP4 `vvc1` input. The
89102
wrapper asks `OH_AVCodec_GetCapabilityByCategory(..., false, HARDWARE)` before

‎ffmpeg/cmake/verify-install.cmake‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ if(TRIPLET MATCHES "ohos")
8181
endforeach()
8282
endif()
8383

84+
if(TRIPLET MATCHES "android")
85+
file(READ "${PREFIX}/include/libavcodec/mediacodec.h"
86+
FFMPEG_MEDIACODEC_HEADER)
87+
if(NOT FFMPEG_MEDIACODEC_HEADER MATCHES
88+
"AVERROR\\(EALREADY\\).*retired by a decoder flush")
89+
message(FATAL_ERROR
90+
"Android FFmpeg is missing explicit MediaCodec repeat-release status"
91+
)
92+
endif()
93+
endif()
94+
8495
file(READ "${PREFIX}/include/libplacebo/config.h" LIBPLACEBO_CONFIG)
8596
if(NOT LIBPLACEBO_CONFIG MATCHES "#define PL_HAVE_VULKAN 1")
8697
message(FATAL_ERROR "libplacebo was built without Vulkan support")

‎ffmpeg/ports/ffmpeg/0055-ohcodec-explicit-surface-release.patch‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ new file mode 100644
1515
index 0000000000..ec0ced37cc
1616
--- /dev/null
1717
+++ b/libavcodec/ohcodec_surface.h
18-
@@ -0,0 +1,55 @@
18+
@@ -0,0 +1,56 @@
1919
+/*
2020
+ * OpenHarmony OHCodec surface-output public API
2121
+ *
@@ -52,7 +52,8 @@ index 0000000000..ec0ced37cc
5252
+ *
5353
+ * @param buffer the surface output from AVFrame.data[3]
5454
+ * @param render nonzero to render to the configured surface, zero to drop
55-
+ * @return 0 on success, a negative AVERROR value on failure
55+
+ * @return 0 on success, AVERROR(EALREADY) when another view already made
56+
+ * the one permitted decision, or another negative AVERROR on failure
5657
+ */
5758
+int av_ohcodec_release_buffer(AVOHCodecBuffer *buffer, int render);
5859
+
@@ -110,7 +111,7 @@ index 7f24d9a943..fb66163e52 100644
110111
+ return AVERROR(EINVAL);
111112
+
112113
+ if (atomic_exchange(&buffer->released, 1))
113-
+ return 0;
114+
+ return AVERROR(EALREADY);
114115
+
115116
+ OH_AVCodec *dec = *buffer->dec_ref;
116117
+ OH_AVErrCode err = render
@@ -125,7 +126,7 @@ index 7f24d9a943..fb66163e52 100644
125126
+ return AVERROR(EINVAL);
126127
+
127128
+ if (atomic_exchange(&buffer->released, 1))
128-
+ return 0;
129+
+ return AVERROR(EALREADY);
129130
+
130131
+ OH_AVCodec *dec = *buffer->dec_ref;
131132
+ return ff_oh_err_to_ff_err(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
diff --git a/libavcodec/mediacodec.c b/libavcodec/mediacodec.c
2+
index 4d95dbd0c9..7bc20cb0f9 100644
3+
--- a/libavcodec/mediacodec.c
4+
+++ b/libavcodec/mediacodec.c
5+
@@ -100,7 +100,7 @@ int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render)
6+
return ff_AMediaCodec_releaseOutputBuffer(ctx->codec, buffer->index, render);
7+
}
8+
9+
- return 0;
10+
+ return AVERROR(EALREADY);
11+
}
12+
13+
int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time)
14+
@@ -117,7 +117,7 @@ int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time
15+
return ff_AMediaCodec_releaseOutputBufferAtTime(ctx->codec, buffer->index, time);
16+
}
17+
18+
- return 0;
19+
+ return AVERROR(EALREADY);
20+
}
21+
22+
#else
23+
diff --git a/libavcodec/mediacodec.h b/libavcodec/mediacodec.h
24+
index 2eb851fc40..87cf9f9d73 100644
25+
--- a/libavcodec/mediacodec.h
26+
+++ b/libavcodec/mediacodec.h
27+
@@ -81,8 +81,10 @@ typedef struct MediaCodecBuffer AVMediaCodecBuffer;
28+
*
29+
* @param buffer the buffer to render
30+
* @param render 1 to release and render the buffer to the surface or 0 to
31+
* discard the buffer
32+
- * @return 0 on success, < 0 otherwise
33+
+ * @return 0 when the native output was released, AVERROR(EALREADY) when the
34+
+ * output was already released or retired by a decoder flush, or another
35+
+ * negative AVERROR value on failure
36+
*/
37+
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
38+
39+
@@ -96,7 +98,9 @@ int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
40+
*
41+
* @param buffer the buffer to render
42+
* @param time timestamp in nanoseconds of when to render the buffer
43+
- * @return 0 on success, < 0 otherwise
44+
+ * @return 0 when the native output was released, AVERROR(EALREADY) when the
45+
+ * output was already released or retired by a decoder flush, or another
46+
+ * negative AVERROR value on failure
47+
*
48+
* [0]: https://developer.android.com/reference/android/media/MediaCodec#releaseOutputBuffer(int,%20long)
49+
*/

‎ffmpeg/ports/ffmpeg/portfile.cmake‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ vcpkg_from_github(
2727
0056-ohcodec-dovi-metadata.patch
2828
0057-d3d11va-reuse-compatible-decoder.patch
2929
0058-ohcodec-vvc-decoder.patch
30+
0059-mediacodec-repeat-release-status.patch
3031
)
3132

3233
file(COPY

‎modern/ARCHITECTURE.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,10 @@ MediaCodec + parsed RPU
337337
│
338338
▼
339339
private AImageReader / AHardwareBuffer
340-
├── Vulkan external image ──► raw Y/Cb/Cr ──┐
340+
├── Vulkan identity (Cr/Y/Cb) ──► .gbr ─────┐
341341
└── EGLImage + GL_EXT_YUV_target ──────────┤
342342
▼
343+
raw Y/Cb/Cr + parsed RPU
343344
libplacebo DOVI reshape
344345
+ color/tone/gamut pipeline
345346
│
@@ -351,7 +352,10 @@ private AImageReader / AHardwareBuffer
351352

352353
An Android import that cannot prove the raw-component contract is rejected for
353354
Dolby Vision. Implicit `SurfaceTexture`/external-OES conversion is not treated
354-
as a valid Profile 5 source.
355+
as a valid Profile 5 source. Android and OHOS opaque Vulkan identity samplers
356+
both retain the driver-provided component mapping. The shared normalizer alone
357+
changes Vulkan's sampled `(Cr,Y,Cb)` to libplacebo's `(Y,Cb,Cr)`; a platform
358+
interop must not preapply the same rotation.
355359

356360
The Windows path applies the same ordering through D3D11 only:
357361

@@ -451,6 +455,16 @@ producer/consumer fence order. Native-buffer queues remain bounded. Cache
451455
entries may reuse GPU imports by AHardwareBuffer identity, but a codec output
452456
and its AImage remain retained until the GPU has finished consuming them.
453457

458+
The FFmpeg MediaCodec decision is a three-way ownership boundary: `Applied`
459+
means the current call released a native output, `AlreadyReleased` means a
460+
retained peer already decided it or decoder flush retired it, and `Failed`
461+
means the native operation failed. An AImageReader interop begins its producer
462+
epoch association before the decision but keeps it only for `Applied`.
463+
`AlreadyReleased` cancels that provisional association and is stale; no
464+
Surface buffer or frame-available callback can follow. Presentation/surface
465+
invalidation may therefore re-offer a retained latest frame without creating
466+
an impossible pending image wait.
467+
454468
Direct-Surface mode is a third, separate path. It has no application-readable
455469
texture and therefore cannot be described as Vulkan, OpenGL ES, libplacebo, or
456470
application-side tone mapping.

‎modern/DECISIONS.md‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ libplacebo.
4444
The decoded source must remain free of CPU mapping, readback, transfer,
4545
staging, or upload. An on-GPU external-format normalization pass is allowed
4646
when it preserves raw Y, Cb, and Cr for libplacebo.
47+
The Vulkan identity sampler retains the driver component mapping, producing
48+
Vulkan's sampled `(Cr,Y,Cb)` convention; the shared `.gbr` normalizer is the
49+
only step that reorders it to `(Y,Cb,Cr)`. Platform interop must not perform
50+
the same rotation in advance.
4751
4. Hardware decode, ZeroCopy, Vulkan, and HDR are the Android player defaults.
4852
Disabling ZeroCopy while hardware decode remains enabled selects MediaCodec
4953
direct-Surface presentation and therefore does not use libplacebo.
@@ -240,7 +244,8 @@ ZeroCopy selection, and HDR output policy.
240244

241245
- Date: 2026-08-03
242246
- Amended: 2026-08-12 to make seek-generation invalidation and late AImage
243-
rejection an interop responsibility shared with the Vulkan path
247+
rejection an interop responsibility shared with the Vulkan path; 2026-08-13
248+
to make an already-consumed MediaCodec output an explicit stale result
244249
- Status: Accepted
245250
- Scope: Android player OpenGL ES MediaCodec/AImageReader scheduling and EGL
246251
release synchronization, plus Android AImageReader producer-generation
@@ -281,6 +286,17 @@ does not wake its bounded image wait; OpenGL has an interop `flush()` but no
281286
automatic renderer-to-interop invalidation path. Correctness must not depend on
282287
the Android application calling those interop methods before Player controls.
283288

289+
A surface-size/fullscreen refresh exposed one more ownership ambiguity shared
290+
with the OHOS failure. Android's renderer invalidates pending presentation
291+
state even when `surfaceChanged` republishes the same native window. A retained
292+
latest `VideoFrame` can then be offered again in the new interop epoch. FFmpeg
293+
8.1.2 atomically suppresses the second native MediaCodec release, but returned
294+
success for both that duplicate and an output retired by decoder flush.
295+
QtAVCore therefore installed a pending timestamp association and waited for a
296+
frame-available callback that could never be emitted. Producer epochs can
297+
reject late callbacks; they cannot infer that an upstream release reported a
298+
false success.
299+
284300
### Decision
285301

286302
1. `MediaCodecOpenGLInterop::queueFrame()` only registers the exact frame key
@@ -316,6 +332,16 @@ the Android application calling those interop methods before Player controls.
316332
overlapping old render crosses the Player generation boundary. It never
317333
waits for already-submitted GPU work; imported images keep their existing
318334
native-fence or timeline-based lifetime.
335+
9. The repository FFmpeg Android overlay returns `AVERROR(EALREADY)` when a
336+
MediaCodec output was already released by another retained frame view or
337+
retired by decoder flush. Zero means that the current call actually invoked
338+
the native release. Installed-package verification rejects the old
339+
ambiguous contract.
340+
10. `MediaCodecFrame` maps the boundary to `Applied`, `AlreadyReleased`, or
341+
`Failed`. Both AImageReader interops cancel their provisional epoch
342+
association and return stale for `AlreadyReleased`; direct-Surface output
343+
also ignores it as stale. No path creates a pending producer association,
344+
retries the frame, probes dimensions, or waits for a callback in that case.
319345

320346
### Consequences
321347

@@ -339,6 +365,10 @@ the Android application calling those interop methods before Player controls.
339365
- This amendment specializes AD-008's generic exact-deferred-frame and
340366
pending-producer invalidation contract for Android AImageReader. It does not
341367
change direct-Surface present/drop ownership.
368+
- The repeat-release repair is at the shared FFmpeg/native-output ownership
369+
boundary, so Vulkan, OpenGL ES, and direct-Surface paths receive the same
370+
truth. It adds no redraw suppression, timeout, decoder reopen, pixel copy, or
371+
media-size special case.
342372

343373
### Seek-generation amendment status
344374

0 commit comments

Comments
 (0)