use crate::backend::Backend;
#[cfg(feature = "controls")]
use crate::backend::BackendControls;
use crate::camera::Camera;
#[cfg(feature = "controls")]
use crate::controls::{
ControlCapabilities, ControlRange, Controls, PowerLineFrequency, PowerLineFrequencyCapability,
};
use crate::error::Error;
use crate::monitor::DeviceMonitor;
use crate::types::{
Capabilities, Device, DeviceId, FormatDescriptor, Frame, FramerateRange, PixelFormat, Position,
Resolution, StreamConfig, Transport,
};
use bytes::Bytes;
use crossbeam_channel::Sender;
use std::collections::HashMap;
use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use v4l::Device as V4lDevice;
use v4l::buffer::Type as BufferType;
use v4l::context;
use v4l::format::Format as V4lFormat;
use v4l::format::FourCC;
use v4l::frameinterval::FrameIntervalEnum;
use v4l::io::mmap::stream::Stream as MmapStream;
use v4l::io::traits::CaptureStream;
use v4l::video::Capture;
pub struct SessionHandle {
shutdown: Arc<AtomicBool>,
worker: Option<std::thread::JoinHandle<()>>,
}
impl Drop for SessionHandle {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::Relaxed);
if let Some(handle) = self.worker.take() {
let _ = handle.join();
}
}
}
pub struct Driver;
impl Backend for Driver {
type SessionHandle = SessionHandle;
fn devices() -> Result<Vec<Device>, Error> {
let entries = context::enum_devices();
let mut result = Vec::with_capacity(entries.len());
for entry in entries {
let name = entry
.name()
.unwrap_or_else(|| format!("/dev/video{}", entry.index()));
let path = entry.path().to_str().unwrap_or("").to_string();
if path.is_empty() {
continue;
}
result.push(Device {
id: DeviceId(path),
name,
position: Position::External,
transport: Transport::Usb,
});
}
Ok(result)
}
fn probe(id: &DeviceId) -> Result<Capabilities, Error> {
let device = V4lDevice::with_path(&id.0).map_err(map_io_error)?;
let format_descriptions = device.enum_formats().map_err(map_io_error)?;
let mut descriptors = Vec::new();
for description in format_descriptions {
let pixel_format = fourcc_to_pixel_format(&description.fourcc);
let sizes = device
.enum_framesizes(description.fourcc)
.map_err(map_io_error)?;
for framesize in sizes {
for discrete in framesize.size.to_discrete() {
let intervals = device
.enum_frameintervals(framesize.fourcc, discrete.width, discrete.height)
.map_err(map_io_error)?;
for interval in intervals {
let (min_fps, max_fps) = interval_to_fps_range(&interval.interval);
descriptors.push(FormatDescriptor {
resolution: Resolution {
width: discrete.width,
height: discrete.height,
},
framerate_range: FramerateRange {
min: min_fps,
max: max_fps,
},
pixel_format,
});
}
}
}
}
Ok(Capabilities {
formats: descriptors,
})
}
fn open(id: &DeviceId, config: StreamConfig) -> Result<Camera, Error> {
let device = V4lDevice::with_path(&id.0).map_err(map_io_error)?;
let requested_fourcc = pixel_format_to_fourcc(config.pixel_format);
let target_format = V4lFormat::new(
config.resolution.width,
config.resolution.height,
requested_fourcc,
);
let applied = device.set_format(&target_format).map_err(map_io_error)?;
let applied_pixel_format = fourcc_to_pixel_format(&applied.fourcc);
let width = applied.width;
let height = applied.height;
let stride = applied.stride;
let (frame_tx, frame_rx) = crossbeam_channel::bounded::<Result<Frame, Error>>(3);
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_for_worker = Arc::clone(&shutdown);
let worker = std::thread::Builder::new()
.name("cameras-v4l".into())
.spawn(move || {
stream_loop(
device,
frame_tx,
shutdown_for_worker,
applied_pixel_format,
width,
height,
stride,
);
})
.map_err(|error| Error::Backend {
platform: "linux",
message: error.to_string(),
})?;
Ok(Camera {
config: StreamConfig {
resolution: Resolution { width, height },
framerate: config.framerate,
pixel_format: applied_pixel_format,
},
frame_rx,
handle: crate::camera::Handle::Native(SessionHandle {
shutdown,
worker: Some(worker),
}),
})
}
fn monitor() -> Result<DeviceMonitor, Error> {
let (event_tx, event_rx) = crossbeam_channel::unbounded();
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_for_thread = Arc::clone(&shutdown);
let initial = Self::devices()?;
for device in &initial {
let _ = event_tx.send(crate::types::DeviceEvent::Added(device.clone()));
}
let worker = std::thread::Builder::new()
.name("cameras-monitor".into())
.spawn(move || {
let mut known: HashMap<DeviceId, Device> = initial
.into_iter()
.map(|device| (device.id.clone(), device))
.collect();
let interval = Duration::from_millis(1000);
while !shutdown_for_thread.load(Ordering::Relaxed) {
std::thread::sleep(interval);
if shutdown_for_thread.load(Ordering::Relaxed) {
break;
}
let Ok(current) = Self::devices() else {
continue;
};
let current_map: HashMap<DeviceId, Device> = current
.into_iter()
.map(|device| (device.id.clone(), device))
.collect();
for (id, device) in ¤t_map {
if !known.contains_key(id) {
let _ = event_tx.send(crate::types::DeviceEvent::Added(device.clone()));
}
}
let removed: Vec<DeviceId> = known
.keys()
.filter(|id| !current_map.contains_key(id))
.cloned()
.collect();
for id in removed {
let _ = event_tx.send(crate::types::DeviceEvent::Removed(id.clone()));
known.remove(&id);
}
for (id, device) in current_map {
known.insert(id, device);
}
}
})
.map_err(|error| Error::Backend {
platform: "linux",
message: error.to_string(),
})?;
Ok(DeviceMonitor {
event_rx,
shutdown,
worker: Some(worker),
})
}
}
fn stream_loop(
device: V4lDevice,
sender: Sender<Result<Frame, Error>>,
shutdown: Arc<AtomicBool>,
pixel_format: PixelFormat,
width: u32,
height: u32,
stride: u32,
) {
let mut stream = match MmapStream::with_buffers(&device, BufferType::VideoCapture, 4) {
Ok(stream) => stream,
Err(error) => {
let _ = sender.try_send(Err(Error::Backend {
platform: "linux",
message: error.to_string(),
}));
return;
}
};
while !shutdown.load(Ordering::Relaxed) {
match stream.next() {
Ok((buffer, _meta)) => {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let (plane_primary, plane_secondary) =
split_planes(buffer, pixel_format, width, height, stride);
let frame = Frame {
width,
height,
stride,
timestamp,
pixel_format,
quality: crate::types::FrameQuality::Intact,
plane_primary,
plane_secondary,
};
let _ = sender.try_send(Ok(frame));
}
Err(error) => {
let _ = sender.try_send(Err(Error::Backend {
platform: "linux",
message: error.to_string(),
}));
break;
}
}
}
}
fn split_planes(
buffer: &[u8],
pixel_format: PixelFormat,
width: u32,
height: u32,
stride: u32,
) -> (Bytes, Bytes) {
if pixel_format == PixelFormat::Nv12 {
let y_stride = if stride == 0 { width } else { stride } as usize;
let y_size = y_stride * height as usize;
if buffer.len() >= y_size {
let primary = Bytes::copy_from_slice(&buffer[..y_size]);
let secondary = Bytes::copy_from_slice(&buffer[y_size..]);
return (primary, secondary);
}
}
(Bytes::copy_from_slice(buffer), Bytes::new())
}
fn fourcc_to_pixel_format(fourcc: &FourCC) -> PixelFormat {
match &fourcc.repr {
b"MJPG" | b"JPEG" => PixelFormat::Mjpeg,
b"YUYV" | b"YUY2" => PixelFormat::Yuyv,
b"NV12" => PixelFormat::Nv12,
b"RGB3" | b"RGB\0" => PixelFormat::Rgb8,
b"BGR3" => PixelFormat::Bgra8,
b"AB24" | b"RGBA" => PixelFormat::Rgba8,
b"BA24" | b"BGRA" => PixelFormat::Bgra8,
_ => PixelFormat::Mjpeg,
}
}
fn pixel_format_to_fourcc(format: PixelFormat) -> FourCC {
match format {
PixelFormat::Mjpeg => FourCC::new(b"MJPG"),
PixelFormat::Yuyv => FourCC::new(b"YUYV"),
PixelFormat::Nv12 => FourCC::new(b"NV12"),
PixelFormat::Rgb8 => FourCC::new(b"RGB3"),
PixelFormat::Rgba8 => FourCC::new(b"AB24"),
PixelFormat::Bgra8 => FourCC::new(b"BA24"),
}
}
fn interval_to_fps_range(interval: &FrameIntervalEnum) -> (f64, f64) {
match interval {
FrameIntervalEnum::Discrete(value) => {
let fps = value.denominator as f64 / value.numerator.max(1) as f64;
(fps, fps)
}
FrameIntervalEnum::Stepwise(range) => {
let max_fps = range.min.denominator as f64 / range.min.numerator.max(1) as f64;
let min_fps = range.max.denominator as f64 / range.max.numerator.max(1) as f64;
(min_fps, max_fps)
}
}
}
fn map_io_error(error: io::Error) -> Error {
match error.kind() {
io::ErrorKind::PermissionDenied => Error::PermissionDenied,
io::ErrorKind::NotFound => Error::DeviceNotFound(error.to_string()),
_ => Error::Backend {
platform: "linux",
message: error.to_string(),
},
}
}
#[cfg(feature = "controls")]
impl BackendControls for Driver {
fn control_capabilities(id: &DeviceId) -> Result<ControlCapabilities, Error> {
let device = V4lDevice::with_path(&id.0).map_err(map_io_error)?;
let descriptions = device.query_controls().map_err(map_io_error)?;
let mut caps = ControlCapabilities::default();
for description in descriptions.iter() {
if description_is_unavailable(description) {
continue;
}
populate_capability(&mut caps, description);
}
Ok(caps)
}
fn read_controls(id: &DeviceId) -> Result<Controls, Error> {
let device = V4lDevice::with_path(&id.0).map_err(map_io_error)?;
let descriptions = device.query_controls().map_err(map_io_error)?;
let mut result = Controls::default();
for description in descriptions.iter() {
if description_is_unavailable(description) {
continue;
}
let Ok(control) = device.control(description.id) else {
continue;
};
populate_control_value(&mut result, description.id, &control.value);
}
Ok(result)
}
fn apply_controls(id: &DeviceId, controls: &Controls) -> Result<(), Error> {
let device = V4lDevice::with_path(&id.0).map_err(map_io_error)?;
let descriptions = device.query_controls().map_err(map_io_error)?;
let auto_batch = collect_auto_mode_writes(controls, &descriptions)?;
if !auto_batch.is_empty() {
device.set_controls(auto_batch).map_err(map_io_error)?;
}
let value_batch = collect_value_writes(controls, &descriptions)?;
if !value_batch.is_empty() {
device.set_controls(value_batch).map_err(map_io_error)?;
}
Ok(())
}
}
#[cfg(feature = "controls")]
use v4l::control::{Control as V4lControl, Description, Flags as V4lFlags, Value as V4lValue};
#[cfg(feature = "controls")]
use v4l::v4l_sys::{
V4L2_CID_AUTO_WHITE_BALANCE, V4L2_CID_BACKLIGHT_COMPENSATION, V4L2_CID_BRIGHTNESS,
V4L2_CID_CONTRAST, V4L2_CID_EXPOSURE_ABSOLUTE, V4L2_CID_EXPOSURE_AUTO, V4L2_CID_FOCUS_ABSOLUTE,
V4L2_CID_FOCUS_AUTO, V4L2_CID_GAIN, V4L2_CID_PAN_ABSOLUTE, V4L2_CID_POWER_LINE_FREQUENCY,
V4L2_CID_SATURATION, V4L2_CID_SHARPNESS, V4L2_CID_TILT_ABSOLUTE,
V4L2_CID_WHITE_BALANCE_TEMPERATURE, V4L2_CID_ZOOM_ABSOLUTE,
};
#[cfg(feature = "controls")]
const V4L2_EXPOSURE_AUTO_FULL: i64 = 0;
#[cfg(feature = "controls")]
const V4L2_EXPOSURE_AUTO_MANUAL: i64 = 1;
#[cfg(feature = "controls")]
const V4L2_POWER_LINE_FREQUENCY_DISABLED: i64 = 0;
#[cfg(feature = "controls")]
const V4L2_POWER_LINE_FREQUENCY_HZ50: i64 = 1;
#[cfg(feature = "controls")]
const V4L2_POWER_LINE_FREQUENCY_HZ60: i64 = 2;
#[cfg(feature = "controls")]
const V4L2_POWER_LINE_FREQUENCY_AUTO: i64 = 3;
#[cfg(feature = "controls")]
fn description_is_unavailable(description: &Description) -> bool {
description.flags.contains(V4lFlags::DISABLED) || description.flags.contains(V4lFlags::INACTIVE)
}
#[cfg(feature = "controls")]
fn description_range(description: &Description) -> ControlRange {
ControlRange {
min: description.minimum as f32,
max: description.maximum as f32,
step: description.step as f32,
default: description.default as f32,
}
}
#[cfg(feature = "controls")]
fn populate_capability(caps: &mut ControlCapabilities, description: &Description) {
let range = description_range(description);
match description.id {
id if id == V4L2_CID_FOCUS_ABSOLUTE => caps.focus = Some(range),
id if id == V4L2_CID_FOCUS_AUTO => {
caps.auto_focus = Some(true);
}
id if id == V4L2_CID_EXPOSURE_ABSOLUTE => caps.exposure = Some(range),
id if id == V4L2_CID_EXPOSURE_AUTO => {
caps.auto_exposure = Some(true);
}
id if id == V4L2_CID_WHITE_BALANCE_TEMPERATURE => {
caps.white_balance_temperature = Some(range);
}
id if id == V4L2_CID_AUTO_WHITE_BALANCE => {
caps.auto_white_balance = Some(true);
}
id if id == V4L2_CID_BRIGHTNESS => caps.brightness = Some(range),
id if id == V4L2_CID_CONTRAST => caps.contrast = Some(range),
id if id == V4L2_CID_SATURATION => caps.saturation = Some(range),
id if id == V4L2_CID_SHARPNESS => caps.sharpness = Some(range),
id if id == V4L2_CID_GAIN => caps.gain = Some(range),
id if id == V4L2_CID_BACKLIGHT_COMPENSATION => caps.backlight_compensation = Some(range),
id if id == V4L2_CID_POWER_LINE_FREQUENCY => {
caps.power_line_frequency = Some(power_line_capability(description));
}
id if id == V4L2_CID_PAN_ABSOLUTE => caps.pan = Some(range),
id if id == V4L2_CID_TILT_ABSOLUTE => caps.tilt = Some(range),
id if id == V4L2_CID_ZOOM_ABSOLUTE => caps.zoom = Some(range),
_ => {}
}
}
#[cfg(feature = "controls")]
fn power_line_capability(description: &Description) -> PowerLineFrequencyCapability {
let items = description.items.as_ref();
let has_value = |target: i64| -> bool {
items
.map(|entries| entries.iter().any(|(value, _)| *value as i64 == target))
.unwrap_or(false)
};
PowerLineFrequencyCapability {
disabled: has_value(V4L2_POWER_LINE_FREQUENCY_DISABLED),
hz50: has_value(V4L2_POWER_LINE_FREQUENCY_HZ50),
hz60: has_value(V4L2_POWER_LINE_FREQUENCY_HZ60),
auto: has_value(V4L2_POWER_LINE_FREQUENCY_AUTO),
default: power_line_from_value(description.default).unwrap_or(PowerLineFrequency::Disabled),
}
}
#[cfg(feature = "controls")]
fn power_line_from_value(value: i64) -> Option<PowerLineFrequency> {
match value {
V4L2_POWER_LINE_FREQUENCY_DISABLED => Some(PowerLineFrequency::Disabled),
V4L2_POWER_LINE_FREQUENCY_HZ50 => Some(PowerLineFrequency::Hz50),
V4L2_POWER_LINE_FREQUENCY_HZ60 => Some(PowerLineFrequency::Hz60),
V4L2_POWER_LINE_FREQUENCY_AUTO => Some(PowerLineFrequency::Auto),
_ => None,
}
}
#[cfg(feature = "controls")]
fn power_line_to_value(frequency: PowerLineFrequency) -> i64 {
match frequency {
PowerLineFrequency::Disabled => V4L2_POWER_LINE_FREQUENCY_DISABLED,
PowerLineFrequency::Hz50 => V4L2_POWER_LINE_FREQUENCY_HZ50,
PowerLineFrequency::Hz60 => V4L2_POWER_LINE_FREQUENCY_HZ60,
PowerLineFrequency::Auto => V4L2_POWER_LINE_FREQUENCY_AUTO,
}
}
#[cfg(feature = "controls")]
fn populate_control_value(target: &mut Controls, id: u32, value: &V4lValue) {
let as_integer = match value {
V4lValue::Integer(number) => Some(*number),
V4lValue::Boolean(flag) => Some(*flag as i64),
_ => None,
};
let Some(number) = as_integer else { return };
match id {
V4L2_CID_FOCUS_ABSOLUTE => target.focus = Some(number as f32),
V4L2_CID_FOCUS_AUTO => target.auto_focus = Some(number != 0),
V4L2_CID_EXPOSURE_ABSOLUTE => target.exposure = Some(number as f32),
V4L2_CID_EXPOSURE_AUTO => {
target.auto_exposure = Some(number != V4L2_EXPOSURE_AUTO_MANUAL);
}
V4L2_CID_WHITE_BALANCE_TEMPERATURE => {
target.white_balance_temperature = Some(number as f32);
}
V4L2_CID_AUTO_WHITE_BALANCE => target.auto_white_balance = Some(number != 0),
V4L2_CID_BRIGHTNESS => target.brightness = Some(number as f32),
V4L2_CID_CONTRAST => target.contrast = Some(number as f32),
V4L2_CID_SATURATION => target.saturation = Some(number as f32),
V4L2_CID_SHARPNESS => target.sharpness = Some(number as f32),
V4L2_CID_GAIN => target.gain = Some(number as f32),
V4L2_CID_BACKLIGHT_COMPENSATION => target.backlight_compensation = Some(number as f32),
V4L2_CID_POWER_LINE_FREQUENCY => {
target.power_line_frequency = power_line_from_value(number)
}
V4L2_CID_PAN_ABSOLUTE => target.pan = Some(number as f32),
V4L2_CID_TILT_ABSOLUTE => target.tilt = Some(number as f32),
V4L2_CID_ZOOM_ABSOLUTE => target.zoom = Some(number as f32),
_ => {}
}
}
#[cfg(feature = "controls")]
fn find_description<'a>(
descriptions: &'a [Description],
id: u32,
reason: &'static str,
) -> Result<&'a Description, Error> {
descriptions
.iter()
.find(|description| description.id == id)
.filter(|description| !description_is_unavailable(description))
.ok_or(Error::Unsupported {
platform: "linux",
reason,
})
}
#[cfg(feature = "controls")]
fn clamp_and_snap_to_description(value: f32, description: &Description) -> i64 {
let clamped = (value as f64)
.clamp(description.minimum as f64, description.maximum as f64)
.round() as i64;
if description.step <= 1 {
return clamped;
}
let offset = clamped - description.minimum;
let step = description.step as i64;
let snapped = (offset / step) * step;
description.minimum + snapped
}
#[cfg(feature = "controls")]
fn ensure_writable(description: &Description, reason: &'static str) -> Result<(), Error> {
if description.flags.contains(V4lFlags::READ_ONLY) {
return Err(Error::Unsupported {
platform: "linux",
reason,
});
}
Ok(())
}
#[cfg(feature = "controls")]
fn collect_auto_mode_writes(
controls: &Controls,
descriptions: &[Description],
) -> Result<Vec<V4lControl>, Error> {
let mut batch = Vec::new();
if let Some(enabled) = controls.auto_focus {
let description = find_description(descriptions, V4L2_CID_FOCUS_AUTO, "auto_focus")?;
ensure_writable(description, "auto_focus")?;
batch.push(V4lControl {
id: V4L2_CID_FOCUS_AUTO,
value: V4lValue::Integer(if enabled { 1 } else { 0 }),
});
}
if let Some(enabled) = controls.auto_exposure {
let description = find_description(descriptions, V4L2_CID_EXPOSURE_AUTO, "auto_exposure")?;
ensure_writable(description, "auto_exposure")?;
let value = if enabled {
V4L2_EXPOSURE_AUTO_FULL
} else {
V4L2_EXPOSURE_AUTO_MANUAL
};
batch.push(V4lControl {
id: V4L2_CID_EXPOSURE_AUTO,
value: V4lValue::Integer(value),
});
}
if let Some(enabled) = controls.auto_white_balance {
let description = find_description(
descriptions,
V4L2_CID_AUTO_WHITE_BALANCE,
"auto_white_balance",
)?;
ensure_writable(description, "auto_white_balance")?;
batch.push(V4lControl {
id: V4L2_CID_AUTO_WHITE_BALANCE,
value: V4lValue::Integer(if enabled { 1 } else { 0 }),
});
}
Ok(batch)
}
#[cfg(feature = "controls")]
fn collect_value_writes(
controls: &Controls,
descriptions: &[Description],
) -> Result<Vec<V4lControl>, Error> {
let mut batch = Vec::new();
let numeric_fields: [(Option<f32>, u32, &'static str); 12] = [
(controls.focus, V4L2_CID_FOCUS_ABSOLUTE, "focus"),
(controls.exposure, V4L2_CID_EXPOSURE_ABSOLUTE, "exposure"),
(
controls.white_balance_temperature,
V4L2_CID_WHITE_BALANCE_TEMPERATURE,
"white_balance_temperature",
),
(controls.brightness, V4L2_CID_BRIGHTNESS, "brightness"),
(controls.contrast, V4L2_CID_CONTRAST, "contrast"),
(controls.saturation, V4L2_CID_SATURATION, "saturation"),
(controls.sharpness, V4L2_CID_SHARPNESS, "sharpness"),
(controls.gain, V4L2_CID_GAIN, "gain"),
(
controls.backlight_compensation,
V4L2_CID_BACKLIGHT_COMPENSATION,
"backlight_compensation",
),
(controls.pan, V4L2_CID_PAN_ABSOLUTE, "pan"),
(controls.tilt, V4L2_CID_TILT_ABSOLUTE, "tilt"),
(controls.zoom, V4L2_CID_ZOOM_ABSOLUTE, "zoom"),
];
for (maybe_value, cid, reason) in numeric_fields {
if let Some(value) = maybe_value {
let description = find_description(descriptions, cid, reason)?;
ensure_writable(description, reason)?;
batch.push(V4lControl {
id: cid,
value: V4lValue::Integer(clamp_and_snap_to_description(value, description)),
});
}
}
if let Some(frequency) = controls.power_line_frequency {
let description = find_description(
descriptions,
V4L2_CID_POWER_LINE_FREQUENCY,
"power_line_frequency",
)?;
ensure_writable(description, "power_line_frequency")?;
batch.push(V4lControl {
id: V4L2_CID_POWER_LINE_FREQUENCY,
value: V4lValue::Integer(power_line_to_value(frequency)),
});
}
Ok(batch)
}