1use std::mem::size_of;
31use std::os::fd::RawFd;
32use std::sync::OnceLock;
33
34use super::codec::{
35 Codec, VIDEO_HEADER_LEN, frame_type_from_key, h264_frame_type, h265_frame_type,
36 push_video_header, vp8_is_key, vp9_is_key,
37};
38use super::reference::Reference;
39use super::sps::{self, ColorSignal};
40use crate::RustCaptureSettings;
41
42const V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE: u32 = 9;
43const V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE: u32 = 10;
44const V4L2_MEMORY_MMAP: u32 = 1;
45const V4L2_FIELD_NONE: u32 = 1;
46const V4L2_CAP_VIDEO_M2M_MPLANE: u32 = 0x0000_8000;
47const V4L2_CAP_VIDEO_M2M: u32 = 0x0000_4000;
48
49const V4L2_PIX_FMT_BGRA: u32 = 0x3432_4241;
52const V4L2_PIX_FMT_RGBA: u32 = 0x3452_4742;
53const V4L2_PIX_FMT_H264: u32 = 0x3436_3248;
54const V4L2_PIX_FMT_HEVC: u32 = 0x4356_4548;
55const V4L2_PIX_FMT_VP8: u32 = 0x3038_5056;
56const V4L2_PIX_FMT_VP9: u32 = 0x3039_5056;
57
58pub fn coded_fourcc(codec: Codec) -> Option<u32> {
62 match codec {
63 Codec::H264 => Some(V4L2_PIX_FMT_H264),
64 Codec::H265 => Some(V4L2_PIX_FMT_HEVC),
65 Codec::Vp8 => Some(V4L2_PIX_FMT_VP8),
66 Codec::Vp9 => Some(V4L2_PIX_FMT_VP9),
67 Codec::Av1 | Codec::Jpeg => None,
68 }
69}
70
71const VIDIOC_QUERYCAP: u64 = 0x8068_5600;
72const VIDIOC_ENUM_FMT: u64 = 0xc040_5602;
73const VIDIOC_S_FMT: u64 = 0xc0d0_5605;
74const VIDIOC_REQBUFS: u64 = 0xc014_5608;
75const VIDIOC_QUERYBUF: u64 = 0xc058_5609;
76const VIDIOC_QBUF: u64 = 0xc058_560f;
77const VIDIOC_DQBUF: u64 = 0xc058_5611;
78const VIDIOC_STREAMON: u64 = 0x4004_5612;
79const VIDIOC_STREAMOFF: u64 = 0x4004_5613;
80const VIDIOC_S_PARM: u64 = 0xc0cc_5616;
81const VIDIOC_S_CTRL: u64 = 0xc008_561c;
82const VIDIOC_ENUM_FRAMESIZES: u64 = 0xc02c_564a;
83
84const CID_GOP_SIZE: u32 = 0x0099_09cb;
85const CID_BITRATE_MODE: u32 = 0x0099_09ce;
86const CID_BITRATE: u32 = 0x0099_09cf;
87const CID_REPEAT_SEQ_HEADER: u32 = 0x0099_09e2;
88const CID_FORCE_KEY_FRAME: u32 = 0x0099_09e5;
89const CID_H264_I_PERIOD: u32 = 0x0099_0a66;
90
91const BITRATE_MODE_VBR: i32 = 0;
94
95const FRMSIZE_TYPE_DISCRETE: u32 = 1;
96const OUTPUT_BUFFERS: u32 = 1;
97const CAPTURE_BUFFERS: usize = 2;
98const FRAME_TIMEOUT_MS: i32 = 500;
101
102#[repr(C)]
103#[derive(Clone, Copy, Default)]
104struct PlaneFormat {
105 sizeimage: u32,
106 bytesperline: u32,
107 reserved: [u16; 6],
108}
109
110#[repr(C)]
111struct PixFormatMplane {
112 width: u32,
113 height: u32,
114 pixelformat: u32,
115 field: u32,
116 colorspace: u32,
117 plane_fmt: [PlaneFormat; 8],
118 num_planes: u8,
119 flags: u8,
120 enc: u8,
121 quantization: u8,
122 xfer_func: u8,
123 reserved: [u8; 7],
124}
125
126#[repr(C)]
127struct Format {
128 type_: u32,
129 _pad: u32,
130 pix_mp: PixFormatMplane,
131 _tail: [u8; 8],
132}
133
134#[repr(C)]
135#[derive(Default)]
136struct RequestBuffers {
137 count: u32,
138 type_: u32,
139 memory: u32,
140 capabilities: u32,
141 flags: u8,
142 reserved: [u8; 3],
143}
144
145#[repr(C)]
146#[derive(Clone, Copy, Default)]
147struct Plane {
148 bytesused: u32,
149 length: u32,
150 m: u64,
151 data_offset: u32,
152 reserved: [u32; 11],
153}
154
155#[repr(C)]
156#[derive(Clone, Copy, Default)]
157struct Buffer {
158 index: u32,
159 type_: u32,
160 bytesused: u32,
161 flags: u32,
162 field: u32,
163 _pad: u32,
164 timestamp: [i64; 2],
165 timecode: [u32; 4],
166 sequence: u32,
167 memory: u32,
168 m: u64,
169 length: u32,
170 reserved2: u32,
171 request_fd: i32,
172}
173
174#[repr(C)]
177struct StreamParm {
178 type_: u32,
179 capability: u32,
180 outputmode: u32,
181 numerator: u32,
182 denominator: u32,
183 extendedmode: u32,
184 buffers: u32,
185 reserved: [u32; 4],
186 _tail: [u8; 160],
187}
188
189impl Default for StreamParm {
190 fn default() -> Self {
191 Self {
192 type_: 0,
193 capability: 0,
194 outputmode: 0,
195 numerator: 0,
196 denominator: 0,
197 extendedmode: 0,
198 buffers: 0,
199 reserved: [0; 4],
200 _tail: [0; 160],
201 }
202 }
203}
204
205#[repr(C)]
206struct Control {
207 id: u32,
208 value: i32,
209}
210
211#[repr(C)]
212struct Capability {
213 driver: [u8; 16],
214 card: [u8; 32],
215 bus_info: [u8; 32],
216 version: u32,
217 capabilities: u32,
218 device_caps: u32,
219 reserved: [u32; 3],
220}
221
222#[repr(C)]
223struct FmtDesc {
224 index: u32,
225 type_: u32,
226 flags: u32,
227 description: [u8; 32],
228 pixelformat: u32,
229 mbus_code: u32,
230 reserved: [u32; 3],
231}
232
233#[repr(C)]
234struct FrameSizeEnum {
235 index: u32,
236 pixel_format: u32,
237 type_: u32,
238 bounds: [u32; 6],
239 reserved: [u32; 2],
240}
241
242fn abi_matches() -> Result<(), String> {
245 let sizes = [
246 ("v4l2_capability", size_of::<Capability>(), 104),
247 ("v4l2_fmtdesc", size_of::<FmtDesc>(), 64),
248 ("v4l2_format", size_of::<Format>(), 208),
249 ("v4l2_requestbuffers", size_of::<RequestBuffers>(), 20),
250 ("v4l2_plane", size_of::<Plane>(), 64),
251 ("v4l2_buffer", size_of::<Buffer>(), 88),
252 ("v4l2_streamparm", size_of::<StreamParm>(), 204),
253 ("v4l2_control", size_of::<Control>(), 8),
254 ("v4l2_frmsizeenum", size_of::<FrameSizeEnum>(), 44),
255 ];
256 for (name, got, want) in sizes {
257 if got != want {
258 return Err(format!(
259 "{name} is {got} bytes here, the interface wants {want}"
260 ));
261 }
262 }
263 Ok(())
264}
265
266unsafe fn ioctl<T>(fd: RawFd, request: u64, arg: *mut T) -> std::io::Result<()> {
267 let rc = unsafe { libc::ioctl(fd, request as _, arg as *mut libc::c_void) };
270 if rc < 0 {
271 return Err(std::io::Error::last_os_error());
272 }
273 Ok(())
274}
275
276fn empty_format(type_: u32) -> Format {
277 Format {
278 type_,
279 _pad: 0,
280 pix_mp: PixFormatMplane {
281 width: 0,
282 height: 0,
283 pixelformat: 0,
284 field: V4L2_FIELD_NONE,
285 colorspace: 0,
286 plane_fmt: [PlaneFormat::default(); 8],
287 num_planes: 1,
288 flags: 0,
289 enc: 0,
290 quantization: 0,
291 xfer_func: 0,
292 reserved: [0; 7],
293 },
294 _tail: [0; 8],
295 }
296}
297
298fn input_format(rgba: bool) -> u32 {
302 if rgba {
303 V4L2_PIX_FMT_RGBA
304 } else {
305 V4L2_PIX_FMT_BGRA
306 }
307}
308
309struct NodeInfo {
311 path: String,
312 driver: String,
313 coded: Vec<u32>,
315 min_width: u32,
316 max_width: u32,
317 step_width: u32,
318 min_height: u32,
319 max_height: u32,
320 step_height: u32,
321}
322
323impl NodeInfo {
324 fn serves(&self, codec: Codec) -> bool {
326 coded_fourcc(codec).is_some_and(|f| self.coded.contains(&f))
327 }
328
329 fn fits(&self, width: u32, height: u32) -> bool {
332 let in_range = |value: u32, min: u32, max: u32, step: u32| {
333 value >= min && value <= max && (step == 0 || (value - min).is_multiple_of(step))
334 };
335 in_range(width, self.min_width, self.max_width, self.step_width)
336 && in_range(height, self.min_height, self.max_height, self.step_height)
337 }
338}
339
340fn enumerates(fd: RawFd, type_: u32, wanted: u32) -> bool {
341 for index in 0..64u32 {
342 let mut desc = FmtDesc {
343 index,
344 type_,
345 flags: 0,
346 description: [0; 32],
347 pixelformat: 0,
348 mbus_code: 0,
349 reserved: [0; 3],
350 };
351 if unsafe { ioctl(fd, VIDIOC_ENUM_FMT, &mut desc) }.is_err() {
352 return false;
353 }
354 if desc.pixelformat == wanted {
355 return true;
356 }
357 }
358 false
359}
360
361fn frame_sizes(fd: RawFd, path: &str, coded: u32) -> Option<NodeInfo> {
362 let mut sizes = FrameSizeEnum {
363 index: 0,
364 pixel_format: coded,
365 type_: 0,
366 bounds: [0; 6],
367 reserved: [0; 2],
368 };
369 unsafe { ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &mut sizes) }.ok()?;
370 let (min_w, max_w, step_w, min_h, max_h, step_h) = if sizes.type_ == FRMSIZE_TYPE_DISCRETE {
371 let (w, h) = (sizes.bounds[0], sizes.bounds[1]);
372 (w, w, 0, h, h, 0)
373 } else {
374 (
375 sizes.bounds[0],
376 sizes.bounds[1],
377 sizes.bounds[2],
378 sizes.bounds[3],
379 sizes.bounds[4],
380 sizes.bounds[5],
381 )
382 };
383 Some(NodeInfo {
384 path: path.to_string(),
385 driver: String::new(),
386 coded: Vec::new(),
387 min_width: min_w,
388 max_width: max_w,
389 step_width: step_w,
390 min_height: min_h,
391 max_height: max_h,
392 step_height: step_h,
393 })
394}
395
396fn node() -> Option<&'static NodeInfo> {
400 static NODE: OnceLock<Option<NodeInfo>> = OnceLock::new();
401 NODE.get_or_init(|| {
402 if let Err(e) = abi_matches() {
403 crate::log::debug!("[pixelflux] V4L2 M2M unusable: {e}");
404 return None;
405 }
406 for index in 0..64 {
407 let path = format!("/dev/video{index}");
408 let Ok(c_path) = std::ffi::CString::new(path.clone()) else {
409 continue;
410 };
411 let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) };
412 if fd < 0 {
413 continue;
414 }
415 let found = (|| {
416 let mut caps: Capability = unsafe { std::mem::zeroed() };
417 unsafe { ioctl(fd, VIDIOC_QUERYCAP, &mut caps) }.ok()?;
418 if caps.device_caps & (V4L2_CAP_VIDEO_M2M_MPLANE | V4L2_CAP_VIDEO_M2M) == 0 {
419 return None;
420 }
421 let coded: Vec<u32> = [Codec::H264, Codec::H265, Codec::Vp8, Codec::Vp9]
422 .into_iter()
423 .filter_map(coded_fourcc)
424 .filter(|&f| enumerates(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, f))
425 .collect();
426 if coded.is_empty() {
427 return None;
428 }
429 if !enumerates(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, V4L2_PIX_FMT_BGRA) {
430 return None;
431 }
432 let driver = String::from_utf8_lossy(&caps.driver)
433 .trim_end_matches('\0')
434 .to_string();
435 frame_sizes(fd, &path, coded[0]).map(|info| NodeInfo {
436 driver,
437 coded,
438 ..info
439 })
440 })();
441 unsafe { libc::close(fd) };
442 if let Some(info) = found {
443 println!(
444 "[pixelflux] V4L2 M2M encoder on {} up to {}x{}.",
445 info.path, info.max_width, info.max_height
446 );
447 return Some(info);
448 }
449 }
450 None
451 })
452 .as_ref()
453}
454
455fn fallback_rates(wanted: f64) -> Vec<f64> {
459 let mut rates = vec![wanted];
460 for step in [30.0, 15.0] {
461 if step < wanted - 0.01 {
462 rates.push(step);
463 }
464 }
465 rates
466}
467
468fn known_conversion(driver: &str) -> Option<ColorSignal> {
475 match driver {
476 "bcm2835-codec" => Some(ColorSignal::BT601_FULL),
477 _ => None,
478 }
479}
480
481enum Color {
483 Unknown,
486 Declared(ColorSignal),
488 Writing(ColorSignal),
491 Untagged,
493}
494
495pub fn available() -> bool {
497 node().is_some()
498}
499
500pub fn encodes(codec: Codec, width: i32, height: i32) -> bool {
503 match node() {
504 Some(info) if width > 0 && height > 0 => {
505 info.serves(codec) && info.fits(width as u32, height as u32)
506 }
507 _ => false,
508 }
509}
510
511pub fn served() -> Vec<Codec> {
513 match node() {
514 Some(info) => [Codec::H264, Codec::H265, Codec::Vp8, Codec::Vp9]
515 .into_iter()
516 .filter(|&c| info.serves(c))
517 .collect(),
518 None => Vec::new(),
519 }
520}
521
522pub struct V4l2M2mEncoder {
523 fd: RawFd,
524 codec: Codec,
525 coded: u32,
526 width: i32,
527 height: i32,
528 row_bytes: usize,
529 bytesperline: usize,
530 output_size: usize,
531 output_map: *mut libc::c_void,
532 capture: [(*mut libc::c_void, usize); CAPTURE_BUFFERS],
533 bitrate_bps: u32,
534 fps: f64,
535 omit_headers: bool,
536 color: Color,
537 fallback: Option<ColorSignal>,
539 written_sps: Option<(Vec<u8>, Vec<u8>)>,
542 decided: bool,
544 reorder: sps::NoReorder,
548}
549
550unsafe impl Send for V4l2M2mEncoder {}
551
552impl V4l2M2mEncoder {
553 pub fn new(codec: Codec, settings: &RustCaptureSettings, rgba: bool) -> Result<Self, String> {
554 let info = node().ok_or("no V4L2 M2M encode node")?;
555 let coded = coded_fourcc(codec)
556 .filter(|f| info.coded.contains(f))
557 .ok_or_else(|| format!("{} encodes no {}", info.path, codec.display()))?;
558 let (width, height) = (settings.width, settings.height);
559 if width <= 0 || height <= 0 {
560 return Err(format!(
561 "the encoder needs positive dimensions, got {width}x{height}"
562 ));
563 }
564 if !info.fits(width as u32, height as u32) {
565 return Err(format!(
566 "{} encodes {}x{} to {}x{}, not {width}x{height}",
567 info.path, info.min_width, info.min_height, info.max_width, info.max_height
568 ));
569 }
570 let path = std::ffi::CString::new(info.path.clone()).map_err(|e| e.to_string())?;
571 let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
572 if fd < 0 {
573 return Err(format!(
574 "{} did not open: {}",
575 info.path,
576 std::io::Error::last_os_error()
577 ));
578 }
579 let mut encoder = Self {
580 fd,
581 codec,
582 coded,
583 width,
584 height,
585 row_bytes: width as usize * 4,
586 bytesperline: 0,
587 output_size: 0,
588 output_map: libc::MAP_FAILED,
589 capture: [(libc::MAP_FAILED, 0); CAPTURE_BUFFERS],
590 bitrate_bps: (settings.video_bitrate_kbps.max(1) as u32).saturating_mul(1000),
591 fps: settings.target_fps.max(1.0),
592 omit_headers: settings.omit_stripe_headers,
593 color: match known_conversion(&info.driver) {
597 Some(signal) => Color::Writing(signal),
598 None => Color::Unknown,
599 },
600 fallback: known_conversion(&info.driver),
601 written_sps: None,
602 decided: false,
603 reorder: sps::NoReorder::new("The M2M encoder", true),
604 };
605 let mut last = String::new();
611 for rate in fallback_rates(settings.target_fps.max(1.0)) {
612 encoder.fps = rate;
613 match encoder.setup(settings, rgba) {
614 Ok(()) => {
615 if rate < settings.target_fps.max(1.0) {
616 println!(
617 "[pixelflux] The M2M encoder refused {:.0} frames a second and took {:.0}.",
618 settings.target_fps.max(1.0),
619 rate
620 );
621 }
622 return Ok(encoder);
623 }
624 Err(e) => {
625 last = e;
626 encoder.release();
627 let reopened = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
628 if reopened < 0 {
629 return Err(last);
630 }
631 encoder.fd = reopened;
632 }
633 }
634 }
635 Err(last)
636 }
637
638 fn release(&mut self) {
642 let _ = self.stream(
643 VIDIOC_STREAMOFF,
644 V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
645 "STREAMOFF output",
646 );
647 let _ = self.stream(
648 VIDIOC_STREAMOFF,
649 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
650 "STREAMOFF capture",
651 );
652 if self.output_map != libc::MAP_FAILED {
653 unsafe { libc::munmap(self.output_map, self.output_size) };
654 self.output_map = libc::MAP_FAILED;
655 }
656 for slot in 0..CAPTURE_BUFFERS {
657 let (map, length) = self.capture[slot];
658 if map != libc::MAP_FAILED {
659 unsafe { libc::munmap(map, length) };
660 self.capture[slot] = (libc::MAP_FAILED, 0);
661 }
662 }
663 if self.fd >= 0 {
664 unsafe { libc::close(self.fd) };
665 self.fd = -1;
666 }
667 }
668
669 fn setup(&mut self, settings: &RustCaptureSettings, rgba: bool) -> Result<(), String> {
670 let input = input_format(rgba);
671 let mut output_format = empty_format(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
672 output_format.pix_mp.width = self.width as u32;
673 output_format.pix_mp.height = self.height as u32;
674 output_format.pix_mp.pixelformat = input;
675 output_format.pix_mp.plane_fmt[0].bytesperline = self.row_bytes as u32;
676 output_format.pix_mp.plane_fmt[0].sizeimage =
677 (self.row_bytes * self.height as usize) as u32;
678 self.call(VIDIOC_S_FMT, &mut output_format, "S_FMT output")?;
679 self.bytesperline = output_format.pix_mp.plane_fmt[0].bytesperline as usize;
680 self.output_size = output_format.pix_mp.plane_fmt[0].sizeimage as usize;
681 if self.bytesperline < self.row_bytes
682 || self.output_size < self.bytesperline * self.height as usize
683 {
684 return Err(format!(
685 "the node offered {} bytes per line and {} per frame for {}x{}",
686 self.bytesperline, self.output_size, self.width, self.height
687 ));
688 }
689
690 let mut capture_format = empty_format(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
691 capture_format.pix_mp.width = self.width as u32;
692 capture_format.pix_mp.height = self.height as u32;
693 capture_format.pix_mp.pixelformat = self.coded;
694 capture_format.pix_mp.plane_fmt[0].sizeimage =
695 ((self.width * self.height) as u32).max(2 << 20);
696 self.call(VIDIOC_S_FMT, &mut capture_format, "S_FMT capture")?;
697
698 self.set_frame_rate(self.fps)?;
699 let seconds = if settings.keyframe_interval_s > 0.0 {
700 settings.keyframe_interval_s
701 } else {
702 10.0
703 };
704 let keyframe = ((self.fps * seconds) as i32).clamp(1, 600);
705 self.set_control(CID_BITRATE_MODE, BITRATE_MODE_VBR, "rate control mode")?;
706 self.set_control(CID_BITRATE, self.bitrate_bps as i32, "bitrate")?;
707 self.set_control(
708 CID_REPEAT_SEQ_HEADER,
709 1,
710 "sequence header with every key frame",
711 )?;
712 self.set_control(CID_H264_I_PERIOD, keyframe, "key frame period")?;
713 self.set_control(CID_GOP_SIZE, keyframe, "GOP size")?;
714
715 self.request_buffers(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, OUTPUT_BUFFERS)?;
716 self.request_buffers(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, CAPTURE_BUFFERS as u32)?;
717 self.output_map = self.map_buffer(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0)?.0;
718 for index in 0..CAPTURE_BUFFERS {
719 self.capture[index] =
720 self.map_buffer(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index as u32)?;
721 }
722
723 self.stream(
724 VIDIOC_STREAMON,
725 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
726 "STREAMON capture",
727 )?;
728 self.stream(
729 VIDIOC_STREAMON,
730 V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
731 "STREAMON output",
732 )?;
733 for index in 0..CAPTURE_BUFFERS {
734 self.queue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index as u32, 0)?;
735 }
736 Ok(())
737 }
738
739 fn call<T>(&self, request: u64, arg: &mut T, what: &str) -> Result<(), String> {
740 unsafe { ioctl(self.fd, request, arg) }.map_err(|e| format!("{what} failed: {e}"))
741 }
742
743 fn set_control(&self, id: u32, value: i32, what: &str) -> Result<(), String> {
748 let mut control = Control { id, value };
749 self.call(VIDIOC_S_CTRL, &mut control, &format!("setting {what}"))
750 }
751
752 fn set_frame_rate(&mut self, fps: f64) -> Result<(), String> {
753 let mut parm = StreamParm {
754 type_: V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
755 numerator: 1000,
756 denominator: (fps * 1000.0).round().max(1000.0) as u32,
757 ..Default::default()
758 };
759 self.call(VIDIOC_S_PARM, &mut parm, "S_PARM frame rate")?;
760 self.fps = fps;
761 Ok(())
762 }
763
764 fn request_buffers(&self, type_: u32, count: u32) -> Result<(), String> {
765 let mut request = RequestBuffers {
766 count,
767 type_,
768 memory: V4L2_MEMORY_MMAP,
769 ..Default::default()
770 };
771 self.call(VIDIOC_REQBUFS, &mut request, "REQBUFS")?;
772 if request.count < count {
773 return Err(format!(
774 "the node gave {} buffers of the {count} asked for",
775 request.count
776 ));
777 }
778 Ok(())
779 }
780
781 fn map_buffer(&self, type_: u32, index: u32) -> Result<(*mut libc::c_void, usize), String> {
782 let mut planes = [Plane::default(); 1];
783 let mut buffer = Buffer {
784 index,
785 type_,
786 memory: V4L2_MEMORY_MMAP,
787 m: planes.as_mut_ptr() as u64,
788 length: 1,
789 ..Default::default()
790 };
791 self.call(VIDIOC_QUERYBUF, &mut buffer, "QUERYBUF")?;
792 let length = planes[0].length as usize;
793 let map = unsafe {
794 libc::mmap(
795 std::ptr::null_mut(),
796 length,
797 libc::PROT_READ | libc::PROT_WRITE,
798 libc::MAP_SHARED,
799 self.fd,
800 planes[0].m as libc::off_t,
801 )
802 };
803 if map == libc::MAP_FAILED {
804 return Err(format!(
805 "mapping a buffer failed: {}",
806 std::io::Error::last_os_error()
807 ));
808 }
809 Ok((map, length))
810 }
811
812 fn queue(&self, type_: u32, index: u32, bytesused: u32) -> Result<(), String> {
813 let mut planes = [Plane::default(); 1];
814 planes[0].bytesused = bytesused;
815 let mut buffer = Buffer {
816 index,
817 type_,
818 memory: V4L2_MEMORY_MMAP,
819 m: planes.as_mut_ptr() as u64,
820 length: 1,
821 ..Default::default()
822 };
823 self.call(VIDIOC_QBUF, &mut buffer, "QBUF")
824 }
825
826 fn dequeue(&self, type_: u32) -> Result<(u32, usize), String> {
827 let mut planes = [Plane::default(); 1];
828 let mut buffer = Buffer {
829 type_,
830 memory: V4L2_MEMORY_MMAP,
831 m: planes.as_mut_ptr() as u64,
832 length: 1,
833 ..Default::default()
834 };
835 self.call(VIDIOC_DQBUF, &mut buffer, "DQBUF")?;
836 Ok((buffer.index, planes[0].bytesused as usize))
837 }
838
839 fn stream(&self, request: u64, type_: u32, what: &str) -> Result<(), String> {
840 let mut argument = type_ as i32;
841 self.call(request, &mut argument, what)
842 }
843
844 fn wait_for_frame(&self) -> Result<(), String> {
848 let mut poll = libc::pollfd {
849 fd: self.fd,
850 events: libc::POLLIN,
851 revents: 0,
852 };
853 let ready = unsafe { libc::poll(&mut poll, 1, FRAME_TIMEOUT_MS) };
854 match ready {
855 1 => Ok(()),
856 0 => Err(format!(
857 "the encoder produced nothing in {FRAME_TIMEOUT_MS} ms"
858 )),
859 _ => Err(format!(
860 "waiting for a frame failed: {}",
861 std::io::Error::last_os_error()
862 )),
863 }
864 }
865
866 fn fill_output(&self, pixels: &[u8], stride: usize) -> Result<(), String> {
867 let height = self.height as usize;
868 let needed = stride * (height - 1) + self.row_bytes;
869 if stride < self.row_bytes || pixels.len() < needed {
870 return Err("input buffer too small".into());
871 }
872 let destination = self.output_map as *mut u8;
873 if stride == self.bytesperline {
874 unsafe {
875 std::ptr::copy_nonoverlapping(
876 pixels.as_ptr(),
877 destination,
878 self.bytesperline * height,
879 )
880 };
881 return Ok(());
882 }
883 for row in 0..height {
884 unsafe {
885 std::ptr::copy_nonoverlapping(
886 pixels.as_ptr().add(row * stride),
887 destination.add(row * self.bytesperline),
888 self.row_bytes,
889 );
890 }
891 }
892 Ok(())
893 }
894
895 pub fn codec(&self) -> Codec {
896 self.codec
897 }
898
899 fn frame_type(&self, bytes: &[u8]) -> u8 {
902 match self.codec {
903 Codec::H265 => h265_frame_type(bytes),
904 Codec::Vp8 => frame_type_from_key(vp8_is_key(bytes)),
905 Codec::Vp9 => frame_type_from_key(vp9_is_key(bytes)),
906 _ => h264_frame_type(bytes),
907 }
908 }
909
910 pub fn is_fullcolor(&self) -> bool {
912 false
913 }
914
915 fn signal(&self) -> Option<ColorSignal> {
917 match self.color {
918 Color::Declared(signal) | Color::Writing(signal) => Some(signal),
919 Color::Unknown | Color::Untagged => None,
920 }
921 }
922
923 pub fn last_reference(&self) -> Reference {
925 Reference::Untracked
926 }
927
928 pub fn invalidate_reference(&mut self, _frame_id: u16) -> bool {
929 false
930 }
931
932 pub fn is_full_range(&self) -> bool {
935 self.signal().is_some_and(|signal| signal.full_range)
936 }
937
938 fn tag_sequence(&mut self, unit: &[u8]) -> Option<Vec<u8>> {
943 if self.codec != Codec::H264 {
944 return None;
945 }
946 let sets = sps::sequence_parameter_sets(unit);
947 if sets.is_empty() {
948 return None;
949 }
950 if !self.decided {
951 self.decided = true;
952 let (start, end) = sets[0];
953 self.color = match sps::read_color(&unit[start..end]) {
954 Some(signal) => {
955 println!(
956 "[pixelflux] The M2M encoder declares its own color: matrix {}, {} range.",
957 signal.matrix,
958 if signal.full_range { "full" } else { "limited" }
959 );
960 Color::Declared(signal)
961 }
962 None => match self.fallback {
963 Some(signal) => {
964 println!(
965 "[pixelflux] The M2M encoder declares no color; writing matrix {}, {} range into its headers.",
966 signal.matrix,
967 if signal.full_range { "full" } else { "limited" }
968 );
969 Color::Writing(signal)
970 }
971 None => {
972 eprintln!(
973 "[pixelflux] The M2M encoder declares no color and none is on record for it; the stream carries none, and a decoder will guess from the frame size."
974 );
975 Color::Untagged
976 }
977 },
978 };
979 }
980
981 let mut out: Option<Vec<u8>> = None;
982 let mut copied = 0;
983 for (start, end) in sets {
984 let original = &unit[start..end];
985 let colored = match self.color {
986 Color::Writing(signal) => self.write_color(original, signal),
987 _ => None,
988 };
989 let set = colored.as_deref().unwrap_or(original);
990 let Some(replacement) = self.reorder.set(set).or(colored) else {
991 continue;
992 };
993 let out = out.get_or_insert_with(|| Vec::with_capacity(unit.len() + 16 * 4));
994 out.extend_from_slice(&unit[copied..start]);
995 out.extend_from_slice(&replacement);
996 copied = end;
997 }
998 let mut out = out?;
999 out.extend_from_slice(&unit[copied..]);
1000 Some(out)
1001 }
1002
1003 fn write_color(&mut self, original: &[u8], signal: ColorSignal) -> Option<Vec<u8>> {
1007 if let Some((was, now)) = &self.written_sps
1008 && was == original
1009 {
1010 return Some(now.clone());
1011 }
1012 match sps::write_color(original, signal) {
1013 Ok(written) => {
1014 self.written_sps = Some((original.to_vec(), written.clone()));
1015 Some(written)
1016 }
1017 Err(e) => {
1018 if self.written_sps.is_none() {
1019 eprintln!("[pixelflux] The sequence parameter set was left as it came: {e}");
1020 self.color = Color::Untagged;
1021 }
1022 None
1023 }
1024 }
1025 }
1026
1027 pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
1031 let wanted = (settings.video_bitrate_kbps.max(1) as u32).saturating_mul(1000);
1032 let fps = settings.target_fps.max(1.0);
1033 if (fps - self.fps).abs() > 0.01 {
1034 self.set_frame_rate(fps)?;
1035 }
1036 if wanted != self.bitrate_bps {
1037 self.set_control(CID_BITRATE, wanted as i32, "bitrate")?;
1038 self.bitrate_bps = wanted;
1039 }
1040 Ok(())
1041 }
1042
1043 pub fn encode_host(
1044 &mut self,
1045 pixels: &[u8],
1046 stride: usize,
1047 _rgba: bool,
1048 frame_number: u64,
1049 _qp: u32,
1050 force_idr: bool,
1051 ) -> Result<Vec<u8>, String> {
1052 self.fill_output(pixels, stride)?;
1053 if force_idr {
1054 self.set_control(CID_FORCE_KEY_FRAME, 1, "force key frame")?;
1055 }
1056 self.queue(
1057 V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
1058 0,
1059 self.output_size as u32,
1060 )?;
1061 self.wait_for_frame()?;
1062 let (index, length) = self.dequeue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE)?;
1063 let slot = index as usize;
1064 if slot >= CAPTURE_BUFFERS {
1065 return Err(format!("the node returned capture buffer {slot}"));
1066 }
1067 let (data, _) = self.capture[slot];
1068 let unit = unsafe { std::slice::from_raw_parts(data as *const u8, length) };
1069 let out = self.frame(unit, frame_number);
1070 self.queue(V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index, 0)?;
1071 self.dequeue(V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)?;
1072 Ok(out)
1073 }
1074
1075 fn frame(&mut self, unit: &[u8], frame_number: u64) -> Vec<u8> {
1078 let tagged = self.tag_sequence(unit);
1079 let bytes = tagged.as_deref().unwrap_or(unit);
1080 let mut out = Vec::with_capacity(VIDEO_HEADER_LEN + bytes.len());
1081 if !self.omit_headers {
1082 push_video_header(
1083 &mut out,
1084 self.codec,
1085 self.frame_type(bytes),
1086 frame_number as u16,
1087 0,
1088 self.width as u16,
1089 self.height as u16,
1090 Reference::Untracked,
1091 );
1092 }
1093 out.extend_from_slice(bytes);
1094 out
1095 }
1096}
1097
1098impl Drop for V4l2M2mEncoder {
1099 fn drop(&mut self) {
1100 let _ = self.stream(
1101 VIDIOC_STREAMOFF,
1102 V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,
1103 "STREAMOFF output",
1104 );
1105 let _ = self.stream(
1106 VIDIOC_STREAMOFF,
1107 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
1108 "STREAMOFF capture",
1109 );
1110 if self.output_map != libc::MAP_FAILED {
1111 unsafe { libc::munmap(self.output_map, self.output_size) };
1112 }
1113 for (map, length) in self.capture {
1114 if map != libc::MAP_FAILED {
1115 unsafe { libc::munmap(map, length) };
1116 }
1117 }
1118 if self.fd >= 0 {
1119 unsafe { libc::close(self.fd) };
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use crate::encoders::sps::fixtures::{PI4_SPS, VCE_SPS, reorder_of, without};
1128
1129 fn stand_in(codec: Codec, driver: &str) -> V4l2M2mEncoder {
1132 V4l2M2mEncoder {
1133 fd: -1,
1134 codec,
1135 coded: coded_fourcc(codec).unwrap_or(0),
1136 width: 1280,
1137 height: 720,
1138 row_bytes: 1280 * 4,
1139 bytesperline: 0,
1140 output_size: 0,
1141 output_map: libc::MAP_FAILED,
1142 capture: [(libc::MAP_FAILED, 0); CAPTURE_BUFFERS],
1143 bitrate_bps: 8_000_000,
1144 fps: 30.0,
1145 omit_headers: true,
1146 color: match known_conversion(driver) {
1147 Some(signal) => Color::Writing(signal),
1148 None => Color::Unknown,
1149 },
1150 fallback: known_conversion(driver),
1151 written_sps: None,
1152 decided: false,
1153 reorder: sps::NoReorder::new("The M2M encoder", true),
1154 }
1155 }
1156
1157 fn unit(sps: &[u8]) -> Vec<u8> {
1159 [
1160 &[0, 0, 0, 1][..],
1161 sps,
1162 &[0, 0, 0, 1, 0x68, 0xee, 0x3c, 0x80],
1163 &[0, 0, 0, 1, 0x65, 0x88, 0x84, 0x00, 0x21],
1164 ]
1165 .concat()
1166 }
1167
1168 fn first_set(unit: &[u8]) -> Vec<u8> {
1170 let (start, end) = sps::sequence_parameter_sets(unit)[0];
1171 unit[start..end].to_vec()
1172 }
1173
1174 #[test]
1178 fn a_device_set_leaves_bounded_at_zero() {
1179 let mut session = stand_in(Codec::H264, "unknown-codec");
1180 let tagged = session
1181 .tag_sequence(&unit(VCE_SPS))
1182 .expect("a set to bound");
1183 assert_eq!(reorder_of(&first_set(&tagged)), Some((0, 2)));
1184 assert_eq!(
1185 sps::read_color(&first_set(&tagged)),
1186 sps::read_color(VCE_SPS),
1187 "the device's own color moved"
1188 );
1189 assert!(
1190 tagged.ends_with(&unit(VCE_SPS)[4 + VCE_SPS.len()..]),
1191 "the units after the set changed"
1192 );
1193 assert_eq!(
1194 session.tag_sequence(&unit(VCE_SPS)),
1195 Some(tagged),
1196 "the next key frame's set differs"
1197 );
1198 assert_eq!(
1199 session.tag_sequence(&[0, 0, 0, 1, 0x41, 0x9a, 0x02]),
1200 None,
1201 "a delta without a set was touched"
1202 );
1203 }
1204
1205 #[test]
1209 fn the_pi_color_and_the_bound_are_written_together() {
1210 let mut session = stand_in(Codec::H264, "bcm2835-codec");
1211 let tagged = first_set(
1212 &session
1213 .tag_sequence(&unit(PI4_SPS))
1214 .expect("a color to write"),
1215 );
1216 assert_eq!(sps::read_color(&tagged), Some(ColorSignal::BT601_FULL));
1217 assert_eq!(reorder_of(&tagged), Some((0, 1)));
1218 let mut session = stand_in(Codec::H264, "bcm2835-codec");
1219 let bare = without(PI4_SPS, false);
1220 let tagged = first_set(&session.tag_sequence(&unit(&bare)).expect("a set to write"));
1221 assert_eq!(sps::read_color(&tagged), Some(ColorSignal::BT601_FULL));
1222 assert_eq!(reorder_of(&tagged), Some((0, 1)));
1223 }
1224
1225 #[test]
1228 fn the_header_names_the_session_codec() {
1229 use crate::encoders::codec::{FRAME_KEY, parse_video_type};
1230 let units: [(Codec, Vec<u8>); 3] = [
1231 (Codec::H264, unit(VCE_SPS)),
1232 (
1233 Codec::H265,
1234 vec![
1235 0, 0, 0, 1, 0x40, 0x01, 0x0c, 0, 0, 0, 1, 0x26, 0x01, 0xaf, 0x10,
1236 ],
1237 ),
1238 (
1239 Codec::Vp8,
1240 vec![0x50, 0x2a, 0x00, 0x9d, 0x01, 0x2a, 0x00, 0x05, 0xd0, 0x02],
1241 ),
1242 ];
1243 for (codec, bytes) in units {
1244 let mut session = stand_in(codec, "unknown-codec");
1245 session.omit_headers = false;
1246 let framed = session.frame(&bytes, 7);
1247 assert_eq!(
1248 parse_video_type(framed[1]),
1249 Some((codec, FRAME_KEY)),
1250 "{codec:?}"
1251 );
1252 assert_eq!(&framed[2..4], &[0, 7], "{codec:?}: the frame number");
1253 }
1254 }
1255
1256 #[test]
1259 fn other_codecs_are_passed_through() {
1260 for codec in [Codec::H265, Codec::Vp8, Codec::Vp9] {
1261 let mut session = stand_in(codec, "bcm2835-codec");
1262 assert_eq!(session.tag_sequence(&unit(VCE_SPS)), None, "{codec:?}");
1263 }
1264 }
1265
1266 #[test]
1269 fn layouts_match_the_ioctl_numbers() {
1270 abi_matches().expect("the V4L2 structures are laid out as their ioctl numbers say");
1271 }
1272
1273 fn node(min: u32, max: u32, step: u32) -> NodeInfo {
1274 NodeInfo {
1275 path: "/dev/videoX".to_string(),
1276 driver: "test".to_string(),
1277 coded: vec![V4L2_PIX_FMT_H264],
1278 min_width: min,
1279 max_width: max,
1280 step_width: step,
1281 min_height: min,
1282 max_height: max,
1283 step_height: step,
1284 }
1285 }
1286
1287 #[test]
1290 fn a_node_serves_the_codecs_it_enumerates() {
1291 let mut info = node(16, 1920, 2);
1292 info.coded = vec![V4L2_PIX_FMT_H264, V4L2_PIX_FMT_HEVC];
1293 assert!(info.serves(Codec::H264) && info.serves(Codec::H265));
1294 assert!(
1295 !info.serves(Codec::Vp8),
1296 "a format it does not enumerate is not served"
1297 );
1298 assert!(
1299 !info.serves(Codec::Av1),
1300 "and one the interface cannot name is never served"
1301 );
1302 info.coded = vec![V4L2_PIX_FMT_VP9];
1303 assert!(
1304 !info.serves(Codec::H264),
1305 "a node without H.264 is not asked for it"
1306 );
1307 assert!(info.serves(Codec::Vp9));
1308 }
1309
1310 #[test]
1313 fn a_codec_maps_to_the_fourcc_the_kernel_names() {
1314 assert_eq!(
1315 coded_fourcc(Codec::H264),
1316 Some(u32::from_le_bytes(*b"H264"))
1317 );
1318 assert_eq!(
1319 coded_fourcc(Codec::H265),
1320 Some(u32::from_le_bytes(*b"HEVC"))
1321 );
1322 assert_eq!(coded_fourcc(Codec::Vp8), Some(u32::from_le_bytes(*b"VP80")));
1323 assert_eq!(coded_fourcc(Codec::Vp9), Some(u32::from_le_bytes(*b"VP90")));
1324 assert_eq!(coded_fourcc(Codec::Av1), None);
1325 assert_eq!(coded_fourcc(Codec::Jpeg), None);
1326 }
1327
1328 #[test]
1329 fn a_node_takes_the_sizes_inside_its_range_and_on_its_step() {
1330 let stepwise = node(32, 1920, 2);
1331 assert!(stepwise.fits(1920, 1080), "1080p is inside 32..1920 by 2");
1332 assert!(
1333 stepwise.fits(1920, 1200),
1334 "the ceiling is square, so 1920x1200 fits"
1335 );
1336 assert!(
1337 !stepwise.fits(2560, 1440),
1338 "a size past the ceiling does not fit"
1339 );
1340 assert!(
1341 !stepwise.fits(30, 30),
1342 "a size below the floor does not fit"
1343 );
1344 assert!(
1345 !stepwise.fits(1921, 1080),
1346 "an odd width is off a step of two"
1347 );
1348 }
1349
1350 #[test]
1352 fn a_discrete_node_takes_only_the_size_it_publishes() {
1353 let discrete = node(1280, 1280, 0);
1354 assert!(discrete.fits(1280, 1280));
1355 assert!(!discrete.fits(1281, 1280));
1356 }
1357
1358 #[test]
1359 fn the_input_format_follows_the_bytes_the_caller_holds() {
1360 assert_eq!(
1361 input_format(false),
1362 V4L2_PIX_FMT_BGRA,
1363 "an X11 capture hands over B,G,R,A"
1364 );
1365 assert_eq!(
1366 input_format(true),
1367 V4L2_PIX_FMT_RGBA,
1368 "an rgba caller hands over R,G,B,A"
1369 );
1370 assert_ne!(V4L2_PIX_FMT_BGRA, V4L2_PIX_FMT_RGBA);
1371 }
1372}
1373
1374#[cfg(test)]
1378mod hardware_tests {
1379 use super::*;
1380
1381 fn settings(width: i32, height: i32) -> RustCaptureSettings {
1382 RustCaptureSettings {
1383 width,
1384 height,
1385 codec: Codec::H264,
1386 video_bitrate_kbps: 8000,
1387 target_fps: 30.0,
1388 keyframe_interval_s: 2.0,
1389 omit_stripe_headers: true,
1390 ..Default::default()
1391 }
1392 }
1393
1394 fn chart(width: usize, height: usize) -> Vec<u8> {
1396 let colors = [
1397 [255u8, 255, 255],
1398 [0, 0, 0],
1399 [0, 0, 255],
1400 [0, 255, 0],
1401 [255, 0, 0],
1402 ];
1403 let mut frame = Vec::with_capacity(width * height * 4);
1404 for _ in 0..height {
1405 for x in 0..width {
1406 let [b, g, r] = colors[(x * colors.len()) / width];
1407 frame.extend_from_slice(&[b, g, r, 255]);
1408 }
1409 }
1410 frame
1411 }
1412
1413 fn nal_types(stream: &[u8]) -> Vec<u8> {
1415 let mut types = Vec::new();
1416 let mut i = 0;
1417 while i + 3 < stream.len() {
1418 if stream[i] == 0 && stream[i + 1] == 0 && stream[i + 2] == 1 {
1419 types.push(stream[i + 3] & 0x1f);
1420 i += 4;
1421 } else {
1422 i += 1;
1423 }
1424 }
1425 types
1426 }
1427
1428 #[test]
1429 #[ignore]
1430 fn v4l2_node_answers_for_the_sizes_it_encodes() {
1431 assert!(available(), "no M2M encode node on this machine");
1432 assert!(
1433 encodes(Codec::H264, 1920, 1080),
1434 "1080p is inside every such device's range"
1435 );
1436 assert!(!encodes(Codec::H264, 0, 0), "a zero size is not encodable");
1437 }
1438
1439 #[test]
1440 #[ignore]
1441 fn v4l2_encodes_host_frames_to_h264() {
1442 let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1443 .expect("the session comes up");
1444 let frame = chart(1280, 720);
1445 let first = encoder
1446 .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1447 .expect("first frame");
1448 let types = nal_types(&first);
1449 assert!(!first.is_empty(), "the first access unit is empty");
1450 assert!(
1451 types.contains(&7),
1452 "the key frame carries no SPS: {types:?}"
1453 );
1454 assert!(
1455 types.contains(&5),
1456 "the first frame is not an IDR: {types:?}"
1457 );
1458
1459 let second = encoder
1460 .encode_host(&frame, 1280 * 4, false, 1, 26, false)
1461 .expect("second");
1462 assert!(!second.is_empty(), "the second access unit is empty");
1463 assert!(
1464 second.len() < first.len(),
1465 "an unchanged frame coded no smaller than the key frame it follows: {} vs {}",
1466 second.len(),
1467 first.len()
1468 );
1469 }
1470
1471 #[test]
1474 #[ignore]
1475 fn v4l2_takes_a_live_rate_change() {
1476 let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1477 .expect("the session comes up");
1478 let frame = chart(1280, 720);
1479 encoder
1480 .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1481 .expect("first frame");
1482 let mut lowered = settings(1280, 720);
1483 lowered.video_bitrate_kbps = 1000;
1484 lowered.target_fps = 15.0;
1485 encoder
1486 .reconfigure_rate(&lowered)
1487 .expect("the device takes a live rate change");
1488 encoder
1489 .encode_host(&frame, 1280 * 4, false, 1, 26, false)
1490 .expect("a frame after it");
1491 }
1492
1493 #[test]
1499 #[ignore]
1500 fn v4l2_bench_1080p() {
1501 const FRAMES: u64 = 90;
1502 let (width, height) = (1920, 1080);
1503 let (w, h) = (width as usize, height as usize);
1504 let contents: [(&str, Vec<Vec<u8>>); 3] = [
1508 ("still ", vec![chart(w, h)]),
1509 ("moving ", (0..4).map(|n| shifted_chart(w, h, n)).collect()),
1510 ("noise ", (0..4).map(|n| moving(w, h, n)).collect()),
1511 ];
1512 for (name, frames) in contents {
1513 let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(width, height), false)
1514 .expect("the session comes up");
1515 let cpu_before = process_cpu_seconds();
1516 let started = std::time::Instant::now();
1517 let mut bytes = 0usize;
1518 for n in 0..FRAMES {
1519 let frame = &frames[(n as usize) % frames.len()];
1520 bytes += encoder
1521 .encode_host(frame, w * 4, false, n, 26, n == 0)
1522 .expect("a frame")
1523 .len();
1524 }
1525 let wall = started.elapsed().as_secs_f64();
1526 let cpu = process_cpu_seconds() - cpu_before;
1527 println!(
1528 "[v4l2-bench] {width}x{height} {name}: {:.1} fps serial, {:.2} ms per frame, \
1529 CPU {:.2} ms per frame, {:.3} of a core at 30 fps, {:.0} kbit/s",
1530 FRAMES as f64 / wall,
1531 wall * 1000.0 / FRAMES as f64,
1532 cpu * 1000.0 / FRAMES as f64,
1533 cpu / FRAMES as f64 * 30.0,
1534 bytes as f64 * 8.0 / 1000.0 / (FRAMES as f64 / 30.0)
1535 );
1536 }
1537 }
1538
1539 fn process_cpu_seconds() -> f64 {
1541 let mut spec = libc::timespec {
1542 tv_sec: 0,
1543 tv_nsec: 0,
1544 };
1545 unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut spec) };
1546 spec.tv_sec as f64 + spec.tv_nsec as f64 / 1e9
1547 }
1548
1549 fn shifted_chart(width: usize, height: usize, step: usize) -> Vec<u8> {
1552 let colors = [
1553 [255u8, 255, 255],
1554 [0, 0, 0],
1555 [0, 0, 255],
1556 [0, 255, 0],
1557 [255, 0, 0],
1558 ];
1559 let mut frame = Vec::with_capacity(width * height * 4);
1560 for _ in 0..height {
1561 for x in 0..width {
1562 let shifted = (x + step * 64) % width;
1563 let [b, g, r] = colors[(shifted * colors.len()) / width];
1564 frame.extend_from_slice(&[b, g, r, 255]);
1565 }
1566 }
1567 frame
1568 }
1569
1570 fn moving(width: usize, height: usize, step: usize) -> Vec<u8> {
1572 let mut frame = Vec::with_capacity(width * height * 4);
1573 for y in 0..height {
1574 for x in 0..width {
1575 let value = ((x + step * 37) ^ (y + step * 11)) as u8;
1576 frame.extend_from_slice(&[
1577 value,
1578 value.wrapping_mul(3),
1579 value.wrapping_mul(7),
1580 255,
1581 ]);
1582 }
1583 }
1584 frame
1585 }
1586
1587 #[test]
1591 #[ignore]
1592 fn v4l2_steps_down_from_a_rate_the_device_refuses() {
1593 let mut asked = settings(1920, 1080);
1594 asked.target_fps = 60.0;
1595 let encoder = V4l2M2mEncoder::new(Codec::H264, &asked, false)
1596 .expect("a session came up at some rate the device takes");
1597 assert!(
1598 encoder.fps <= 60.0,
1599 "the session reports a rate it never asked for"
1600 );
1601 assert!(encoder.fps >= 15.0, "the session stepped below the floor");
1602 }
1603
1604 #[test]
1609 #[ignore]
1610 fn v4l2_the_color_reads_back_through_a_decoder() {
1611 use crate::webcam::decode::{ColorTags, Decoder as _, VideoDecoder};
1612
1613 let mut encoder = V4l2M2mEncoder::new(Codec::H264, &settings(1280, 720), false)
1614 .expect("the session comes up");
1615 let frame = chart(1280, 720);
1616 let unit = encoder
1617 .encode_host(&frame, 1280 * 4, false, 0, 26, true)
1618 .expect("a frame");
1619 let mut decoder = VideoDecoder::new(Codec::H264).expect("a decoder");
1620 assert!(
1621 decoder.decode(&unit).expect("the stream decodes"),
1622 "no frame came back"
1623 );
1624
1625 let Some(signal) = encoder.signal() else {
1626 assert_eq!(
1627 decoder.color_tags().map(|tags| tags.matrix),
1628 Some(2),
1629 "the session claims nothing, so the stream must claim nothing either"
1630 );
1631 return;
1632 };
1633 assert_eq!(
1634 decoder.color_tags(),
1635 Some(ColorTags {
1636 matrix: signal.matrix,
1637 full_range: signal.full_range
1638 }),
1639 "the decoder reads a different signal than the session declares"
1640 );
1641 }
1642
1643 #[test]
1644 #[ignore]
1645 fn v4l2_refuses_a_size_past_the_ceiling() {
1646 let info = node().expect("a node");
1647 let too_wide = (info.max_width + info.step_width.max(2)) as i32;
1648 assert!(
1649 !encodes(Codec::H264, too_wide, 1080),
1650 "a size past the ceiling reported as encodable"
1651 );
1652 let error = match V4l2M2mEncoder::new(Codec::H264, &settings(too_wide, 1080), false) {
1653 Ok(_) => panic!("a session past the ceiling came up"),
1654 Err(e) => e,
1655 };
1656 assert!(
1657 error.contains("encodes"),
1658 "the refusal does not name the range: {error}"
1659 );
1660 }
1661}