1use 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#[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 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 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 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}