pub mod camera_control;
mod pixel_format;
mod stream;
pub use pixel_format::PixelFormat;
pub use stream::MmapBuffer;
use crate::v4l::camera_control::{CameraControlTrait, ControlType};
use kornia_image::{Image, ImageSize};
use v4l::{
buffer::Type, control::Value, video::capture::Parameters, video::Capture, Device, Timestamp,
};
use std::any::Any;
use std::sync::Arc;
use kornia_tensor::resource::{MemoryDomain, MemoryResource};
pub struct V4lResource {
buffer: MmapBuffer,
}
unsafe impl Send for V4lResource {}
unsafe impl Sync for V4lResource {}
impl MemoryResource for V4lResource {
fn as_ptr(&self) -> *mut u8 {
self.buffer.as_slice().as_ptr() as *mut u8
}
fn len_bytes(&self) -> usize {
self.buffer.len()
}
fn domain(&self) -> MemoryDomain {
MemoryDomain::Host
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
#[allow(dead_code)]
pub(crate) fn image_from_v4l_buffer(
size: kornia_image::ImageSize,
buffer: MmapBuffer,
) -> Result<kornia_image::Image<u8, 3>, kornia_image::ImageError> {
let data_ptr: *const u8 = buffer.as_slice().as_ptr();
let data_len: usize = buffer.len();
let expected_len = size
.width
.checked_mul(size.height)
.and_then(|n| n.checked_mul(3))
.ok_or(kornia_tensor::TensorError::InvalidShape(usize::MAX))?;
if data_len < expected_len {
return Err(kornia_image::ImageError::InvalidImageShape(
kornia_tensor::TensorError::InvalidShape(expected_len),
));
}
let resource = V4lResource { buffer };
let keepalive: Arc<dyn Any + Send + Sync> = Arc::new(resource);
let image = unsafe { Image::<u8, 3>::from_borrowed_host_readonly(size, data_ptr, keepalive)? };
Ok(image)
}
#[derive(Debug, thiserror::Error)]
pub enum V4L2Error {
#[error(transparent)]
ImageError(#[from] kornia_image::ImageError),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
pub struct V4LCameraConfig {
pub device_path: String,
pub size: ImageSize,
pub fps: u32,
pub format: PixelFormat,
pub buffer_size: u32,
}
impl Default for V4LCameraConfig {
fn default() -> Self {
Self {
device_path: "/dev/video0".to_string(),
size: ImageSize {
width: 640,
height: 480,
},
fps: 30,
format: PixelFormat::default(),
buffer_size: 4,
}
}
}
pub struct V4lVideoCapture {
stream: stream::MmapStream,
pixel_format: PixelFormat,
device: Device,
size: ImageSize,
}
pub struct EncodedFrame {
pub buffer: MmapBuffer,
pub size: ImageSize,
pub pixel_format: PixelFormat,
pub timestamp: Timestamp,
pub sequence: u32,
}
impl V4lVideoCapture {
pub fn new(config: V4LCameraConfig) -> Result<Self, V4L2Error> {
let device = Device::with_path(&config.device_path)?;
let mut format = device.format()?;
format.width = config.size.width as u32;
format.height = config.size.height as u32;
format.fourcc = config.format.to_fourcc();
device.set_format(&format)?;
let actual_format = device.format()?;
if actual_format.fourcc != format.fourcc {
eprintln!(
"Warning: Requested format {} not supported, using {}",
config.format,
PixelFormat::from_fourcc(actual_format.fourcc)
);
}
let actual_size = ImageSize {
width: actual_format.width as usize,
height: actual_format.height as usize,
};
if actual_size.width != config.size.width || actual_size.height != config.size.height {
eprintln!(
"Warning: Requested size {}x{} not supported, using {}x{}",
config.size.width, config.size.height, actual_size.width, actual_size.height
);
}
let params = Parameters::with_fps(config.fps);
device.set_params(¶ms)?;
let mut stream =
stream::MmapStream::with_buffers(&device, Type::VideoCapture, config.buffer_size)?;
stream.next_frame()?;
Ok(Self {
stream,
pixel_format: PixelFormat::from_fourcc(actual_format.fourcc),
device,
size: actual_size,
})
}
#[inline]
pub fn pixel_format(&self) -> PixelFormat {
self.pixel_format
}
#[inline]
pub fn size(&self) -> ImageSize {
self.size
}
pub fn set_control<T: CameraControlTrait>(&mut self, control: T) -> Result<(), V4L2Error> {
self.device
.set_control(v4l::Control {
id: control.control_id(),
value: match control.value() {
ControlType::Integer(value) => Value::Integer(value),
ControlType::Boolean(value) => Value::Boolean(value),
},
})
.map_err(V4L2Error::IoError)
}
pub fn grab_frame(&mut self) -> Result<Option<EncodedFrame>, V4L2Error> {
let Ok((buffer, metadata)) = self.stream.next_frame() else {
return Ok(None);
};
let frame = EncodedFrame {
buffer,
size: self.size,
pixel_format: self.pixel_format,
timestamp: metadata.timestamp,
sequence: metadata.sequence,
};
Ok(Some(frame))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v4l::stream::MmapInfo;
#[test]
fn v4l_resource_anonymous_mmap_drop_once() {
let page_size = 4096_usize;
let total_bytes = page_size;
let raw_ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
total_bytes,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert_ne!(
raw_ptr,
libc::MAP_FAILED,
"anonymous mmap failed: {}",
std::io::Error::last_os_error()
);
unsafe {
let bytes = std::slice::from_raw_parts_mut(raw_ptr as *mut u8, total_bytes);
for (i, b) in bytes.iter_mut().enumerate() {
*b = (i % 251) as u8; }
}
let mmap_info = Arc::new(MmapInfo {
ptr: raw_ptr as *mut u8,
length: total_bytes,
offset: 0,
});
let arc_weak = Arc::downgrade(&mmap_info);
const W: usize = 12;
const H: usize = 4;
const FRAME_BYTES: usize = W * H * 3;
assert!(FRAME_BYTES <= total_bytes, "frame fits in one page");
let buffer =
unsafe { MmapBuffer::new(raw_ptr as *const u8, FRAME_BYTES, mmap_info.clone()) };
drop(mmap_info);
assert!(
arc_weak.upgrade().is_some(),
"MmapInfo still alive via buffer"
);
let size = ImageSize {
width: W,
height: H,
};
let image = image_from_v4l_buffer(size, buffer)
.expect("image_from_v4l_buffer must succeed for a valid mmap region");
assert_eq!(image.width(), W);
assert_eq!(image.height(), H);
assert_eq!(image.num_channels(), 3);
let slice = image.as_slice();
assert_eq!(slice.len(), FRAME_BYTES);
for (i, &byte) in slice.iter().enumerate() {
assert_eq!(byte, (i % 251) as u8, "pixel mismatch at index {i}");
}
assert!(
arc_weak.upgrade().is_some(),
"MmapInfo must still be alive while Image is alive"
);
drop(image);
assert!(
arc_weak.upgrade().is_none(),
"MmapInfo must have been released (munmap'd) when Image was dropped"
);
}
#[test]
#[should_panic(expected = "read-only")]
fn v4l_captured_frame_readonly_rejects_as_mut_slice() {
let page_size = 4096_usize;
let raw_ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
page_size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert_ne!(raw_ptr, libc::MAP_FAILED, "mmap failed");
let mmap_info = Arc::new(MmapInfo {
ptr: raw_ptr as *mut u8,
length: page_size,
offset: 0,
});
const W: usize = 12;
const H: usize = 4;
const FRAME_BYTES: usize = W * H * 3;
let buffer = unsafe { MmapBuffer::new(raw_ptr as *const u8, FRAME_BYTES, mmap_info) };
let size = ImageSize {
width: W,
height: H,
};
let mut image = image_from_v4l_buffer(size, buffer)
.expect("image_from_v4l_buffer must succeed for a valid mmap region");
assert!(!image.as_slice().is_empty());
let _ = image.as_slice_mut();
}
#[test]
fn v4l_resource_memory_resource_accessors() {
let data: [u8; 9] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let mmap_info = Arc::new(MmapInfo {
ptr: std::ptr::null_mut(), length: 9,
offset: 0,
});
let buffer = unsafe { MmapBuffer::new(data.as_ptr(), 9, mmap_info) };
let resource = V4lResource { buffer };
assert!(!resource.as_ptr().is_null(), "as_ptr must be non-null");
assert_eq!(resource.len_bytes(), 9);
assert!(
matches!(resource.domain(), MemoryDomain::Host),
"V4L2 frames are host-accessible"
);
assert!(
resource.as_any().downcast_ref::<V4lResource>().is_some(),
"as_any downcast must succeed"
);
}
}