feat(output): --output json machine-readable event stream
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>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
//! 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}"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user