use std::collections::VecDeque;
use std::fs::File;
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
use std::time::Duration;
use v4l::v4l_sys::{
V4L2_BUF_FLAG_ERROR, V4L2_BUF_FLAG_LAST, V4L2_CAP_DEVICE_CAPS, V4L2_CAP_STREAMING, V4L2_CAP_VIDEO_M2M,
V4L2_CAP_VIDEO_M2M_MPLANE, V4L2_EVENT_SOURCE_CHANGE, V4L2_SEL_TGT_COMPOSE, timeval,
v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, v4l2_buffer,
v4l2_capability, v4l2_colorspace_V4L2_COLORSPACE_REC709, v4l2_colorspace_V4L2_COLORSPACE_SMPTE170M, v4l2_control,
v4l2_decoder_cmd, v4l2_encoder_cmd, v4l2_event, v4l2_event_subscription, v4l2_field_V4L2_FIELD_NONE, v4l2_fmtdesc,
v4l2_format, v4l2_memory_V4L2_MEMORY_MMAP, v4l2_plane, v4l2_quantization_V4L2_QUANTIZATION_FULL_RANGE,
v4l2_quantization_V4L2_QUANTIZATION_LIM_RANGE, v4l2_requestbuffers, v4l2_selection, v4l2_streamparm,
v4l2_xfer_func_V4L2_XFER_FUNC_709, v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_601, v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_709,
};
use v4l::v4l2::vidioc;
use crate::frame::I420;
use crate::{Color, Error, Size};
const fn fourcc(code: [u8; 4]) -> u32 {
u32::from_le_bytes(code)
}
pub(crate) const NV12: u32 = fourcc(*b"NV12");
pub(crate) const NV12M: u32 = fourcc(*b"NM12");
pub(crate) const YUV420: u32 = fourcc(*b"YU12");
pub(crate) const YUV420M: u32 = fourcc(*b"YM12");
pub(crate) const H264: u32 = fourcc(*b"H264");
pub(crate) const RAW: &[u32] = &[NV12, NV12M, YUV420, YUV420M];
const MAX_PLANES: usize = 3;
mod request {
use v4l::v4l_sys::{v4l2_decoder_cmd, v4l2_event, v4l2_event_subscription, v4l2_selection};
use v4l::v4l2::vidioc::_IOC_TYPE;
const READ: u32 = 2;
const WRITE: u32 = 1;
const fn code(dir: u32, nr: u32, size: usize) -> _IOC_TYPE {
((dir as _IOC_TYPE) << 30) | ((size as _IOC_TYPE) << 16) | ((b'V' as _IOC_TYPE) << 8) | nr as _IOC_TYPE
}
pub(super) const DQEVENT: _IOC_TYPE = code(READ, 89, size_of::<v4l2_event>());
pub(super) const SUBSCRIBE_EVENT: _IOC_TYPE = code(WRITE, 90, size_of::<v4l2_event_subscription>());
pub(super) const G_SELECTION: _IOC_TYPE = code(READ | WRITE, 94, size_of::<v4l2_selection>());
pub(super) const DECODER_CMD: _IOC_TYPE = code(READ | WRITE, 96, size_of::<v4l2_decoder_cmd>());
#[cfg(test)]
mod tests {
use v4l::v4l_sys::{v4l2_capability, v4l2_format};
use v4l::v4l2::vidioc;
use super::*;
#[test]
fn the_codes_are_built_the_way_the_crate_builds_its_own() {
assert_eq!(code(READ, 0, size_of::<v4l2_capability>()), vidioc::VIDIOC_QUERYCAP);
assert_eq!(code(WRITE, 18, size_of::<std::ffi::c_int>()), vidioc::VIDIOC_STREAMON);
assert_eq!(code(READ | WRITE, 5, size_of::<v4l2_format>()), vidioc::VIDIOC_S_FMT);
assert_eq!(DECODER_CMD, 0xc048_5660);
}
}
}
unsafe trait Arg: Sized {
fn zeroed() -> Self {
unsafe { std::mem::zeroed() }
}
}
unsafe impl Arg for v4l2_buffer {}
unsafe impl Arg for v4l2_capability {}
unsafe impl Arg for v4l2_decoder_cmd {}
unsafe impl Arg for v4l2_encoder_cmd {}
unsafe impl Arg for v4l2_event {}
unsafe impl Arg for v4l2_event_subscription {}
unsafe impl Arg for v4l2_fmtdesc {}
unsafe impl Arg for v4l2_format {}
unsafe impl Arg for v4l2_requestbuffers {}
unsafe impl Arg for v4l2_selection {}
unsafe impl Arg for v4l2_streamparm {}
unsafe impl Arg for [v4l2_plane; MAX_PLANES] {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Dir {
Output,
Capture,
}
impl Dir {
fn buf_type(self) -> u32 {
match self {
Dir::Output => v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
Dir::Capture => v4l2_buf_type_V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
}
}
}
pub(crate) struct Request {
pub pixelformat: u32,
pub size: Size,
pub sizeimage: Option<u32>,
pub color: Option<Color>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Plane {
pub stride: u32,
pub sizeimage: u32,
}
#[derive(Clone, Debug)]
pub(crate) struct Format {
pub pixelformat: u32,
pub size: Size,
pub planes: Vec<Plane>,
}
pub(crate) struct Role {
pub env: &'static str,
pub input: &'static [u32],
pub output: &'static [u32],
}
pub(crate) fn open(role: &Role) -> Result<Device, Error> {
if let Ok(path) = std::env::var(role.env) {
let device = Device::open(Path::new(&path))?;
device.check(role)?;
return Ok(device);
}
let mut nodes = v4l::context::enum_devices();
nodes.sort_by_key(v4l::context::Node::index);
let mut refused = Vec::new();
for node in nodes {
let path = node.path();
match Device::open(path).and_then(|device| device.check(role).map(|()| device)) {
Ok(device) => return Ok(device),
Err(err) => refused.push(format!("{}: {err}", path.display())),
}
}
Err(Error::Codec(anyhow::anyhow!(
"no V4L2 M2M node converts {} to {} (set {} to name one; tried {})",
join_fourcc(role.input),
join_fourcc(role.output),
role.env,
match refused.is_empty() {
true => "no nodes".to_owned(),
false => refused.join(", "),
}
)))
}
fn join_fourcc(codes: &[u32]) -> String {
codes.iter().copied().map(name).collect::<Vec<_>>().join("/")
}
pub(crate) fn name(code: u32) -> String {
String::from_utf8_lossy(&code.to_le_bytes()).into_owned()
}
pub(crate) struct Device {
file: File,
path: PathBuf,
}
impl Device {
fn open(path: &Path) -> Result<Self, Error> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)
.map_err(|err| Error::Codec(anyhow::anyhow!("{}: open: {err}", path.display())))?;
let device = Self {
file,
path: path.to_path_buf(),
};
let caps = device.capabilities()?;
if caps & V4L2_CAP_VIDEO_M2M_MPLANE == 0 {
let what = match caps & V4L2_CAP_VIDEO_M2M {
0 => "not an M2M device",
_ => "single-planar M2M only",
};
return Err(Error::Codec(anyhow::anyhow!("{}: {what}", path.display())));
}
if caps & V4L2_CAP_STREAMING == 0 {
return Err(Error::Codec(anyhow::anyhow!("{}: no streaming I/O", path.display())));
}
Ok(device)
}
fn check(&self, role: &Role) -> Result<(), Error> {
for (dir, wanted) in [(Dir::Output, role.input), (Dir::Capture, role.output)] {
let offered = self.formats(dir)?;
if !wanted.iter().any(|code| offered.contains(code)) {
return Err(Error::Codec(anyhow::anyhow!(
"offers {} where {} is needed",
join_fourcc(&offered),
join_fourcc(wanted)
)));
}
}
Ok(())
}
pub(crate) fn path(&self) -> &Path {
&self.path
}
unsafe fn ioctl<T>(&self, request: vidioc::_IOC_TYPE, arg: &mut T) -> std::io::Result<()> {
unsafe { v4l::v4l2::ioctl(self.file.as_raw_fd(), request, (arg as *mut T).cast()) }
}
fn err(&self, what: impl std::fmt::Display, err: std::io::Error) -> Error {
Error::Codec(anyhow::anyhow!("{}: {what}: {err}", self.path.display()))
}
fn capabilities(&self) -> Result<u32, Error> {
let mut caps = v4l2_capability::zeroed();
unsafe { self.ioctl(vidioc::VIDIOC_QUERYCAP, &mut caps) }.map_err(|err| self.err("QUERYCAP", err))?;
Ok(match caps.capabilities & V4L2_CAP_DEVICE_CAPS {
0 => caps.capabilities,
_ => caps.device_caps,
})
}
pub(crate) fn formats(&self, dir: Dir) -> Result<Vec<u32>, Error> {
let mut formats = Vec::new();
for index in 0.. {
let mut desc = v4l2_fmtdesc::zeroed();
desc.index = index;
desc.type_ = dir.buf_type();
if unsafe { self.ioctl(vidioc::VIDIOC_ENUM_FMT, &mut desc) }.is_err() {
break;
}
formats.push(desc.pixelformat);
}
Ok(formats)
}
pub(crate) fn set_format(&self, dir: Dir, request: &Request) -> Result<Format, Error> {
let mut format = v4l2_format::zeroed();
format.type_ = dir.buf_type();
unsafe { self.ioctl(vidioc::VIDIOC_G_FMT, &mut format) }.map_err(|err| self.err("G_FMT", err))?;
{
let pix = unsafe { &mut format.fmt.pix_mp };
pix.width = request.size.width;
pix.height = request.size.height;
pix.pixelformat = request.pixelformat;
pix.field = v4l2_field_V4L2_FIELD_NONE;
pix.num_planes = 1;
for plane in &mut pix.plane_fmt {
plane.bytesperline = 0;
plane.sizeimage = 0;
}
if let Some(sizeimage) = request.sizeimage {
pix.plane_fmt[0].sizeimage = sizeimage;
}
if let Some(color) = request.color {
let (colorspace, ycbcr) = match color {
Color::Bt601Limited | Color::Bt601Full => (
v4l2_colorspace_V4L2_COLORSPACE_SMPTE170M,
v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_601,
),
Color::Bt709Limited | Color::Bt709Full => (
v4l2_colorspace_V4L2_COLORSPACE_REC709,
v4l2_ycbcr_encoding_V4L2_YCBCR_ENC_709,
),
};
pix.colorspace = colorspace;
pix.__bindgen_anon_1.ycbcr_enc = ycbcr as u8;
pix.xfer_func = v4l2_xfer_func_V4L2_XFER_FUNC_709 as u8;
pix.quantization = match color.limited() {
true => v4l2_quantization_V4L2_QUANTIZATION_LIM_RANGE as u8,
false => v4l2_quantization_V4L2_QUANTIZATION_FULL_RANGE as u8,
};
}
}
unsafe { self.ioctl(vidioc::VIDIOC_S_FMT, &mut format) }.map_err(|err| self.err("S_FMT", err))?;
self.read_format(&format)
}
fn read_format(&self, format: &v4l2_format) -> Result<Format, Error> {
let pix = unsafe { &format.fmt.pix_mp };
let count = (pix.num_planes as usize).clamp(1, MAX_PLANES);
Ok(Format {
pixelformat: pix.pixelformat,
size: Size::new(pix.width, pix.height),
planes: pix.plane_fmt[..count]
.iter()
.map(|plane| Plane {
stride: plane.bytesperline,
sizeimage: plane.sizeimage,
})
.collect(),
})
}
pub(crate) fn format(&self, dir: Dir) -> Result<Format, Error> {
let mut format = v4l2_format::zeroed();
format.type_ = dir.buf_type();
unsafe { self.ioctl(vidioc::VIDIOC_G_FMT, &mut format) }.map_err(|err| self.err("G_FMT", err))?;
self.read_format(&format)
}
pub(crate) fn visible(&self, dir: Dir) -> Option<Rect> {
let mut selection = v4l2_selection::zeroed();
selection.type_ = dir.buf_type();
selection.target = V4L2_SEL_TGT_COMPOSE;
unsafe { self.ioctl(request::G_SELECTION, &mut selection) }.ok()?;
let rect = Rect {
left: selection.r.left.max(0) as u32,
top: selection.r.top.max(0) as u32,
size: Size::new(selection.r.width, selection.r.height),
};
match rect.size.width == 0 || rect.size.height == 0 {
true => None,
false => Some(rect),
}
}
pub(crate) fn subscribe_source_change(&self) -> Result<(), Error> {
let mut subscription = v4l2_event_subscription::zeroed();
subscription.type_ = V4L2_EVENT_SOURCE_CHANGE;
unsafe { self.ioctl(request::SUBSCRIBE_EVENT, &mut subscription) }
.map_err(|err| self.err("SUBSCRIBE_EVENT", err))
}
pub(crate) fn take_source_change(&self) -> bool {
let mut changed = false;
loop {
let mut event = v4l2_event::zeroed();
if unsafe { self.ioctl(request::DQEVENT, &mut event) }.is_err() {
return changed;
}
changed |= event.type_ == V4L2_EVENT_SOURCE_CHANGE;
}
}
pub(crate) fn control(&self, id: u32) -> Result<i32, Error> {
let mut control = v4l2_control { id, value: 0 };
unsafe { self.ioctl(vidioc::VIDIOC_G_CTRL, &mut control) }
.map_err(|err| self.err(format_args!("G_CTRL {id:#x}"), err))?;
Ok(control.value)
}
pub(crate) fn set_framerate(&self, dir: Dir, framerate: u32) -> Result<(), Error> {
let mut parm = v4l2_streamparm::zeroed();
parm.type_ = dir.buf_type();
let time_per_frame = unsafe {
match dir {
Dir::Output => &mut parm.parm.output.timeperframe,
Dir::Capture => &mut parm.parm.capture.timeperframe,
}
};
time_per_frame.numerator = 1;
time_per_frame.denominator = framerate;
unsafe { self.ioctl(vidioc::VIDIOC_S_PARM, &mut parm) }.map_err(|err| self.err("S_PARM", err))
}
pub(crate) fn encoder_cmd(&self, cmd: u32) -> Result<(), Error> {
let mut command = v4l2_encoder_cmd::zeroed();
command.cmd = cmd;
unsafe { self.ioctl(vidioc::VIDIOC_ENCODER_CMD, &mut command) }
.map_err(|err| self.err(format_args!("ENCODER_CMD {cmd}"), err))
}
pub(crate) fn decoder_cmd(&self, cmd: u32) -> Result<(), Error> {
let mut command = v4l2_decoder_cmd::zeroed();
command.cmd = cmd;
unsafe { self.ioctl(request::DECODER_CMD, &mut command) }
.map_err(|err| self.err(format_args!("DECODER_CMD {cmd}"), err))
}
pub(crate) fn set_control(&self, id: u32, value: i32) -> Result<(), Error> {
let mut control = v4l2_control { id, value };
unsafe { self.ioctl(vidioc::VIDIOC_S_CTRL, &mut control) }
.map_err(|err| self.err(format_args!("S_CTRL {id:#x} = {value}"), err))
}
pub(crate) fn try_control(&self, id: u32, value: i32) -> bool {
match self.set_control(id, value) {
Ok(()) => true,
Err(err) => {
tracing::debug!(control = format!("{id:#x}"), value, %err, "V4L2 control not supported");
false
}
}
}
fn request_buffers(&self, dir: Dir, count: u32) -> Result<u32, Error> {
let mut request = v4l2_requestbuffers::zeroed();
request.count = count;
request.type_ = dir.buf_type();
request.memory = v4l2_memory_V4L2_MEMORY_MMAP;
unsafe { self.ioctl(vidioc::VIDIOC_REQBUFS, &mut request) }
.map_err(|err| self.err(format_args!("REQBUFS {count}"), err))?;
Ok(request.count)
}
fn stream(&self, dir: Dir, on: bool) -> Result<(), Error> {
let mut buf_type = dir.buf_type();
let request = match on {
true => vidioc::VIDIOC_STREAMON,
false => vidioc::VIDIOC_STREAMOFF,
};
unsafe { self.ioctl(request, &mut buf_type) }.map_err(|err| {
let what = match on {
true => "STREAMON",
false => "STREAMOFF",
};
self.err(what, err)
})
}
pub(crate) fn wait(&self, timeout: Duration) {
let mut event = libc::pollfd {
fd: self.file.as_raw_fd(),
events: libc::POLLIN | libc::POLLOUT | libc::POLLPRI,
revents: 0,
};
let ready = unsafe { libc::poll(&mut event, 1, timeout.as_millis().min(i32::MAX as u128) as libc::c_int) };
if ready > 0 && event.revents & (libc::POLLIN | libc::POLLOUT | libc::POLLPRI) == 0 {
std::thread::sleep(timeout);
}
}
}
struct Mapping {
ptr: NonNull<u8>,
len: usize,
}
unsafe impl Send for Mapping {}
impl Drop for Mapping {
fn drop(&mut self) {
let _ = unsafe { v4l::v4l2::munmap(self.ptr.as_ptr().cast(), self.len) };
}
}
pub(crate) struct Queue {
dir: Dir,
format: Format,
buffers: Vec<Vec<Mapping>>,
free: VecDeque<u32>,
streaming: bool,
}
impl Queue {
pub(crate) fn alloc(device: &Device, dir: Dir, format: Format, count: u32) -> Result<Self, Error> {
let count = device.request_buffers(dir, count)?;
if count == 0 {
return Err(device.err("REQBUFS", std::io::Error::from(std::io::ErrorKind::OutOfMemory)));
}
let mut buffers = Vec::with_capacity(count as usize);
for index in 0..count {
let mut planes = zeroed_planes();
let mut buffer = new_buffer(dir, format.planes.len());
buffer.index = index;
buffer.m.planes = planes.as_mut_ptr();
unsafe { device.ioctl(vidioc::VIDIOC_QUERYBUF, &mut buffer) }
.map_err(|err| device.err(format_args!("QUERYBUF {index}"), err))?;
let mut mappings = Vec::with_capacity(format.planes.len());
for plane in &planes[..format.planes.len()] {
let len = plane.length as usize;
let ptr = unsafe {
v4l::v4l2::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
device.file.as_raw_fd(),
plane.m.mem_offset as libc::off_t,
)
}
.map_err(|err| device.err(format_args!("mmap buffer {index}"), err))?;
let ptr = NonNull::new(ptr.cast::<u8>())
.ok_or_else(|| device.err("mmap", std::io::Error::from(std::io::ErrorKind::InvalidData)))?;
unsafe { std::ptr::write_bytes(ptr.as_ptr(), 0, len) };
mappings.push(Mapping { ptr, len });
}
buffers.push(mappings);
}
Ok(Self {
dir,
format,
buffers,
free: (0..count).collect(),
streaming: false,
})
}
pub(crate) fn format(&self) -> &Format {
&self.format
}
pub(crate) fn take_free(&mut self) -> Option<u32> {
self.free.pop_front()
}
pub(crate) fn reclaim(&mut self, index: u32) {
self.free.push_back(index);
}
pub(crate) fn plane(&self, index: u32, plane: usize) -> &[u8] {
let mapping = &self.buffers[index as usize][plane];
unsafe { std::slice::from_raw_parts(mapping.ptr.as_ptr(), mapping.len) }
}
pub(crate) fn payload(&self, buffer: &Dequeued, plane: usize) -> &[u8] {
let mapping = self.plane(buffer.index, plane);
&mapping[(buffer.data_offset[plane] as usize).min(mapping.len())..]
}
pub(crate) fn plane_mut(&mut self, index: u32, plane: usize) -> &mut [u8] {
let mapping = &mut self.buffers[index as usize][plane];
unsafe { std::slice::from_raw_parts_mut(mapping.ptr.as_ptr(), mapping.len) }
}
pub(crate) fn queue(
&self,
device: &Device,
index: u32,
bytesused: &[u32],
timestamp: Duration,
) -> Result<(), Error> {
let mut planes = zeroed_planes();
for (index, mapping) in self.buffers[index as usize].iter().enumerate() {
planes[index].bytesused = bytesused.get(index).copied().unwrap_or(0);
planes[index].length = mapping.len as u32;
}
let mut buffer = new_buffer(self.dir, self.buffers[index as usize].len());
buffer.index = index;
buffer.m.planes = planes.as_mut_ptr();
buffer.timestamp.tv_sec = timestamp.as_secs() as _;
buffer.timestamp.tv_usec = timestamp.subsec_micros() as _;
unsafe { device.ioctl(vidioc::VIDIOC_QBUF, &mut buffer) }
.map_err(|err| device.err(format_args!("QBUF {index}"), err))
}
pub(crate) fn dequeue(&self, device: &Device) -> Result<Dequeue, Error> {
if !self.streaming {
return Ok(Dequeue::Empty);
}
let mut planes = zeroed_planes();
let mut buffer = new_buffer(self.dir, self.format.planes.len());
buffer.m.planes = planes.as_mut_ptr();
if let Err(err) = unsafe { device.ioctl(vidioc::VIDIOC_DQBUF, &mut buffer) } {
return match err.kind() {
std::io::ErrorKind::WouldBlock => Ok(Dequeue::Empty),
std::io::ErrorKind::BrokenPipe => Ok(Dequeue::Ended),
_ => Err(device.err("DQBUF", err)),
};
}
let mut bytesused = [0; MAX_PLANES];
let mut data_offset = [0; MAX_PLANES];
for (index, plane) in planes.iter().enumerate() {
bytesused[index] = plane.bytesused;
data_offset[index] = plane.data_offset;
}
Ok(Dequeue::Buffer(Dequeued {
index: buffer.index,
bytesused,
data_offset,
timestamp: timestamp(buffer.timestamp),
flags: buffer.flags,
}))
}
pub(crate) fn stream_on(&mut self, device: &Device) -> Result<(), Error> {
device.stream(self.dir, true)?;
self.streaming = true;
Ok(())
}
pub(crate) fn restart(&mut self, device: &Device) -> Result<(), Error> {
if self.streaming {
device.stream(self.dir, false)?;
self.streaming = false;
}
self.free = (0..self.buffers.len() as u32).collect();
self.stream_on(device)
}
pub(crate) fn streaming(&self) -> bool {
self.streaming
}
pub(crate) fn outstanding(&self) -> usize {
self.buffers.len() - self.free.len()
}
pub(crate) fn release(mut self, device: &Device) -> Result<(), Error> {
if self.streaming {
device.stream(self.dir, false)?;
self.streaming = false;
}
self.buffers.clear();
self.free.clear();
device.request_buffers(self.dir, 0).map(|_| ())
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Dequeue {
Buffer(Dequeued),
Empty,
Ended,
}
impl Dequeue {
pub(crate) fn buffer(self) -> Option<Dequeued> {
match self {
Dequeue::Buffer(buffer) => Some(buffer),
Dequeue::Empty | Dequeue::Ended => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Dequeued {
pub index: u32,
pub bytesused: [u32; MAX_PLANES],
data_offset: [u32; MAX_PLANES],
pub timestamp: Duration,
flags: u32,
}
impl Dequeued {
pub(crate) fn written(&self, plane: usize) -> u32 {
self.bytesused[plane].saturating_sub(self.data_offset[plane])
}
pub(crate) fn failed(&self) -> bool {
self.flags & V4L2_BUF_FLAG_ERROR != 0
}
pub(crate) fn last(&self) -> bool {
self.flags & V4L2_BUF_FLAG_LAST != 0
}
}
fn timestamp(time: timeval) -> Duration {
Duration::from_secs(time.tv_sec.max(0) as u64) + Duration::from_micros(time.tv_usec.max(0) as u64)
}
fn zeroed_planes() -> [v4l2_plane; MAX_PLANES] {
<[v4l2_plane; MAX_PLANES]>::zeroed()
}
fn new_buffer(dir: Dir, planes: usize) -> v4l2_buffer {
let mut buffer = v4l2_buffer::zeroed();
buffer.type_ = dir.buf_type();
buffer.memory = v4l2_memory_V4L2_MEMORY_MMAP;
buffer.length = planes as u32;
buffer
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Rect {
pub left: u32,
pub top: u32,
pub size: Size,
}
impl Rect {
pub(crate) fn whole(size: Size) -> Self {
Self { left: 0, top: 0, size }
}
}
pub(crate) struct Planes {
y: Component,
u: Component,
v: Option<Component>,
size: Size,
}
#[derive(Clone, Copy)]
struct Component {
plane: usize,
offset: usize,
stride: usize,
}
impl Planes {
pub(crate) fn new(format: &Format, rect: Rect) -> Result<Self, Error> {
let size = rect.size;
size.validate("V4L2 4:2:0 frame")?;
if rect.left + size.width > format.size.width || rect.top + size.height > format.size.height {
return Err(Error::Codec(anyhow::anyhow!(
"V4L2 negotiated {} for a {size} picture at {},{}",
format.size,
rect.left,
rect.top
)));
}
let (left, top) = (rect.left as usize, rect.top as usize);
let (chroma_left, chroma_top) = (left / 2, top / 2);
let interleaved = match format.pixelformat {
NV12 | NV12M => true,
YUV420 | YUV420M => false,
other => {
return Err(Error::Codec(anyhow::anyhow!(
"V4L2 chose the unsupported raw format {}",
name(other)
)));
}
};
let luma = *format
.planes
.first()
.ok_or_else(|| Error::Codec(anyhow::anyhow!("V4L2 reported a format with no planes")))?;
let stride = luma.stride.max(format.size.width) as usize;
let y = Component {
plane: 0,
offset: top * stride + left,
stride,
};
let separate = format.planes.len() > 1;
let rows = padded_rows(luma, format.size.height);
let (u, v) = match (interleaved, separate) {
(true, true) => {
let stride = format.planes[1].stride.max(format.size.width) as usize;
(
Component {
plane: 1,
offset: chroma_top * stride + left,
stride,
},
None,
)
}
(true, false) => (
Component {
plane: 0,
offset: stride * rows + chroma_top * stride + left,
stride,
},
None,
),
(false, true) if format.planes.len() >= 3 => {
let u_stride = format.planes[1].stride.max(format.size.width / 2) as usize;
let v_stride = format.planes[2].stride.max(format.size.width / 2) as usize;
(
Component {
plane: 1,
offset: chroma_top * u_stride + chroma_left,
stride: u_stride,
},
Some(Component {
plane: 2,
offset: chroma_top * v_stride + chroma_left,
stride: v_stride,
}),
)
}
(false, true) => {
return Err(Error::Codec(anyhow::anyhow!(
"V4L2 chose planar {} with {} planes",
name(format.pixelformat),
format.planes.len()
)));
}
(false, false) => {
let chroma_stride = stride / 2;
let chroma_origin = chroma_top * chroma_stride + chroma_left;
(
Component {
plane: 0,
offset: stride * rows + chroma_origin,
stride: chroma_stride,
},
Some(Component {
plane: 0,
offset: stride * rows + chroma_stride * rows.div_ceil(2) + chroma_origin,
stride: chroma_stride,
}),
)
}
};
Ok(Self { y, u, v, size })
}
pub(crate) fn write(&self, queue: &mut Queue, index: u32, frame: &I420) -> Result<(), Error> {
let (width, height) = (self.size.width as usize, self.size.height as usize);
let (chroma_width, chroma_rows) = (width / 2, height / 2);
scatter(queue.plane_mut(index, self.y.plane), self.y, frame.y(), width, height)?;
match self.v {
Some(v) => {
scatter(
queue.plane_mut(index, self.u.plane),
self.u,
frame.u(),
chroma_width,
chroma_rows,
)?;
scatter(queue.plane_mut(index, v.plane), v, frame.v(), chroma_width, chroma_rows)?;
}
None => interleave(
queue.plane_mut(index, self.u.plane),
self.u,
frame.u(),
frame.v(),
chroma_width,
chroma_rows,
)?,
}
Ok(())
}
pub(crate) fn read(&self, queue: &Queue, buffer: &Dequeued) -> Result<I420, Error> {
let (width, height) = (self.size.width as usize, self.size.height as usize);
let (chroma_width, chroma_rows) = (width / 2, height / 2);
let mut data = vec![0u8; I420::len(self.size.width, self.size.height)];
let (luma, chroma) = data.split_at_mut(width * height);
let (u, v) = chroma.split_at_mut(chroma_width * chroma_rows);
gather(luma, queue.payload(buffer, self.y.plane), self.y, width, height)?;
match self.v {
Some(at) => {
gather(
u,
queue.payload(buffer, self.u.plane),
self.u,
chroma_width,
chroma_rows,
)?;
gather(v, queue.payload(buffer, at.plane), at, chroma_width, chroma_rows)?;
}
None => deinterleave(
u,
v,
queue.payload(buffer, self.u.plane),
self.u,
chroma_width,
chroma_rows,
)?,
}
I420::new(self.size.width, self.size.height, data)
}
}
fn padded_rows(luma: Plane, height: u32) -> usize {
let height = height as usize;
match luma.stride as usize {
0 => height,
stride => (luma.sizeimage as usize * 2 / (stride * 3)).max(height),
}
}
fn scatter(dst: &mut [u8], at: Component, src: &[u8], width: usize, rows: usize) -> Result<(), Error> {
let len = dst.len();
for row in 0..rows {
let start = at.offset + row * at.stride;
dst.get_mut(start..start + width)
.ok_or_else(|| short(len, start + width))?
.copy_from_slice(&src[row * width..][..width]);
}
Ok(())
}
fn interleave(dst: &mut [u8], at: Component, u: &[u8], v: &[u8], width: usize, rows: usize) -> Result<(), Error> {
let len = dst.len();
for row in 0..rows {
let start = at.offset + row * at.stride;
let out = dst
.get_mut(start..start + width * 2)
.ok_or_else(|| short(len, start + width * 2))?;
let (u, v) = (&u[row * width..][..width], &v[row * width..][..width]);
for (pair, (u, v)) in out.chunks_exact_mut(2).zip(u.iter().zip(v)) {
pair[0] = *u;
pair[1] = *v;
}
}
Ok(())
}
fn gather(dst: &mut [u8], src: &[u8], at: Component, width: usize, rows: usize) -> Result<(), Error> {
for row in 0..rows {
let start = at.offset + row * at.stride;
let line = src
.get(start..start + width)
.ok_or_else(|| short(src.len(), start + width))?;
dst[row * width..][..width].copy_from_slice(line);
}
Ok(())
}
fn deinterleave(u: &mut [u8], v: &mut [u8], src: &[u8], at: Component, width: usize, rows: usize) -> Result<(), Error> {
for row in 0..rows {
let start = at.offset + row * at.stride;
let line = src
.get(start..start + width * 2)
.ok_or_else(|| short(src.len(), start + width * 2))?;
let (u, v) = (&mut u[row * width..][..width], &mut v[row * width..][..width]);
for (pair, (u, v)) in line.chunks_exact(2).zip(u.iter_mut().zip(v)) {
*u = pair[0];
*v = pair[1];
}
}
Ok(())
}
fn short(len: usize, needed: usize) -> Error {
Error::Codec(anyhow::anyhow!(
"V4L2 buffer of {len} bytes is too small for the {needed} its format implies"
))
}
#[cfg(test)]
mod tests {
use super::*;
fn format(pixelformat: u32, size: Size, stride: u32, rows: u32) -> Format {
Format {
pixelformat,
size,
planes: vec![Plane {
stride,
sizeimage: stride * rows * 3 / 2,
}],
}
}
#[test]
fn a_buffer_timestamp_survives_any_timeval() {
assert_eq!(
timestamp(timeval {
tv_sec: 12,
tv_usec: 345_678,
}),
Duration::from_micros(12_345_678)
);
assert_eq!(
timestamp(timeval {
tv_sec: 0,
tv_usec: 5_000_000,
}),
Duration::from_secs(5)
);
assert_eq!(
timestamp(timeval {
tv_sec: -1,
tv_usec: -1,
}),
Duration::ZERO
);
}
#[test]
fn the_payload_is_past_the_data_offset() {
let buffer = Dequeued {
index: 0,
bytesused: [10, 0, 0],
data_offset: [4, 0, 0],
timestamp: Duration::ZERO,
flags: 0,
};
assert_eq!(buffer.written(0), 6);
let buffer = Dequeued {
data_offset: [16, 0, 0],
..buffer
};
assert_eq!(buffer.written(0), 0);
}
#[test]
fn chroma_follows_the_padded_height() {
let planes = Planes::new(
&format(NV12, Size::new(640, 368), 640, 368),
Rect::whole(Size::new(640, 360)),
)
.unwrap();
assert_eq!(planes.u.offset, 640 * 368);
assert!(planes.v.is_none());
}
#[test]
fn chroma_follows_the_padded_stride() {
let planes = Planes::new(
&format(YUV420, Size::new(360, 240), 384, 240),
Rect::whole(Size::new(360, 240)),
)
.unwrap();
assert_eq!(planes.y.stride, 384);
assert_eq!(planes.u.offset, 384 * 240);
assert_eq!(planes.u.stride, 192);
let v = planes.v.unwrap();
assert_eq!(v.offset, 384 * 240 + 192 * 120);
assert_eq!(v.stride, 192);
}
#[test]
fn the_picture_starts_at_the_compose_origin() {
let rect = Rect {
left: 16,
top: 8,
size: Size::new(1888, 1072),
};
let planes = Planes::new(&format(NV12, Size::new(1920, 1088), 1920, 1088), rect).unwrap();
assert_eq!(planes.y.offset, 8 * 1920 + 16);
assert_eq!(planes.u.offset, 1920 * 1088 + 4 * 1920 + 16);
let planes = Planes::new(&format(YUV420, Size::new(1920, 1088), 1920, 1088), rect).unwrap();
assert_eq!(planes.y.offset, 8 * 1920 + 16);
assert_eq!(planes.u.offset, 1920 * 1088 + 4 * 960 + 8);
assert_eq!(planes.v.unwrap().offset, 1920 * 1088 + 960 * 544 + 4 * 960 + 8);
}
#[test]
fn separate_planes_start_at_zero() {
let format = Format {
pixelformat: NV12M,
size: Size::new(320, 240),
planes: vec![
Plane {
stride: 320,
sizeimage: 320 * 240,
},
Plane {
stride: 320,
sizeimage: 320 * 120,
},
],
};
let planes = Planes::new(&format, Rect::whole(Size::new(320, 240))).unwrap();
assert_eq!(planes.u.plane, 1);
assert_eq!(planes.u.offset, 0);
}
#[test]
fn a_picture_larger_than_the_format_is_refused() {
let format = format(NV12, Size::new(320, 240), 320, 240);
assert!(Planes::new(&format, Rect::whole(Size::new(640, 480))).is_err());
assert!(
Planes::new(
&format,
Rect {
left: 16,
top: 0,
size: Size::new(320, 240)
}
)
.is_err()
);
}
#[test]
fn an_unsupported_raw_format_is_refused() {
let format = format(fourcc(*b"RGB3"), Size::new(320, 240), 960, 240);
assert!(Planes::new(&format, Rect::whole(Size::new(320, 240))).is_err());
}
#[test]
fn writing_respects_the_stride() {
let width = 4;
let height = 4;
let stride = 8;
let at = Component {
plane: 0,
offset: 0,
stride,
};
let mut dst = vec![0u8; stride * height];
let src: Vec<u8> = (0..(width * height) as u8).collect();
scatter(&mut dst, at, &src, width, height).unwrap();
assert_eq!(&dst[..width], &src[..width]);
assert_eq!(&dst[stride..stride + width], &src[width..width * 2]);
assert_eq!(&dst[width..stride], &[0; 4]);
let mut chroma = vec![0u8; stride * height];
interleave(&mut chroma, at, &[1, 2], &[3, 4], 2, 1).unwrap();
assert_eq!(&chroma[..4], &[1, 3, 2, 4]);
}
#[test]
fn reading_undoes_writing() {
let (width, rows, stride) = (4, 4, 8);
let at = Component {
plane: 0,
offset: 16,
stride,
};
let luma: Vec<u8> = (0..(width * rows) as u8).collect();
let mut device = vec![0u8; at.offset + stride * rows];
scatter(&mut device, at, &luma, width, rows).unwrap();
let mut back = vec![0u8; width * rows];
gather(&mut back, &device, at, width, rows).unwrap();
assert_eq!(back, luma);
let (u, v) = (vec![1, 2, 3, 4], vec![5, 6, 7, 8]);
let mut device = vec![0u8; at.offset + stride * rows];
interleave(&mut device, at, &u, &v, 2, 2).unwrap();
let (mut back_u, mut back_v) = (vec![0u8; 4], vec![0u8; 4]);
deinterleave(&mut back_u, &mut back_v, &device, at, 2, 2).unwrap();
assert_eq!(back_u, u);
assert_eq!(back_v, v);
}
#[test]
fn a_short_buffer_errors() {
let at = Component {
plane: 0,
offset: 0,
stride: 8,
};
let mut dst = vec![0u8; 8];
assert!(scatter(&mut dst, at, &[0; 16], 4, 4).is_err());
let mut back = vec![0u8; 16];
assert!(gather(&mut back, &[0; 8], at, 4, 4).is_err());
}
}