Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++;

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class MediaCodecHelper {
private static final List<String> exynosDecoderPrefixes;
private static final List<String> amlogicDecoderPrefixes;
private static final List<String> knownVendorLowLatencyOptions;
private static final List<String> mediaTeKTvDecoderPrefixes;

public static final boolean SHOULD_BYPASS_SOFTWARE_BLOCK =
Build.HARDWARE.equals("ranchu") || Build.HARDWARE.equals("cheets") || Build.BRAND.equals("Android-x86");
Expand All @@ -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 {
Expand Down Expand Up @@ -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 <Feature name="adaptive-playback" /> 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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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<>();

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down