//! Stdout video backend for piping video output to other tools use crate::error::{Result, VideoError}; use crate::video::{VideoBackendTrait, VideoStats}; use std::io::{self, Write}; use std::sync::Mutex; use tracing::{debug, info}; /// Configuration for the stdout video backend #[derive(Debug, Clone)] pub struct StdoutConfig { /// Whether to output frame headers with metadata pub include_headers: bool, /// Whether to flush stdout after each frame pub flush_after_frame: bool, /// Output format for headers (if enabled) pub header_format: HeaderFormat, } impl Default for StdoutConfig { fn default() -> Self { Self { include_headers: false, flush_after_frame: true, header_format: HeaderFormat::Simple, } } } /// Header format for frame metadata #[derive(Debug, Clone)] pub enum HeaderFormat { /// Simple text format: "FRAME:size:timestamp\n" Simple, /// JSON format: {"frame": {"size": size, "timestamp": timestamp}} Json, /// Binary format: 4-byte size + 8-byte timestamp Binary, } /// Stdout video backend that outputs raw video frames to stdout pub struct StdoutBackend { config: StdoutConfig, stats: Mutex, frame_count: Mutex, start_time: Mutex, } impl StdoutBackend { /// Create a new stdout backend with default configuration pub fn new() -> Self { Self { config: StdoutConfig::default(), stats: Mutex::new(VideoStats::default()), frame_count: Mutex::new(0), start_time: Mutex::new(std::time::Instant::now()), } } /// Create a new stdout backend with custom configuration pub fn with_config(config: StdoutConfig) -> Self { Self { config, stats: Mutex::new(VideoStats::default()), frame_count: Mutex::new(0), start_time: Mutex::new(std::time::Instant::now()), } } /// Write frame header if enabled fn write_frame_header(&self, frame_size: usize) -> Result<()> { if !self.config.include_headers { return Ok(()); } let start_time = self.start_time.lock().unwrap(); let timestamp = start_time.elapsed().as_micros(); drop(start_time); match self.config.header_format { HeaderFormat::Simple => { let header = format!("FRAME:{}:{}\n", frame_size, timestamp); io::stdout().write_all(header.as_bytes())?; } HeaderFormat::Json => { let header = format!( r#"{{"frame": {{"size": {}, "timestamp": {}}}}}{}"#, frame_size, timestamp, '\n' ); io::stdout().write_all(header.as_bytes())?; } HeaderFormat::Binary => { // 4-byte size + 8-byte timestamp let size_bytes = (frame_size as u32).to_le_bytes(); let timestamp_bytes = timestamp.to_le_bytes(); let mut header = Vec::with_capacity(12); header.extend_from_slice(&size_bytes); header.extend_from_slice(×tamp_bytes); io::stdout().write_all(&header)?; } } Ok(()) } /// Update statistics fn update_stats(&self, frame_size: usize) { let mut frame_count = self.frame_count.lock().unwrap(); *frame_count += 1; let current_frame_count = *frame_count; drop(frame_count); let mut stats = self.stats.lock().unwrap(); stats.frames_pushed = current_frame_count; stats.total_bytes += frame_size as u64; // Calculate FPS based on elapsed time let start_time = self.start_time.lock().unwrap(); let elapsed = start_time.elapsed().as_secs_f64(); drop(start_time); if elapsed > 0.0 { stats.fps = current_frame_count as f64 / elapsed; } // Log frame information for debugging if current_frame_count % 30 == 0 { // Log every 30 frames tracing::debug!( "Stdout backend: frame {}, size: {} bytes, total: {} bytes, fps: {:.2}", current_frame_count, frame_size, stats.total_bytes, stats.fps ); } } } impl VideoBackendTrait for StdoutBackend { fn initialize(&mut self) -> Result<()> { info!("Initializing stdout video backend"); { let mut start_time = self.start_time.lock().unwrap(); *start_time = std::time::Instant::now(); } { let mut stats = self.stats.lock().unwrap(); stats.is_ready = true; } // Write initial header if enabled if self.config.include_headers { match self.config.header_format { HeaderFormat::Simple => { io::stdout().write_all(b"STDOUT_VIDEO_STREAM_START\n")?; } HeaderFormat::Json => { io::stdout().write_all(b"{\"stream\": \"start\"}\n")?; } HeaderFormat::Binary => { // Magic number: "STDO" (4 bytes) io::stdout().write_all(b"STDO")?; } } } debug!("Stdout video backend initialized successfully"); Ok(()) } fn push_frame(&self, frame_data: &[u8]) -> Result<()> { let stats = self.stats.lock().unwrap(); if !stats.is_ready { return Err(VideoError::DeviceNotReady.into()); } drop(stats); // Write frame header if enabled self.write_frame_header(frame_data.len())?; // Write frame data to stdout io::stdout().write_all(frame_data)?; // Flush if configured if self.config.flush_after_frame { io::stdout().flush()?; } // Update statistics self.update_stats(frame_data.len()); debug!("Pushed frame to stdout: {} bytes", frame_data.len()); Ok(()) } fn get_stats(&self) -> VideoStats { let mut stats = self.stats.lock().unwrap().clone(); stats.backend_type = crate::video::VideoBackendType::Stdout; stats } fn is_ready(&self) -> bool { self.stats.lock().unwrap().is_ready } fn shutdown(&mut self) -> Result<()> { info!("Shutting down stdout video backend"); // Write final header if enabled if self.config.include_headers { match self.config.header_format { HeaderFormat::Simple => { io::stdout().write_all(b"STDOUT_VIDEO_STREAM_END\n")?; } HeaderFormat::Json => { io::stdout().write_all(b"{\"stream\": \"end\"}\n")?; } HeaderFormat::Binary => { // End marker: "END\0" (4 bytes) io::stdout().write_all(b"END\0")?; } } } // Final flush io::stdout().flush()?; { let mut stats = self.stats.lock().unwrap(); stats.is_ready = false; } Ok(()) } } impl Drop for StdoutBackend { fn drop(&mut self) { let is_ready = { let stats = self.stats.lock().unwrap(); stats.is_ready }; if is_ready { let _ = self.shutdown(); } } } #[cfg(test)] mod tests { use super::*; use crate::video::VideoBackendTrait; #[test] fn test_stdout_backend_creation() { let backend = StdoutBackend::new(); assert!(!backend.is_ready()); assert_eq!(backend.get_stats().frames_pushed, 0); } #[test] fn test_stdout_backend_with_config() { let config = StdoutConfig { include_headers: true, flush_after_frame: false, header_format: HeaderFormat::Json, }; let backend = StdoutBackend::with_config(config); assert!(!backend.is_ready()); } #[test] fn test_header_format_creation() { let simple = HeaderFormat::Simple; let json = HeaderFormat::Json; let binary = HeaderFormat::Binary; // Just test that they can be created assert!(matches!(simple, HeaderFormat::Simple)); assert!(matches!(json, HeaderFormat::Json)); assert!(matches!(binary, HeaderFormat::Binary)); } }