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.
|
// "Automatic" resolves to a concrete pad type from the connected controller's VID/PID.
|
||||||
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
val gamepadPref = Gamepad.resolvePref(settings.gamepad)
|
||||||
return withContext(Dispatchers.IO) {
|
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(
|
NativeBridge.nativeConnect(
|
||||||
host, port, w, h, hz,
|
host, port, w, h, hz,
|
||||||
identity.certPem, identity.privateKeyPem, pinHex,
|
identity.certPem, identity.privateKeyPem, pinHex,
|
||||||
|
|||||||
@@ -55,13 +55,14 @@ data class Settings(
|
|||||||
*/
|
*/
|
||||||
val libraryEnabled: Boolean = true,
|
val libraryEnabled: Boolean = true,
|
||||||
/**
|
/**
|
||||||
* Aggressive decoder latency tuning — the master escape hatch. On (default): the decoder runs
|
* "Low-latency mode (experimental)" — the master switch over the whole latency overhaul: decoder
|
||||||
* the full low-latency profile (per-SoC vendor keys + max-clock operating-rate on Qualcomm).
|
* ranking + per-SoC vendor keys + the async decode loop (native), pipeline thread boosts + ADPF
|
||||||
* Off: a conservative profile (the standard `low-latency` key only), for a device that thermally
|
* max-performance, game-tagged AAudio, DSCP marking on the media sockets, the Wi-Fi low-latency
|
||||||
* throttles or misbehaves under the aggressive clocks. Decoder ranking, the Wi-Fi low-latency
|
* lock, HDMI ALLM, and the forced TV mode switch. Off (default): the original pre-overhaul
|
||||||
* lock and HDMI game-mode signalling stay on regardless — they're harmless.
|
* 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):
|
* 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
|
* 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,
|
?: if (prefs.getBoolean(K_TRACKPAD, true)) TouchMode.TRACKPAD else TouchMode.POINTER,
|
||||||
gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true),
|
gamepadUiEnabled = prefs.getBoolean(K_GAMEPAD_UI, true),
|
||||||
libraryEnabled = prefs.getBoolean(K_LIBRARY, 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),
|
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_TOUCH_MODE = "touch_mode"
|
||||||
const val K_GAMEPAD_UI = "gamepad_ui_enabled"
|
const val K_GAMEPAD_UI = "gamepad_ui_enabled"
|
||||||
const val K_LIBRARY = "library_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"
|
const val K_AUTO_WAKE = "auto_wake_enabled"
|
||||||
|
|
||||||
/** Legacy Boolean the enum replaced — read once as the migration default, never written. */
|
/** 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)) }
|
) { c -> update(s.copy(compositor = c)) }
|
||||||
|
|
||||||
ToggleRow(
|
ToggleRow(
|
||||||
title = "Low-latency mode",
|
title = "Low-latency mode (experimental)",
|
||||||
subtitle = "Run the decoder at max clocks for the lowest latency. Turn off only if a " +
|
subtitle = "Aggressive decoder and system tuning (per-device decoder selection, async " +
|
||||||
"device overheats or glitches during long sessions.",
|
"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,
|
checked = s.lowLatencyMode,
|
||||||
onCheckedChange = { on -> update(s.copy(lowLatencyMode = on)) },
|
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) }
|
var showStats by remember { mutableStateOf(initialSettings.statsHudEnabled) }
|
||||||
// Touch model is fixed per session (re-keys the gesture handler below if it ever changes).
|
// Touch model is fixed per session (re-keys the gesture handler below if it ever changes).
|
||||||
val touchMode = initialSettings.touchMode
|
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
|
val lowLatencyMode = initialSettings.lowLatencyMode
|
||||||
// TV form factor (leanback): the decoder actively switches the HDMI output mode to the stream
|
// 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.
|
// 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
|
// 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
|
// 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.
|
// 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) {
|
val wifiLock = remember(handle) {
|
||||||
|
if (!lowLatencyMode) return@remember null
|
||||||
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||||
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
WifiManager.WIFI_MODE_FULL_LOW_LATENCY
|
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)
|
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
runCatching { wifiLock?.acquire() }
|
runCatching { wifiLock?.acquire() }
|
||||||
// HDMI Auto Low-Latency Mode: ask the display to drop its post-processing (game mode) —
|
// 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+.
|
// the biggest panel-side latency win on the TV boxes. No-op where ALLM isn't supported. API
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
// 30+. Part of the experimental low-latency stack.
|
||||||
|
if (lowLatencyMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
window?.setPreferMinimalPostProcessing(true)
|
window?.setPreferMinimalPostProcessing(true)
|
||||||
}
|
}
|
||||||
controller?.let {
|
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
|
activity?.setConsoleHighRefreshRate(true) // back to the console UI's max refresh
|
||||||
controller?.show(WindowInsetsCompat.Type.systemBars())
|
controller?.show(WindowInsetsCompat.Type.systemBars())
|
||||||
window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
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)
|
window?.setPreferMinimalPostProcessing(false)
|
||||||
}
|
}
|
||||||
runCatching { if (wifiLock?.isHeld == true) wifiLock.release() }
|
runCatching { if (wifiLock?.isHeld == true) wifiLock.release() }
|
||||||
@@ -191,11 +197,13 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
SurfaceView(ctx).apply {
|
SurfaceView(ctx).apply {
|
||||||
holder.addCallback(object : SurfaceHolder.Callback {
|
holder.addCallback(object : SurfaceHolder.Callback {
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
// Rank MediaCodecList decoders for the negotiated MIME (framework-only
|
// Low-latency mode: rank MediaCodecList decoders for the negotiated
|
||||||
// API) and hand the chosen one to Rust, which creates it by name and
|
// MIME (framework-only API) and hand the chosen one to Rust, which
|
||||||
// applies the per-SoC vendor low-latency keys.
|
// 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 mime = NativeBridge.nativeVideoMime(handle)
|
||||||
val choice = VideoDecoders.pickDecoder(mime)
|
val choice = if (lowLatencyMode) VideoDecoders.pickDecoder(mime) else null
|
||||||
NativeBridge.nativeStartVideo(
|
NativeBridge.nativeStartVideo(
|
||||||
handle,
|
handle,
|
||||||
holder.surface,
|
holder.surface,
|
||||||
@@ -204,7 +212,7 @@ fun StreamScreen(handle: Long, micEnabled: Boolean, onDisconnect: () -> Unit) {
|
|||||||
choice?.lowLatencyFeature ?: false,
|
choice?.lowLatencyFeature ?: false,
|
||||||
isTv,
|
isTv,
|
||||||
)
|
)
|
||||||
NativeBridge.nativeStartAudio(handle)
|
NativeBridge.nativeStartAudio(handle, lowLatencyMode)
|
||||||
if (micWanted) NativeBridge.nativeStartMic(handle)
|
if (micWanted) NativeBridge.nativeStartMic(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ object NativeBridge {
|
|||||||
*/
|
*/
|
||||||
external fun nativeWakeOnLan(macsCsv: String, lastIp: String): Boolean
|
external fun nativeWakeOnLan(macsCsv: String, lastIp: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the user's "Low-latency mode (experimental)" toggle to the process-wide transport
|
||||||
|
* defaults — today just DSCP/QoS marking on the media sockets. Must be called BEFORE
|
||||||
|
* [nativeConnect] (the tag is applied at socket creation); `HostConnect.connectToHost` does.
|
||||||
|
* The rest of the toggle rides explicit per-session parameters ([nativeStartVideo] /
|
||||||
|
* [nativeStartAudio]). Cheap (one atomic store); UI-safe.
|
||||||
|
*/
|
||||||
|
external fun nativeSetLowLatencyMode(enabled: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MediaCodec MIME the host resolved for this session (`"video/hevc"` / `"video/avc"` /
|
* The MediaCodec MIME the host resolved for this session (`"video/hevc"` / `"video/avc"` /
|
||||||
* `"video/av01"`), or `""` on a `0` handle. Kotlin ranks `MediaCodecList` decoders for this
|
* `"video/av01"`), or `""` on a `0` handle. Kotlin ranks `MediaCodecList` decoders for this
|
||||||
@@ -134,10 +143,12 @@ object NativeBridge {
|
|||||||
* Start the decode thread rendering onto [surface] (a SurfaceView's surface). Decode runs
|
* Start the decode thread rendering onto [surface] (a SurfaceView's surface). Decode runs
|
||||||
* entirely in Rust (NDK AMediaCodec → ANativeWindow) — no per-frame JNI. [decoderName] is the
|
* entirely in Rust (NDK AMediaCodec → ANativeWindow) — no per-frame JNI. [decoderName] is the
|
||||||
* decoder Kotlin ranked from `MediaCodecList` (`""` = let the platform resolve the default for
|
* decoder Kotlin ranked from `MediaCodecList` (`""` = let the platform resolve the default for
|
||||||
* the MIME); [lowLatencyMode] is the user's master toggle (default on → aggressive per-SoC
|
* the MIME — what the pre-overhaul client always did); [lowLatencyMode] is the user's
|
||||||
* tuning; off → conservative); [lowLatencyFeature] is whether [decoderName] advertised
|
* "Low-latency mode (experimental)" toggle (off, the default, runs the original decode
|
||||||
* `FEATURE_LowLatency` (HUD label only). [isTv] drives an active HDMI mode switch to the stream
|
* pipeline; on, the aggressive per-SoC tuning + async loop); [lowLatencyFeature] is whether
|
||||||
* refresh on TV boxes (vs. the softer seamless hint on phones). No-op if already started.
|
* [decoderName] advertised `FEATURE_LowLatency` (HUD label only). [isTv] drives an active HDMI
|
||||||
|
* mode switch to the stream refresh on TV boxes when the toggle is on (vs. the softer seamless
|
||||||
|
* hint otherwise). No-op if already started.
|
||||||
*/
|
*/
|
||||||
external fun nativeStartVideo(
|
external fun nativeStartVideo(
|
||||||
handle: Long,
|
handle: Long,
|
||||||
@@ -183,10 +194,12 @@ object NativeBridge {
|
|||||||
external fun nativeSetVideoStatsEnabled(handle: Long, enabled: Boolean)
|
external fun nativeSetVideoStatsEnabled(handle: Long, enabled: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start host→client audio: Opus decode → jitter ring → AAudio (LowLatency), all in Rust. No-op
|
* Start host→client audio: Opus decode → jitter ring → AAudio (LowLatency), all in Rust.
|
||||||
* if already started. Best-effort — a failure leaves video streaming.
|
* [lowLatencyMode] (the experimental toggle) additionally tags the stream usage=Game for the
|
||||||
|
* HAL's game-audio routing. No-op if already started. Best-effort — a failure leaves video
|
||||||
|
* streaming.
|
||||||
*/
|
*/
|
||||||
external fun nativeStartAudio(handle: Long)
|
external fun nativeStartAudio(handle: Long, lowLatencyMode: Boolean)
|
||||||
|
|
||||||
/** Stop + join the audio thread and close AAudio, without closing the session. No-op on `0`. */
|
/** Stop + join the audio thread and close AAudio, without closing the session. No-op on `0`. */
|
||||||
external fun nativeStopAudio(handle: Long)
|
external fun nativeStopAudio(handle: Long)
|
||||||
|
|||||||
@@ -57,7 +57,17 @@ object VideoDecoders {
|
|||||||
val name = info.name
|
val name = info.name
|
||||||
val lower = name.lowercase()
|
val lower = name.lowercase()
|
||||||
if (BLOCKED_PREFIXES.any { lower.startsWith(it) } || lower in BLOCKED_EXACT) continue
|
if (BLOCKED_PREFIXES.any { lower.startsWith(it) } || lower in BLOCKED_EXACT) continue
|
||||||
|
// Never a secure decoder: `.secure` names are the DRM-pipeline twins of the real
|
||||||
|
// decoder and require a secure surface — configuring one for a clear stream fails (or
|
||||||
|
// renders black). The plain twin is also in the list, so drop rather than rank
|
||||||
|
// (a `.secure` twin can otherwise OUT-score its plain sibling when only it advertises
|
||||||
|
// FEATURE_LowLatency). Moonlight filters the same way.
|
||||||
|
if (lower.endsWith(".secure")) continue
|
||||||
val caps = runCatching { info.getCapabilitiesForType(mime) }.getOrNull() ?: continue
|
val caps = runCatching { info.getCapabilitiesForType(mime) }.getOrNull() ?: continue
|
||||||
|
val secureRequired = runCatching {
|
||||||
|
caps.isFeatureRequired(CodecCapabilities.FEATURE_SecurePlayback)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
if (secureRequired) continue
|
||||||
|
|
||||||
val hardware = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
val hardware = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
info.isHardwareAccelerated
|
info.isHardwareAccelerated
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ pub struct HintSession {
|
|||||||
impl HintSession {
|
impl HintSession {
|
||||||
/// Open a session hinting `tids` with an initial per-frame target of `target_ns` nanoseconds.
|
/// Open a session hinting `tids` with an initial per-frame target of `target_ns` nanoseconds.
|
||||||
/// `None` when ADPF is unavailable (device API < 33) or the platform declines — the caller then
|
/// `None` when ADPF is unavailable (device API < 33) or the platform declines — the caller then
|
||||||
/// runs unhinted (a no-op, not an error).
|
/// runs unhinted (a no-op, not an error). `prefer_performance` (the experimental low-latency
|
||||||
pub fn create(target_ns: i64, tids: &[i32]) -> Option<Self> {
|
/// mode) additionally biases the governor away from power efficiency (API 35+); off, the
|
||||||
|
/// session runs with the platform default, as it did before the overhaul.
|
||||||
|
pub fn create(target_ns: i64, tids: &[i32], prefer_performance: bool) -> Option<Self> {
|
||||||
if target_ns <= 0 || tids.is_empty() {
|
if target_ns <= 0 || tids.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -119,9 +121,11 @@ impl HintSession {
|
|||||||
// Tell the governor NOT to bias this session toward power efficiency (API 35+): our loop is
|
// Tell the governor NOT to bias this session toward power efficiency (API 35+): our loop is
|
||||||
// latency-critical, so we want it kept on fast cores at high clocks over battery savings.
|
// latency-critical, so we want it kept on fast cores at high clocks over battery savings.
|
||||||
// Best-effort; absent below API 35.
|
// Best-effort; absent below API 35.
|
||||||
if let Some(f) = api.set_prefer_power_efficiency {
|
if prefer_performance {
|
||||||
// SAFETY: `session` is the live session just created; the fn takes it + a bool.
|
if let Some(f) = api.set_prefer_power_efficiency {
|
||||||
unsafe { f(session, false) };
|
// SAFETY: `session` is the live session just created; the fn takes it + a bool.
|
||||||
|
unsafe { f(session, false) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(Self { api, session })
|
Some(Self { api, session })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,8 +116,10 @@ pub struct AudioPlayback {
|
|||||||
impl AudioPlayback {
|
impl AudioPlayback {
|
||||||
/// Open AAudio (LowLatency, 48 kHz/f32, the host-resolved channel layout) with a realtime
|
/// Open AAudio (LowLatency, 48 kHz/f32, the host-resolved channel layout) with a realtime
|
||||||
/// callback draining a jitter ring, then spawn the Opus decode thread. `None` on failure (the
|
/// callback draining a jitter ring, then spawn the Opus decode thread. `None` on failure (the
|
||||||
/// caller leaves video streaming).
|
/// caller leaves video streaming). `game_audio` (the experimental low-latency mode) tags the
|
||||||
pub fn start(client: Arc<NativeClient>) -> Option<AudioPlayback> {
|
/// stream usage=Game for the HAL's game-audio routing; off, the stream is untagged as it was
|
||||||
|
/// before the overhaul.
|
||||||
|
pub fn start(client: Arc<NativeClient>, game_audio: bool) -> Option<AudioPlayback> {
|
||||||
// Build playback from the host-RESOLVED channel count (never the request): 2 = stereo /
|
// Build playback from the host-RESOLVED channel count (never the request): 2 = stereo /
|
||||||
// 6 = 5.1 / 8 = 7.1, canonical wire order FL FR FC LFE RL RR SL SR.
|
// 6 = 5.1 / 8 = 7.1, canonical wire order FL FR FC LFE RL RR SL SR.
|
||||||
let channels = punktfunk_core::audio::normalize_channels(client.audio_channels) as usize;
|
let channels = punktfunk_core::audio::normalize_channels(client.audio_channels) as usize;
|
||||||
@@ -226,7 +228,7 @@ impl AudioPlayback {
|
|||||||
AudioCallbackResult::Continue
|
AudioCallbackResult::Continue
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = AudioStreamBuilder::new()?
|
let builder = AudioStreamBuilder::new()?
|
||||||
.direction(AudioDirection::Output)
|
.direction(AudioDirection::Output)
|
||||||
.sample_rate(SAMPLE_RATE)
|
.sample_rate(SAMPLE_RATE)
|
||||||
// The wire order (FL FR FC LFE RL RR SL SR) is the standard AAudio/Android channel
|
// The wire order (FL FR FC LFE RL RR SL SR) is the standard AAudio/Android channel
|
||||||
@@ -234,12 +236,19 @@ impl AudioPlayback {
|
|||||||
// from `channel_count` (the ndk crate's builder exposes no setChannelMask); the host
|
// from `channel_count` (the ndk crate's builder exposes no setChannelMask); the host
|
||||||
// captures + Opus-encodes in exactly this order.
|
// captures + Opus-encodes in exactly this order.
|
||||||
.channel_count(channels as i32)
|
.channel_count(channels as i32)
|
||||||
.format(AudioFormat::PCM_Float)
|
.format(AudioFormat::PCM_Float);
|
||||||
// Tag the stream as game audio (usage=Game / content=Movie): the audio HAL applies
|
// Tag the stream as game audio (usage=Game / content=Movie): the audio HAL applies
|
||||||
// its low-latency game-audio routing/policy and it's grouped correctly with the
|
// its low-latency game-audio routing/policy and it's grouped correctly with the
|
||||||
// game-mode profile. Advisory — ignored where the device has no such policy.
|
// game-mode profile. Advisory — ignored where the device has no such policy. Part of
|
||||||
.usage(AudioUsage::Game)
|
// the experimental low-latency stack; off, the stream stays untagged.
|
||||||
.content_type(AudioContentType::Movie)
|
let builder = if game_audio {
|
||||||
|
builder
|
||||||
|
.usage(AudioUsage::Game)
|
||||||
|
.content_type(AudioContentType::Movie)
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
};
|
||||||
|
let stream = builder
|
||||||
.performance_mode(AudioPerformanceMode::LowLatency)
|
.performance_mode(AudioPerformanceMode::LowLatency)
|
||||||
.sharing_mode(sharing)
|
.sharing_mode(sharing)
|
||||||
.data_callback(Box::new(callback))
|
.data_callback(Box::new(callback))
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ const IN_FLIGHT_CAP: usize = 64;
|
|||||||
/// this deep is a lost datagram (or an old host that never sends any) and gets evicted.
|
/// this deep is a lost datagram (or an old host that never sends any) and gets evicted.
|
||||||
const PENDING_SPLIT_CAP: usize = 256;
|
const PENDING_SPLIT_CAP: usize = 256;
|
||||||
|
|
||||||
/// Whether to run the event-driven async decode loop (default) or the synchronous poll loop kept as
|
/// Whether low-latency mode uses the event-driven async decode loop (default) or the synchronous
|
||||||
/// a bring-up fallback. Flip to `false` to A/B the two on the HUD (`design/…`); the async loop
|
/// poll loop. Flip to `false` to A/B the two on the HUD (`design/…`); the async loop presents a
|
||||||
/// presents a decoded frame the instant it's ready instead of waiting out a poll interval.
|
/// decoded frame the instant it's ready instead of waiting out a poll interval. Only consulted when
|
||||||
|
/// the user's "Low-latency mode (experimental)" toggle is ON — off, the sync loop always runs (the
|
||||||
|
/// original pipeline).
|
||||||
const USE_ASYNC_DECODE: bool = true;
|
const USE_ASYNC_DECODE: bool = true;
|
||||||
|
|
||||||
/// Per-session decode configuration, resolved by the JNI layer (`nativeStartVideo`) and passed to
|
/// Per-session decode configuration, resolved by the JNI layer (`nativeStartVideo`) and passed to
|
||||||
@@ -50,8 +52,10 @@ pub(crate) struct DecodeOptions {
|
|||||||
/// Whether Kotlin found the chosen decoder advertises `FEATURE_LowLatency` (queryable only via
|
/// Whether Kotlin found the chosen decoder advertises `FEATURE_LowLatency` (queryable only via
|
||||||
/// the Java `CodecCapabilities` API) — surfaced on the HUD next to the decoder name.
|
/// the Java `CodecCapabilities` API) — surfaced on the HUD next to the decoder name.
|
||||||
pub ll_feature: bool,
|
pub ll_feature: bool,
|
||||||
/// The user's "Low-latency mode" master toggle (default on ⇒ full aggressive profile; off ⇒
|
/// The user's "Low-latency mode (experimental)" master toggle. On ⇒ the full overhaul: async
|
||||||
/// conservative, an escape hatch for a device that throttles under the clocks).
|
/// decode loop, per-SoC vendor keys, pipeline thread boosts, ADPF max-performance, forced TV
|
||||||
|
/// mode switch. Off (default) ⇒ the original pre-overhaul pipeline, kept as the known-good
|
||||||
|
/// baseline while the overhaul is experimental.
|
||||||
pub low_latency_mode: bool,
|
pub low_latency_mode: bool,
|
||||||
/// TV form factor (Kotlin's `UiModeManager`): actively drive the HDMI output into the stream's
|
/// TV form factor (Kotlin's `UiModeManager`): actively drive the HDMI output into the stream's
|
||||||
/// refresh mode, vs. the softer seamless hint on a phone/tablet.
|
/// refresh mode, vs. the softer seamless hint on a phone/tablet.
|
||||||
@@ -67,17 +71,16 @@ pub fn run(
|
|||||||
stats: Arc<crate::stats::VideoStats>,
|
stats: Arc<crate::stats::VideoStats>,
|
||||||
opts: DecodeOptions,
|
opts: DecodeOptions,
|
||||||
) {
|
) {
|
||||||
if USE_ASYNC_DECODE {
|
if opts.low_latency_mode && USE_ASYNC_DECODE {
|
||||||
run_async(client, window, shutdown, stats, opts);
|
run_async(client, window, shutdown, stats, opts);
|
||||||
} else {
|
} else {
|
||||||
run_sync(client, window, shutdown, stats, opts);
|
run_sync(client, window, shutdown, stats, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The synchronous poll loop — the original decode path, kept as a bring-up fallback behind
|
/// The synchronous poll loop — the original decode path: the only one when low-latency mode is off,
|
||||||
/// [`USE_ASYNC_DECODE`]. Feeds and drains on this one thread; the only blocking wait is a short
|
/// and the [`USE_ASYNC_DECODE`] A/B fallback when it's on. Feeds and drains on this one thread; the
|
||||||
/// output dequeue while input is backed up.
|
/// only blocking wait is a short output dequeue while input is backed up.
|
||||||
#[allow(dead_code)]
|
|
||||||
fn run_sync(
|
fn run_sync(
|
||||||
client: Arc<NativeClient>,
|
client: Arc<NativeClient>,
|
||||||
window: NativeWindow,
|
window: NativeWindow,
|
||||||
@@ -160,7 +163,11 @@ fn run_sync(
|
|||||||
// above our API-28 floor, so we resolve it at runtime (see `try_set_frame_rate`) rather than link
|
// above our API-28 floor, so we resolve it at runtime (see `try_set_frame_rate`) rather than link
|
||||||
// it — a hard import would stop `libpunktfunk_android.so` loading at all on API 28/29. Absent
|
// it — a hard import would stop `libpunktfunk_android.so` loading at all on API 28/29. Absent
|
||||||
// there ⇒ we simply skip the hint (non-fatal; the stream renders fine without it).
|
// there ⇒ we simply skip the hint (non-fatal; the stream renders fine without it).
|
||||||
if mode.refresh_hz > 0 && !try_set_frame_rate(&window, mode.refresh_hz as f32, is_tv) {
|
// The forced TV mode switch (`is_tv` ⇒ ALWAYS strategy) is part of the experimental stack;
|
||||||
|
// off, every form factor gets the original soft seamless hint.
|
||||||
|
if mode.refresh_hz > 0
|
||||||
|
&& !try_set_frame_rate(&window, mode.refresh_hz as f32, is_tv && low_latency_mode)
|
||||||
|
{
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"decode: set_frame_rate({} Hz) unavailable/declined (non-fatal)",
|
"decode: set_frame_rate({} Hz) unavailable/declined (non-fatal)",
|
||||||
mode.refresh_hz
|
mode.refresh_hz
|
||||||
@@ -319,8 +326,12 @@ fn run_sync(
|
|||||||
// or where the platform declines → `None`, and the loop runs unhinted).
|
// or where the platform declines → `None`, and the loop runs unhinted).
|
||||||
hint_tried = true;
|
hint_tried = true;
|
||||||
let tids = client.hot_thread_ids();
|
let tids = client.hot_thread_ids();
|
||||||
boost_hot_threads(&tids);
|
// The pump/audio priority boost is part of the experimental low-latency stack; the
|
||||||
hint = crate::adpf::HintSession::create(frame_period_ns, &tids);
|
// ADPF session itself predates it and always runs (max-performance bias gated inside).
|
||||||
|
if low_latency_mode {
|
||||||
|
boost_hot_threads(&tids);
|
||||||
|
}
|
||||||
|
hint = crate::adpf::HintSession::create(frame_period_ns, &tids, low_latency_mode);
|
||||||
log::info!(
|
log::info!(
|
||||||
"decode: ADPF hint session {} — {} hot thread(s), target {frame_period_ns} ns",
|
"decode: ADPF hint session {} — {} hot thread(s), target {frame_period_ns} ns",
|
||||||
if hint.is_some() {
|
if hint.is_some() {
|
||||||
@@ -396,12 +407,15 @@ fn create_codec(mime: &str, preferred: Option<&str>) -> Option<MediaCodec> {
|
|||||||
MediaCodec::from_decoder_type(mime)
|
MediaCodec::from_decoder_type(mime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the low-latency MediaFormat keys for `codec_name`. The standard AOSP `low-latency` key is
|
/// Apply the low-latency MediaFormat keys for `codec_name`.
|
||||||
/// always set (API 30+, harmless/ignored elsewhere). When `aggressive` (the "Low-latency mode"
|
///
|
||||||
/// master toggle) we additionally set MediaTek's `vdec-lowlatency` (unconditionally — ignored off
|
/// `aggressive` = the "Low-latency mode (experimental)" master toggle. **Off** (default) ⇒ the
|
||||||
/// MediaTek), the per-SoC vendor extension keys (gated on the decoder-name prefix the way
|
/// pre-overhaul key set, byte-for-byte — the standard `low-latency` key, the blind Qualcomm vendor
|
||||||
/// Moonlight-Android does, since a key one vendor honours is meaningless on another), and one clock
|
/// twin, `priority = 0` AND `operating-rate = MAX` set together — kept as the known-good baseline
|
||||||
/// hint. Off ⇒ the standard key only, a gentler profile for a device that throttles under max clocks.
|
/// (the profile every device streamed with before the overhaul). **On** ⇒ the Moonlight-parity
|
||||||
|
/// profile: MediaTek's `vdec-lowlatency` (unconditionally — ignored off MediaTek), the per-SoC
|
||||||
|
/// vendor extension keys (gated on the decoder-name prefix the way Moonlight-Android does, since a
|
||||||
|
/// key one vendor honours is meaningless on another), and one *mutually exclusive* clock hint.
|
||||||
///
|
///
|
||||||
/// Vendor keys mirror Moonlight's `MediaCodecHelper` (verified against current source): Qualcomm
|
/// Vendor keys mirror Moonlight's `MediaCodecHelper` (verified against current source): Qualcomm
|
||||||
/// picture-order + low-latency, Exynos (also Google Tensor), Amlogic, HiSilicon, MediaTek. NVIDIA
|
/// picture-order + low-latency, Exynos (also Google Tensor), Amlogic, HiSilicon, MediaTek. NVIDIA
|
||||||
@@ -411,6 +425,12 @@ fn configure_low_latency(format: &mut MediaFormat, codec_name: &str, aggressive:
|
|||||||
// Standard key: request the no-reorder low-latency path where the platform decoder supports it.
|
// Standard key: request the no-reorder low-latency path where the platform decoder supports it.
|
||||||
format.set_i32("low-latency", 1);
|
format.set_i32("low-latency", 1);
|
||||||
if !aggressive {
|
if !aggressive {
|
||||||
|
// The original profile: the Qualcomm vendor twin set blind (unknown keys are ignored by
|
||||||
|
// other vendors' codecs), realtime priority, and the AOSP "unbounded" operating-rate
|
||||||
|
// sentinel — decode each frame at max clocks rather than pacing to the frame rate.
|
||||||
|
format.set_i32("vendor.qti-ext-dec-low-latency.enable", 1);
|
||||||
|
format.set_i32("priority", 0); // 0 = realtime
|
||||||
|
format.set_i32("operating-rate", i16::MAX as i32); // 32767 = "as fast as possible"
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// MediaTek's low-latency key — very common (mid/budget phones + many Google TV / Fire TV boxes).
|
// MediaTek's low-latency key — very common (mid/budget phones + many Google TV / Fire TV boxes).
|
||||||
@@ -600,7 +620,11 @@ fn run_async(
|
|||||||
mode.width,
|
mode.width,
|
||||||
mode.height
|
mode.height
|
||||||
);
|
);
|
||||||
if mode.refresh_hz > 0 && !try_set_frame_rate(&window, mode.refresh_hz as f32, is_tv) {
|
// The forced TV mode switch (`is_tv` ⇒ ALWAYS strategy) is part of the experimental stack;
|
||||||
|
// off, every form factor gets the original soft seamless hint.
|
||||||
|
if mode.refresh_hz > 0
|
||||||
|
&& !try_set_frame_rate(&window, mode.refresh_hz as f32, is_tv && low_latency_mode)
|
||||||
|
{
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"decode: set_frame_rate({} Hz) unavailable/declined (non-fatal)",
|
"decode: set_frame_rate({} Hz) unavailable/declined (non-fatal)",
|
||||||
mode.refresh_hz
|
mode.refresh_hz
|
||||||
@@ -716,8 +740,12 @@ fn run_async(
|
|||||||
if !hint_tried {
|
if !hint_tried {
|
||||||
hint_tried = true;
|
hint_tried = true;
|
||||||
let tids = client.hot_thread_ids();
|
let tids = client.hot_thread_ids();
|
||||||
boost_hot_threads(&tids);
|
// The pump/audio priority boost is part of the experimental low-latency stack; the
|
||||||
hint = crate::adpf::HintSession::create(frame_period_ns, &tids);
|
// ADPF session itself predates it and always runs (max-performance bias gated inside).
|
||||||
|
if low_latency_mode {
|
||||||
|
boost_hot_threads(&tids);
|
||||||
|
}
|
||||||
|
hint = crate::adpf::HintSession::create(frame_period_ns, &tids, low_latency_mode);
|
||||||
log::info!(
|
log::info!(
|
||||||
"decode: ADPF hint session {} — {} hot thread(s), target {frame_period_ns} ns",
|
"decode: ADPF hint session {} — {} hot thread(s), target {frame_period_ns} ns",
|
||||||
if hint.is_some() {
|
if hint.is_some() {
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeGenerateIde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `NativeBridge.nativeSetLowLatencyMode(enabled)` — apply the user's "Low-latency mode
|
||||||
|
/// (experimental)" toggle to the process-wide transport defaults, today just DSCP/QoS marking on
|
||||||
|
/// the media sockets. Must be called BEFORE `nativeConnect` (the tag is applied at socket
|
||||||
|
/// creation); Kotlin's one connect choke point (`HostConnect.connectToHost`) does. The rest of the
|
||||||
|
/// toggle rides explicit per-session parameters (`nativeStartVideo` / `nativeStartAudio`).
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetLowLatencyMode(
|
||||||
|
_env: JNIEnv,
|
||||||
|
_this: JObject,
|
||||||
|
enabled: jboolean,
|
||||||
|
) {
|
||||||
|
punktfunk_core::transport::set_dscp_default(enabled != 0);
|
||||||
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
||||||
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs, launch): Long`.
|
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs, launch): Long`.
|
||||||
/// `launch` (empty ⇒ none) is a store-qualified library id to boot straight into a game.
|
/// `launch` (empty ⇒ none) is a store-qualified library id to boot straight into a game.
|
||||||
|
|||||||
@@ -233,14 +233,17 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetVideoSta
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeStartAudio(handle)` — start the Opus→AAudio playback thread. No-op if already
|
/// `NativeBridge.nativeStartAudio(handle, lowLatencyMode)` — start the Opus→AAudio playback thread.
|
||||||
/// started or on a `0` handle. Best-effort: a failure leaves video streaming.
|
/// `lowLatencyMode` (the experimental toggle) tags the stream usage=Game for the HAL's game-audio
|
||||||
|
/// routing. No-op if already started or on a `0` handle. Best-effort: a failure leaves video
|
||||||
|
/// streaming.
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio(
|
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio(
|
||||||
_env: JNIEnv,
|
_env: JNIEnv,
|
||||||
_this: JObject,
|
_this: JObject,
|
||||||
handle: jlong,
|
handle: jlong,
|
||||||
|
low_latency_mode: jboolean,
|
||||||
) {
|
) {
|
||||||
if handle == 0 {
|
if handle == 0 {
|
||||||
return;
|
return;
|
||||||
@@ -251,7 +254,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio(
|
|||||||
if guard.is_some() {
|
if guard.is_some() {
|
||||||
return; // already playing
|
return; // already playing
|
||||||
}
|
}
|
||||||
match crate::audio::AudioPlayback::start(h.client.clone()) {
|
match crate::audio::AudioPlayback::start(h.client.clone(), low_latency_mode != 0) {
|
||||||
Some(p) => *guard = Some(p),
|
Some(p) => *guard = Some(p),
|
||||||
None => log::error!("nativeStartAudio: playback init failed (video unaffected)"),
|
None => log::error!("nativeStartAudio: playback init failed (video unaffected)"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ mod qos;
|
|||||||
mod udp;
|
mod udp;
|
||||||
|
|
||||||
pub use loopback::{loopback_pair, LoopbackTransport};
|
pub use loopback::{loopback_pair, LoopbackTransport};
|
||||||
pub use qos::{grow_socket_buffers, set_media_qos, MediaClass};
|
pub use qos::{grow_socket_buffers, set_dscp_default, set_media_qos, MediaClass};
|
||||||
/// Windows-only: reusable USO (UDP Send Offload) batch send for callers that own their own connected
|
/// Windows-only: reusable USO (UDP Send Offload) batch send for callers that own their own connected
|
||||||
/// socket (the GameStream video sender) rather than going through [`UdpTransport`].
|
/// socket (the GameStream video sender) rather than going through [`UdpTransport`].
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
//! [`set_media_qos`] DSCP-tags the latency-sensitive video/audio traffic (+ Linux `SO_PRIORITY`) so a
|
//! [`set_media_qos`] DSCP-tags the latency-sensitive video/audio traffic (+ Linux `SO_PRIORITY`) so a
|
||||||
//! QoS-aware path (Wi-Fi WMM access categories, a managed switch, a shaped uplink) can prioritize it
|
//! QoS-aware path (Wi-Fi WMM access categories, a managed switch, a shaped uplink) can prioritize it
|
||||||
//! over bulk flows. Mirrors what Apollo/Sunshine tag — DSCP **CS5** for video, **CS6** for audio. It
|
//! over bulk flows. Mirrors what Apollo/Sunshine tag — DSCP **CS5** for video, **CS6** for audio. It
|
||||||
//! is **opt-in** (`PUNKTFUNK_DSCP=1`): DSCP can interact badly with some consumer ISPs/routers, and on
|
//! is **opt-in** (`PUNKTFUNK_DSCP=1`, or [`set_dscp_default`] from an embedder — the Android client
|
||||||
|
//! ties it to its experimental low-latency mode): DSCP can interact badly with some consumer ISPs/routers, and on
|
||||||
//! Windows a plain `IP_TOS` is silently stripped unless a qWAVE policy is active (Apollo uses the
|
//! Windows a plain `IP_TOS` is silently stripped unless a qWAVE policy is active (Apollo uses the
|
||||||
//! qWAVE API there — that port is a follow-up; today this is a no-op on the wire on Windows).
|
//! qWAVE API there — that port is a follow-up; today this is a no-op on the wire on Windows).
|
||||||
|
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
/// Target kernel socket-buffer size (`SO_SNDBUF`/`SO_RCVBUF`). A high-resolution frame is a burst (a
|
/// Target kernel socket-buffer size (`SO_SNDBUF`/`SO_RCVBUF`). A high-resolution frame is a burst (a
|
||||||
/// 5120×1440 keyframe is ~130 packets the send thread hands to `sendmmsg` at once); the default UDP
|
/// 5120×1440 keyframe is ~130 packets the send thread hands to `sendmmsg` at once); the default UDP
|
||||||
@@ -66,17 +68,27 @@ impl MediaClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether DSCP/QoS marking is enabled. Default **on for Android**, **off elsewhere**: on Wi-Fi
|
/// Runtime default for DSCP marking when `PUNKTFUNK_DSCP` is unset (see [`set_dscp_default`]).
|
||||||
/// (where most Android clients live) access points commonly map DSCP to WMM access categories, so
|
/// Off unless an embedder opts in — on Wi-Fi, access points commonly map DSCP to WMM access
|
||||||
/// tagging the video/audio sockets can win real airtime priority against other traffic on the link;
|
/// categories (a real airtime-priority win), but wired paths rarely honour it and some bleach or
|
||||||
/// on the wired paths the other clients use it's rarely honoured and some paths bleach or reject
|
/// reject marked packets, so it never turns on by itself.
|
||||||
/// marked packets, so it stays opt-in there. `PUNKTFUNK_DSCP` overrides either way — `1`/`true`/`on`
|
static DSCP_DEFAULT: AtomicBool = AtomicBool::new(false);
|
||||||
/// forces it on, `0`/`false`/`off` forces it off (e.g. to rule QoS out while debugging a flaky AP).
|
|
||||||
|
/// Opt in to (or back out of) DSCP marking for sockets created from now on. Must be called BEFORE
|
||||||
|
/// connecting — the tag is applied at socket creation. The Android client ties this to its
|
||||||
|
/// experimental low-latency mode; `PUNKTFUNK_DSCP` still overrides in either direction.
|
||||||
|
pub fn set_dscp_default(enabled: bool) {
|
||||||
|
DSCP_DEFAULT.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether DSCP/QoS marking is enabled: `PUNKTFUNK_DSCP` when set (`1`/`true`/`on` forces it on,
|
||||||
|
/// `0`/`false`/`off` forces it off — e.g. to rule QoS out while debugging a flaky AP), else the
|
||||||
|
/// [`set_dscp_default`] runtime default.
|
||||||
pub(crate) fn dscp_enabled() -> bool {
|
pub(crate) fn dscp_enabled() -> bool {
|
||||||
match std::env::var("PUNKTFUNK_DSCP").as_deref() {
|
match std::env::var("PUNKTFUNK_DSCP").as_deref() {
|
||||||
Ok("1") | Ok("true") | Ok("on") => true,
|
Ok("1") | Ok("true") | Ok("on") => true,
|
||||||
Ok("0") | Ok("false") | Ok("off") => false,
|
Ok("0") | Ok("false") | Ok("off") => false,
|
||||||
_ => cfg!(target_os = "android"),
|
_ => DSCP_DEFAULT.load(Ordering::Relaxed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user