use v4l::buffer::Type as BufType;
use v4l::io::mmap::Stream as MmapStream;
use v4l::io::traits::CaptureStream;
use v4l::video::Capture;
use v4l::video::capture::Parameters;
use v4l::{Device, Format, FourCC};
use zune_jpeg::zune_core::bytestream::ZCursor;
use super::channel::FrameChannel;
use super::pump::{self, Geometry};
use super::{Config, FrameStream};
use crate::Error;
use crate::frame::{Frame, I420};
pub(super) async fn open(config: &Config, device: Option<&str>) -> Result<FrameStream, Error> {
let config = config.clone();
let device = device.map(str::to_string);
let chan = FrameChannel::new();
let (geo, guard) = pump::spawn(
chan.clone(),
move || {
let camera = Camera::open(&config, device.as_deref())?;
let geometry = Geometry {
width: camera.width,
height: camera.height,
framerate: camera.framerate,
device: camera.name.clone(),
};
Ok((camera, geometry))
},
Camera::read,
)
.await?;
Ok(FrameStream::new(
chan,
geo.width,
geo.height,
geo.framerate,
geo.device,
None,
Box::new(guard),
))
}
const DEFAULT_WIDTH: u32 = 1280;
const DEFAULT_HEIGHT: u32 = 720;
const BUFFER_COUNT: u32 = 4;
const FOURCC_YUYV: &[u8; 4] = b"YUYV";
const FOURCC_MJPG: &[u8; 4] = b"MJPG";
#[derive(Clone, Copy)]
enum Source {
Yuyv,
Mjpeg,
}
pub(crate) struct Camera {
stream: MmapStream<'static>,
source: Source,
width: u32,
height: u32,
stride: u32,
framerate: Option<u32>,
name: String,
}
impl Camera {
fn open(config: &Config, selector: Option<&str>) -> Result<Self, Error> {
let (device, name) = open_device(selector)?;
let width = config.width.unwrap_or(DEFAULT_WIDTH);
let height = config.height.unwrap_or(DEFAULT_HEIGHT);
let format = negotiate(&device, width, height)?;
let source = match &format.fourcc.repr {
f if f == FOURCC_YUYV => Source::Yuyv,
f if f == FOURCC_MJPG => Source::Mjpeg,
other => {
return Err(Error::Codec(anyhow::anyhow!(
"camera {name} supports neither YUYV nor MJPEG (negotiated {})",
FourCC::new(other)
)));
}
};
let (width, height, stride) = (format.width, format.height, format.stride);
if width % 2 != 0 || height % 2 != 0 {
return Err(Error::Codec(anyhow::anyhow!(
"camera resolution {width}x{height} must be even for H.264 encoding"
)));
}
if let Some(fps) = config.framerate {
let _ = Capture::set_params(&device, &Parameters::with_fps(fps));
}
let framerate = Capture::params(&device).ok().and_then(|p| {
(p.interval.numerator != 0).then(|| (p.interval.denominator / p.interval.numerator).max(1))
});
let stream = MmapStream::with_buffers(&device, BufType::VideoCapture, BUFFER_COUNT)
.map_err(|e| Error::Codec(anyhow::anyhow!("V4L2 stream init: {e}")))?;
tracing::info!(device = %name, width, height, "opened V4L2 capture");
Ok(Self {
stream,
source,
width,
height,
stride,
framerate,
name,
})
}
fn read(&mut self) -> Result<Option<Frame>, Error> {
let (buf, meta) =
CaptureStream::next(&mut self.stream).map_err(|e| Error::Codec(anyhow::anyhow!("V4L2 capture: {e}")))?;
let i420 = match self.source {
Source::Yuyv => I420::from_yuyv(buf, self.stride, self.width, self.height)?,
Source::Mjpeg => {
let jpeg = buf.get(..meta.bytesused as usize).unwrap_or(buf);
let mut decoder = zune_jpeg::JpegDecoder::new(ZCursor::new(jpeg));
let rgb = decoder
.decode()
.map_err(|e| Error::Codec(anyhow::anyhow!("MJPEG decode: {e:?}")))?;
let (w, h) = decoder
.dimensions()
.ok_or_else(|| Error::Codec(anyhow::anyhow!("MJPEG frame had no dimensions")))?;
I420::from_rgb(&rgb, w as u32, h as u32)?
}
};
Ok(Some(Frame::I420(i420)))
}
}
fn open_device(device: Option<&str>) -> Result<(Device, String), Error> {
match device {
None => {
let device = Device::new(0).map_err(|e| Error::Codec(anyhow::anyhow!("open /dev/video0: {e}")))?;
Ok((device, "/dev/video0".to_string()))
}
Some(spec) => match spec.parse::<usize>() {
Ok(index) => {
let device =
Device::new(index).map_err(|e| Error::Codec(anyhow::anyhow!("open /dev/video{index}: {e}")))?;
Ok((device, format!("/dev/video{index}")))
}
Err(_) => {
let device = Device::with_path(spec).map_err(|e| Error::Codec(anyhow::anyhow!("open {spec}: {e}")))?;
Ok((device, spec.to_string()))
}
},
}
}
fn negotiate(device: &Device, width: u32, height: u32) -> Result<Format, Error> {
let mut last = None;
for want in [FOURCC_YUYV, FOURCC_MJPG] {
let requested = Format::new(width, height, FourCC::new(want));
let got = Capture::set_format(device, &requested)
.map_err(|e| Error::Codec(anyhow::anyhow!("V4L2 set format: {e}")))?;
if &got.fourcc.repr == FOURCC_YUYV || &got.fourcc.repr == FOURCC_MJPG {
return Ok(got);
}
last = Some(got.fourcc);
}
Err(Error::Codec(anyhow::anyhow!(
"camera supports neither YUYV nor MJPEG (negotiated {last:?})"
)))
}