The host-audio switch reaches every client, not just the desktop ones #423
@@ -101,6 +101,9 @@ suspend fun connectToHost(
|
||||
// Tier-A pad audio: ask for the 0xD1 plane only when a setting would render it, so a
|
||||
// user with it off does not make the host provision endpoints it will never feed.
|
||||
settings.padHaptics || settings.padSpeaker,
|
||||
// "Keep host audio playing": the host taps its own default output rather than
|
||||
// silencing it for the session. Free to ask for — an older host just ignores it.
|
||||
settings.keepHostAudio,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ data class SettingsOverlay(
|
||||
val audioFormat: String? = null,
|
||||
val micEnabled: Boolean? = null,
|
||||
val echoCancel: Boolean? = null,
|
||||
val keepHostAudio: Boolean? = null,
|
||||
val touchMode: TouchMode? = null,
|
||||
val mouseMode: MouseMode? = null,
|
||||
val invertScroll: Boolean? = null,
|
||||
@@ -82,6 +83,7 @@ data class SettingsOverlay(
|
||||
audioFormat = audioFormat ?: base.audioFormat,
|
||||
micEnabled = micEnabled ?: base.micEnabled,
|
||||
echoCancel = echoCancel ?: base.echoCancel,
|
||||
keepHostAudio = keepHostAudio ?: base.keepHostAudio,
|
||||
touchMode = touchMode ?: base.touchMode,
|
||||
mouseMode = mouseMode ?: base.mouseMode,
|
||||
invertScroll = invertScroll ?: base.invertScroll,
|
||||
@@ -120,6 +122,8 @@ data class SettingsOverlay(
|
||||
audioFormat = if (after.audioFormat != before.audioFormat) after.audioFormat else audioFormat,
|
||||
micEnabled = if (after.micEnabled != before.micEnabled) after.micEnabled else micEnabled,
|
||||
echoCancel = if (after.echoCancel != before.echoCancel) after.echoCancel else echoCancel,
|
||||
keepHostAudio =
|
||||
if (after.keepHostAudio != before.keepHostAudio) after.keepHostAudio else keepHostAudio,
|
||||
touchMode = if (after.touchMode != before.touchMode) after.touchMode else touchMode,
|
||||
mouseMode = if (after.mouseMode != before.mouseMode) after.mouseMode else mouseMode,
|
||||
invertScroll = if (after.invertScroll != before.invertScroll) after.invertScroll else invertScroll,
|
||||
@@ -152,6 +156,7 @@ data class SettingsOverlay(
|
||||
"audio_format" -> copy(audioFormat = null)
|
||||
"mic_enabled" -> copy(micEnabled = null)
|
||||
"echo_cancel" -> copy(echoCancel = null)
|
||||
"keep_host_audio" -> copy(keepHostAudio = null)
|
||||
"touch_mode" -> copy(touchMode = null)
|
||||
"mouse_mode" -> copy(mouseMode = null)
|
||||
"invert_scroll" -> copy(invertScroll = null)
|
||||
@@ -179,6 +184,7 @@ data class SettingsOverlay(
|
||||
if (audioFormat != null) add("audio_format")
|
||||
if (micEnabled != null) add("mic_enabled")
|
||||
if (echoCancel != null) add("echo_cancel")
|
||||
if (keepHostAudio != null) add("keep_host_audio")
|
||||
if (touchMode != null) add("touch_mode")
|
||||
if (mouseMode != null) add("mouse_mode")
|
||||
if (invertScroll != null) add("invert_scroll")
|
||||
@@ -214,6 +220,7 @@ data class SettingsOverlay(
|
||||
audioFormat?.let { j.put("audio_format", it) }
|
||||
micEnabled?.let { j.put("mic_enabled", it) }
|
||||
echoCancel?.let { j.put("echo_cancel", it) }
|
||||
keepHostAudio?.let { j.put("keep_host_audio", it) }
|
||||
touchMode?.let { j.put("touch_mode", it.name) }
|
||||
mouseMode?.let { j.put("mouse_mode", it.storedName) }
|
||||
invertScroll?.let { j.put("invert_scroll", it) }
|
||||
@@ -236,6 +243,7 @@ data class SettingsOverlay(
|
||||
private val KNOWN = setOf(
|
||||
"width", "height", "refresh_hz", "bitrate_kbps", "render_scale", "codec",
|
||||
"hdr_enabled", "compositor", "audio_channels", "audio_format", "mic_enabled", "echo_cancel",
|
||||
"keep_host_audio",
|
||||
"touch_mode", "mouse_mode", "invert_scroll", "gamepad", "gamepad_forwarding",
|
||||
"system_buttons", "guide_gesture",
|
||||
"stats_verbosity",
|
||||
@@ -255,6 +263,7 @@ data class SettingsOverlay(
|
||||
audioFormat = j.optStringOrNull("audio_format"),
|
||||
micEnabled = j.optBooleanOrNull("mic_enabled"),
|
||||
echoCancel = j.optBooleanOrNull("echo_cancel"),
|
||||
keepHostAudio = j.optBooleanOrNull("keep_host_audio"),
|
||||
touchMode = j.optStringOrNull("touch_mode")
|
||||
?.let { n -> TouchMode.entries.firstOrNull { it.name == n } },
|
||||
mouseMode = j.optStringOrNull("mouse_mode")
|
||||
|
||||
@@ -91,6 +91,17 @@ data class Settings(
|
||||
* Only meaningful while [micEnabled] is on.
|
||||
*/
|
||||
val echoCancel: Boolean = true,
|
||||
/**
|
||||
* Ask the host to leave ITS OWN audio devices alone for this session
|
||||
* (`CLIENT_CAP_KEEP_HOST_AUDIO`): it captures whatever its default playback device already is,
|
||||
* so the speakers or headphones on the host PC keep playing while this device hears the same
|
||||
* audio. Off — the default, and what every build before this did — has the host park playback
|
||||
* on a silent endpoint, which is why the host goes quiet the moment a stream starts.
|
||||
*
|
||||
* REQUEST-only: there is no host-cap echo, so an older host ignores the ask and re-routes as it
|
||||
* always did ("audio still works, the host went quiet"), never a broken session.
|
||||
*/
|
||||
val keepHostAudio: Boolean = false,
|
||||
/**
|
||||
* How much the in-stream stats overlay shows — see [StatsVerbosity]. Defaults to
|
||||
* [StatsVerbosity.NORMAL] (the res/fps line + latency headline + reliability counters); the full
|
||||
@@ -330,6 +341,7 @@ class SettingsStore(context: Context) {
|
||||
codec = prefs.getString(K_CODEC, "auto") ?: "auto",
|
||||
micEnabled = prefs.getBoolean(K_MIC, false),
|
||||
echoCancel = prefs.getBoolean(K_ECHO_CANCEL, true),
|
||||
keepHostAudio = prefs.getBoolean(K_KEEP_HOST_AUDIO, false),
|
||||
statsVerbosity = prefs.getString(K_STATS_VERBOSITY, null)
|
||||
?.let { name -> StatsVerbosity.entries.firstOrNull { it.name == name } }
|
||||
// Migration from the pre-tier Boolean "stats_hud_enabled": an explicit OFF stays off;
|
||||
@@ -387,6 +399,7 @@ class SettingsStore(context: Context) {
|
||||
.putString(K_CODEC, s.codec)
|
||||
.putBoolean(K_MIC, s.micEnabled)
|
||||
.putBoolean(K_ECHO_CANCEL, s.echoCancel)
|
||||
.putBoolean(K_KEEP_HOST_AUDIO, s.keepHostAudio)
|
||||
.putString(K_STATS_VERBOSITY, s.statsVerbosity.name)
|
||||
.putString(K_TOUCH_MODE, s.touchMode.name)
|
||||
.putBoolean(K_GAMEPAD_UI, s.gamepadUiEnabled)
|
||||
@@ -426,6 +439,7 @@ class SettingsStore(context: Context) {
|
||||
const val K_CODEC = "codec"
|
||||
const val K_MIC = "mic_enabled"
|
||||
const val K_ECHO_CANCEL = "echo_cancel"
|
||||
const val K_KEEP_HOST_AUDIO = "keep_host_audio"
|
||||
const val K_STATS_VERBOSITY = "stats_verbosity"
|
||||
|
||||
/** Pre-tier Boolean the [K_STATS_VERBOSITY] enum replaced — read once for migration, never
|
||||
|
||||
@@ -844,6 +844,13 @@ private fun AudioSettings(s: Settings, update: (Settings) -> Unit, onMicChange:
|
||||
"otherwise the session stays on Opus, which is already effectively " +
|
||||
"transparent. The overlay shows what a session actually got.",
|
||||
) { f -> update(s.copy(audioFormat = f)) }
|
||||
ToggleRow(
|
||||
title = "Keep host audio playing",
|
||||
subtitle = "The host's speakers or headphones keep playing while you stream",
|
||||
checked = s.keepHostAudio,
|
||||
field = "keep_host_audio",
|
||||
onCheckedChange = { on -> update(s.copy(keepHostAudio = on)) },
|
||||
)
|
||||
ToggleRow(
|
||||
title = "Microphone",
|
||||
subtitle = "Feeds this device's microphone to the host",
|
||||
|
||||
@@ -304,6 +304,7 @@ internal object ConsoleJson {
|
||||
j.put("mouse_mode", s.mouseMode.storedName)
|
||||
j.put("mic_enabled", s.micEnabled)
|
||||
j.put("echo_cancel", s.echoCancel)
|
||||
j.put("keep_host_audio", s.keepHostAudio)
|
||||
j.put("audio_channels", s.audioChannels)
|
||||
j.put("audio_format", s.audioFormat)
|
||||
j.put("codec", s.codec)
|
||||
@@ -361,6 +362,7 @@ internal object ConsoleJson {
|
||||
?: s.mouseMode,
|
||||
micEnabled = j.optBoolean("mic_enabled", s.micEnabled),
|
||||
echoCancel = j.optBoolean("echo_cancel", s.echoCancel),
|
||||
keepHostAudio = j.optBoolean("keep_host_audio", s.keepHostAudio),
|
||||
audioChannels = j.optInt("audio_channels", s.audioChannels),
|
||||
audioFormat = str("audio_format", s.audioFormat),
|
||||
codec = str("codec", s.codec),
|
||||
|
||||
@@ -95,6 +95,11 @@ object NativeBridge {
|
||||
* DualSense plane. Without it the host never sets `HOST_CAP_PAD_AUDIO` and emits nothing,
|
||||
* so a captured pad's own render capabilities would have nothing to gate. */
|
||||
padAudioOk: Boolean,
|
||||
/** Advertise `CLIENT_CAP_KEEP_HOST_AUDIO` — ask the host to tap its default playback
|
||||
* device instead of parking it on a silent endpoint, so the host PC's own speakers keep
|
||||
* playing. REQUEST-only (no host-cap echo): an older host ignores it and goes quiet
|
||||
* exactly as it always did. */
|
||||
keepHostAudio: Boolean,
|
||||
): Long
|
||||
|
||||
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
||||
|
||||
@@ -275,6 +275,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
||||
launch: JString<'local>,
|
||||
device_name: JString<'local>,
|
||||
pad_audio_ok: jboolean,
|
||||
keep_host_audio: jboolean,
|
||||
) -> jlong {
|
||||
// Every JNI string this method needs, read up front in the one `Env` scope jni 0.22 grants a
|
||||
// native method; everything below is pure Rust over owned `String`s. `None` = the mandatory
|
||||
@@ -438,6 +439,16 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
||||
punktfunk_core::quic::CLIENT_CAP_PAD_AUDIO
|
||||
} else {
|
||||
0
|
||||
}
|
||||
// The user's "Keep host audio playing" setting: the host taps whatever its default
|
||||
// playback device already is instead of parking the desktop mix on a silent
|
||||
// endpoint, so the speakers on the host PC stay live. REQUEST-only — there is no
|
||||
// host-cap echo — so an older host ignores the bit and re-routes exactly as it
|
||||
// always did ("the host went quiet"), never a broken session.
|
||||
| if keep_host_audio {
|
||||
punktfunk_core::quic::CLIENT_CAP_KEEP_HOST_AUDIO
|
||||
} else {
|
||||
0
|
||||
},
|
||||
// Slice-progressive delivery, by decoder truth (Kotlin probes FEATURE_PartialFrame on
|
||||
// every decoder this device would use; `debug.punktfunk.force_parts` overrides for the
|
||||
|
||||
@@ -511,14 +511,20 @@ final class SessionModel: ObservableObject {
|
||||
// the pointer and forwards shape/state, which StreamView draws as the real
|
||||
// NSCursor. Capture-mode sessions keep today's composited pointer.
|
||||
#if os(macOS)
|
||||
let clientCaps: UInt8 =
|
||||
let presentCaps: UInt8 =
|
||||
(MouseInputMode(rawValue: effective.mouseMode) ?? .capture) == .desktop ? 0x01 : 0
|
||||
#else
|
||||
// iOS/tvOS run the stage-4 deadline presenter, whose link thread feeds
|
||||
// reportPhase — advertise the vsync-aware presenter (0x02, CLIENT_CAP_PHASE_LOCK).
|
||||
// macOS stays without it: the stage-2 arrival presenter has no latch grid.
|
||||
let clientCaps: UInt8 = 0x02
|
||||
let presentCaps: UInt8 = 0x02
|
||||
#endif
|
||||
// "Keep host audio playing": the host taps its default playback device instead of
|
||||
// parking it on a silent endpoint, so the speakers on the host PC stay live. Pure
|
||||
// REQUEST — no host-cap echo — so an older host simply goes quiet as it always did.
|
||||
let clientCaps =
|
||||
presentCaps
|
||||
| (effective.keepHostAudio ? PunktfunkConnection.clientCapKeepHostAudio : 0)
|
||||
let result = Result { try PunktfunkConnection(
|
||||
host: host.address, port: host.port,
|
||||
width: width, height: height, refreshHz: hz,
|
||||
|
||||
@@ -106,6 +106,10 @@ enum SettingsFields {
|
||||
.init(name: "echo_cancel", key: DefaultsKey.echoCancel,
|
||||
overlay: \.echoCancel, effective: \.echoCancel)
|
||||
}
|
||||
static var keepHostAudio: SettingsField<Bool> {
|
||||
.init(name: "keep_host_audio", key: DefaultsKey.keepHostAudio,
|
||||
overlay: \.keepHostAudio, effective: \.keepHostAudio)
|
||||
}
|
||||
static var touchMode: SettingsField<String> {
|
||||
.init(name: "touch_mode", key: DefaultsKey.touchMode,
|
||||
overlay: \.touchMode, effective: \.touchMode)
|
||||
@@ -201,6 +205,7 @@ extension SettingsView {
|
||||
base.audioFormat = audioFormat
|
||||
base.micEnabled = micEnabled
|
||||
base.echoCancel = echoCancel
|
||||
base.keepHostAudio = keepHostAudio
|
||||
base.gamepadType = gamepadType
|
||||
base.gamepadForwarding = gamepadForwarding
|
||||
base.statsVerbosity = statsVerbosityRaw
|
||||
|
||||
@@ -627,6 +627,11 @@ extension SettingsView {
|
||||
}
|
||||
}
|
||||
}
|
||||
described("The host's speakers or headphones keep playing while you stream — "
|
||||
+ "needs a host on 0.32+",
|
||||
field: "keep_host_audio") {
|
||||
Toggle("Keep host audio playing", isOn: scoped(SettingsFields.keepHostAudio))
|
||||
}
|
||||
#if os(macOS)
|
||||
// Which speaker THIS Mac plays through is this device's audio routing (tier G).
|
||||
if !inProfileScope {
|
||||
|
||||
@@ -71,6 +71,7 @@ struct SettingsView: View {
|
||||
@AppStorage(DefaultsKey.fullscreenWhileStreaming) var fullscreenWhileStreaming = true
|
||||
@AppStorage(DefaultsKey.micEnabled) var micEnabled = true
|
||||
@AppStorage(DefaultsKey.echoCancel) var echoCancel = true
|
||||
@AppStorage(DefaultsKey.keepHostAudio) var keepHostAudio = false
|
||||
@AppStorage(DefaultsKey.audioChannels) var audioChannels = 2
|
||||
@AppStorage(DefaultsKey.audioFormat) var audioFormat = AudioFormatChoice.opus.rawValue
|
||||
@AppStorage(DefaultsKey.codec) var codec = "auto"
|
||||
|
||||
@@ -1446,6 +1446,13 @@ public final class PunktfunkConnection {
|
||||
/// auto-selected. Decoded by the Metal wavelet decoder, not VideoToolbox.
|
||||
public static let codecPyroWave: UInt8 = UInt8(PUNKTFUNK_CODEC_PYROWAVE)
|
||||
|
||||
/// `clientCaps` bit: ask the host to leave ITS OWN audio devices alone for this session —
|
||||
/// it taps whatever its default playback device already is instead of parking the desktop
|
||||
/// mix on a silent endpoint, so the host PC's speakers keep playing and this device hears
|
||||
/// the same audio. REQUEST-only, no host-cap echo: an older host ignores it and goes quiet
|
||||
/// exactly as it always did, so it is safe to set unconditionally from the user's setting.
|
||||
public static let clientCapKeepHostAudio: UInt8 = UInt8(PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO)
|
||||
|
||||
/// The `codec` SETTING (a `DefaultsKey.codec` / profile-overlay string) as a soft-preference
|
||||
/// byte; `0` = Automatic, i.e. the host decides. Lives here beside the bits so the settings
|
||||
/// string is mapped to the wire in exactly one place — a session and a speed test that
|
||||
|
||||
@@ -89,6 +89,13 @@ public enum DefaultsKey {
|
||||
/// speaker/mic or mic channel also bypasses it (the voice processor only follows the
|
||||
/// system default devices) — see SessionAudio's topology note.
|
||||
public static let echoCancel = "punktfunk.echoCancel"
|
||||
/// Ask the host to leave ITS OWN audio devices alone for this session
|
||||
/// (`PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO`): it captures whatever its default playback
|
||||
/// device already is, so the speakers/headphones on the host PC keep playing while this
|
||||
/// device hears the same audio. Off (the default) is today's behaviour — the host parks
|
||||
/// playback on a silent endpoint and goes quiet for the session. Best-effort: an older
|
||||
/// host ignores the ask and re-routes as it always did.
|
||||
public static let keepHostAudio = "punktfunk.keepHostAudio"
|
||||
public static let speakerUID = "punktfunk.speakerUID"
|
||||
public static let micUID = "punktfunk.micUID"
|
||||
/// macOS: which input channel of the chosen mic device feeds the host. 0 = "Auto" (sum every
|
||||
|
||||
@@ -33,6 +33,7 @@ public struct EffectiveSettings: Equatable, Sendable {
|
||||
public var audioFormat = AudioFormatChoice.opus.rawValue
|
||||
public var micEnabled = true
|
||||
public var echoCancel = true
|
||||
public var keepHostAudio = false
|
||||
public var touchMode = "trackpad"
|
||||
public var mouseMode = "capture"
|
||||
public var invertScroll = false
|
||||
@@ -101,6 +102,7 @@ public struct EffectiveSettings: Equatable, Sendable {
|
||||
audioFormat = str(DefaultsKey.audioFormat, audioFormat)
|
||||
micEnabled = bool(DefaultsKey.micEnabled, micEnabled)
|
||||
echoCancel = bool(DefaultsKey.echoCancel, echoCancel)
|
||||
keepHostAudio = bool(DefaultsKey.keepHostAudio, keepHostAudio)
|
||||
touchMode = str(DefaultsKey.touchMode, touchMode)
|
||||
mouseMode = str(DefaultsKey.mouseMode, mouseMode)
|
||||
invertScroll = bool(DefaultsKey.invertScroll, invertScroll)
|
||||
@@ -183,6 +185,7 @@ public struct EffectiveSettings: Equatable, Sendable {
|
||||
if let v = overlay.audioFormat { s.audioFormat = v }
|
||||
if let v = overlay.micEnabled { s.micEnabled = v }
|
||||
if let v = overlay.echoCancel { s.echoCancel = v }
|
||||
if let v = overlay.keepHostAudio { s.keepHostAudio = v }
|
||||
if let v = overlay.touchMode { s.touchMode = v }
|
||||
if let v = overlay.mouseMode { s.mouseMode = v }
|
||||
if let v = overlay.invertScroll { s.invertScroll = v }
|
||||
|
||||
@@ -115,6 +115,7 @@ public struct SettingsOverlay: Codable, Equatable, Sendable {
|
||||
public var audioFormat: String?
|
||||
public var micEnabled: Bool?
|
||||
public var echoCancel: Bool?
|
||||
public var keepHostAudio: Bool?
|
||||
public var touchMode: String?
|
||||
public var mouseMode: String?
|
||||
public var invertScroll: Bool?
|
||||
@@ -161,6 +162,7 @@ public struct SettingsOverlay: Codable, Equatable, Sendable {
|
||||
case audioFormat = "audio_format"
|
||||
case micEnabled = "mic_enabled"
|
||||
case echoCancel = "echo_cancel"
|
||||
case keepHostAudio = "keep_host_audio"
|
||||
case touchMode = "touch_mode"
|
||||
case mouseMode = "mouse_mode"
|
||||
case invertScroll = "invert_scroll"
|
||||
@@ -199,6 +201,7 @@ public struct SettingsOverlay: Codable, Equatable, Sendable {
|
||||
audioFormat = str(.audioFormat)
|
||||
micEnabled = bool(.micEnabled)
|
||||
echoCancel = bool(.echoCancel)
|
||||
keepHostAudio = bool(.keepHostAudio)
|
||||
touchMode = str(.touchMode)
|
||||
mouseMode = str(.mouseMode)
|
||||
invertScroll = bool(.invertScroll)
|
||||
@@ -239,6 +242,7 @@ public struct SettingsOverlay: Codable, Equatable, Sendable {
|
||||
try c.encodeIfPresent(audioFormat, forKey: AnyKey(Key.audioFormat.rawValue))
|
||||
try c.encodeIfPresent(micEnabled, forKey: AnyKey(Key.micEnabled.rawValue))
|
||||
try c.encodeIfPresent(echoCancel, forKey: AnyKey(Key.echoCancel.rawValue))
|
||||
try c.encodeIfPresent(keepHostAudio, forKey: AnyKey(Key.keepHostAudio.rawValue))
|
||||
try c.encodeIfPresent(touchMode, forKey: AnyKey(Key.touchMode.rawValue))
|
||||
try c.encodeIfPresent(mouseMode, forKey: AnyKey(Key.mouseMode.rawValue))
|
||||
try c.encodeIfPresent(invertScroll, forKey: AnyKey(Key.invertScroll.rawValue))
|
||||
@@ -297,6 +301,7 @@ public enum OverlayField {
|
||||
case "audio_format": overlay.audioFormat = nil
|
||||
case "mic_enabled": overlay.micEnabled = nil
|
||||
case "echo_cancel": overlay.echoCancel = nil
|
||||
case "keep_host_audio": overlay.keepHostAudio = nil
|
||||
case "touch_mode": overlay.touchMode = nil
|
||||
case "mouse_mode": overlay.mouseMode = nil
|
||||
case "invert_scroll": overlay.invertScroll = nil
|
||||
@@ -337,6 +342,7 @@ public enum OverlayField {
|
||||
case "audio_format": return o.audioFormat != nil
|
||||
case "mic_enabled": return o.micEnabled != nil
|
||||
case "echo_cancel": return o.echoCancel != nil
|
||||
case "keep_host_audio": return o.keepHostAudio != nil
|
||||
case "touch_mode": return o.touchMode != nil
|
||||
case "mouse_mode": return o.mouseMode != nil
|
||||
case "invert_scroll": return o.invertScroll != nil
|
||||
|
||||
@@ -55,7 +55,7 @@ enum RowId {
|
||||
/// see the `enabled` note in [`row_spec`].
|
||||
AudioFormat,
|
||||
/// The per-session `CLIENT_CAP_KEEP_HOST_AUDIO` ask — the host keeps playing on its own
|
||||
/// output while it streams. Desktop-only until the Android session advertises the bit.
|
||||
/// output while it streams. Every platform: the Android session advertises the bit too.
|
||||
KeepHostAudio,
|
||||
Mic,
|
||||
EchoCancel,
|
||||
@@ -990,9 +990,6 @@ fn row_on(id: RowId, platform: crate::platform::Platform) -> bool {
|
||||
| RowId::AllowVrr
|
||||
| RowId::Fullscreen
|
||||
| RowId::Shortcuts
|
||||
// Desktop-only until the Android session advertises CLIENT_CAP_KEEP_HOST_AUDIO —
|
||||
// a row whose bit never goes out would be a dead toggle.
|
||||
| RowId::KeepHostAudio
|
||||
);
|
||||
match platform {
|
||||
Platform::Desktop => !android_only,
|
||||
@@ -2665,7 +2662,6 @@ pub(super) mod tests {
|
||||
RowId::TenBitSdr,
|
||||
RowId::Vsync,
|
||||
RowId::AllowVrr,
|
||||
RowId::KeepHostAudio,
|
||||
RowId::Shortcuts,
|
||||
RowId::Fullscreen,
|
||||
]
|
||||
|
||||
@@ -98,6 +98,7 @@ include = ["PunktfunkEndReason"]
|
||||
# a benign redefinition in C. One name for one bit is the point — an embedder that reaches for
|
||||
# either spelling gets the same constant.
|
||||
"CLIENT_CAP_AUDIO_HIRES" = "PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES"
|
||||
"CLIENT_CAP_KEEP_HOST_AUDIO" = "PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO"
|
||||
"HOST_CAP_AUDIO_HIRES" = "PUNKTFUNK_HOST_CAP_AUDIO_HIRES"
|
||||
|
||||
# R21: every remaining exported constant, prefixed. cbindgen emits a bare `#define` per
|
||||
|
||||
@@ -1743,6 +1743,7 @@ const _: () = {
|
||||
assert!(PUNKTFUNK_HOST_CAP_AUDIO_HIRES == crate::quic::HOST_CAP_AUDIO_HIRES);
|
||||
assert!(PUNKTFUNK_CLIENT_CAP_PAD_AUDIO == crate::quic::CLIENT_CAP_PAD_AUDIO);
|
||||
assert!(PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES == crate::quic::CLIENT_CAP_AUDIO_HIRES);
|
||||
assert!(PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO == crate::quic::CLIENT_CAP_KEEP_HOST_AUDIO);
|
||||
assert!(PUNKTFUNK_PAD_AUDIO_KIND_HAPTICS == crate::quic::PAD_AUDIO_KIND_HAPTICS);
|
||||
assert!(PUNKTFUNK_PAD_AUDIO_KIND_SPEAKER == crate::quic::PAD_AUDIO_KIND_SPEAKER);
|
||||
// The setter's caps bits are the arrival flags bits 8/9 shifted down (the wire packing
|
||||
@@ -2567,6 +2568,17 @@ pub const PUNKTFUNK_CLIENT_CAP_PAD_AUDIO: u8 = 0x08;
|
||||
/// 24-bit is where the plane earns its bandwidth. (Mirrors `quic::CLIENT_CAP_AUDIO_HIRES`.)
|
||||
pub const PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES: u8 = 0x10;
|
||||
|
||||
/// [`punktfunk_connect_ex9`] `client_caps` bit: ask the host to leave its OWN audio devices
|
||||
/// alone for this session — capture whatever the operator's default playback device already
|
||||
/// is, instead of parking the desktop mix on a silent endpoint. The host keeps playing (the
|
||||
/// headphones plugged into the host PC stay live) and this client hears the same audio:
|
||||
/// Moonlight's "Mute host PC speakers" box, unchecked, per session.
|
||||
///
|
||||
/// REQUEST-only — there is no host-cap echo. An older host ignores the bit and re-routes as it
|
||||
/// always did, which degrades to "audio still works, the host went quiet", so an embedder may
|
||||
/// set it unconditionally from its user's setting. (Mirrors `quic::CLIENT_CAP_KEEP_HOST_AUDIO`.)
|
||||
pub const PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO: u8 = 0x20;
|
||||
|
||||
/// A [`punktfunk_connect_ex10`] device name cut to what a [`crate::quic::Hello`] carries.
|
||||
/// [`crate::quic::HELLO_NAME_MAX`] is a BYTE cap while the cut must land on a character
|
||||
/// boundary — "Wohnzimmer-Fernseher überm Sofa" is 33 characters and 34 bytes, and slicing a
|
||||
|
||||
@@ -129,8 +129,7 @@ headphones plugged into the host keep playing, and both ends hear the same audio
|
||||
host's headphones live while the TV profile mutes them. Best-effort: it needs a host on 0.32 or
|
||||
newer, and with several clients streaming at once, any one asking wins for all of them. The
|
||||
host-wide equivalent is
|
||||
[`PUNKTFUNK_AUDIO_OUTPUT_MODE=follow_default`](/docs/configuration). Linux, Windows and the
|
||||
desktop console.
|
||||
[`PUNKTFUNK_AUDIO_OUTPUT_MODE=follow_default`](/docs/configuration). Offered everywhere.
|
||||
|
||||
**Microphone** — *default: off on Linux, Windows, Android and the console home; on in the Apple
|
||||
app.* Sends this device's microphone to the host's virtual mic. Spelled *Stream microphone* on
|
||||
|
||||
@@ -482,6 +482,17 @@
|
||||
// 24-bit is where the plane earns its bandwidth. (Mirrors `quic::CLIENT_CAP_AUDIO_HIRES`.)
|
||||
#define PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES 16
|
||||
|
||||
// [`punktfunk_connect_ex9`] `client_caps` bit: ask the host to leave its OWN audio devices
|
||||
// alone for this session — capture whatever the operator's default playback device already
|
||||
// is, instead of parking the desktop mix on a silent endpoint. The host keeps playing (the
|
||||
// headphones plugged into the host PC stay live) and this client hears the same audio:
|
||||
// Moonlight's "Mute host PC speakers" box, unchecked, per session.
|
||||
//
|
||||
// REQUEST-only — there is no host-cap echo. An older host ignores the bit and re-routes as it
|
||||
// always did, which degrades to "audio still works, the host went quiet", so an embedder may
|
||||
// set it unconditionally from its user's setting. (Mirrors `quic::CLIENT_CAP_KEEP_HOST_AUDIO`.)
|
||||
#define PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO 32
|
||||
|
||||
// `*ttl_ms` sentinel written by [`punktfunk_connection_next_rumble2`] for a legacy (v1) rumble
|
||||
// datagram — an old host that sent no self-termination lease. The client then falls back to its
|
||||
// own staleness heuristic for that update instead of a host-supplied deadline.
|
||||
@@ -1100,7 +1111,7 @@
|
||||
// asked wins for all of them until it ends. Composes with the host-wide
|
||||
// `PUNKTFUNK_AUDIO_OUTPUT_MODE=follow_default`, which is this behaviour for every session.
|
||||
// `0x20` — `0x10` is [`CLIENT_CAP_AUDIO_HIRES`]; `0x40`/`0x80` remain free.
|
||||
#define CLIENT_CAP_KEEP_HOST_AUDIO 32
|
||||
#define PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO 32
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
|
||||
@@ -61,6 +61,7 @@ PUNKTFUNK_CLIENT_444
|
||||
PUNKTFUNK_CLIENT_CAP_AUDIO_HIRES
|
||||
PUNKTFUNK_CLIENT_CAP_AUDIO_RED
|
||||
PUNKTFUNK_CLIENT_CAP_CURSOR
|
||||
PUNKTFUNK_CLIENT_CAP_KEEP_HOST_AUDIO
|
||||
PUNKTFUNK_CLIENT_CAP_PHASE_LOCK
|
||||
PUNKTFUNK_CLIENT_CHACHA20
|
||||
PUNKTFUNK_CLIENT_PEAK_NITS
|
||||
|
||||
Reference in New Issue
Block a user