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 =
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 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 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 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}