mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
361 lines
11 KiB
Rust
361 lines
11 KiB
Rust
//! 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<Self> {
|
||
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<Self> {
|
||
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<usize> {
|
||
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<usize> {
|
||
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<PixelFormat> {
|
||
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<Self, Self::Err> {
|
||
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 }
|
||
}
|
||
}
|