//! Codec creation, low-latency config, thread/frame-rate tuning, HDR static-info encode. use ndk::media::media_codec::MediaCodec; use ndk::media::media_format::MediaFormat; use ndk::native_window::NativeWindow; use std::ffi::c_void; /// The MediaCodec MIME for the codec the host resolved (`Welcome.codec`). Shared by the decode /// thread and `nativeVideoMime` (which tells Kotlin what to rank decoders for). AV1 uses the /// AOSP `video/av01` type; anything not H.264/AV1 is treated as HEVC (every pre-negotiation host /// emitted HEVC). pub(crate) fn codec_mime(codec: u8) -> &'static str { match codec { punktfunk_core::quic::CODEC_H264 => "video/avc", punktfunk_core::quic::CODEC_AV1 => "video/av01", _ => "video/hevc", } } /// A short human label for the codec the host resolved, for the stats HUD's video-feed line /// (`"H.264"` / `"HEVC"` / `"AV1"` / `"PyroWave"`). Mirrors [`codec_mime`]'s fallback: anything /// not H.264/AV1/PyroWave is reported as HEVC (every pre-negotiation host emitted HEVC). Kept /// beside [`codec_mime`] because the MIME collapses PyroWave onto `video/hevc` and so can't name it. pub(crate) fn codec_label(codec: u8) -> &'static str { match codec { punktfunk_core::quic::CODEC_H264 => "H.264", punktfunk_core::quic::CODEC_AV1 => "AV1", punktfunk_core::quic::CODEC_PYROWAVE => "PyroWave", _ => "HEVC", } } /// Create the decoder: prefer the specific codec Kotlin ranked from `MediaCodecList` /// (`from_codec_name`), falling back to the platform's default decoder for the MIME /// (`from_decoder_type`) if that name can't be created (codec busy / renamed across an OS update). pub(super) fn create_codec(mime: &str, preferred: Option<&str>) -> Option { if let Some(name) = preferred.filter(|n| !n.is_empty()) { if let Some(c) = MediaCodec::from_codec_name(name) { return Some(c); } log::warn!( "decode: from_codec_name({name}) failed — falling back to default {mime} decoder" ); } MediaCodec::from_decoder_type(mime) } /// Apply the low-latency MediaFormat keys for `codec_name`. /// /// `aggressive` = the "Low-latency mode" master toggle. **Off** ⇒ the pre-overhaul key set, /// byte-for-byte — the standard `low-latency` key, the blind Qualcomm vendor twin, `priority = 0` AND /// `operating-rate = MAX` set together — kept as the per-device escape hatch (the profile every device /// streamed with before the overhaul). **On** (default) ⇒ 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 /// picture-order + low-latency, Exynos (also Google Tensor), Amlogic, HiSilicon, MediaTek. NVIDIA /// Tegra / Rockchip / Realtek expose no such key (nor does Moonlight) — they're covered by the /// standard key + clock hint + being ranked first in `VideoDecoders`. pub(super) fn configure_low_latency(format: &mut MediaFormat, codec_name: &str, aggressive: bool) { // Standard key: request the no-reorder low-latency path where the platform decoder supports it. format.set_i32("low-latency", 1); 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; } // MediaTek's low-latency key — very common (mid/budget phones + many Google TV / Fire TV boxes). // Set unconditionally like the standard key: MediaTek decoders honour it, others ignore it, so it // covers MediaTek whatever the exact decoder name (omx.mtk / c2.mtk / an OEM rename). Moonlight // does the same, and also relies on it for Amazon's Amlogic fork. format.set_i32("vdec-lowlatency", 1); let name = codec_name.to_ascii_lowercase(); let is = |prefix: &str| name.starts_with(prefix); // Qualcomm Snapdragon (the most common phone SoC): picture-order forces decode-order output // (kills the reorder buffer on decoders that predate the standard key); low-latency is the older // vendor twin. if is("omx.qcom") || is("c2.qti") { format.set_i32("vendor.qti-ext-dec-picture-order.enable", 1); format.set_i32("vendor.qti-ext-dec-low-latency.enable", 1); } // Samsung Exynos — also covers Google Tensor (Pixel 6+), whose hardware decoder is `c2.exynos.*`. if is("omx.exynos") || is("c2.exynos") { format.set_i32("vendor.rtc-ext-dec-low-latency.enable", 1); } // Amlogic — the Android TV boxes (onn 4K, Chromecast w/ Google TV, Homatics). if is("omx.amlogic") || is("c2.amlogic") { format.set_i32("vendor.low-latency.enable", 1); } // HiSilicon / Kirin (older Huawei; paired req/rdy keys). if is("omx.hisi") || is("c2.hisi") { format.set_i32( "vendor.hisi-ext-low-latency-video-dec.video-scene-for-low-latency-req", 1, ); format.set_i32( "vendor.hisi-ext-low-latency-video-dec.video-scene-for-low-latency-rdy", -1, ); } // NVIDIA Tegra (Shield TV) and Rockchip/Realtek (budget TV boxes / smart TVs) expose no // low-latency vendor key (Moonlight has none either) — their decoders are already low-latency // oriented, so the standard `low-latency` key + the clock hint below + being ranked first // (see `VideoDecoders`) is their treatment. // // Clock hint, mutually exclusive (matching Moonlight): the AOSP "unbounded" operating-rate // sentinel (Short.MAX) tells the decoder to run each frame at max clocks and finish ASAP rather // than pace to the frame rate — shaving per-frame decode latency at a power/heat cost. Only // Qualcomm is known to handle the sentinel; every other vendor mis-paces on it, so they get the // plain realtime `priority` hint instead. if decoder_supports_max_operating_rate(&name) { format.set_i32("operating-rate", i16::MAX as i32); // 32767 = "as fast as possible" } else { format.set_i32("priority", 0); // 0 = realtime } } /// Whether a decoder tolerates `operating-rate = Short.MAX` rather than regressing on it. Follows /// Moonlight's allowlist: Qualcomm decoders honour the sentinel (the Adreno 620 generation is the /// known exception Moonlight excludes by GPU model — undetectable from native code here, so it /// rides the master toggle as its escape hatch). Other vendors fall back to the plain `priority` /// hint above. fn decoder_supports_max_operating_rate(name_lower: &str) -> bool { name_lower.starts_with("omx.qcom") || name_lower.starts_with("c2.qti") } /// Raise the pipeline's OTHER hot threads — the core's data-plane pump (UDP receive + FEC /// reassembly) and the audio decode thread — toward the display band, matching this decode thread's /// own boost. `setpriority(PRIO_PROCESS, tid)` targets any task in the process, so we do it from /// here once their tids are known (the same set ADPF hints), without a per-platform priority hook /// in the shared core. Slightly below the decode thread's -10 so the display path still wins. /// Best-effort; skips this thread (already boosted) and is non-fatal if the platform refuses. pub(super) fn boost_hot_threads(tids: &[i32]) { // SAFETY: `gettid` is an always-safe syscall on the calling thread. let self_tid = unsafe { libc::gettid() }; for &tid in tids { if tid == self_tid { continue; } // SAFETY: `setpriority` with PRIO_PROCESS + a live tid in our own process is an always-safe // syscall; a refusal is reported via the return value, not UB. unsafe { if libc::setpriority(libc::PRIO_PROCESS, tid as libc::id_t, -8) != 0 { log::debug!("decode: setpriority(-8) on hot tid {tid} failed (non-fatal)"); } } } } /// Best-effort: raise the decode thread toward Android's URGENT_DISPLAY band so background work /// can't preempt it under load (which shows up as late/dropped frames). Non-fatal if the platform /// refuses (foreground apps may set their own threads; the exact floor is policy-dependent). pub(super) fn boost_thread_priority() { // SAFETY: `gettid`/`setpriority` on the calling thread are always-safe syscalls. PRIO_PROCESS // with a TID targets that one task on Linux — the same idiom `Process.setThreadPriority` uses. unsafe { let tid = libc::gettid(); if libc::setpriority(libc::PRIO_PROCESS, tid as libc::id_t, -10) != 0 { log::warn!( "decode: setpriority(-10) failed (non-fatal): {}", std::io::Error::last_os_error() ); } } } /// Set the surface's frame-rate hint to the stream's refresh so SurfaceFlinger picks a matching /// display mode and aligns vsync (no 60-in-120 judder). Both NDK entry points sit above our API-28 /// floor, so both are dlsym-resolved at runtime (a hard import of a >floor symbol makes /// `dlopen`/`System.load` fail on every API-28/29 device, even where this path is never hit — /// mirrors [`crate::adpf`]): /// - On a **TV** (`is_tv`): `ANativeWindow_setFrameRateWithChangeStrategy` (**API 31**) with /// `changeFrameRateStrategy = ALWAYS`, which actively drives the HDMI output into the matching /// mode (e.g. 60↔120) instead of leaving the panel at its default and judder-matching. The /// forced switch may blank the panel briefly — acceptable once at stream start, not wanted on a /// phone. Falls through to the 2-arg hint on API 30. /// - Otherwise: `ANativeWindow_setFrameRate` (**API 30**) with `compatibility = DEFAULT` — the /// softer, seamless-preferred hint for phones/tablets and the universal fallback. /// /// Returns `true` when the platform accepted a hint; `false` on API < 30 (symbols absent) or a /// decline. pub(super) fn try_set_frame_rate(window: &NativeWindow, frame_rate: f32, is_tv: bool) -> bool { // int32_t ANativeWindow_setFrameRate(ANativeWindow*, float frameRate, int8_t compatibility) type SetFrameRateFn = unsafe extern "C" fn(*mut c_void, f32, i8) -> i32; // int32_t ANativeWindow_setFrameRateWithChangeStrategy( // ANativeWindow*, float frameRate, int8_t compatibility, int8_t changeFrameRateStrategy) type SetFrameRateStrategyFn = unsafe extern "C" fn(*mut c_void, f32, i8, i8) -> i32; // SAFETY: `dlopen` of the always-mapped `libandroid.so` (only bumps its refcount; never closed — // process-lifetime handle). Each `dlsym` returns null when the symbol is absent (device below the // symbol's API level), checked before transmuting the non-null pointer to its fn-pointer type. // `window.ptr()` is the live `ANativeWindow` this `NativeWindow` owns for the call's duration. unsafe { let lib = libc::dlopen(c"libandroid.so".as_ptr(), libc::RTLD_NOW); if lib.is_null() { return false; } // TV: prefer the API-31 change-strategy form to force the mode switch (strategy 1 = ALWAYS, // compatibility 0 = DEFAULT). Absent on API 30 ⇒ fall through to the 2-arg hint below. if is_tv { let sym = libc::dlsym( lib, c"ANativeWindow_setFrameRateWithChangeStrategy".as_ptr(), ); if !sym.is_null() { let set = std::mem::transmute::<*mut c_void, SetFrameRateStrategyFn>(sym); return set(window.ptr().as_ptr().cast(), frame_rate, 0, 1) == 0; } } let sym = libc::dlsym(lib, c"ANativeWindow_setFrameRate".as_ptr()); if sym.is_null() { return false; // device API < 30 — no per-surface frame-rate hint } let set_frame_rate = std::mem::transmute::<*mut c_void, SetFrameRateFn>(sym); set_frame_rate(window.ptr().as_ptr().cast(), frame_rate, 0) == 0 } } /// Serialize [`HdrMeta`](punktfunk_core::quic::HdrMeta) into Android's `KEY_HDR_STATIC_INFO` /// (`hdr-static-info`) layout: a 25-byte CTA-861.3 / `HDRStaticInfo.Type1` blob — descriptor id 0, /// then primaries in **R, G, B** order, white point, max/min display luminance, MaxCLL, MaxFALL, all /// **little-endian** `u16`. Two conversions vs our wire form: HdrMeta stores primaries in ST.2086 /// **G, B, R** order (reorder to R, G, B), and `max_display_mastering_luminance` is in 0.0001-cd/m² /// units while Android wants **whole nits** (min stays 0.0001-nit). Chromaticities (1/50000) and /// MaxCLL/MaxFALL (nits) match 1:1. pub(super) fn android_hdr_static_info(m: &punktfunk_core::quic::HdrMeta) -> [u8; 25] { let [g, b_, r] = m.display_primaries; // ST.2086 G, B, R let max_nits = (m.max_display_mastering_luminance / 10_000).min(u16::MAX as u32) as u16; let min_units = m.min_display_mastering_luminance.min(u16::MAX as u32) as u16; let fields: [u16; 12] = [ r[0], r[1], g[0], g[1], b_[0], b_[1], // R, G, B primaries m.white_point[0], m.white_point[1], // white point max_nits, min_units, // max (nits) / min (0.0001-nit) display luminance m.max_cll, m.max_fall, // MaxCLL / MaxFALL (nits) ]; let mut out = [0u8; 25]; // out[0] = 0 (Type 1 descriptor id), already zero for (i, v) in fields.iter().enumerate() { out[1 + i * 2..3 + i * 2].copy_from_slice(&v.to_le_bytes()); } out }