multi-viewer: broadcast fanout + supervisor lifecycle

One gst capture pipeline now fans out to N concurrent viewers via a
tokio::sync::broadcast<Arc<Vec<u8>>>. The HTTP listener accepts forever;
each accepted connection spawns a sender task draining its own
broadcast::Receiver. Slow consumers see Lagged and skip ahead — MPEG-TS
resyncs at the next keyframe.

Host runtime is now lazy + sticky: a supervisor task owns the capture
handle and viewer count. First viewer triggers capture::spawn; last
viewer triggers shutdown. Subsequent reconnects re-trigger the portal
dialog as expected. --max-viewers (default 2) caps concurrent viewers;
additional connections get a "host is full" refusal and are dropped.

Banner updated to reflect the new lifecycle and viewer cap.

NOT YET RUNTIME-VERIFIED. cargo build is clean and the pipeline-level
smoke test still passes, but the multi-viewer behavior (cap enforcement,
lazy-sticky restart, concurrent fanout) requires manual end-to-end
testing with the portal dialog + multiple mpv instances.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:11:43 -04:00
parent 74b4101d4f
commit ffe5a90686
3 changed files with 262 additions and 54 deletions
+7
View File
@@ -45,6 +45,11 @@ pub struct Cli {
#[arg(long)]
pub low_latency: bool,
/// Maximum number of concurrent viewers. Additional connections are
/// politely refused with a "host full" message.
#[arg(long, default_value_t = 2)]
pub max_viewers: u32,
// ── viewer options ────────────────────────────────────────────────
/// Local TCP port for the viewer to expose (default: random).
#[arg(long, default_value_t = 0)]
@@ -76,6 +81,7 @@ pub struct HostOpts {
pub framerate: u32,
pub no_hwencode: bool,
pub low_latency: bool,
pub max_viewers: u32,
pub interactive: bool,
}
@@ -96,6 +102,7 @@ impl Cli {
framerate: self.framerate,
no_hwencode: self.no_hwencode,
low_latency: self.low_latency,
max_viewers: self.max_viewers,
interactive,
}
}