Adds common/output.rs: a process-global JSON-lines emitter for non-interactive front-ends. With --output json, host and viewer emit one JSON object per line on stdout (ticket, host_info, viewer_count, capture start/stop, viewer_refused, connected), flushed per line; the human banner and tracing logs stay on stderr so the two never interleave. No-op when the flag is absent, so call sites emit unconditionally. This is the shell-out counterpart to an in-process event channel: the upcoming --gui front-end re-execs this binary as `pixelpass --host --output json` and parses these lines to drive its window. serde_json was already in the tree from the bandwidth pre-flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
2.8 KiB
Rust
80 lines
2.8 KiB
Rust
//! 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":"<tag>", ...}`.
|
|
#[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}"),
|
|
}
|
|
}
|