Version
Media3 1.10.1 (built and tested via the demo module directly from the androidx/media release branch, and independently confirmed in Plex, Jellyfin (Android TV), and Plezzy — all separate ExoPlayer/Media3 integrations, ruling out any single app's integration code as the cause).
More version details
Device: Google TV Streamer 4K ("kirkwood"), Android 14, MediaTek MT8696 SoC (PowerVR Rogue GE9215 GPU).
Symptom: playing specific HEVC (H.265) Main10 content causes sustained, severe visible stutter — not a one-time hiccup, but continuous throughout playback for as long as the affected content plays.
Root cause, precisely measured: we instrumented MediaCodecVideoRenderer directly (custom build, logging every frame's earlyUs value at the point of the drop/render decision) and found that ~30% of decoded video frames from c2.mtk.hevc.decoder arrive between 31ms and 131ms late relative to their presentation deadline (median ~70ms late; at 23.976fps one frame period is ~42ms, so this is roughly 1-3 frame periods behind schedule). This is a bounded, repeating per-frame latency pattern — not a runaway drift into seconds, and not correlated with the popFromStashAndRegister log line described below.
Two log signatures accompany playback, though we now believe only the first is the actual cause and the second is a red herring:
- The actual cause — decode/delivery latency, directly measured via instrumented logging (see above). Dropped-frame sample (
earlyUs in microseconds, negative = late): -130838, -129795, -128975, -128942, -128582, ..., -46831, -46611, -41407, -37913, -30670. Full distribution: 41 frames <-100ms late, 15 in [-100,-70)ms, 42 in [-70,-40)ms, 17 in [-40,0)ms (n=115 of 360 total frame outcomes in this test run, 31.9% — matches the originally-reported ~32% independently).
- A red herring we chased for a long time: a
CCodecBufferChannel/CCodecBuffers "stale input buffer" + popFromStashAndRegister: at 999999916585us (implausible future timestamp) log pair on decoder initialization. This log pair appears on every single test we ran, including files/synthetic content that played back with zero drops — so it cannot be the actual cause, despite superficially looking like the smoking gun early in our investigation. We mention it here so other reporters searching for this exact log text land on the correct diagnosis (the real cause is decode latency, not this log line) rather than chasing it as we initially did.
We confirmed threshold-tuning does not fix this — we patched MediaCodecVideoRenderer.shouldDropOutputBuffer's threshold from the default -30ms up to -500ms (very lenient) and rebuilt/retested: on short (10-15s) test clips this produced zero logged droppedFrames events, which looked like a fix — but on a longer, representative 90-second real clip of the same content, zero drops were logged yet the stutter was still clearly visible. Loosening the threshold doesn't fix anything real: it just changes the failure mode from "drop the late frame" (visible as a skip) to "render the late frame anyway" (visible as a hold/judder) — a frame delivered 70-130ms late is visually disruptive either way at this frame rate. This rules out any simple ExoPlayer-side threshold/config fix.
Critically, VLC on the exact same device plays the identical content with no visible stutter — and this is not simply VLC "tolerating" lateness better: we checked VLC's own late-frame-drop logic (src/video_output/video_output.c), and its threshold (~one frame period, ~42ms, when frame rate is known) is actually stricter than the worst lateness we measured in ExoPlayer (131ms). If VLC's decode path were producing the same late frames, VLC's own drop logic would react to them too. We confirmed this directly: we installed VLC-Android and captured a full adb logcat spanning an entire 90-second real-content playback of the same underlying source movie — zero "picture is too late to be displayed" warnings, zero drop-related log lines of any kind, for the full 90 seconds, with visually smooth playback confirmed throughout. So VLC's decode is not falling behind in the first place — something about how VLC drives this decoder avoids the latency spike, rather than merely tolerating it.
We tested three concrete, source-motivated hypotheses for what VLC does differently, all disproven by direct on-device A/B testing against the same 90-second real-content clip:
- Force the synchronous
MediaCodecAdapter (DefaultRenderersFactory.forceDisableMediaCodecAsynchronousQueueing()), matching VLC's exclusive use of the Android NDK's synchronous AMediaCodec_dequeueInputBuffer/dequeueOutputBuffer polling API (VLC never uses the async callback API anywhere in its Android backend — confirmed by reading modules/codec/omxil/mediacodec_ndk.c/mediacodec_jni.c) — no change, identical drop pattern. (Both sync and async adapters sit on top of the same native CCodecBufferChannel plumbing, so this was always a long shot in retrospect.)
- Loosen the drop threshold from -30ms to -500ms — eliminated logged drops on short clips but stutter remained clearly visible on the 90s clip with zero drops logged, proving this only changes which visual artifact appears (skip vs. hold), not the underlying lateness.
- Force
mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_AdaptivePlayback, true) on the codec configuration, matching VLC's unconditional feature-adaptive-playback=1 (confirmed via VLC's own log line mediacodec configured for adaptative playback — VLC sets this for essentially all Surface-based playback, while ExoPlayer only requests equivalent KEY_MAX_WIDTH/KEY_MAX_HEIGHT headroom for genuinely multi-format adaptive streams, not a single local file) — no change, identical drop pattern.
We're continuing to investigate (next candidate: input-buffer queuing depth/pipelining — VLC's decoder-output draining runs on a dedicated thread decoupled from input queueing, which may let more compressed frames be queued ahead of the decoder than ExoPlayer's tighter render-loop-coupled pacing) and will update this issue if we isolate the actual mechanism. Given three plausible, testable hypotheses have now been ruled out, it's also plausible the real difference is inside MediaTek's closed-source Codec2 HAL in a way that isn't reachable from app-level MediaCodec configuration at all — in which case this may need MediaTek's own engineers to resolve, and this report may be primarily useful as precise symptom documentation rather than pointing to a specific ExoPlayer code fix. Note that ExoPlayer already has a c2.mtk.hevc.decoder/c2.mtk.avc.decoder-specific workaround elsewhere in the codebase (shouldReleaseCodecInsteadOfFlushing), confirming this decoder family is already known to the maintainers as needing special-casing.
Content dependency, precisely isolated: the bug is tied to output from x265 encoder builds around version 3.3 (~2020-era) specifically, not to file content, resolution, or container metadata:
- Ruled out:
hev1 vs hvc1 FourCC tag, non-standard/cropped resolution + conformance-window offsets (reproduces identically on a second, unrelated file with a clean macroblock-aligned resolution needing zero cropping), GOP/keyframe interval, container timescale precision, SAO on/off, and general "visual complexity" (tested flat synthetic patterns, fractal motion, synthetic film-grain noise — none reproduced it).
- A byte-level VPS/SPS/PPS bitstream parse confirmed real bug-triggering files and non-triggering synthetic files were byte-identical on every SPS/PPS field checked (profile/tier/level, chroma format, bit depth, conformance window, VUI timing, PPS/VPS bytes).
- The isolating experiment: building x265 from source at git tag
3.3 and encoding real, non-copyrighted Creative Commons footage with it reproduces the bug; the same footage encoded with current x265 4.1 (even with closely matched encoder settings) does not. This points to something in the actual encoded coefficient/motion-vector/mode-decision data produced by that encoder generation — not any inspectable high-level setting — as what triggers the decoder's slow path.
Devices that reproduce the issue
Google TV Streamer 4K (MediaTek MT8696), Android 14.
Devices that do not reproduce the issue
Not tested on other devices. VLC (non-ExoPlayer) on the same device/content is unaffected — see analysis above.
Reproducible in the demo app?
Yes — confirmed directly in the official Media3 demo app (demo module, built from source, androidx.media3.demo.main.PlayerActivity, launched via am start -a androidx.media3.demo.main.action.VIEW -d file://... --es mime_type video/mp4), with no third-party app involved.
Reproduction steps
- Play the sample file below on a Google TV Streamer 4K via the Media3 demo app (or any ExoPlayer/Media3-based app).
- Observe sustained visible stutter throughout playback.
adb logcat shows EventLogger: droppedFrames events recurring continuously (roughly every ~7s in batches, ~30% of frames total) for the duration of playback.
- For comparison: play the same file in VLC on the same device — no stutter, no drops.
Sample file
https://github.com/2bitoperations/google-tv-streamer-hevc-decoder-repro — a 15-second HEVC Main10 MP4 (tos_x265_3.3_final.mp4, ~2.3MB, 1916x816, hvc1 tag, 24fps, AAC 5.1) that reliably reproduces the issue.
This sample is entirely legally clean: the source footage is Blender Foundation's Tears of Steel, licensed CC BY 3.0, encoded using x265 (open source) built from the official source tag 3.3. No copyrighted commercial content is involved anywhere in the pipeline. The linked repo's README also documents the exact, fully reproducible command pipeline used to build the sample from public sources, in case it's useful for building further variants.
Full adb logcat capture and the raw per-frame earlyUs instrumentation log are available on request.
Version
Media3 1.10.1 (built and tested via the
demomodule directly from theandroidx/mediareleasebranch, and independently confirmed in Plex, Jellyfin (Android TV), and Plezzy — all separate ExoPlayer/Media3 integrations, ruling out any single app's integration code as the cause).More version details
Device: Google TV Streamer 4K ("kirkwood"), Android 14, MediaTek MT8696 SoC (PowerVR Rogue GE9215 GPU).
Symptom: playing specific HEVC (H.265) Main10 content causes sustained, severe visible stutter — not a one-time hiccup, but continuous throughout playback for as long as the affected content plays.
Root cause, precisely measured: we instrumented
MediaCodecVideoRendererdirectly (custom build, logging every frame'searlyUsvalue at the point of the drop/render decision) and found that ~30% of decoded video frames fromc2.mtk.hevc.decoderarrive between 31ms and 131ms late relative to their presentation deadline (median ~70ms late; at 23.976fps one frame period is ~42ms, so this is roughly 1-3 frame periods behind schedule). This is a bounded, repeating per-frame latency pattern — not a runaway drift into seconds, and not correlated with thepopFromStashAndRegisterlog line described below.Two log signatures accompany playback, though we now believe only the first is the actual cause and the second is a red herring:
earlyUsin microseconds, negative = late):-130838, -129795, -128975, -128942, -128582, ..., -46831, -46611, -41407, -37913, -30670. Full distribution: 41 frames <-100ms late, 15 in [-100,-70)ms, 42 in [-70,-40)ms, 17 in [-40,0)ms (n=115 of 360 total frame outcomes in this test run, 31.9% — matches the originally-reported ~32% independently).CCodecBufferChannel/CCodecBuffers"stale input buffer" +popFromStashAndRegister: at 999999916585us(implausible future timestamp) log pair on decoder initialization. This log pair appears on every single test we ran, including files/synthetic content that played back with zero drops — so it cannot be the actual cause, despite superficially looking like the smoking gun early in our investigation. We mention it here so other reporters searching for this exact log text land on the correct diagnosis (the real cause is decode latency, not this log line) rather than chasing it as we initially did.We confirmed threshold-tuning does not fix this — we patched
MediaCodecVideoRenderer.shouldDropOutputBuffer's threshold from the default-30msup to-500ms(very lenient) and rebuilt/retested: on short (10-15s) test clips this produced zero loggeddroppedFramesevents, which looked like a fix — but on a longer, representative 90-second real clip of the same content, zero drops were logged yet the stutter was still clearly visible. Loosening the threshold doesn't fix anything real: it just changes the failure mode from "drop the late frame" (visible as a skip) to "render the late frame anyway" (visible as a hold/judder) — a frame delivered 70-130ms late is visually disruptive either way at this frame rate. This rules out any simple ExoPlayer-side threshold/config fix.Critically, VLC on the exact same device plays the identical content with no visible stutter — and this is not simply VLC "tolerating" lateness better: we checked VLC's own late-frame-drop logic (
src/video_output/video_output.c), and its threshold (~one frame period, ~42ms, when frame rate is known) is actually stricter than the worst lateness we measured in ExoPlayer (131ms). If VLC's decode path were producing the same late frames, VLC's own drop logic would react to them too. We confirmed this directly: we installed VLC-Android and captured a fulladb logcatspanning an entire 90-second real-content playback of the same underlying source movie — zero "picture is too late to be displayed" warnings, zero drop-related log lines of any kind, for the full 90 seconds, with visually smooth playback confirmed throughout. So VLC's decode is not falling behind in the first place — something about how VLC drives this decoder avoids the latency spike, rather than merely tolerating it.We tested three concrete, source-motivated hypotheses for what VLC does differently, all disproven by direct on-device A/B testing against the same 90-second real-content clip:
MediaCodecAdapter(DefaultRenderersFactory.forceDisableMediaCodecAsynchronousQueueing()), matching VLC's exclusive use of the Android NDK's synchronousAMediaCodec_dequeueInputBuffer/dequeueOutputBufferpolling API (VLC never uses the async callback API anywhere in its Android backend — confirmed by readingmodules/codec/omxil/mediacodec_ndk.c/mediacodec_jni.c) — no change, identical drop pattern. (Both sync and async adapters sit on top of the same nativeCCodecBufferChannelplumbing, so this was always a long shot in retrospect.)mediaFormat.setFeatureEnabled(CodecCapabilities.FEATURE_AdaptivePlayback, true)on the codec configuration, matching VLC's unconditionalfeature-adaptive-playback=1(confirmed via VLC's own log linemediacodec configured for adaptative playback— VLC sets this for essentially all Surface-based playback, while ExoPlayer only requests equivalentKEY_MAX_WIDTH/KEY_MAX_HEIGHTheadroom for genuinely multi-format adaptive streams, not a single local file) — no change, identical drop pattern.We're continuing to investigate (next candidate: input-buffer queuing depth/pipelining — VLC's decoder-output draining runs on a dedicated thread decoupled from input queueing, which may let more compressed frames be queued ahead of the decoder than ExoPlayer's tighter render-loop-coupled pacing) and will update this issue if we isolate the actual mechanism. Given three plausible, testable hypotheses have now been ruled out, it's also plausible the real difference is inside MediaTek's closed-source Codec2 HAL in a way that isn't reachable from app-level MediaCodec configuration at all — in which case this may need MediaTek's own engineers to resolve, and this report may be primarily useful as precise symptom documentation rather than pointing to a specific ExoPlayer code fix. Note that ExoPlayer already has a
c2.mtk.hevc.decoder/c2.mtk.avc.decoder-specific workaround elsewhere in the codebase (shouldReleaseCodecInsteadOfFlushing), confirming this decoder family is already known to the maintainers as needing special-casing.Content dependency, precisely isolated: the bug is tied to output from x265 encoder builds around version 3.3 (~2020-era) specifically, not to file content, resolution, or container metadata:
hev1vshvc1FourCC tag, non-standard/cropped resolution + conformance-window offsets (reproduces identically on a second, unrelated file with a clean macroblock-aligned resolution needing zero cropping), GOP/keyframe interval, container timescale precision, SAO on/off, and general "visual complexity" (tested flat synthetic patterns, fractal motion, synthetic film-grain noise — none reproduced it).3.3and encoding real, non-copyrighted Creative Commons footage with it reproduces the bug; the same footage encoded with current x265 4.1 (even with closely matched encoder settings) does not. This points to something in the actual encoded coefficient/motion-vector/mode-decision data produced by that encoder generation — not any inspectable high-level setting — as what triggers the decoder's slow path.Devices that reproduce the issue
Google TV Streamer 4K (MediaTek MT8696), Android 14.
Devices that do not reproduce the issue
Not tested on other devices. VLC (non-ExoPlayer) on the same device/content is unaffected — see analysis above.
Reproducible in the demo app?
Yes — confirmed directly in the official Media3 demo app (
demomodule, built from source,androidx.media3.demo.main.PlayerActivity, launched viaam start -a androidx.media3.demo.main.action.VIEW -d file://... --es mime_type video/mp4), with no third-party app involved.Reproduction steps
adb logcatshowsEventLogger: droppedFramesevents recurring continuously (roughly every ~7s in batches, ~30% of frames total) for the duration of playback.Sample file
https://github.com/2bitoperations/google-tv-streamer-hevc-decoder-repro — a 15-second HEVC Main10 MP4 (
tos_x265_3.3_final.mp4, ~2.3MB, 1916x816,hvc1tag, 24fps, AAC 5.1) that reliably reproduces the issue.This sample is entirely legally clean: the source footage is Blender Foundation's Tears of Steel, licensed CC BY 3.0, encoded using x265 (open source) built from the official source tag
3.3. No copyrighted commercial content is involved anywhere in the pipeline. The linked repo's README also documents the exact, fully reproducible command pipeline used to build the sample from public sources, in case it's useful for building further variants.Full
adb logcatcapture and the raw per-frameearlyUsinstrumentation log are available on request.