f439b69451
Break the 1747-line clients/android/native/src/decode.rs into a decode/ directory
module (mod.rs + 5 concern submodules):
- decode/setup.rs : codec creation + low-latency config + thread/frame-rate
tuning + HDR static-info encode
- decode/display.rs : DisplayTracker + render-callback registration + HDR dataspace
- decode/latency.rs : realtime clock + decoded-pts / user-flags stat recording
- decode/sync_loop.rs : the synchronous poll decode loop (+ feed/drain) — moved WHOLE
- decode/async_loop.rs : the event-driven async decode loop (+ helpers) — moved WHOLE
decode/mod.rs keeps the consts, DecodeOptions, and the `run` entry point + the
`codec_mime`/`codec_label` re-export, so every crate::decode::X path stays byte-stable.
The module has no decoder struct (free functions + small types), so both decode loops
move byte-for-byte and their separately-inlined received-stat recording is NOT unified.
16 helper fns/types became pub(super) for sibling access; zero field bumps. lib.rs
unchanged (`#[cfg(target_os="android")] mod decode;` resolves to decode/mod.rs).
Verified: cargo-ndk check (aarch64-linux-android, clean) + the gradle cargoNdkDebug
build (arm64-v8a / armeabi-v7a / x86_64). On-device runtime re-verification still owed
per the plan (the two decode loops are a hot path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.9 KiB
Rust
84 lines
3.9 KiB
Rust
//! Decode-latency bookkeeping: realtime clock + decoded-pts / user-flags stat recording.
|
||
|
||
use punktfunk_core::client::NativeClient;
|
||
use std::collections::VecDeque;
|
||
|
||
/// Wall-clock now in nanoseconds (CLOCK_REALTIME basis), to compare against the host-stamped
|
||
/// capture `pts_ns` after the skew offset is applied.
|
||
pub(super) fn now_realtime_ns() -> i128 {
|
||
use std::time::{SystemTime, UNIX_EPOCH};
|
||
SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.map(|d| d.as_nanos() as i128)
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
/// HUD `decoded` point for one dequeued output frame, keyed by the echoed `presentationTimeUs`:
|
||
/// build the end-to-end (capture→decoded, skew-corrected, clamped to (0, 10 s)) and `decode`
|
||
/// (received→decoded, single-clock local, ≥ 0) samples and hand them to
|
||
/// [`crate::stats::VideoStats::note_decoded`]. The pts keys the receipt stamp in `in_flight`;
|
||
/// entries older than it are evicted (decode order == input order here — low-latency, no
|
||
/// B-frames — so anything before it was dropped inside the codec or stamped before a flush).
|
||
/// `decoded_ns` is the availability instant: the dequeue (sync loop) or the output callback's
|
||
/// stamp (async loop).
|
||
pub(super) fn note_decoded_pts(
|
||
client: &NativeClient,
|
||
measure_decode: bool,
|
||
stats: &crate::stats::VideoStats,
|
||
in_flight: &mut VecDeque<(u64, i128)>,
|
||
clock_offset: i64,
|
||
pts_us: u64,
|
||
decoded_ns: i128,
|
||
) {
|
||
// Pair the echoed pts back to its receipt stamp, evicting stale (older) entries as we go.
|
||
let mut received_ns = None;
|
||
while let Some(&(p, r)) = in_flight.front() {
|
||
if p > pts_us {
|
||
break; // future frame — leave it for its own output buffer
|
||
}
|
||
in_flight.pop_front();
|
||
if p == pts_us {
|
||
received_ns = Some(r);
|
||
break;
|
||
}
|
||
}
|
||
let decode_us = received_ns.map(|r| ((decoded_ns - r).max(0) / 1000) as u64);
|
||
// Adaptive bitrate: the `decode` stage (received→decoded, single-clock local) IS the decoder-
|
||
// backlog signal — the only bottleneck the host-side network signals can't see (a fast LAN
|
||
// feeding a slower mobile decoder). Report it whenever the controller is armed, regardless of
|
||
// the HUD; `report_decode_us` is a cheap accumulate the pump windows.
|
||
if measure_decode {
|
||
if let Some(us) = decode_us {
|
||
client.report_decode_us(us.min(u32::MAX as u64) as u32);
|
||
}
|
||
}
|
||
// HUD histogram: only while the overlay is visible (a measure-only caller enters here for the
|
||
// ABR report alone). `end-to-end` = capture→decoded (skew-corrected) tiles the `decode` stage.
|
||
// pts_us is the truncated frame.pts_ns/1000 we queued, so ×1000 re-approximates capture time to
|
||
// < 1 µs — negligible against the ms-scale figures shown.
|
||
if stats.enabled() {
|
||
let e2e_ns = decoded_ns + clock_offset as i128 - pts_us as i128 * 1000;
|
||
let e2e_us = (e2e_ns > 0 && e2e_ns < 10_000_000_000).then_some((e2e_ns / 1000) as u64);
|
||
stats.note_decoded(e2e_us, decode_us);
|
||
}
|
||
}
|
||
|
||
/// The AU `user_flags` for a decoded output, keyed by the echoed `presentationTimeUs`. Recovery
|
||
/// signalling (FLAG_SOF IDR marker / RECOVERY_ANCHOR / RECOVERY_POINT) rides the AU's flags, which are
|
||
/// only in scope at feed time — so the feed side parks `(pts_us, flags)` here and the present side
|
||
/// looks them up to fold [`ReanchorGate::on_decoded`]. Decode order == input order (low-latency, no
|
||
/// B-frames), so this evicts entries older than `pts_us` as it goes; a miss (probe filler, or an entry
|
||
/// aged past the cap) reads `0` — no recovery flags, decoded normally.
|
||
pub(super) fn take_flags(map: &mut VecDeque<(u64, u32)>, pts_us: u64) -> u32 {
|
||
while let Some(&(p, f)) = map.front() {
|
||
if p > pts_us {
|
||
break; // future frame — leave it for its own output buffer
|
||
}
|
||
map.pop_front();
|
||
if p == pts_us {
|
||
return f;
|
||
}
|
||
}
|
||
0
|
||
}
|