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:
+37
-7
@@ -16,8 +16,8 @@ use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::cli::HostOpts;
|
||||
use crate::common::{
|
||||
alpn::ALPN, bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, signal,
|
||||
tunnel,
|
||||
alpn::ALPN, bandwidth, config, config::BandwidthStatus, deps, display::DisplayServer, output,
|
||||
signal, tunnel,
|
||||
};
|
||||
|
||||
use self::pipeline::CaptureHandle;
|
||||
@@ -85,9 +85,25 @@ pub async fn run(opts: HostOpts) -> Result<()> {
|
||||
let relay_only =
|
||||
EndpointAddr::new(addr.id).with_addrs(addr.addrs.iter().filter(|a| a.is_relay()).cloned());
|
||||
let ticket = EndpointTicket::new(relay_only);
|
||||
let clipboard_ok = opts.interactive && copy_to_clipboard(&ticket.to_string());
|
||||
let ticket_str = ticket.to_string();
|
||||
let clipboard_ok = opts.interactive && copy_to_clipboard(&ticket_str);
|
||||
print_host_banner(&ticket, display, &opts, &quality, &resolution, clipboard_ok);
|
||||
|
||||
output::emit(output::Event::Ticket { value: &ticket_str });
|
||||
let display_str = format!("{display:?}");
|
||||
let capture = capture_summary(&opts);
|
||||
let dims = quality.dimensions_summary();
|
||||
let cap_source = resolution.source.label();
|
||||
output::emit(output::Event::HostInfo {
|
||||
display_server: &display_str,
|
||||
capture: &capture,
|
||||
quality: &quality.label,
|
||||
dimensions: &dims,
|
||||
hw_encode: !opts.no_hwencode,
|
||||
max_viewers: resolution.value,
|
||||
max_viewers_source: &cap_source,
|
||||
});
|
||||
|
||||
let (sup_tx, sup_rx) = mpsc::channel::<SupervisorMsg>(16);
|
||||
let supervisor = tokio::spawn(supervise(
|
||||
opts.clone(),
|
||||
@@ -215,16 +231,22 @@ async fn supervise(
|
||||
match msg {
|
||||
SupervisorMsg::AddViewer(reply) => {
|
||||
if count >= max_viewers {
|
||||
let _ = reply.send(Err(format!(
|
||||
"host is full ({count} of {max_viewers} viewers connected)"
|
||||
)));
|
||||
let reason =
|
||||
format!("host is full ({count} of {max_viewers} viewers connected)");
|
||||
output::emit(output::Event::ViewerRefused { reason: &reason });
|
||||
let _ = reply.send(Err(reason));
|
||||
continue;
|
||||
}
|
||||
|
||||
if handle.is_none() {
|
||||
tracing::info!("first viewer arriving — spawning capture");
|
||||
match capture::spawn(display, &opts, &quality).await {
|
||||
Ok(h) => handle = Some(h),
|
||||
Ok(h) => {
|
||||
handle = Some(h);
|
||||
output::emit(output::Event::Capture {
|
||||
state: output::CaptureState::Started,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = reply.send(Err(format!("capture spawn failed: {e:#}")));
|
||||
continue;
|
||||
@@ -235,16 +257,21 @@ async fn supervise(
|
||||
let port = handle.as_ref().expect("handle was just set").local_port();
|
||||
count += 1;
|
||||
let _ = reply.send(Ok(port));
|
||||
output::emit(output::Event::ViewerCount { active: count, max: max_viewers });
|
||||
tracing::info!(active = count, cap = max_viewers, "viewer joined");
|
||||
}
|
||||
SupervisorMsg::RemoveViewer => {
|
||||
count = count.saturating_sub(1);
|
||||
output::emit(output::Event::ViewerCount { active: count, max: max_viewers });
|
||||
tracing::info!(active = count, cap = max_viewers, "viewer left");
|
||||
if count == 0
|
||||
&& let Some(h) = handle.take()
|
||||
{
|
||||
tracing::info!("last viewer left — tearing down capture");
|
||||
h.shutdown().await;
|
||||
output::emit(output::Event::Capture {
|
||||
state: output::CaptureState::Stopped,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +280,9 @@ async fn supervise(
|
||||
if let Some(h) = handle.take() {
|
||||
tracing::info!("host shutdown — tearing down capture");
|
||||
h.shutdown().await;
|
||||
output::emit(output::Event::Capture {
|
||||
state: output::CaptureState::Stopped,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user