fix(android): hold BOTH Wi-Fi locks while streaming — HIGH_PERF alone is a no-op
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 5m40s
arch / build-publish (push) Successful in 6m0s
audit / bun-audit (push) Failing after 17s
audit / cargo-audit (push) Failing after 1m40s
windows-host / package (push) Successful in 7m41s
ci / web (push) Successful in 59s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m21s
ci / docs-site (push) Successful in 1m23s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m14s
release / apple (push) Successful in 9m12s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 56s
ci / bench (push) Successful in 4m51s
ci / rust (push) Successful in 9m59s
decky / build-publish (push) Successful in 24s
apple / screenshots (push) Successful in 7m10s
deb / build-publish (push) Successful in 4m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 34s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m32s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
flatpak / build-publish (push) Successful in 4m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m47s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m7s
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 5m40s
arch / build-publish (push) Successful in 6m0s
audit / bun-audit (push) Failing after 17s
audit / cargo-audit (push) Failing after 1m40s
windows-host / package (push) Successful in 7m41s
ci / web (push) Successful in 59s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m21s
ci / docs-site (push) Successful in 1m23s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m14s
release / apple (push) Successful in 9m12s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 56s
ci / bench (push) Successful in 4m51s
ci / rust (push) Successful in 9m59s
decky / build-publish (push) Successful in 24s
apple / screenshots (push) Successful in 7m10s
deb / build-publish (push) Successful in 4m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m42s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 34s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m32s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
flatpak / build-publish (push) Successful in 4m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m47s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m7s
The baseline stream held only WIFI_MODE_FULL_HIGH_PERF, which is deprecated AND non-functional on recent Android — so with the low-latency toggle off (the default) Wi-Fi power save stayed fully active: downlink delivery clumped at beacon intervals (a few hundred ms of latency mush, sawtoothing bitrate) and the AP's power-save buffer periodically overflowed, killing whole frames every few seconds (the host log's alternating loss_ppm=0/50000). Now every stream holds FULL_LOW_LATENCY (API 29+, the only effective power-save disable; foreground + screen-on, which a stream always is) AND FULL_HIGH_PERF (covers older releases) — the same pair Moonlight holds. The experimental toggle no longer selects the lock mode. Also: declare tracing's "log" feature explicitly in the native crate (core transport warnings → logcat must not hinge on quinn's default features), and align the low-latency toggle's copy with its actual scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -2918,6 +2918,7 @@ dependencies = [
|
||||
"ndk",
|
||||
"opus",
|
||||
"punktfunk-core",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -55,12 +55,12 @@ data class Settings(
|
||||
*/
|
||||
val libraryEnabled: Boolean = true,
|
||||
/**
|
||||
* "Low-latency mode (experimental)" — the master switch over the whole latency overhaul: decoder
|
||||
* "Low-latency mode (experimental)" — the master switch over the 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.
|
||||
* max-performance, game-tagged AAudio, DSCP marking on the media sockets, HDMI ALLM, and the
|
||||
* forced TV mode switch. (The Wi-Fi locks are NOT part of this — both are always held while
|
||||
* streaming; see StreamScreen.) Off (default): the original decode pipeline, kept as the
|
||||
* known-good baseline until the aggressive stack is proven per-device.
|
||||
*/
|
||||
val lowLatencyMode: Boolean = false,
|
||||
/**
|
||||
|
||||
@@ -328,7 +328,7 @@ private fun DisplaySettings(s: Settings, update: (Settings) -> Unit, context: an
|
||||
ToggleRow(
|
||||
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 " +
|
||||
"decode, HDMI game mode). 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)) },
|
||||
|
||||
@@ -65,9 +65,9 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
// Touch model is fixed per session (re-keys the gesture handler below if it ever changes).
|
||||
val touchMode = initialSettings.touchMode
|
||||
// "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).
|
||||
// default) runs the original decode pipeline; on enables the aggressive stack — decoder
|
||||
// ranking + vendor keys + async loop (native side), 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.
|
||||
@@ -117,26 +117,34 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
// main thread, so a plain flag is race-free; AtomicBoolean just makes the intent explicit.
|
||||
val closed = remember { AtomicBoolean(false) }
|
||||
|
||||
// A Wi-Fi low-latency lock held for the stream's duration: asks the Wi-Fi firmware to drop its
|
||||
// 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
|
||||
// Wi-Fi locks held for the stream's duration — BOTH of them, unconditionally (Moonlight does
|
||||
// the same). Without an effective lock, Wi-Fi power save batches downlink delivery into
|
||||
// beacon-interval clumps: hundreds of ms of latency mush, sawtoothing bitrate, and periodic
|
||||
// whole-frame loss when the AP's power-save buffer overflows (all observed live on a phone).
|
||||
// - FULL_LOW_LATENCY (API 29+) is the only lock that actually disables power save on modern
|
||||
// Android; it needs the app foreground + screen on, which a stream always is.
|
||||
// - FULL_HIGH_PERF covers older releases — it is deprecated AND a documented no-op on recent
|
||||
// Android, which is exactly why it can't be the only lock (a lesson learned: holding just
|
||||
// HIGH_PERF left power save fully active on Android 13+).
|
||||
// Needs no permission beyond ACCESS_WIFI_STATE (declared). Non-reference-counted: one explicit
|
||||
// acquire/release each.
|
||||
val wifiLocks = remember(handle) {
|
||||
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
|
||||
} else {
|
||||
?: return@remember emptyList<WifiManager.WifiLock>()
|
||||
buildList {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "punktfunk:stream-ll")
|
||||
?.let(::add)
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
WifiManager.WIFI_MODE_FULL_HIGH_PERF
|
||||
}
|
||||
wm?.createWifiLock(mode, "punktfunk:stream")?.apply { setReferenceCounted(false) }
|
||||
wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "punktfunk:stream-hp")
|
||||
?.let(::add)
|
||||
}.onEach { it.setReferenceCounted(false) }
|
||||
}
|
||||
|
||||
DisposableEffect(handle) {
|
||||
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
runCatching { wifiLock?.acquire() }
|
||||
wifiLocks.forEach { runCatching { it.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+. Part of the experimental low-latency stack.
|
||||
@@ -175,7 +183,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
||||
if (lowLatencyMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window?.setPreferMinimalPostProcessing(false)
|
||||
}
|
||||
runCatching { if (wifiLock?.isHeld == true) wifiLock.release() }
|
||||
wifiLocks.forEach { runCatching { if (it.isHeld) it.release() } }
|
||||
// Release the landscape lock so the rest of the app follows the device/system again.
|
||||
activity?.requestedOrientation =
|
||||
priorOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
|
||||
@@ -31,6 +31,14 @@ mdns-sd = "0.20"
|
||||
# via `ndk`, the Opus codec) is only pulled in for the real `*-linux-android` targets.
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.14"
|
||||
# Feature bridge, no code here: punktfunk-core logs through `tracing`, but this client only
|
||||
# installs `android_logger` (a `log` backend). Core transport warnings (e.g. "UDP socket buffer
|
||||
# capped well below target") reach logcat only via tracing's "log" feature, which forwards events
|
||||
# as `log` records when no tracing subscriber is set (always, here). Today that feature happens to
|
||||
# be enabled transitively — quinn's default `log` feature unifies `tracing/log` onto the whole
|
||||
# graph — but nothing about this client's logging should hinge on a QUIC crate's default feature
|
||||
# set, so declare it explicitly.
|
||||
tracing = { version = "0.1", default-features = false, features = ["std", "log"] }
|
||||
# NDK bindings. "media" = AMediaCodec/ANativeWindow (video); "audio" = AAudio (audio playback).
|
||||
# Pure-Rust FFI to libmediandk/libnativewindow/libaaudio — no C++/libc++_shared to bundle. Decode +
|
||||
# audio run entirely in Rust on native threads (the "no async on the hot path" invariant).
|
||||
|
||||
@@ -44,7 +44,10 @@ mod stats;
|
||||
mod wol;
|
||||
|
||||
/// Initialize `android_logger` once when the JVM loads the library. Logs land in logcat under the
|
||||
/// `punktfunk` tag. Android-only — there is no JVM (and no logcat) on the host build.
|
||||
/// `punktfunk` tag. Core `tracing` events (transport warnings: socket-buffer clamp, QoS failures)
|
||||
/// arrive here too: tracing's "log" feature — declared explicitly in Cargo.toml rather than relied
|
||||
/// on via quinn's defaults — forwards them as `log` records since no tracing subscriber is ever
|
||||
/// installed. Android-only — there is no JVM (and no logcat) on the host build.
|
||||
#[cfg(target_os = "android")]
|
||||
#[no_mangle]
|
||||
pub extern "system" fn JNI_OnLoad(
|
||||
|
||||
Reference in New Issue
Block a user