Skip to main content

pixelflux/webcam/
v4l2out.rs

1//! Kernel-device sink: writes frames into a v4l2loopback output device.
2//!
3//! Where the host (or a privileged container) carries the v4l2loopback module, a real `/dev/videoN`
4//! is the zero-configuration path for consumers — no preload, no socket — exactly as `/dev/uinput`
5//! is for the gamepads. The device is opened for output, set to the ring's raw format and fed one
6//! `write()` per published frame. It is a best-effort mirror of the ring: an error disables the
7//! sink and is reported once, never failing the camera.
8
9use std::ffi::CStr;
10use std::io;
11use std::mem;
12use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
13
14use super::ring::{RingFormat, V4L2_PIX_FMT_MJPEG};
15
16const V4L2_BUF_TYPE_VIDEO_OUTPUT: u32 = 2;
17const V4L2_CAP_VIDEO_OUTPUT: u32 = 0x0000_0002;
18const V4L2_CAP_READWRITE: u32 = 0x0100_0000;
19const V4L2_CAP_DEVICE_CAPS: u32 = 0x8000_0000;
20const V4L2_FIELD_NONE: u32 = 1;
21const V4L2_COLORSPACE_SMPTE170M: u32 = 1;
22const V4L2_COLORSPACE_SRGB: u32 = 8;
23
24const IOC_WRITE: u32 = 1;
25const IOC_READ: u32 = 2;
26
27const fn ioc(dir: u32, typ: u8, nr: u8, size: usize) -> libc::c_ulong {
28    ((dir << 30) | ((size as u32) << 16) | ((typ as u32) << 8) | nr as u32) as libc::c_ulong
29}
30
31#[repr(C)]
32struct V4l2Capability {
33    driver: [u8; 16],
34    card: [u8; 32],
35    bus_info: [u8; 32],
36    version: u32,
37    capabilities: u32,
38    device_caps: u32,
39    reserved: [u32; 3],
40}
41
42#[repr(C)]
43#[derive(Clone, Copy)]
44struct V4l2PixFormat {
45    width: u32,
46    height: u32,
47    pixelformat: u32,
48    field: u32,
49    bytesperline: u32,
50    sizeimage: u32,
51    colorspace: u32,
52    private: u32,
53    flags: u32,
54    ycbcr_enc: u32,
55    quantization: u32,
56    xfer_func: u32,
57}
58
59/// `struct v4l2_format`: the type word, padding to the 8-byte aligned union, then the union's
60/// 200 bytes, of which the pixel format occupies the first 48.
61#[repr(C)]
62struct V4l2Format {
63    type_: u32,
64    _pad: u32,
65    pix: V4l2PixFormat,
66    _rest: [u8; 200 - mem::size_of::<V4l2PixFormat>()],
67}
68
69const VIDIOC_QUERYCAP: libc::c_ulong = ioc(IOC_READ, b'V', 0, mem::size_of::<V4l2Capability>());
70const VIDIOC_S_FMT: libc::c_ulong = ioc(IOC_READ | IOC_WRITE, b'V', 5, mem::size_of::<V4l2Format>());
71
72fn query_cap(fd: libc::c_int) -> io::Result<V4l2Capability> {
73    let mut cap: V4l2Capability = unsafe { mem::zeroed() };
74    if unsafe { libc::ioctl(fd, VIDIOC_QUERYCAP as _, &mut cap as *mut V4l2Capability) } != 0 {
75        return Err(io::Error::last_os_error());
76    }
77    Ok(cap)
78}
79
80fn cstr_field(b: &[u8]) -> String {
81    let end = b.iter().position(|c| *c == 0).unwrap_or(b.len());
82    String::from_utf8_lossy(&b[..end]).into_owned()
83}
84
85fn is_loopback_output(cap: &V4l2Capability) -> bool {
86    let caps = if cap.capabilities & V4L2_CAP_DEVICE_CAPS != 0 { cap.device_caps } else { cap.capabilities };
87    caps & V4L2_CAP_VIDEO_OUTPUT != 0 && caps & V4L2_CAP_READWRITE != 0
88}
89
90pub struct V4l2Output {
91    fd: OwnedFd,
92    path: String,
93    frame_bytes: usize,
94    failed: bool,
95}
96
97impl V4l2Output {
98    /// Open `path` for output and set the ring's format on it.
99    pub fn open(path: &str, fmt: &RingFormat) -> Result<Self, String> {
100        let c_path = std::ffi::CString::new(path).map_err(|_| "device path contains NUL".to_string())?;
101        let raw = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
102        if raw < 0 {
103            return Err(format!("open({}): {}", path, io::Error::last_os_error()));
104        }
105        let fd = unsafe { OwnedFd::from_raw_fd(raw) };
106        let cap = query_cap(fd.as_raw_fd()).map_err(|e| format!("VIDIOC_QUERYCAP({}): {}", path, e))?;
107        if !is_loopback_output(&cap) {
108            return Err(format!("{} ({}) is not a writable video output device", path, cstr_field(&cap.driver)));
109        }
110        let mut f: V4l2Format = unsafe { mem::zeroed() };
111        f.type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT;
112        f.pix = V4l2PixFormat {
113            width: fmt.width,
114            height: fmt.height,
115            pixelformat: fmt.fourcc,
116            field: V4L2_FIELD_NONE,
117            bytesperline: fmt.bytesperline,
118            sizeimage: fmt.sizeimage,
119            colorspace: if fmt.fourcc == V4L2_PIX_FMT_MJPEG { V4L2_COLORSPACE_SRGB } else { V4L2_COLORSPACE_SMPTE170M },
120            private: 0,
121            flags: 0,
122            ycbcr_enc: 0,
123            quantization: 0,
124            xfer_func: 0,
125        };
126        if unsafe { libc::ioctl(fd.as_raw_fd(), VIDIOC_S_FMT as _, &mut f as *mut V4l2Format) } != 0 {
127            return Err(format!("VIDIOC_S_FMT({}): {}", path, io::Error::last_os_error()));
128        }
129        if f.pix.pixelformat != fmt.fourcc || f.pix.width != fmt.width || f.pix.height != fmt.height {
130            return Err(format!("{} refused {}x{} {}", path, fmt.width, fmt.height, fourcc_str(fmt.fourcc)));
131        }
132        let frame_bytes = if f.pix.sizeimage != 0 { f.pix.sizeimage as usize } else { fmt.sizeimage as usize };
133        Ok(V4l2Output { fd, path: path.to_string(), frame_bytes, failed: false })
134    }
135
136    /// First v4l2loopback output device among `/dev/video0..63`, if any.
137    pub fn find_loopback_device() -> Option<String> {
138        for n in 0..64 {
139            let path = format!("/dev/video{}", n);
140            let c_path = std::ffi::CString::new(path.clone()).ok()?;
141            let raw = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC | libc::O_NONBLOCK) };
142            if raw < 0 {
143                continue;
144            }
145            let fd = unsafe { OwnedFd::from_raw_fd(raw) };
146            if let Ok(cap) = query_cap(fd.as_raw_fd())
147                && cstr_field(&cap.driver).starts_with("v4l2 loopback")
148                && is_loopback_output(&cap) {
149                    return Some(path);
150                }
151        }
152        None
153    }
154
155    pub fn path(&self) -> &str {
156        &self.path
157    }
158
159    pub fn is_failed(&self) -> bool {
160        self.failed
161    }
162
163    /// Write one device frame; a short or failed write disables the sink.
164    pub fn write_frame(&mut self, frame: &[u8]) {
165        if self.failed {
166            return;
167        }
168        let len = frame.len().min(self.frame_bytes.max(frame.len()));
169        let n = unsafe { libc::write(self.fd.as_raw_fd(), frame.as_ptr() as *const _, len) };
170        if n < 0 {
171            let err = io::Error::last_os_error();
172            if matches!(err.raw_os_error(), Some(libc::EAGAIN) | Some(libc::EINTR)) {
173                return;
174            }
175            eprintln!("[webcam] {}: write failed ({}); kernel device sink disabled", self.path, err);
176            self.failed = true;
177        } else if (n as usize) < len {
178            eprintln!("[webcam] {}: short write ({} of {}); kernel device sink disabled", self.path, n, len);
179            self.failed = true;
180        }
181    }
182}
183
184pub fn fourcc_str(f: u32) -> String {
185    let b = f.to_le_bytes();
186    CStr::from_bytes_until_nul(&[b[0], b[1], b[2], b[3], 0])
187        .map(|c| c.to_string_lossy().into_owned())
188        .unwrap_or_default()
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn ioctl_numbers_match_kernel_headers() {
197        assert_eq!(mem::size_of::<V4l2Capability>(), 104);
198        assert_eq!(mem::size_of::<V4l2Format>(), 208);
199        assert_eq!(VIDIOC_QUERYCAP, 0x8068_5600);
200        assert_eq!(VIDIOC_S_FMT, 0xC0D0_5605);
201    }
202
203    #[test]
204    fn missing_device_is_an_error_not_a_panic() {
205        let fmt = RingFormat::raw(super::super::ring::V4L2_PIX_FMT_YUV420, 64, 48, 30, 1).unwrap();
206        assert!(V4l2Output::open("/dev/video-does-not-exist", &fmt).is_err());
207        assert!(V4l2Output::open("/dev/null", &fmt).is_err());
208    }
209}