mod camera;
mod error;
mod pod;
pub use camera::{Camera, Frame, Plane, QuitHandle, State};
pub use error::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Format {
Rgba,
Bgra,
Bgrx,
Rgbx,
Bgr,
Rgb,
I420,
Nv12,
Nv21,
Yuy2,
Uvyvy,
Grey,
}
const ALL_FORMATS: &[Format] = &[
Format::Rgba,
Format::Bgra,
Format::Bgrx,
Format::Rgbx,
Format::Bgr,
Format::Rgb,
Format::I420,
Format::Nv12,
Format::Nv21,
Format::Yuy2,
Format::Uvyvy,
Format::Grey,
];
impl Format {
fn video_format(self) -> libspa::param::video::VideoFormat {
use libspa::param::video::VideoFormat as V;
match self {
Format::Rgba => V::RGBA,
Format::Bgra => V::BGRA,
Format::Bgrx => V::BGRx,
Format::Rgbx => V::RGBx,
Format::Bgr => V::BGR,
Format::Rgb => V::RGB,
Format::I420 => V::I420,
Format::Nv12 => V::NV12,
Format::Nv21 => V::NV21,
Format::Yuy2 => V::YUY2,
Format::Uvyvy => V::UYVY,
Format::Grey => V::GRAY8,
}
}
pub fn planes(&self, width: u32, height: u32) -> Vec<(u32, u32)> {
match self {
Format::Rgba | Format::Bgra | Format::Bgrx | Format::Rgbx => {
vec![(width * 4, height)]
}
Format::Bgr | Format::Rgb => vec![(width * 3, height)],
Format::I420 => {
vec![
(width, height),
(width / 2, height / 2),
(width / 2, height / 2),
]
}
Format::Nv12 | Format::Nv21 => {
vec![(width, height), (width, height / 2)]
}
Format::Yuy2 | Format::Uvyvy => vec![(width * 2, height)],
Format::Grey => vec![(width, height)],
}
}
pub fn all() -> &'static [Format] {
ALL_FORMATS
}
pub fn spa_id(self) -> u32 {
self.video_format().as_raw()
}
fn from_spa_id(id: u32) -> Option<Format> {
ALL_FORMATS.iter().copied().find(|f| f.spa_id() == id)
}
pub fn as_str(self) -> &'static str {
match self {
Format::Rgba => "rgba",
Format::Bgra => "bgra",
Format::Bgrx => "bgrx",
Format::Rgbx => "rgbx",
Format::Bgr => "bgr",
Format::Rgb => "rgb",
Format::I420 => "i420",
Format::Nv12 => "nv12",
Format::Nv21 => "nv21",
Format::Yuy2 => "yuy2",
Format::Uvyvy => "uyvy",
Format::Grey => "grey",
}
}
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for Format {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
ALL_FORMATS
.iter()
.copied()
.find(|f| f.as_str() == s)
.ok_or_else(|| {
format!(
"unknown format \"{s}\" (try one of: {})",
ALL_FORMATS
.iter()
.map(|f| f.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})
}
}
#[derive(Clone, Debug)]
pub struct Mode {
pub width: u32,
pub height: u32,
pub fps: Vec<u32>,
pub formats: Vec<Format>,
}
#[derive(Clone, Debug)]
pub struct Config {
pub name: String,
pub media_name: String,
pub modes: Vec<Mode>,
pub max_buffers: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Negotiated {
pub format: Format,
pub width: u32,
pub height: u32,
pub fps_num: u32,
pub fps_denom: u32,
pub stride: u32,
pub node_id: u32,
}
impl Negotiated {
pub fn fps(&self) -> f64 {
self.fps_num as f64 / self.fps_denom.max(1) as f64
}
}
#[cfg(test)]
mod tests {
use super::{Format, QuitHandle, ALL_FORMATS};
#[test]
fn quit_handle_is_static() {
fn assert_static<T: 'static>() {}
assert_static::<QuitHandle>();
}
#[test]
fn format_spa_id_roundtrip() {
for f in ALL_FORMATS {
let back = Format::from_spa_id(f.spa_id());
assert_eq!(back, Some(*f), "from_spa_id({}) != {f:?}", f.spa_id());
}
}
#[test]
fn format_spa_ids_are_unique() {
for (i, a) in ALL_FORMATS.iter().enumerate() {
for b in ALL_FORMATS.iter().skip(i + 1) {
assert_ne!(
a.spa_id(),
b.spa_id(),
"{} and {} share spa_id {}",
a.spa_id(),
b.spa_id(),
a.spa_id()
);
}
}
}
#[test]
fn format_from_str_roundtrip() {
for f in ALL_FORMATS {
let parsed: Format = f.as_str().parse().unwrap_or_else(|e| panic!("{f:?}: {e}"));
assert_eq!(parsed, *f);
}
assert!("mjpg".parse::<Format>().is_err());
assert!("".parse::<Format>().is_err());
assert!("RGBA".parse::<Format>().is_err());
}
#[test]
fn from_spa_id_rejects_unknown() {
assert_eq!(Format::from_spa_id(0), None); assert_eq!(Format::from_spa_id(3), None); assert_eq!(Format::from_spa_id(9999), None);
assert_eq!(Format::from_spa_id(u32::MAX), None);
}
#[test]
fn format_planes_layout() {
let (w, h) = (1920u32, 1080u32);
assert_eq!(Format::Rgba.planes(w, h), vec![(w * 4, h)]);
assert_eq!(Format::Bgra.planes(w, h), vec![(w * 4, h)]);
assert_eq!(Format::Bgrx.planes(w, h), vec![(w * 4, h)]);
assert_eq!(Format::Rgbx.planes(w, h), vec![(w * 4, h)]);
assert_eq!(Format::Bgr.planes(w, h), vec![(w * 3, h)]);
assert_eq!(Format::Rgb.planes(w, h), vec![(w * 3, h)]);
assert_eq!(
Format::I420.planes(w, h),
vec![(w, h), (w / 2, h / 2), (w / 2, h / 2)]
);
assert_eq!(Format::Nv12.planes(w, h), vec![(w, h), (w, h / 2)]);
assert_eq!(Format::Nv21.planes(w, h), vec![(w, h), (w, h / 2)]);
assert_eq!(Format::Yuy2.planes(w, h), vec![(w * 2, h)]);
assert_eq!(Format::Uvyvy.planes(w, h), vec![(w * 2, h)]);
assert_eq!(Format::Grey.planes(w, h), vec![(w, h)]);
}
#[test]
fn planes_are_sane_for_all_formats() {
let (w, h) = (1920u32, 1080u32);
for f in ALL_FORMATS {
let planes = f.planes(w, h);
assert!(!planes.is_empty(), "{f:?} has no planes");
for (stride, ph) in planes {
assert_eq!(stride % 4, 0, "{f:?} stride {stride} not 4-aligned");
assert_eq!(h % ph, 0, "{f:?} height {h} not divisible by plane {ph}");
}
}
}
}