//! Video backend module for the Geek szitman supercamera mod pipewire; mod v4l2; mod stdout; pub use pipewire::{PipeWireBackend, PipeWireConfig}; pub use v4l2::V4L2Backend; pub use stdout::{StdoutBackend, StdoutConfig, HeaderFormat}; use crate::error::{Result, VideoError}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::sync::Mutex; use tracing::{info}; /// Video backend trait for different video output methods pub trait VideoBackendTrait: Send + Sync { /// Initialize the video backend fn initialize(&mut self) -> Result<()>; /// Push a frame to the video backend fn push_frame(&self, frame_data: &[u8]) -> Result<()>; /// Get backend statistics fn get_stats(&self) -> VideoStats; /// Check if backend is ready fn is_ready(&self) -> bool; /// Shutdown the backend fn shutdown(&mut self) -> Result<()>; } /// Video backend types #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum VideoBackendType { /// PipeWire backend PipeWire, /// V4L2 backend (for future use) V4L2, /// Stdout backend for piping to other tools Stdout, } /// Video backend configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoConfig { pub backend_type: VideoBackendType, pub width: u32, pub height: u32, pub fps: u32, pub format: VideoFormat, pub device_path: Option, } impl Default for VideoConfig { fn default() -> Self { Self { backend_type: VideoBackendType::PipeWire, width: 640, height: 480, fps: 30, format: VideoFormat::MJPEG, device_path: None, } } } /// Video format types #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum VideoFormat { /// Motion JPEG MJPEG, /// YUV420 YUV420, /// RGB24 RGB24, } impl VideoFormat { /// Get the format name as a string pub fn as_str(&self) -> &'static str { match self { VideoFormat::MJPEG => "MJPEG", VideoFormat::YUV420 => "YUV420", VideoFormat::RGB24 => "RGB24", } } /// Get the bytes per pixel pub fn bytes_per_pixel(&self) -> usize { match self { VideoFormat::MJPEG => 0, // Variable for MJPEG VideoFormat::YUV420 => 1, VideoFormat::RGB24 => 3, } } } /// Video statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoStats { pub frames_pushed: u64, pub frames_dropped: u64, pub total_bytes: u64, pub fps: f64, pub backend_type: VideoBackendType, pub is_ready: bool, } impl Default for VideoStats { fn default() -> Self { Self { frames_pushed: 0, frames_dropped: 0, total_bytes: 0, fps: 0.0, backend_type: VideoBackendType::PipeWire, is_ready: false, } } } /// Video backend factory pub struct VideoBackend; impl VideoBackend { /// Create a new PipeWire backend pub fn new_pipewire() -> Result> { Ok(Box::new(PipeWireBackend::new(PipeWireConfig::default()))) } /// Create a new V4L2 backend (for future use) pub fn new_v4l2() -> Result> { Ok(Box::new(V4L2Backend::new()?)) } /// Create a new stdout backend pub fn new_stdout() -> Result> { Ok(Box::new(StdoutBackend::new())) } /// Create a backend based on configuration pub fn from_config(config: &VideoConfig) -> Result> { match config.backend_type { VideoBackendType::PipeWire => Self::new_pipewire(), VideoBackendType::V4L2 => Self::new_v4l2(), VideoBackendType::Stdout => Self::new_stdout(), } } /// Create a backend from type pub fn from_type(backend_type: VideoBackendType) -> Result> { match backend_type { VideoBackendType::PipeWire => Self::new_pipewire(), VideoBackendType::V4L2 => Self::new_v4l2(), VideoBackendType::Stdout => Self::new_stdout(), } } } /// Video frame metadata #[derive(Debug, Clone)] pub struct VideoFrame { pub data: Vec, pub width: u32, pub height: u32, pub format: VideoFormat, pub timestamp: std::time::Instant, } impl VideoFrame { /// Create a new video frame pub fn new(data: Vec, width: u32, height: u32, format: VideoFormat) -> Self { Self { data, width, height, format, timestamp: std::time::Instant::now(), } } /// Get frame size in bytes pub fn size(&self) -> usize { self.data.len() } /// Get frame dimensions pub fn dimensions(&self) -> (u32, u32) { (self.width, self.height) } /// Check if frame is valid pub fn is_valid(&self) -> bool { !self.data.is_empty() && self.width > 0 && self.height > 0 } } /// Video backend manager pub struct VideoBackendManager { backend: Arc>>, config: VideoConfig, stats: Arc>, } impl VideoBackendManager { /// Create a new video backend manager pub fn new(config: VideoConfig) -> Result { let backend = VideoBackend::from_config(&config)?; let stats = Arc::new(Mutex::new(VideoStats::default())); let manager = Self { backend: Arc::new(Mutex::new(backend)), config, stats, }; // Initialize the backend let mut backend_guard = manager.backend.lock().unwrap(); backend_guard.initialize()?; drop(backend_guard); Ok(manager) } /// Push a frame to the video backend pub fn push_frame(&self, frame_data: &[u8]) -> Result<()> { let backend = self.backend.lock().unwrap(); if !backend.is_ready() { return Err(VideoError::DeviceNotReady.into()); } // Update statistics let mut stats = self.stats.lock().unwrap(); stats.frames_pushed += 1; stats.total_bytes += frame_data.len() as u64; drop(stats); // Push frame to backend backend.push_frame(frame_data)?; Ok(()) } /// Get current statistics pub fn get_stats(&self) -> VideoStats { let stats = self.stats.lock().unwrap(); stats.clone() } /// Switch video backend pub fn switch_backend(&mut self, new_type: VideoBackendType) -> Result<()> { // Shutdown current backend let mut backend = self.backend.lock().unwrap(); backend.shutdown()?; drop(backend); // Create new backend let new_backend = VideoBackend::from_type(new_type)?; let mut backend = self.backend.lock().unwrap(); *backend = new_backend; // Initialize new backend backend.initialize()?; // Update config self.config.backend_type = new_type; info!("Switched to {:?} backend", new_type); Ok(()) } /// Get configuration pub fn config(&self) -> &VideoConfig { &self.config } /// Update configuration pub fn update_config(&mut self, config: VideoConfig) -> Result<()> { // Recreate backend if type changed if self.config.backend_type != config.backend_type { self.switch_backend(config.backend_type)?; } self.config = config; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_video_backend_factory() { // Test PipeWire backend creation let pipewire_backend = VideoBackend::new_pipewire(); assert!(pipewire_backend.is_ok()); // Test V4L2 backend creation let v4l2_backend = VideoBackend::new_v4l2(); assert!(v4l2_backend.is_ok()); } #[test] fn test_video_frame_creation() { let frame_data = vec![0u8; 1024]; let frame = VideoFrame::new(frame_data.clone(), 32, 32, VideoFormat::RGB24); assert_eq!(frame.data, frame_data); assert_eq!(frame.width, 32); assert_eq!(frame.height, 32); assert_eq!(frame.format, VideoFormat::RGB24); assert!(frame.is_valid()); } #[test] fn test_video_format_conversions() { assert_eq!(VideoFormat::MJPEG.as_str(), "MJPEG"); assert_eq!(VideoFormat::YUV420.as_str(), "YUV420"); assert_eq!(VideoFormat::RGB24.as_str(), "RGB24"); assert_eq!(VideoFormat::MJPEG.bytes_per_pixel(), 0); assert_eq!(VideoFormat::YUV420.bytes_per_pixel(), 1); assert_eq!(VideoFormat::RGB24.bytes_per_pixel(), 3); } }