#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "macos")]
use macos as platform;
#[cfg(target_os = "windows")]
use windows as platform;
use std::time::Instant;
use super::{
devices::CameraDevice,
video::{Pixels, VideoFrame, VideoSource, nv12_to_i420, yuyv_to_i420},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Device {
pub id: String,
pub name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Layout {
Yuyv,
Nv12,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Format {
pub width: u32,
pub height: u32,
pub fps: u32,
pub layout: Layout,
}
pub enum Wait {
Timeout,
Ended(String),
}
pub fn cameras() -> Vec<CameraDevice> {
platform::devices()
.into_iter()
.enumerate()
.map(|(i, device)| {
let detail = platform::formats(&device.id)
.ok()
.and_then(|formats| {
formats
.into_iter()
.max_by_key(|f| (f.width * f.height, f.fps))
})
.map(|f| format!("{}x{} @ {} fps", f.width, f.height, f.fps))
.unwrap_or_else(|| "no usable mode".into());
CameraDevice {
id: device.id,
name: device.name,
is_default: i == 0,
detail,
}
})
.collect()
}
fn choose(formats: &[Format], width: u32, height: u32, fps: u32) -> Option<Format> {
let want = width as i64 * height as i64;
let distance = |f: &Format| (f.width as i64 * f.height as i64 - want).abs();
let fast: Vec<&Format> = formats.iter().filter(|f| f.fps >= fps).collect();
let pool = if fast.is_empty() {
formats.iter().collect()
} else {
fast
};
pool.into_iter()
.min_by_key(|f| (distance(f), std::cmp::Reverse(f.fps)))
.copied()
}
pub struct Camera {
stream: platform::Stream,
format: Format,
opened: Instant,
y: Vec<u8>,
u: Vec<u8>,
v: Vec<u8>,
ended: bool,
}
impl Camera {
pub fn open(id: Option<&str>, width: u32, height: u32, fps: f32) -> Result<Self, String> {
let id = match id {
Some(id) => id.to_string(),
None => platform::devices()
.into_iter()
.next()
.map(|d| d.id)
.ok_or("no camera")?,
};
let formats = platform::formats(&id).map_err(|e| format!("cannot enumerate {id}: {e}"))?;
let format = choose(&formats, width, height, fps.round() as u32)
.ok_or_else(|| format!("{id} offers nothing usable"))?;
let stream = platform::Stream::open(&id, format).map_err(|e| format!("camera: {e}"))?;
tracing::info!(
"bevy_iroh: camera {id} at {}x{} {:?} @ {} fps",
format.width,
format.height,
format.layout,
format.fps
);
Ok(Self {
stream,
format,
opened: Instant::now(),
y: Vec::new(),
u: Vec::new(),
v: Vec::new(),
ended: false,
})
}
}
impl VideoSource for Camera {
fn next_frame(&mut self) -> Option<VideoFrame> {
if self.ended {
return None;
}
let (w, h) = (self.format.width as usize, self.format.height as usize);
let layout = self.format.layout;
let Self {
stream, y, u, v, ..
} = self;
let got = stream.next_frame(|bytes| match layout {
Layout::Yuyv => yuyv_to_i420(bytes, w, h, w * 2, y, u, v),
Layout::Nv12 => nv12_to_i420(bytes, w, h, w, y, u, v),
});
match got {
Ok(()) => {}
Err(Wait::Timeout) => return None,
Err(Wait::Ended(why)) => {
tracing::warn!("bevy_iroh: camera: {why}");
self.ended = true;
return None;
}
}
Some(VideoFrame {
width: self.format.width,
height: self.format.height,
pixels: Pixels::I420 {
y: std::mem::take(&mut self.y),
u: std::mem::take(&mut self.u),
v: std::mem::take(&mut self.v),
},
timestamp_ms: self.opened.elapsed().as_millis() as u64,
})
}
fn ended(&self) -> bool {
self.ended
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(width: u32, height: u32, fps: u32) -> Format {
Format {
width,
height,
fps,
layout: Layout::Yuyv,
}
}
#[test]
fn the_rate_is_reached_before_the_size_is_matched() {
let offered = [f(1920, 1080, 5), f(1280, 720, 30), f(640, 480, 30)];
assert_eq!(choose(&offered, 1920, 1080, 30), Some(f(1280, 720, 30)));
assert_eq!(choose(&offered, 1920, 1080, 5), Some(f(1920, 1080, 5)));
assert_eq!(choose(&offered, 640, 480, 60), Some(f(640, 480, 30)));
assert_eq!(choose(&[], 640, 480, 30), None);
}
}