summaryrefslogtreecommitdiff
path: root/src/video/stdout.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/video/stdout.rs')
-rw-r--r--src/video/stdout.rs277
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(&timestamp_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));
+ }
+}