//! Pixel format definitions and conversions use serde::{Deserialize, Serialize}; use std::fmt; #[cfg(unix)] use v4l2r::PixelFormat as V4l2rPixelFormat; /// Supported pixel formats #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub enum PixelFormat { /// MJPEG compressed format (preferred for capture cards) Mjpeg, /// JPEG compressed format Jpeg, /// YUYV 4:2:2 packed format Yuyv, /// YVYU 4:2:2 packed format Yvyu, /// UYVY 4:2:2 packed format Uyvy, /// NV12 semi-planar format (Y plane + interleaved UV) Nv12, /// NV21 semi-planar format (Y plane + interleaved VU) Nv21, /// NV16 semi-planar format Nv16, /// NV24 semi-planar format Nv24, /// YUV420 planar format Yuv420, /// YVU420 planar format Yvu420, /// RGB565 format Rgb565, /// RGB24 format (3 bytes per pixel) Rgb24, /// BGR24 format (3 bytes per pixel) Bgr24, /// Grayscale format Grey, } impl PixelFormat { /// Convert to V4L2 FourCC bytes pub fn to_fourcc(&self) -> [u8; 4] { match self { PixelFormat::Mjpeg => *b"MJPG", PixelFormat::Jpeg => *b"JPEG", PixelFormat::Yuyv => *b"YUYV", PixelFormat::Yvyu => *b"YVYU", PixelFormat::Uyvy => *b"UYVY", PixelFormat::Nv12 => *b"NV12", PixelFormat::Nv21 => *b"NV21", PixelFormat::Nv16 => *b"NV16", PixelFormat::Nv24 => *b"NV24", PixelFormat::Yuv420 => *b"YU12", PixelFormat::Yvu420 => *b"YV12", PixelFormat::Rgb565 => *b"RGBP", PixelFormat::Rgb24 => *b"RGB3", PixelFormat::Bgr24 => *b"BGR3", PixelFormat::Grey => *b"GREY", } } /// Try to convert from V4L2 FourCC pub fn from_fourcc(repr: [u8; 4]) -> Option { match &repr { b"MJPG" => Some(PixelFormat::Mjpeg), b"JPEG" => Some(PixelFormat::Jpeg), b"YUYV" => Some(PixelFormat::Yuyv), b"YVYU" => Some(PixelFormat::Yvyu), b"UYVY" => Some(PixelFormat::Uyvy), b"NV12" => Some(PixelFormat::Nv12), b"NV21" => Some(PixelFormat::Nv21), b"NV16" => Some(PixelFormat::Nv16), b"NV24" => Some(PixelFormat::Nv24), b"YU12" | b"I420" => Some(PixelFormat::Yuv420), b"YV12" => Some(PixelFormat::Yvu420), b"RGBP" => Some(PixelFormat::Rgb565), b"RGB3" => Some(PixelFormat::Rgb24), b"BGR3" => Some(PixelFormat::Bgr24), b"GREY" | b"Y800" => Some(PixelFormat::Grey), _ => None, } } /// Convert to v4l2r PixelFormat #[cfg(unix)] pub fn to_v4l2r(&self) -> V4l2rPixelFormat { V4l2rPixelFormat::from(&self.to_fourcc()) } /// Convert from v4l2r PixelFormat #[cfg(unix)] pub fn from_v4l2r(format: V4l2rPixelFormat) -> Option { let repr: [u8; 4] = format.into(); Self::from_fourcc(repr) } /// Check if format is compressed (JPEG/MJPEG) pub fn is_compressed(&self) -> bool { matches!(self, PixelFormat::Mjpeg | PixelFormat::Jpeg) } /// Get bytes per pixel for uncompressed formats /// Returns None for compressed formats pub fn bytes_per_pixel(&self) -> Option { match self { PixelFormat::Mjpeg | PixelFormat::Jpeg => None, PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(2), PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => { None } // Variable PixelFormat::Nv16 => None, PixelFormat::Nv24 => None, PixelFormat::Rgb565 => Some(2), PixelFormat::Rgb24 | PixelFormat::Bgr24 => Some(3), PixelFormat::Grey => Some(1), } } /// Calculate expected frame size for a given resolution /// Returns None for compressed formats (variable size) pub fn frame_size(&self, resolution: Resolution) -> Option { let pixels = (resolution.width * resolution.height) as usize; match self { PixelFormat::Mjpeg | PixelFormat::Jpeg => None, PixelFormat::Yuyv | PixelFormat::Yvyu | PixelFormat::Uyvy => Some(pixels * 2), PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Yuv420 | PixelFormat::Yvu420 => { Some(pixels * 3 / 2) } PixelFormat::Nv16 => Some(pixels * 2), PixelFormat::Nv24 => Some(pixels * 3), PixelFormat::Rgb565 => Some(pixels * 2), PixelFormat::Rgb24 | PixelFormat::Bgr24 => Some(pixels * 3), PixelFormat::Grey => Some(pixels), } } /// Get priority for format selection (higher is better) /// MJPEG is preferred for HDMI capture cards pub fn priority(&self) -> u8 { match self { PixelFormat::Mjpeg => 100, PixelFormat::Jpeg => 99, PixelFormat::Nv12 => 80, PixelFormat::Yuyv => 75, PixelFormat::Nv21 => 74, PixelFormat::Yuv420 => 70, PixelFormat::Uyvy => 65, PixelFormat::Yvyu => 64, PixelFormat::Yvu420 => 63, PixelFormat::Nv16 => 60, PixelFormat::Nv24 => 55, PixelFormat::Rgb24 => 50, PixelFormat::Bgr24 => 49, PixelFormat::Rgb565 => 40, PixelFormat::Grey => 10, } } /// Get recommended format for video encoding (WebRTC) /// /// Prefers NV12 over YUYV (matches [`Self::priority`]) /// /// Returns None if no suitable format is available pub fn recommended_for_encoding( available: &[PixelFormat], _is_hardware: bool, ) -> Option { if available.contains(&PixelFormat::Nv12) { return Some(PixelFormat::Nv12); } if available.contains(&PixelFormat::Yuyv) { return Some(PixelFormat::Yuyv); } // Fallback to any non-compressed format available.iter().find(|f| !f.is_compressed()).copied() } /// Get all supported formats pub fn all() -> &'static [PixelFormat] { &[ PixelFormat::Mjpeg, PixelFormat::Jpeg, PixelFormat::Yuyv, PixelFormat::Yvyu, PixelFormat::Uyvy, PixelFormat::Nv12, PixelFormat::Nv21, PixelFormat::Nv16, PixelFormat::Nv24, PixelFormat::Yuv420, PixelFormat::Yvu420, PixelFormat::Rgb565, PixelFormat::Rgb24, PixelFormat::Bgr24, PixelFormat::Grey, ] } } impl fmt::Display for PixelFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self { PixelFormat::Mjpeg => "MJPEG", PixelFormat::Jpeg => "JPEG", PixelFormat::Yuyv => "YUYV", PixelFormat::Yvyu => "YVYU", PixelFormat::Uyvy => "UYVY", PixelFormat::Nv12 => "NV12", PixelFormat::Nv21 => "NV21", PixelFormat::Nv16 => "NV16", PixelFormat::Nv24 => "NV24", PixelFormat::Yuv420 => "YUV420", PixelFormat::Yvu420 => "YVU420", PixelFormat::Rgb565 => "RGB565", PixelFormat::Rgb24 => "RGB24", PixelFormat::Bgr24 => "BGR24", PixelFormat::Grey => "GREY", }; write!(f, "{}", name) } } impl std::str::FromStr for PixelFormat { type Err = String; fn from_str(s: &str) -> Result { match s.to_uppercase().as_str() { "MJPEG" | "MJPG" => Ok(PixelFormat::Mjpeg), "JPEG" => Ok(PixelFormat::Jpeg), "YUYV" => Ok(PixelFormat::Yuyv), "YVYU" => Ok(PixelFormat::Yvyu), "UYVY" => Ok(PixelFormat::Uyvy), "NV12" => Ok(PixelFormat::Nv12), "NV21" => Ok(PixelFormat::Nv21), "NV16" => Ok(PixelFormat::Nv16), "NV24" => Ok(PixelFormat::Nv24), "YUV420" | "I420" => Ok(PixelFormat::Yuv420), "YVU420" | "YV12" => Ok(PixelFormat::Yvu420), "RGB565" => Ok(PixelFormat::Rgb565), "RGB24" => Ok(PixelFormat::Rgb24), "BGR24" => Ok(PixelFormat::Bgr24), "GREY" | "GRAY" => Ok(PixelFormat::Grey), _ => Err(format!("Unknown pixel format: {}", s)), } } } /// Resolution (width x height) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Resolution { pub width: u32, pub height: u32, } impl Resolution { pub fn new(width: u32, height: u32) -> Self { Self { width, height } } /// Check if resolution is valid pub fn is_valid(&self) -> bool { self.width >= 160 && self.width <= 15360 && self.height >= 120 && self.height <= 8640 } /// Get total pixels pub fn pixels(&self) -> u64 { self.width as u64 * self.height as u64 } /// Common resolutions. /// /// All constants here are 8-pixel aligned on both axes so they survive /// the `step=8` constraint imposed by most CSI bridge drivers (e.g. /// `rkcif` on Rockchip). If you add a new entry, make sure /// `width % 8 == 0 && height % 8 == 0`, otherwise the driver will /// silently round it at `S_FMT` time and the UI will report a /// different resolution than the one the user picked. pub const VGA: Resolution = Resolution { width: 640, height: 480, }; /// CEA-2/3 NTSC SD (480p) pub const NTSC: Resolution = Resolution { width: 720, height: 480, }; /// CEA-5/17/18 PAL SD (576p) pub const PAL: Resolution = Resolution { width: 720, height: 576, }; /// SVGA — legacy BIOS / POST output pub const SVGA: Resolution = Resolution { width: 800, height: 600, }; /// XGA — very common BIOS / server console output pub const XGA: Resolution = Resolution { width: 1024, height: 768, }; pub const HD720: Resolution = Resolution { width: 1280, height: 720, }; /// WXGA — older laptop panels pub const WXGA: Resolution = Resolution { width: 1280, height: 800, }; /// SXGA — 4:3 / 5:4 legacy desktop displays pub const SXGA: Resolution = Resolution { width: 1280, height: 1024, }; /// 1360×768 — fallback for 8-aligned "1366×768"-like panels pub const HDTV: Resolution = Resolution { width: 1360, height: 768, }; /// UXGA — industrial / 4:3 legacy displays pub const UXGA: Resolution = Resolution { width: 1600, height: 1200, }; pub const HD1080: Resolution = Resolution { width: 1920, height: 1080, }; /// WUXGA — 16:10 professional monitors pub const WUXGA: Resolution = Resolution { width: 1920, height: 1200, }; /// QHD / 2K — modern PC monitors pub const QHD: Resolution = Resolution { width: 2560, height: 1440, }; pub const UHD4K: Resolution = Resolution { width: 3840, height: 2160, }; } impl fmt::Display for Resolution { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}x{}", self.width, self.height) } } impl From<(u32, u32)> for Resolution { fn from((width, height): (u32, u32)) -> Self { Self { width, height } } }