5262e28b79
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m1s
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows / build (push) Successful in 55s
audit / cargo-audit (push) Failing after 1m8s
android / android (push) Failing after 2m12s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m31s
ci / rust (push) Successful in 6m31s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m44s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m25s
flatpak / build-publish (push) Successful in 5m5s
docker / deploy-docs (push) Successful in 20s
Stats HUD (mirrors the Apple client): the decode thread accumulates FPS, receive throughput, and capture->client latency (p50/p95, skew-corrected) in Rust (clients/android/native/src/stats.rs); nativeVideoStats drains a snapshot ~1 Hz over JNI as a DoubleArray. StreamScreen renders a Compose overlay (W*H@Hz / fps / Mb/s / latency, + dropped-under-loss), toggled by a Settings switch (persisted, default on) or a 3-finger tap. Performance (decode.rs): - ANativeWindow_setFrameRate(refresh_hz): align display vsync to the stream rate (no 60-in-120 judder); safe since minSdk 31 >= API 30. - Raise the decode thread toward URGENT_DISPLAY (best-effort setpriority) so background work can't preempt it under load. - Codec low-latency hints KEY_PRIORITY=0 (realtime) + KEY_OPERATING_RATE. Verified host-side: cargo build/clippy/fmt --workspace (the ungated stats + JNI accessor). The android-gated decode.rs (NDK) and the Kotlin build only in CI (android.yml: gradle + cargo-ndk) -- APIs verified against crate sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.4 KiB
Rust
94 lines
3.4 KiB
Rust
//! Live decode stats for the on-stream HUD (mirrors the Apple client's stats overlay): FPS,
|
|
//! receive throughput, and capture→client-receipt latency (p50/p95). The decode thread is the sole
|
|
//! writer (`note` per access unit); the JNI accessor `nativeVideoStats` drains a snapshot ~1 Hz and
|
|
//! resets the window. Pure `std` so it compiles on the host build too (the decode thread is
|
|
//! android-only, but `VideoThread` holds the shared handle unconditionally).
|
|
|
|
use std::sync::Mutex;
|
|
use std::time::Instant;
|
|
|
|
/// Rolling per-window accumulator. Rates are computed over the actual elapsed wall-time at drain
|
|
/// (robust to poll jitter), so a poll that lands at 0.9 s or 1.1 s still reports the right FPS.
|
|
pub struct VideoStats {
|
|
inner: Mutex<Inner>,
|
|
}
|
|
|
|
struct Inner {
|
|
window_start: Instant,
|
|
frames: u64,
|
|
bytes: u64,
|
|
/// capture→client-receipt latency samples for this window, in microseconds.
|
|
lat_us: Vec<u64>,
|
|
/// Whether the host answered the clock-skew handshake (latency is cross-machine valid).
|
|
skew_corrected: bool,
|
|
}
|
|
|
|
/// A drained, computed view of one window. `lat_valid` is false when no in-range latency sample
|
|
/// landed (then p50/p95 are 0 and the HUD hides the latency line, exactly like the Apple client).
|
|
pub struct Snapshot {
|
|
pub fps: f64,
|
|
pub mbps: f64,
|
|
pub lat_p50_ms: f64,
|
|
pub lat_p95_ms: f64,
|
|
pub lat_valid: bool,
|
|
pub skew_corrected: bool,
|
|
}
|
|
|
|
impl VideoStats {
|
|
// `new`/`note` are driven only by the android-only decode thread; `drain` (the JNI accessor) is
|
|
// ungated, so on the host build these two are unreferenced — that's expected, not dead code.
|
|
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
|
|
pub fn new() -> VideoStats {
|
|
VideoStats {
|
|
inner: Mutex::new(Inner {
|
|
window_start: Instant::now(),
|
|
frames: 0,
|
|
bytes: 0,
|
|
lat_us: Vec::with_capacity(256),
|
|
skew_corrected: false,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Record one decoded access unit: its wire size and (if in range) its capture→client latency.
|
|
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
|
|
pub fn note(&self, bytes: usize, lat_us: Option<u64>, skew_corrected: bool) {
|
|
let mut g = self.inner.lock().unwrap();
|
|
g.frames += 1;
|
|
g.bytes += bytes as u64;
|
|
g.skew_corrected = skew_corrected;
|
|
if let Some(l) = lat_us {
|
|
g.lat_us.push(l);
|
|
}
|
|
}
|
|
|
|
/// Compute the window's rates + latency percentiles, then reset for the next window.
|
|
pub fn drain(&self) -> Snapshot {
|
|
let mut g = self.inner.lock().unwrap();
|
|
let elapsed = g.window_start.elapsed().as_secs_f64().max(1e-3);
|
|
let fps = g.frames as f64 / elapsed;
|
|
let mbps = g.bytes as f64 * 8.0 / 1_000_000.0 / elapsed;
|
|
let (p50, p95, valid) = if g.lat_us.is_empty() {
|
|
(0.0, 0.0, false)
|
|
} else {
|
|
g.lat_us.sort_unstable();
|
|
let n = g.lat_us.len();
|
|
let at = |p: f64| g.lat_us[((n as f64 * p) as usize).min(n - 1)] as f64 / 1000.0;
|
|
(at(0.50), at(0.95), true)
|
|
};
|
|
let skew = g.skew_corrected;
|
|
g.window_start = Instant::now();
|
|
g.frames = 0;
|
|
g.bytes = 0;
|
|
g.lat_us.clear();
|
|
Snapshot {
|
|
fps,
|
|
mbps,
|
|
lat_p50_ms: p50,
|
|
lat_p95_ms: p95,
|
|
lat_valid: valid,
|
|
skew_corrected: skew,
|
|
}
|
|
}
|
|
}
|