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 =
71    ioc(IOC_READ | IOC_WRITE, b'V', 5, mem::size_of::<V4l2Format>());
72
73fn query_cap(fd: libc::c_int) -> io::Result<V4l2Capability> {
74    let mut cap: V4l2Capability = unsafe { mem::zeroed() };
75    if unsafe { libc::ioctl(fd, VIDIOC_QUERYCAP as _, &mut cap as *mut V4l2Capability) } != 0 {
76        return Err(io::Error::last_os_error());
77    }
78    Ok(cap)
79}
80
81fn cstr_field(b: &[u8]) -> String {
82    let end = b.iter().position(|c| *c == 0).unwrap_or(b.len());
83    String::from_utf8_lossy(&b[..end]).into_owned()
84}
85
86fn is_loopback_output(cap: &V4l2Capability) -> bool {
87    let caps = if cap.capabilities & V4L2_CAP_DEVICE_CAPS != 0 {
88        cap.device_caps
89    } else {
90        cap.capabilities
91    };
92    caps & V4L2_CAP_VIDEO_OUTPUT != 0 && caps & V4L2_CAP_READWRITE != 0
93}
94
95pub struct V4l2Output {
96    fd: OwnedFd,
97    path: String,
98    frame_bytes: usize,
99    failed: bool,
100}
101
102impl V4l2Output {
103    /// Open `path` for output and set the ring's format on it.
104    pub fn open(path: &str, fmt: &RingFormat) -> Result<Self, String> {
105        let c_path =
106            std::ffi::CString::new(path).map_err(|_| "device path contains NUL".to_string())?;
107        let raw = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
108        if raw < 0 {
109            return Err(format!("open({}): {}", path, io::Error::last_os_error()));
110        }
111        let fd = unsafe { OwnedFd::from_raw_fd(raw) };
112        let cap =
113            query_cap(fd.as_raw_fd()).map_err(|e| format!("VIDIOC_QUERYCAP({}): {}", path, e))?;
114        if !is_loopback_output(&cap) {
115            return Err(format!(
116                "{} ({}) is not a writable video output device",
117                path,
118                cstr_field(&cap.driver)
119            ));
120        }
121        let mut f: V4l2Format = unsafe { mem::zeroed() };
122        f.type_ = V4L2_BUF_TYPE_VIDEO_OUTPUT;
123        f.pix = V4l2PixFormat {
124            width: fmt.width,
125            height: fmt.height,
126            pixelformat: fmt.fourcc,
127            field: V4L2_FIELD_NONE,
128            bytesperline: fmt.bytesperline,
129            sizeimage: fmt.sizeimage,
130            colorspace: if fmt.fourcc == V4L2_PIX_FMT_MJPEG {
131                V4L2_COLORSPACE_SRGB
132            } else {
133                V4L2_COLORSPACE_SMPTE170M
134            },
135            private: 0,
136            flags: 0,
137            ycbcr_enc: 0,
138            quantization: 0,
139            xfer_func: 0,
140        };
141        if unsafe { libc::ioctl(fd.as_raw_fd(), VIDIOC_S_FMT as _, &mut f as *mut V4l2Format) } != 0
142        {
143            return Err(format!(
144                "VIDIOC_S_FMT({}): {}",
145                path,
146                io::Error::last_os_error()
147            ));
148        }
149        if f.pix.pixelformat != fmt.fourcc || f.pix.width != fmt.width || f.pix.height != fmt.height
150        {
151            return Err(format!(
152                "{} refused {}x{} {}",
153                path,
154                fmt.width,
155                fmt.height,
156                fourcc_str(fmt.fourcc)
157            ));
158        }
159        let frame_bytes = if f.pix.sizeimage != 0 {
160            f.pix.sizeimage as usize
161        } else {
162            fmt.sizeimage as usize
163        };
164        Ok(V4l2Output {
165            fd,
166            path: path.to_string(),
167            frame_bytes,
168            failed: false,
169        })
170    }
171
172    /// First v4l2loopback output device among `/dev/video0..63`, if any.
173    pub fn find_loopback_device() -> Option<String> {
174        for n in 0..64 {
175            let path = format!("/dev/video{}", n);
176            let c_path = std::ffi::CString::new(path.clone()).ok()?;
177            let raw = unsafe {
178                libc::open(
179                    c_path.as_ptr(),
180                    libc::O_RDWR | libc::O_CLOEXEC | libc::O_NONBLOCK,
181                )
182            };
183            if raw < 0 {
184                continue;
185            }
186            let fd = unsafe { OwnedFd::from_raw_fd(raw) };
187            if let Ok(cap) = query_cap(fd.as_raw_fd())
188                && cstr_field(&cap.driver).starts_with("v4l2 loopback")
189                && is_loopback_output(&cap)
190            {
191                return Some(path);
192            }
193        }
194        None
195    }
196
197    pub fn path(&self) -> &str {
198        &self.path
199    }
200
201    pub fn is_failed(&self) -> bool {
202        self.failed
203    }
204
205    /// Write one device frame; a short or failed write disables the sink.
206    pub fn write_frame(&mut self, frame: &[u8]) {
207        if self.failed {
208            return;
209        }
210        let len = frame.len().min(self.frame_bytes.max(frame.len()));
211        let n = unsafe { libc::write(self.fd.as_raw_fd(), frame.as_ptr() as *const _, len) };
212        if n < 0 {
213            let err = io::Error::last_os_error();
214            if matches!(err.raw_os_error(), Some(libc::EAGAIN) | Some(libc::EINTR)) {
215                return;
216            }
217            eprintln!(
218                "[webcam] {}: write failed ({}); kernel device sink disabled",
219                self.path, err
220            );
221            self.failed = true;
222        } else if (n as usize) < len {
223            eprintln!(
224                "[webcam] {}: short write ({} of {}); kernel device sink disabled",
225                self.path, n, len
226            );
227            self.failed = true;
228        }
229    }
230}
231
232pub fn fourcc_str(f: u32) -> String {
233    let b = f.to_le_bytes();
234    CStr::from_bytes_until_nul(&[b[0], b[1], b[2], b[3], 0])
235        .map(|c| c.to_string_lossy().into_owned())
236        .unwrap_or_default()
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn ioctl_numbers_match_kernel_headers() {
245        assert_eq!(mem::size_of::<V4l2Capability>(), 104);
246        assert_eq!(mem::size_of::<V4l2Format>(), 208);
247        assert_eq!(VIDIOC_QUERYCAP, 0x8068_5600);
248        assert_eq!(VIDIOC_S_FMT, 0xC0D0_5605);
249    }
250
251    #[test]
252    fn missing_device_is_an_error_not_a_panic() {
253        let fmt = RingFormat::raw(super::super::ring::V4L2_PIX_FMT_YUV420, 64, 48, 30, 1).unwrap();
254        assert!(V4l2Output::open("/dev/video-does-not-exist", &fmt).is_err());
255        assert!(V4l2Output::open("/dev/null", &fmt).is_err());
256    }
257}