Skip to content

c2.mtk.hevc.decoder (MediaTek MT8696 / Google TV Streamer 4K) delivers ~30% of frames 30-130ms late on specific x265-encoded HEVC content, causing sustained stutter; VLC on identical hardware/content is unaffected #3347

Description

@2bitoperations

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:

  1. 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).
  2. 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:

  1. 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.)
  2. 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.
  3. 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

  1. Play the sample file below on a Google TV Streamer 4K via the Media3 demo app (or any ExoPlayer/Media3-based app).
  2. Observe sustained visible stutter throughout playback.
  3. adb logcat shows EventLogger: droppedFrames events recurring continuously (roughly every ~7s in batches, ~30% of frames total) for the duration of playback.
  4. 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions