fix(host): native sessions on the console + GPU-aware codecs + gamestream capability

The web console Dashboard read AppState.{streaming,launch,stream}, which only the
GameStream path writes, so a native punktfunk/1 session (the DEFAULT plane) showed
"Idle / no session" while actively streaming — only the Stats page (shared recorder)
reflected it. Add a plane-neutral per-session registry (session_status.rs) the native
video loop publishes to; /status now merges both planes, reports active_sessions, and
the Stop / Request-IDR buttons reach native sessions too (so surfacing them doesn't
leave dead buttons). LocalSummary (tray) gets the same fix.

Also on the management API:
- /host codecs derive from Codec::host_wire_caps() instead of a hardcoded
  [H264,H265,AV1], so codecs the GPU can't encode no longer appear.
- ApiCodec serializes HEVC as "hevc" (matching the wire/SDP/stats label) so the same
  codec reads identically across console pages.
- HostInfo.gamestream reports whether the GameStream planes run (--gamestream), so the
  console can hide the Moonlight-only pairing UI on the native-only default host.
- StatsStatus.elapsed_ms (host-monotonic) so the capture timer doesn't mix host/browser
  clocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 21:06:03 +02:00
parent c4645a8938
commit b8da32e8b6
7 changed files with 343 additions and 40 deletions
+23 -1
View File
@@ -3584,7 +3584,7 @@ fn pack_mode(width: u32, height: u32, refresh_hz: u32) -> u64 {
}
/// Unpack a [`pack_mode`] word back into `(width, height, refresh_hz)`.
fn unpack_mode(packed: u64) -> (u32, u32, u32) {
pub(crate) fn unpack_mode(packed: u64) -> (u32, u32, u32) {
(
((packed >> 32) & 0xffff) as u32,
((packed >> 16) & 0xffff) as u32,
@@ -4210,6 +4210,11 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
mode.height,
interval_hz(interval),
)));
// One-shot force-keyframe flag driven by the management API (`POST /session/idr`, the web-console
// Dashboard's "Request IDR" button) — drained in the encode loop below exactly like a client
// decode-recovery request. Registered with `session_status` so the mgmt handler can reach THIS
// session (the native plane never touches the GameStream `AppState.force_idr`).
let force_idr = Arc::new(AtomicBool::new(false));
// The send thread emits the web-console stats sample (it owns `session.stats()`); clone the
// recorder so the capture loop keeps its own handle for the per-frame `is_armed()` gate.
let send_stats = SendStats {
@@ -4241,6 +4246,18 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
})
.context("spawn send thread")?;
// Publish this session to the plane-neutral live-session registry so the web-console Dashboard
// (`GET /status`) shows the native stream — resolution/fps/codec/bitrate resolve live from the
// same handles a mid-stream mode switch / adaptive-bitrate change updates. The guard clears the
// entry when this loop exits (return / `?` / panic), so the Dashboard tracks the session's life.
let _live_session = crate::session_status::register(
live_mode.clone(),
live_bitrate.clone(),
plan.codec,
stop.clone(),
force_idr.clone(),
);
// Mid-stream session-switch watcher (opt-in via PUNKTFUNK_SESSION_WATCH; never under an explicit
// PUNKTFUNK_COMPOSITOR pin). It self-baselines and signals the loop below to swap the backend in
// place when the box flips Gaming↔Desktop. When not spawned, session_rx just stays empty.
@@ -4574,6 +4591,11 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
while keyframe.try_recv().is_ok() {
want_kf = true;
}
// Management API `POST /session/idr` (web-console Dashboard) targets this session's registry
// flag; drain it into the same forced-keyframe path a client decode-recovery request takes.
if force_idr.swap(false, Ordering::Relaxed) {
want_kf = true;
}
// Client LTR-RFI recovery: prefer re-referencing a known-good older frame (a clean recovery
// P-frame — no 20-40× IDR spike) over a full keyframe when the encoder supports it (native
// AMF LTR / Windows NVENC). Drain the backlog (the client re-requests until the recovery