use std::{
ffi::{CStr, CString},
fs::OpenOptions,
io::ErrorKind,
os::fd::{IntoRawFd, RawFd},
slice::from_raw_parts,
};
use nix::{errno::Errno, ioctl_read_bad, ioctl_readwrite_bad, ioctl_write_int_bad};
use thiserror::Error;
mod ffi {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/v4l2loopback.rs"));
impl Default for v4l2_loopback_config {
fn default() -> Self {
Self {
output_nr: -1,
card_label: [0; 32],
min_width: 0,
max_width: 0,
min_height: 0,
max_height: 0,
max_buffers: 0,
max_openers: 0,
announce_all_caps: 0,
unused: 0,
debug: 0,
}
}
}
}
pub use ffi::V4L2LOOPBACK_VERSION_BUGFIX;
pub use ffi::V4L2LOOPBACK_VERSION_MAJOR;
pub use ffi::V4L2LOOPBACK_VERSION_MINOR;
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct DeviceConfig {
pub label: String,
pub min_width: u32,
pub max_width: u32,
pub min_height: u32,
pub max_height: u32,
pub max_buffers: u32,
pub max_openers: u32,
}
impl TryInto<ffi::v4l2_loopback_config> for DeviceConfig {
type Error = Box<dyn std::error::Error>;
fn try_into(self) -> Result<ffi::v4l2_loopback_config, Self::Error> {
let mut cfg = ffi::v4l2_loopback_config::default();
let mut slice: [i8; 32] = [0; 32];
unsafe { from_raw_parts(CString::new(self.label)?.as_ptr(), 32) }
.iter()
.enumerate()
.for_each(|(i, v)| slice[i] = *v);
cfg.card_label = slice;
cfg.min_width = self.min_width;
cfg.max_width = self.max_width;
cfg.min_height = self.min_height;
cfg.max_height = self.max_height;
cfg.max_buffers = self.max_buffers.try_into()?;
cfg.max_openers = self.max_openers.try_into()?;
Ok(cfg)
}
}
impl TryFrom<ffi::v4l2_loopback_config> for DeviceConfig {
type Error = Box<dyn std::error::Error>;
fn try_from(value: ffi::v4l2_loopback_config) -> Result<Self, Self::Error> {
let ffi::v4l2_loopback_config {
output_nr: _,
unused: _,
card_label,
min_width,
max_width,
min_height,
max_height,
max_buffers,
max_openers,
debug: _,
announce_all_caps: _,
} = value;
let label = unsafe { CStr::from_ptr(card_label.as_ptr()) }
.to_str()?
.to_string();
Ok(Self {
label,
min_width,
max_width,
min_height,
max_height,
max_buffers: max_buffers.try_into()?,
max_openers: max_openers.try_into()?,
})
}
}
#[derive(Debug, Error)]
pub enum ControlDeviceError {
#[error("You don't have the right permissions")]
PermissionDenied,
#[error("Can't find control device /dev/v4l2loopback, check if the kernel module is properly loaded")]
NotFound,
#[error("Error when opening the control device: {0}")]
Other(Box<dyn std::error::Error>),
}
fn open_control_device() -> Result<RawFd, ControlDeviceError> {
match OpenOptions::new().read(true).open("/dev/v4l2loopback") {
Ok(f) => Ok(f.into_raw_fd()),
Err(e) => match e.kind() {
ErrorKind::NotFound => Err(ControlDeviceError::NotFound),
ErrorKind::PermissionDenied => Err(ControlDeviceError::PermissionDenied),
_ => Err(ControlDeviceError::Other(Box::new(e))),
},
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Couldn't open control device: {0}")]
ControlDevice(#[from] ControlDeviceError),
#[error("Error returned from ioctl: {0}")]
Ioctl(#[from] Errno),
#[error("Failed to create device")]
DeviceCreationFailed,
#[error("Device /dev/video{0} not found")]
DeviceNotFound(u32),
#[error("Failed to convert device configuration: {0}")]
ConfigConversionError(Box<dyn std::error::Error>),
#[error(transparent)]
Other(Box<dyn std::error::Error>),
}
pub fn add_device(num: Option<u32>, config: DeviceConfig) -> Result<u32, Error> {
let mut cfg: ffi::v4l2_loopback_config = match config.try_into() {
Ok(cfg) => cfg,
Err(e) => return Err(Error::ConfigConversionError(e)),
};
cfg.output_nr = num.map(i32::try_from).and_then(Result::ok).unwrap_or(-1);
let fd = open_control_device()?;
ioctl_readwrite_bad!(
v4l2loopback_ctl_add,
ffi::V4L2LOOPBACK_CTL_ADD,
ffi::v4l2_loopback_config
);
let dev = unsafe { v4l2loopback_ctl_add(fd, &mut cfg as *mut ffi::v4l2_loopback_config) }?;
if dev.is_negative() {
return Err(Error::DeviceCreationFailed);
}
Ok(dev as u32)
}
pub fn delete_device(device_num: u32) -> Result<(), Error> {
let fd = open_control_device()?;
let converted_num = match device_num.try_into() {
Ok(n) => n,
Err(e) => return Err(Error::Other(Box::new(e))),
};
ioctl_write_int_bad!(v4l2loopback_ctl_remove, ffi::V4L2LOOPBACK_CTL_REMOVE);
let res = unsafe { v4l2loopback_ctl_remove(fd, converted_num) }?;
if res.is_negative() {
return Err(Error::DeviceNotFound(device_num));
}
Ok(())
}
pub fn query_device(device_num: u32) -> Result<DeviceConfig, Error> {
let mut cfg = ffi::v4l2_loopback_config {
output_nr: match device_num.try_into() {
Ok(n) => n,
Err(e) => return Err(Error::Other(Box::new(e))),
},
..Default::default()
};
let fd = open_control_device()?;
ioctl_read_bad!(
v4l2loopback_ctl_query,
ffi::V4L2LOOPBACK_CTL_QUERY,
ffi::v4l2_loopback_config
);
let res = unsafe { v4l2loopback_ctl_query(fd, &mut cfg as *mut ffi::v4l2_loopback_config) }?;
if res.is_negative() {
return Err(Error::DeviceNotFound(device_num));
}
let device_config = match DeviceConfig::try_from(cfg) {
Ok(cfg) => cfg,
Err(e) => return Err(Error::ConfigConversionError(e)),
};
Ok(device_config)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::{add_device, delete_device};
#[test]
fn device_with_num() {
let mut next_num = 0;
while Path::new(&format!("/dev/video{}", next_num)).exists() {
next_num += 1;
}
let device_num =
add_device(Some(next_num), Default::default()).expect("Error when creating the device");
assert!(Path::new(&format!("/dev/video{}", device_num)).exists());
delete_device(device_num).expect("Error when removing device");
assert!(!Path::new(&format!("/dev/video{}", device_num)).exists());
}
#[test]
fn device_with_used_num() {
let create_device_0 = !Path::new("/dev/video0").exists();
if create_device_0 {
add_device(Some(0), Default::default()).expect("Error when creating the device");
}
assert!(Path::new("/dev/video0").exists());
let res = add_device(Some(0), Default::default());
assert!(res.is_err());
if create_device_0 {
delete_device(0).expect("Error when removing device");
assert!(!Path::new("/dev/video0").exists());
}
}
}