summaryrefslogtreecommitdiff
path: root/src/video/mod.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2026-02-08 12:44:10 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2026-02-08 12:44:10 +0100
commit0c20fb86633104744dbccf30ad732296694fff1b (patch)
tree02ffb8494086960b4a84decf3bdc2c8c61bfc4f6 /src/video/mod.rs
Initial pipewiremain
Diffstat (limited to 'src/video/mod.rs')
-rw-r--r--src/video/mod.rs330
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);
+ }
+}