diff options
Diffstat (limited to 'src/video/stdout.rs')
| -rw-r--r-- | src/video/stdout.rs | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/src/video/stdout.rs b/src/video/stdout.rs new file mode 100644 index 0000000..ce9882a --- /dev/null +++ b/src/video/stdout.rs @@ -0,0 +1,277 @@ +//! 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<VideoStats>, + frame_count: Mutex<u64>, + start_time: Mutex<std::time::Instant>, +} + +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)); + } +} |
