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 805d8227..9e96414a 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 @@ -49,7 +49,7 @@ suspend fun connectToHost( host, port, w, h, hz, identity.certPem, identity.privateKeyPem, pinHex, settings.bitrateKbps, settings.compositor, gamepadPref, - hdrEnabled, settings.audioChannels, + hdrEnabled, VideoDecoders.multiSliceTolerant(), 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, diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt index d1394b0f..5a3c815b 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt @@ -47,6 +47,10 @@ object NativeBridge { compositorPref: Int, gamepadPref: Int, hdrEnabled: Boolean, + /** Every decoder this device would use tolerates multi-slice AUs + * ([VideoDecoders.multiSliceTolerant]) — advertises `VIDEO_CAP_MULTI_SLICE`; false keeps + * the host at single-slice frames (the safe pre-0.17 wire shape). */ + multiSliceOk: Boolean, audioChannels: Int, /** `quic::CODEC_*` bitfield of codecs this device decodes ([VideoDecoders.decodableCodecBits]); * `0` falls back to H.264|HEVC. The host resolves the emitted codec from this ∩ its GPU. */ diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/VideoDecoders.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/VideoDecoders.kt index bbab8e67..b2dfcce2 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/VideoDecoders.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/VideoDecoders.kt @@ -53,6 +53,28 @@ object VideoDecoders { fun decodableCodecBits(): Int = 1 or 2 or (if (pickDecoder("video/av01") != null) 4 else 0) + /** + * Whether EVERY decoder this device would use tolerates multi-slice access units — the + * `VIDEO_CAP_MULTI_SLICE` advertisement (decoder truth, per the 0.17.0 field regression: + * Amlogic HEVC decoders wedge the whole DEVICE on multi-slice frames — Chromecast with + * Google TV, onn 4K — which is why Moonlight requests single-slice for every hardware + * decoder). We advertise per-family instead: tolerant unless the pick for ANY advertised + * codec is an Amlogic decoder or can't be inspected (a null pick = the platform default — + * uninspectable, so conservatively single-slice). Probed once at connect time; the host + * defaults to >1 slice only toward clients that set the bit. + */ + fun multiSliceTolerant(): Boolean { + val mimes = buildList { + add("video/avc") + add("video/hevc") + if (decodableCodecBits() and 4 != 0) add("video/av01") + } + return mimes.all { mime -> + val name = pickDecoder(mime)?.name?.lowercase() ?: return@all false + !name.startsWith("omx.amlogic") && !name.startsWith("c2.amlogic") + } + } + fun pickDecoder(mime: String): DecoderChoice? { if (mime.isEmpty()) return null val infos = runCatching { MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos } diff --git a/clients/android/native/src/session/connect.rs b/clients/android/native/src/session/connect.rs index 6d2e60e6..839e0835 100644 --- a/clients/android/native/src/session/connect.rs +++ b/clients/android/native/src/session/connect.rs @@ -115,6 +115,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo compositor_pref: jint, gamepad_pref: jint, hdr_enabled: jboolean, + multi_slice_ok: jboolean, audio_channels: jint, video_codecs: jint, preferred_codec: jint, @@ -182,11 +183,19 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo // sends a proper 8-bit BT.709 stream rather than PQ the panel would mis-tone-map. AMediaCodec // decodes Main10 from the SPS and the decode loop signals the Surface HDR dataspace + static // metadata (see crate::decode). - if hdr_enabled != 0 { + // 10-bit/HDR by panel truth (above) + multi-slice by DECODER truth: Kotlin probes every + // decoder this device would use (`VideoDecoders.multiSliceTolerant` — Amlogic wedges the + // whole device on multi-slice AUs, the 0.17.0 field regression) and only then may the + // host default to >1 slice per frame (its sub-frame readback / the P2 slice pipeline). + (if hdr_enabled != 0 { punktfunk_core::quic::VIDEO_CAP_10BIT | punktfunk_core::quic::VIDEO_CAP_HDR } else { 0 - }, + }) | (if multi_slice_ok != 0 { + punktfunk_core::quic::VIDEO_CAP_MULTI_SLICE + } else { + 0 + }), // Requested surround layout (2 = stereo / 6 = 5.1 / 8 = 7.1). The host clamps to what it can // capture and echoes the resolved count in `connector.audio_channels`, which drives the // decoder + AAudio layout (read in `crate::audio::AudioPlayback::start`). Anything else