diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt index 37ed8a26..100a8e27 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt @@ -47,15 +47,27 @@ suspend fun connectToHost( // sockets) — must be applied before connect, since sockets are tagged at creation. NativeBridge.nativeSetLowLatencyMode(settings.lowLatencyMode) val multiSlice = VideoDecoders.multiSliceTolerant() + val partialFrame = VideoDecoders.partialFrameCapable() // Slice-progressive delivery: decoder truth AND the async decode loop — the legacy // sync loop feeds whole AUs only, so parts must never arrive when it is selected. - val frameParts = settings.lowLatencyMode && VideoDecoders.partialFrameCapable() + val frameParts = settings.lowLatencyMode && partialFrame + val codecBits = VideoDecoders.decodableCodecBits() + // Automatic codec (P5, measured NP3 ↔ RTX 4090): AV1 beat HEVC by ~1.2 ms end-to-end at + // identical conditions, so under "Automatic" this device prefers AV1 when it hardware- + // decodes it (the AV1 bit is only ever set for a real, non-blocked hardware decoder) AND + // it lacks FEATURE_PartialFrame — a partial-frame device keeps HEVC, whose slice overlap + // AV1 cannot ride (AV1 has no slices; the host's chunked poll never arms). The host + // honors the preference only inside the probed shared codec set, so an AV1-less encoder + // still resolves HEVC. An explicit user choice always wins unchanged. + val preferredCodec = settings.preferredCodec().takeIf { it != 0 } + ?: if (codecBits and 4 != 0 && !partialFrame) 4 else 0 // The connect-time capability readout (`adb logcat -s pf.caps`): the P2 slice pipeline // is client-inert unless BOTH probes pass — this line says which decoder failed one. Log.i( "pf.caps", VideoDecoders.capsReport() + - " → multiSlice=$multiSlice parts=$frameParts (lowLatency=${settings.lowLatencyMode})", + " → multiSlice=$multiSlice parts=$frameParts prefer=$preferredCodec" + + " (lowLatency=${settings.lowLatencyMode})", ) NativeBridge.nativeConnect( host, port, w, h, hz, @@ -65,8 +77,9 @@ suspend fun connectToHost( frameParts, settings.audioChannels, // What this device can decode (H.264|HEVC always, AV1 when a real decoder exists) + - // the user's soft codec preference — the host resolves the emitted codec from both. - VideoDecoders.decodableCodecBits(), settings.preferredCodec(), timeoutMs, + // the soft codec preference (user choice, or the Automatic AV1 rule above) — the + // host resolves the emitted codec from both. + codecBits, preferredCodec, timeoutMs, launch, // The host's approval-list / trust-store label for this device — the same // Build.MODEL convention the pairing dialogs use for nativePair. diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt index 20ae8660..7ae7c11c 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt @@ -673,12 +673,22 @@ private fun DisplaySettings(s: Settings, update: (Settings) -> Unit, context: an // Only codecs this device can actually decode are offered — a preference the client never // advertises would be a dead setting (see [codecOptionsFor]). val av1Capable = remember { VideoDecoders.pickDecoder("video/av01") != null } + // Mirror the Automatic AV1 rule in HostConnect (hardware AV1 AND no partial-frame + // support) so the picker says what "Automatic" actually does on THIS device. + val autoPrefersAv1 = remember { + VideoDecoders.decodableCodecBits() and 4 != 0 && !VideoDecoders.partialFrameCapable() + } SettingDropdown( label = "Video codec", options = codecOptionsFor(s.codec, av1Capable), selected = s.codec, field = "codec", - caption = "A preference — the host falls back if it can't encode this one.", + caption = if (autoPrefersAv1) { + "A preference — the host falls back if it can't encode this one. " + + "Automatic prefers AV1 on this device." + } else { + "A preference — the host falls back if it can't encode this one." + }, ) { c -> update(s.copy(codec = c)) } // HDR is only meaningful on a panel that can present HDR10; on an SDR display the toggle is