//! Machine-readable event stream for non-interactive front-ends. //! //! When enabled with `--output json`, the host and viewer emit one JSON //! object per line on **stdout**. The human banner and `tracing` logs stay //! on **stderr**, so the two streams never interleave and a parser reading //! stdout sees only events. Each line is flushed immediately so a front-end //! reading the pipe gets events live rather than in block-buffered chunks. //! //! This is the shell-out counterpart to an in-process event channel: the //! `--gui` front-end re-execs this binary as `pixelpass --host --output json` //! and parses these lines to drive its window. use std::io::Write; use std::sync::atomic::{AtomicBool, Ordering}; use serde::Serialize; static JSON_ENABLED: AtomicBool = AtomicBool::new(false); /// Turn JSON event output on. Called once at startup from `--output json`. pub fn set_json(enabled: bool) { JSON_ENABLED.store(enabled, Ordering::Relaxed); } fn json_enabled() -> bool { JSON_ENABLED.load(Ordering::Relaxed) } /// One event in the stdout stream. Serialized as `{"event":"", ...}`. #[derive(Serialize)] #[serde(tag = "event", rename_all = "snake_case")] pub enum Event<'a> { /// The relay-only ticket the viewer needs. Emitted once at host startup. Ticket { value: &'a str }, /// One-shot host configuration summary, mirroring the banner fields. HostInfo { display_server: &'a str, capture: &'a str, quality: &'a str, dimensions: &'a str, hw_encode: bool, max_viewers: u32, max_viewers_source: &'a str, }, /// Active viewer count changed. ViewerCount { active: u32, max: u32 }, /// Capture pipeline lifecycle (spawned on first viewer, torn down on last). Capture { state: CaptureState }, /// A viewer was turned away (host full, or capture spawn failed). ViewerRefused { reason: &'a str }, /// Viewer-side: the local player URL is ready to open. Connected { url: &'a str }, } #[derive(Serialize)] #[serde(rename_all = "snake_case")] pub enum CaptureState { Started, Stopped, } /// Emit one event as a JSON line on stdout, flushed. No-op unless JSON /// output was enabled with [`set_json`], so call sites can sprinkle these /// unconditionally without branching. pub fn emit(event: Event) { if !json_enabled() { return; } match serde_json::to_string(&event) { Ok(line) => { let mut out = std::io::stdout().lock(); // Best-effort: a closed pipe (front-end gone) shouldn't crash the // host — it keeps streaming to any viewers already connected. let _ = writeln!(out, "{line}"); let _ = out.flush(); } Err(e) => tracing::warn!("failed to serialize event: {e}"), } }