diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-08 12:44:10 +0100 |
| commit | 0c20fb86633104744dbccf30ad732296694fff1b (patch) | |
| tree | 02ffb8494086960b4a84decf3bdc2c8c61bfc4f6 /src/video/mod.rs | |
Initial pipewiremain
Diffstat (limited to 'src/video/mod.rs')
| -rw-r--r-- | src/video/mod.rs | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/src/video/mod.rs b/src/video/mod.rs new file mode 100644 index 0000000..4fbab4f --- /dev/null +++ b/src/video/mod.rs @@ -0,0 +1,330 @@ +//! 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<String>, +} + +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<Box<dyn VideoBackendTrait>> { + Ok(Box::new(PipeWireBackend::new(PipeWireConfig::default()))) + } + + /// Create a new V4L2 backend (for future use) + pub fn new_v4l2() -> Result<Box<dyn VideoBackendTrait>> { + Ok(Box::new(V4L2Backend::new()?)) + } + + /// Create a new stdout backend + pub fn new_stdout() -> Result<Box<dyn VideoBackendTrait>> { + Ok(Box::new(StdoutBackend::new())) + } + + /// Create a backend based on configuration + pub fn from_config(config: &VideoConfig) -> Result<Box<dyn VideoBackendTrait>> { + 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<Box<dyn VideoBackendTrait>> { + 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<u8>, + 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<u8>, 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<Mutex<Box<dyn VideoBackendTrait>>>, + config: VideoConfig, + stats: Arc<Mutex<VideoStats>>, +} + +impl VideoBackendManager { + /// Create a new video backend manager + pub fn new(config: VideoConfig) -> Result<Self> { + 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); + } +} |
