From 8329abedf6d696061b9ba0493b119cb15d71ead4 Mon Sep 17 00:00:00 2001 From: janblade Date: Sun, 21 Jun 2026 17:00:10 +0800 Subject: [PATCH 1/4] Add support for MediaTek Smart TV SoC decoders (omx.ms / MT5889) MediaTek Smart TV SoCs use the 'omx.ms' codec prefix (e.g. MT5889 in TCL C7xx series). These are distinct from MediaTek mobile SoCs ('omx.mtk') and were previously missing proper decoder support in several areas. Changes: - Whitelist omx.ms for HEVC decoding on Android 11+ (API 30+). The MT5889 properly declares HEVC Main10 support in its media_codecs.xml. - Enable direct submit for omx.ms. Fixed-function HW decoders with low input-buffer latency, matching the behaviour of other C2/HW decoders. - Remove omx.ms from the adaptive playback blacklist. The original comment incorrectly grouped omx.ms with omx.mtk (mobile). TCL/MT5889 devices properly declare adaptive-playback support and are stable with it enabled. - Enable HEVC RFI (reference frame invalidation) for omx.ms on Android 11+. Confirmed on TCL C735 (MT5889/Mali-G57); unlike omx.mtk this does not require GPU-family gating. - Add mediaTeKTvDecoderPrefixes and a dedicated KEY_LOW_LATENCY + vdec-lowlatency code path. The MT5889 ACodec layer requires both KEY_LOW_LATENCY (standard Android R API) and the vendor 'vdec-lowlatency' key to fully engage low-latency I-frame decoding. The existing FEATURE_LowLatency early-return is bypassed for these decoders so both keys are always applied together. - Fix decoderSupportsFusedIdrFrame() to return false for decoders on the adaptive playback blacklist. Fused IDR submission requires the codec to have been configured with KEY_MAX_WIDTH/KEY_MAX_HEIGHT; blacklisted decoders are not configured that way, so submitting fused IDR buffers would be incorrect. Tested on TCL C735 (MT5889/Mali-G57, Android 11) via WiFi and Ethernet with HEVC Main10 HDR at 4K 60fps. --- .../binding/video/MediaCodecHelper.java | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index 24d1f01ab0..8135204232 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -44,6 +44,7 @@ public class MediaCodecHelper { private static final List exynosDecoderPrefixes; private static final List amlogicDecoderPrefixes; private static final List knownVendorLowLatencyOptions; + private static final List mediaTeKTvDecoderPrefixes; public static final boolean SHOULD_BYPASS_SOFTWARE_BLOCK = Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets") || Build.BRAND.equals("Android-x86"); @@ -68,6 +69,10 @@ public class MediaCodecHelper { // All Codec2 decoders directSubmitPrefixes.add("c2."); + + // MediaTek Smart TV SoCs (omx.ms, e.g. MT5889). These are fixed-function HW + // decoders with low input-buffer latency; tested on TCL C735 (MT5889/Mali-G57). + directSubmitPrefixes.add("omx.ms"); } static { @@ -123,9 +128,13 @@ public class MediaCodecHelper { // The Intel decoder on Lollipop on Nexus Player would increase latency badly // if adaptive playback was enabled so let's avoid it to be safe. blacklistedAdaptivePlaybackPrefixes.add("omx.intel"); - // The MediaTek decoder crashes at 1080p when adaptive playback is enabled - // on some Android TV devices with HEVC only. + // MediaTek MOBILE decoders (omx.mtk / c2.mtk) crash at 1080p when adaptive playback + // is enabled for HEVC streams on some Android TV devices (e.g. Fire TV with Helio SoC). + // This does NOT apply to MediaTek Smart TV SoCs (omx.ms prefix, e.g. MT5889 in TCL C7xx): + // those decoders properly declare in their + // media_codecs.xml and are stable with adaptive playback enabled. blacklistedAdaptivePlaybackPrefixes.add("omx.mtk"); + blacklistedAdaptivePlaybackPrefixes.add("c2.mtk"); constrainedHighProfilePrefixes = new LinkedList<>(); constrainedHighProfilePrefixes.add("omx.intel"); @@ -191,6 +200,14 @@ public class MediaCodecHelper { whitelistedHevcDecoders.add("omx.realtek"); } + // MediaTek Smart TV SoCs (MT58xx/MT98xx series, used in TCL, Hisense, Philips, and others) + // use the "omx.ms" codec prefix rather than "omx.mtk". These chips have reliable HEVC decoders + // on Android 11 (R)+. FEATURE_LowLatency is used to select them at runtime; this entry serves + // as a fallback whitelist for devices that do not report that feature. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + whitelistedHevcDecoders.add("omx.ms"); + } + // These theoretically have good HEVC decoding capabilities (potentially better than // their AVC decoders), but haven't been tested enough //whitelistedHevcDecoders.add("omx.rk"); @@ -223,6 +240,14 @@ public class MediaCodecHelper { knownVendorLowLatencyOptions.add("vendor.low-latency.enable"); } + static { + // MediaTek Smart TV SoCs use the "omx.ms" prefix (e.g. MT5889 in TCL C7xx). + // They are distinct from MediaTek mobile SoCs ("omx.mtk") and require both + // KEY_LOW_LATENCY and "vdec-lowlatency" to fully enable low-latency I-frame decoding. + mediaTeKTvDecoderPrefixes = new LinkedList<>(); + mediaTeKTvDecoderPrefixes.add("omx.ms"); + } + static { qualcommDecoderPrefixes = new LinkedList<>(); @@ -412,6 +437,14 @@ public static void initialize(Context context, String glRenderer) { } } + // MediaTek Smart TV SoCs (omx.ms prefix, e.g. MT5889) support HEVC RFI on Android 11+. + // Confirmed on TCL C735 (MT5889/Mali-G57). Unlike omx.mtk (mobile), omx.ms does not + // require GPU-family gating — all known omx.ms devices on Android R+ have reliable decoders. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + LimeLog.info("Added omx.ms to HEVC RFI list for MediaTek Smart TV SoCs (Android 11+)"); + refFrameInvalidationHevcPrefixes.add("omx.ms"); + } + initialized = true; } @@ -500,16 +533,33 @@ public static boolean setDecoderLowLatencyOptions(MediaFormat videoFormat, Media if (tryNumber < 1) { // Official Android 11+ low latency option (KEY_LOW_LATENCY). - videoFormat.setInteger("low-latency", 1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1); + } else { + videoFormat.setInteger("low-latency", 1); + } setNewOption = true; // If this decoder officially supports FEATURE_LowLatency, we will just use that alone - // for try 0. Otherwise, we'll include it as best effort with other options. - if (decoderSupportsAndroidRLowLatency(decoderInfo, videoFormat.getString(MediaFormat.KEY_MIME))) { + // for try 0 — UNLESS it's a MediaTek Smart TV SoC (omx.ms prefix). These chips + // have a proprietary ACodec layer that recognizes "vdec-lowlatency" to set + // OMX.MTK.index.param.video.LowLatencyDecode internally. Setting KEY_LOW_LATENCY + // alone via FEATURE_LowLatency may not fully engage low-latency I-frame decoding, + // so we fall through to also apply vdec-lowlatency in tryNumber < 2. + if (decoderSupportsAndroidRLowLatency(decoderInfo, videoFormat.getString(MediaFormat.KEY_MIME)) + && !isDecoderInList(mediaTeKTvDecoderPrefixes, decoderInfo.getName())) { return true; } } + // For MediaTek Smart TV SoCs (omx.ms), always keep KEY_LOW_LATENCY set even in later + // retry rounds (tryNumber >= 1), so that if try 0 (KEY_LOW_LATENCY + vdec-lowlatency) + // fails, retry 1 still carries KEY_LOW_LATENCY alongside vdec-lowlatency alone. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + isDecoderInList(mediaTeKTvDecoderPrefixes, decoderInfo.getName())) { + videoFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1); + } + if (tryNumber < 2 && (!Build.MANUFACTURER.equalsIgnoreCase("xiaomi") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M)) { // MediaTek decoders don't use vendor-defined keys for low latency mode. Instead, they have a modified @@ -595,7 +645,17 @@ else if (isDecoderInList(amlogicDecoderPrefixes, decoderInfo.getName())) { } public static boolean decoderSupportsFusedIdrFrame(MediaCodecInfo decoderInfo, String mimeType) { - // If adaptive playback is supported, we can submit new CSD together with a keyframe + // Fused IDR frames require the codec to be configured in adaptive playback mode + // (KEY_MAX_WIDTH/KEY_MAX_HEIGHT set). Decoders blacklisted for adaptive playback + // are also not configured in that mode, so we must exclude them here to avoid + // submitting new CSD mid-stream without the corresponding adaptive playback setup. + if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) { + LimeLog.info("Decoder blacklisted for adaptive playback; disabling fused IDR frames"); + return false; + } + + // If adaptive playback is supported (and not blacklisted above), we can submit + // new CSD together with a keyframe. try { if (decoderInfo.getCapabilitiesForType(mimeType). isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) { From 66e59e5a07202261beaafed32d8d9db223a59d34 Mon Sep 17 00:00:00 2001 From: janblade Date: Sun, 21 Jun 2026 17:00:10 +0800 Subject: [PATCH 2/4] Game: Enable Game Mode / ALLM via preferMinimalPostProcessing on Android 11+ On Android 11+ (API 30), Window.preferMinimalPostProcessing maps to HDMI Auto Low Latency Mode (ALLM) on connected displays. This instructs the TV to switch to its Game Mode picture preset, bypassing MEMC (Motion Smoothing / frame interpolation), post-sharpening, and other processing that adds display lag and can introduce periodic visual artefacts during streaming. Some TV vendor overlays (notably TCL/MediaTek) ignore the equivalent AndroidManifest attribute; setting it programmatically on WindowManager LayoutParams at runtime is more reliable. The setAttributes() calls in the display-mode selection paths are also added to ensure the flag is applied to the window regardless of which branch is taken (mode change vs. already-optimal mode). --- app/src/main/java/com/limelight/Game.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 5d214508a3..ac0b02fcc1 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -785,6 +785,14 @@ private float prepareDisplayForRendering() { WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes(); float displayRefreshRate; + // Force minimal post-processing (Game Mode / ALLM) programmatically on Android 11+ (API 30+). + // Some TV vendor overlays (like TCL/MediaTek) ignore the AndroidManifest attribute, but + // programmatically setting it on the Window LayoutParams forces the OS compositor to + // bypass MEMC (Motion Smoothing) and post-processing, eliminating periodic stutter. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + windowLayoutParams.preferMinimalPostProcessing = true; + } + // On M, we can explicitly set the optimal display mode if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Display.Mode bestMode = display.getMode(); @@ -889,10 +897,12 @@ else if (!isRefreshRateGoodMatch(candidate.getRefreshRate())) { } else { LimeLog.info("Using setFrameRate() instead of preferredDisplayModeId due to matching resolution"); + getWindow().setAttributes(windowLayoutParams); } } else { LimeLog.info("Current display mode is already the best display mode"); + getWindow().setAttributes(windowLayoutParams); } displayRefreshRate = bestMode.getRefreshRate(); From 07eb581e4e9bace8910ae9b474da01ffcc10abbd Mon Sep 17 00:00:00 2001 From: janblade Date: Sun, 21 Jun 2026 17:00:11 +0800 Subject: [PATCH 3/4] MediaCodecDecoderRenderer: Fix MAX_SMOOTHNESS rendering and TV compositor timestamp Two related rendering fixes: 1. MAX_SMOOTHNESS / CAP_FPS frame queue drain: when multiple output buffers are queued, the previous code called releaseOutputBuffer(index, false) for all intermediate frames, silently discarding them. In MAX_SMOOTHNESS and CAP_FPS modes the intent is to never drop frames; intermediate frames are now released with timestamp=0 (immediate render) so every decoded frame reaches the display. 2. Lowest-latency mode final frame timestamp: the previous code passed System.nanoTime() as the presentation timestamp. On smart TV SoCs (e.g. MediaTek MT5889), this causes SurfaceFlinger to queue frames against future VSync deadlines it cannot meet, leading to constant 2-3 fps drops and periodic compositor stalls. Passing timestamp=0 requests immediate release and avoids compositor-side queue build-up. --- .../video/MediaCodecDecoderRenderer.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java index d24ec0dc72..abdc93327a 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -1064,7 +1064,21 @@ public void run() { if (prefs.framePacing != PreferenceConfiguration.FRAME_PACING_BALANCED) { // Get the last output buffer in the queue while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) { - videoDecoder.releaseOutputBuffer(lastIndex, false); + if (prefs.framePacing == PreferenceConfiguration.FRAME_PACING_MAX_SMOOTHNESS || + prefs.framePacing == PreferenceConfiguration.FRAME_PACING_CAP_FPS) { + // Render every frame in the queue to maintain complete smoothness without dropping frames + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + videoDecoder.releaseOutputBuffer(lastIndex, 0); + } + else { + videoDecoder.releaseOutputBuffer(lastIndex, true); + } + activeWindowVideoStats.totalFramesRendered++; + } + else { + // In min latency mode, drop older queued frames to catch up to the latest one + videoDecoder.releaseOutputBuffer(lastIndex, false); + } numFramesOut++; @@ -1085,9 +1099,11 @@ public void run() { } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Use a PTS that will cause this frame to be dropped if another comes in within - // the same V-sync period - videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime()); + // On smart TVs, using a dynamic System.nanoTime() as the presentation time + // causes Android's SurfaceFlinger compositor to miss VSync boundaries, + // leading to constant 2-3 FPS drops and periodic stuttering. Releasing with + // 0 (which defaults to immediate rendering) avoids compositor-side queue build-up. + videoDecoder.releaseOutputBuffer(lastIndex, 0); } else { videoDecoder.releaseOutputBuffer(lastIndex, true); From 446f2e6c64f7ac063f47aa0c36455a877fe840ce Mon Sep 17 00:00:00 2001 From: janblade Date: Sun, 21 Jun 2026 17:00:11 +0800 Subject: [PATCH 4/4] AndroidAudioRenderer: Fix audio queue threshold and buffer selection with effects Two audio reliability fixes: 1. Audio queue threshold raised from 40 ms to 120 ms. At 40 ms the queue limit is hit during normal AudioTrack hardware buffering, causing constant packet drops that manifest as periodic audio stuttering. 120 ms safely absorbs the hardware buffer depth and brief network jitter without introducing perceptible latency. 2. Skip buffer size index 2 when audio effects processing is active. With an equalizer or other effects chain enabled, standard-latency AudioTrack paths require a larger minimum buffer to avoid underruns under high CPU/GPU load (e.g. 4K AV1 decode). Index 2 produces a size too small for reliable playback in that case. --- .../binding/audio/AndroidAudioRenderer.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java index dc25cc8912..ad5857b7f8 100644 --- a/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java +++ b/app/src/main/java/com/limelight/binding/audio/AndroidAudioRenderer.java @@ -158,6 +158,14 @@ public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRat continue; } + // Skip sub-minimal buffers with standard latency when using audio effects. + // Standard-latency paths require robust buffer sizes when effects processing + // (like equalizers) is active, otherwise underruns easily occur under high + // CPU/GPU loads (such as heavy AV1 video decoding). + if (enableAudioFx && i == 2) { + continue; + } + try { track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency); track.play(); @@ -187,15 +195,18 @@ public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRat @Override public void playDecodedAudio(short[] audioData) { - // Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us. - if (MoonBridge.getPendingAudioDuration() < 40) { + // Only queue up to 120 ms of pending audio data in addition to what AudioTrack is buffering for us. + // On many Android TV and mobile platforms, AudioTrack's recommended hardware buffer length is 60-100 ms. + // A low threshold like 40 ms mathematically guarantees constant packet drops during normal playback, + // manifesting as periodic stuttering. Using 120 ms absorbs the hardware buffer and brief network jitter safely. + if (MoonBridge.getPendingAudioDuration() < 120) { // This will block until the write is completed. That can cause a backlog // of pending audio data, so we do the above check to be able to bound - // latency at 40 ms in that situation. + // latency at 120 ms in that situation. track.write(audioData, 0, audioData.length); } else { - LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms"); + LimeLog.info("Too much pending audio data (discarding): " + MoonBridge.getPendingAudioDuration() +" ms"); } }