fix(android): gate the latency overhaul behind an experimental toggle, default off
apple / swift (push) Successful in 2m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 50s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 59s
apple / screenshots (push) Successful in 5m55s
windows-host / package (push) Successful in 7m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m19s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
release / apple (push) Successful in 12m3s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m18s
ci / rust (push) Successful in 4m52s
ci / bench (push) Successful in 6m44s
android / android (push) Successful in 5m9s
arch / build-publish (push) Successful in 5m40s
decky / build-publish (push) Successful in 15s
deb / build-publish (push) Successful in 3m20s
linux-client-screenshots / screenshots (push) Successful in 2m0s
flatpak / build-publish (push) Successful in 4m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m13s
docker / deploy-docs (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
web-screenshots / screenshots (push) Successful in 2m59s
android-screenshots / screenshots (push) Failing after 3m25s
apple / swift (push) Successful in 2m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 50s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 59s
apple / screenshots (push) Successful in 5m55s
windows-host / package (push) Successful in 7m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m19s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m13s
release / apple (push) Successful in 12m3s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m18s
ci / rust (push) Successful in 4m52s
ci / bench (push) Successful in 6m44s
android / android (push) Successful in 5m9s
arch / build-publish (push) Successful in 5m40s
decky / build-publish (push) Successful in 15s
deb / build-publish (push) Successful in 3m20s
linux-client-screenshots / screenshots (push) Successful in 2m0s
flatpak / build-publish (push) Successful in 4m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m13s
docker / deploy-docs (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
web-screenshots / screenshots (push) Successful in 2m59s
android-screenshots / screenshots (push) Failing after 3m25s
The 27c53a4 low-latency overhaul regressed badly on some phones. Every piece
of it — decoder ranking, per-SoC vendor keys, the async decode loop, pipeline
thread boosts, the ADPF max-performance bias, game-tagged AAudio, DSCP marking,
the Wi-Fi low-latency lock, HDMI ALLM and the forced TV mode switch — now rides
the "Low-latency mode (experimental)" toggle, default OFF. Off restores the
pre-overhaul pipeline byte-for-byte: the sync poll loop, the platform-default
decoder, and the original format keys (standard low-latency + blind Qualcomm
twin + priority=0 + operating-rate=MAX together).
- New pref key (low_latency_mode_experimental): the old key shipped default-ON,
so any install that ever saved settings persisted true — flipping the default
under the old key would leave exactly the regressed devices stuck on.
- DSCP is applied at socket creation, so the toggle reaches the transport via
NativeBridge.nativeSetLowLatencyMode → transport::set_dscp_default, called in
the connect choke point before nativeConnect; the core DSCP default reverts
to off everywhere.
- nativeStartAudio(handle, lowLatencyMode) gates AAudio usage=Game.
- VideoDecoders.pickDecoder now skips `.secure` decoder twins and decoders that
require FEATURE_SecurePlayback: they need a secure surface, and a secure twin
could out-score its plain sibling (only it advertising FEATURE_LowLatency),
which black-screens a clear stream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,9 @@ suspend fun connectToHost(
|
||||
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID.
|
||||
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
||||
return withContext(Dispatchers.IO) {
|
||||
// Transport-level half of "Low-latency mode (experimental)" (DSCP marking on the media
|
||||
// sockets) — must be applied before connect, since sockets are tagged at creation.
|
||||
NativeBridge.nativeSetLowLatencyMode(settings.lowLatencyMode)
|
||||
NativeBridge.nativeConnect(
|
||||
host, port, w, h, hz,
|
||||
identity.certPem, identity.privateKeyPem, pinHex,
|
||||
|
||||
@@ -55,13 +55,14 @@ data class Settings(
|
||||
*/
|
||||
val libraryEnabled: Boolean = true,
|
||||
/**
|
||||
* Aggressive decoder latency tuning — the master escape hatch. On (default): the decoder runs
|
||||
* the full low-latency profile (per-SoC vendor keys + max-clock operating-rate on Qualcomm).
|
||||
* Off: a conservative profile (the standard `low-latency` key only), for a device that thermally
|
||||
* throttles or misbehaves under the aggressive clocks. Decoder ranking, the Wi-Fi low-latency
|
||||
* lock and HDMI game-mode signalling stay on regardless — they're harmless.
|
||||
* "Low-latency mode (experimental)" — the master switch over the whole latency overhaul: decoder
|
||||
* ranking + per-SoC vendor keys + the async decode loop (native), pipeline thread boosts + ADPF
|
||||
* max-performance, game-tagged AAudio, DSCP marking on the media sockets, the Wi-Fi low-latency
|
||||
* lock, HDMI ALLM, and the forced TV mode switch. Off (default): the original pre-overhaul
|
||||
* pipeline, kept byte-for-byte as the known-good baseline — the overhaul regressed badly on some
|
||||
* phones, so it's opt-in until it's proven per-device.
|
||||
*/
|
||||
val lowLatencyMode: Boolean = true,
|
||||
val lowLatencyMode: Boolean = false,
|
||||
/**
|
||||
* Wake-on-LAN a saved host before connecting when it isn't currently seen on mDNS. On (default):
|
||||
* a connect to a host with a learned MAC that isn't advertising sends a magic packet and waits
|
||||
@@ -99,7 +100,7 @@ class SettingsStore(context: Context) {
|
||||
?: if (prefs.getBoolean(K_TRACKPAD, true)) TouchMode.TRACKPAD else TouchMode.POINTER,
|
||||
gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true),
|
||||
libraryEnabled = prefs.getBoolean(K_LIBRARY, true),
|
||||
lowLatencyMode = prefs.getBoolean(K_LOW_LATENCY, true),
|
||||
lowLatencyMode = prefs.getBoolean(K_LOW_LATENCY, false),
|
||||
autoWakeEnabled = prefs.getBoolean(K_AUTO_WAKE, true),
|
||||
)
|
||||
|
||||
@@ -139,7 +140,14 @@ class SettingsStore(context: Context) {
|
||||
const val K_TOUCH_MODE = "touch_mode"
|
||||
const val K_GAMEPAD_UI = "gamepad_ui_enabled"
|
||||
const val K_LIBRARY = "library_enabled"
|
||||
const val K_LOW_LATENCY = "low_latency_mode"
|
||||
|
||||
/**
|
||||
* Deliberately NOT the original `"low_latency_mode"` key: that one shipped default-ON, so
|
||||
* any install that ever saved settings persisted `true` — under the old key, flipping the
|
||||
* default to off would leave exactly the regressed devices stuck on the overhaul. The fresh
|
||||
* key restarts everyone at the safe default; the stale one is abandoned unread.
|
||||
*/
|
||||
const val K_LOW_LATENCY = "low_latency_mode_experimental"
|
||||
const val K_AUTO_WAKE = "auto_wake_enabled"
|
||||
|
||||
/** Legacy Boolean the enum replaced — read once as the migration default, never written. */
|
||||
|
||||
@@ -326,9 +326,10 @@ private fun DisplaySettings(s: Settings, update: (Settings) -> Unit, context: an
|
||||
) { c -> update(s.copy(compositor = c)) }
|
||||
|
||||
ToggleRow(
|
||||
title = "Low-latency mode",
|
||||
subtitle = "Run the decoder at max clocks for the lowest latency. Turn off only if a " +
|
||||
"device overheats or glitches during long sessions.",
|
||||
title = "Low-latency mode (experimental)",
|
||||
subtitle = "Aggressive decoder and system tuning (per-device decoder selection, async " +
|
||||
"decode, Wi-Fi and HDMI hints). Can lower latency, but may stutter or glitch on " +
|
||||
"some devices — turn off if the stream misbehaves.",
|
||||
checked = s.lowLatencyMode,
|
||||
onCheckedChange = { on -> update(s.copy(lowLatencyMode = on)) },
|
||||
)
|
||||
|
||||
@@ -64,7 +64,10 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
var showStats by remember { mutableStateOf(initialSettings.statsHudEnabled) }
|
||||
// Touch model is fixed per session (re-keys the gesture handler below if it ever changes).
|
||||
val touchMode = initialSettings.touchMode
|
||||
// Master low-latency toggle, resolved once for the session and passed to the decoder at start.
|
||||
// "Low-latency mode (experimental)" master toggle, resolved once for the session. Off (the
|
||||
// default) runs the original pre-overhaul pipeline; on enables the whole aggressive stack —
|
||||
// decoder ranking + vendor keys + async loop (native side), the Wi-Fi low-latency lock and
|
||||
// HDMI ALLM below, game-tagged audio, and DSCP marking (applied earlier, at connect).
|
||||
val lowLatencyMode = initialSettings.lowLatencyMode
|
||||
// TV form factor (leanback): the decoder actively switches the HDMI output mode to the stream
|
||||
// refresh; a phone/tablet gets the softer seamless frame-rate hint instead.
|
||||
@@ -118,7 +121,9 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
// power-save polling (a common source of tens-of-ms jitter). WIFI_MODE_FULL_LOW_LATENCY (API
|
||||
// 29+) is the strongest; older releases fall back to FULL_HIGH_PERF. Needs no extra permission
|
||||
// beyond ACCESS_WIFI_STATE (already declared). Non-reference-counted: one explicit acquire/release.
|
||||
// Part of the experimental low-latency stack — not created at all when the toggle is off.
|
||||
val wifiLock = remember(handle) {
|
||||
if (!lowLatencyMode) return@remember null
|
||||
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
WifiManager.WIFI_MODE_FULL_LOW_LATENCY
|
||||
@@ -133,8 +138,9 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
runCatching { wifiLock?.acquire() }
|
||||
// HDMI Auto Low-Latency Mode: ask the display to drop its post-processing (game mode) —
|
||||
// the biggest panel-side latency win on the TV boxes. No-op where ALLM isn't supported. API 30+.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// the biggest panel-side latency win on the TV boxes. No-op where ALLM isn't supported. API
|
||||
// 30+. Part of the experimental low-latency stack.
|
||||
if (lowLatencyMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window?.setPreferMinimalPostProcessing(true)
|
||||
}
|
||||
controller?.let {
|
||||
@@ -166,7 +172,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
activity?.setConsoleHighRefreshRate(true) // back to the console UI's max refresh
|
||||
controller?.show(WindowInsetsCompat.Type.systemBars())
|
||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (lowLatencyMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window?.setPreferMinimalPostProcessing(false)
|
||||
}
|
||||
runCatching { if (wifiLock?.isHeld == true) wifiLock.release() }
|
||||
@@ -191,11 +197,13 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
SurfaceView(ctx).apply {
|
||||
holder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
// Rank MediaCodecList decoders for the negotiated MIME (framework-only
|
||||
// API) and hand the chosen one to Rust, which creates it by name and
|
||||
// applies the per-SoC vendor low-latency keys.
|
||||
// Low-latency mode: rank MediaCodecList decoders for the negotiated
|
||||
// MIME (framework-only API) and hand the chosen one to Rust, which
|
||||
// creates it by name and applies the per-SoC vendor low-latency keys.
|
||||
// Off ⇒ no ranking: the platform resolves its default decoder for the
|
||||
// MIME, exactly as before the overhaul.
|
||||
val mime = NativeBridge.nativeVideoMime(handle)
|
||||
val choice = VideoDecoders.pickDecoder(mime)
|
||||
val choice = if (lowLatencyMode) VideoDecoders.pickDecoder(mime) else null
|
||||
NativeBridge.nativeStartVideo(
|
||||
handle,
|
||||
holder.surface,
|
||||
@@ -204,7 +212,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
choice?.lowLatencyFeature ?: false,
|
||||
isTv,
|
||||
)
|
||||
NativeBridge.nativeStartAudio(handle)
|
||||
NativeBridge.nativeStartAudio(handle, lowLatencyMode)
|
||||
if (micWanted) NativeBridge.nativeStartMic(handle)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user