1pub mod bits;
13pub(crate) mod blit_semaphore;
15pub mod codec;
17pub mod frame_rate;
19pub mod hevc;
21pub mod nvenc;
24#[cfg(any(feature = "openh264", test))]
27pub mod oh264;
28#[cfg(any(feature = "openh264", test))]
30pub mod oh264_refs;
31pub mod overlay;
33pub mod reference;
34pub mod session;
36pub mod software;
39pub mod sps;
42pub mod svtav1;
44#[cfg(target_arch = "aarch64")]
49pub mod tegra;
50pub mod v4l2m2m;
54pub mod vaapi;
56pub mod vpx;
58
59pub use codec::*;
60
61use std::collections::HashMap;
62use std::ffi::c_void;
63use std::sync::{Mutex, OnceLock};
64
65use nvenc::NvencEncoder;
66use smithay::backend::allocator::dmabuf::Dmabuf;
67
68use crate::RustCaptureSettings;
69
70#[cfg(not(any(feature = "gpl", feature = "openh264")))]
71compile_error!(
72 "pixelflux needs a software H.264 encoder: enable the `gpl` feature (libx264, the default) or `openh264`."
73);
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct SoftwareEncoder {
79 pub library: &'static str,
80}
81
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
86pub struct Formats {
87 pub fullcolor: bool,
88 pub ten_bit: [bool; 2],
89}
90
91impl Formats {
92 pub fn names(&self) -> Vec<String> {
94 let mut names = Vec::new();
95 for (fullcolor, chroma) in [(false, "420"), (true, "444")] {
96 for (ten_bit, depth) in [(false, 8), (true, 10)] {
97 let carried = match (fullcolor, ten_bit) {
98 (false, false) => true,
99 (true, false) => self.fullcolor,
100 (_, true) => self.ten_bit[fullcolor as usize],
101 };
102 if carried {
103 names.push(format!("{chroma}-{depth}"));
104 }
105 }
106 }
107 names
108 }
109}
110
111pub type HardwareEncoders = Vec<(Codec, &'static str, Formats)>;
114
115pub fn hardware_encoders(encode_node_index: i32) -> HardwareEncoders {
125 probe_node(encode_node_index).unwrap_or_default()
126}
127
128type ProbeAnswer = Result<HardwareEncoders, (&'static str, String)>;
131
132fn probe_node(encode_node_index: i32) -> ProbeAnswer {
136 static PROBED: OnceLock<Mutex<HashMap<i32, ProbeAnswer>>> = OnceLock::new();
137 let node = encode_node_index.max(0);
138 let mut probed = PROBED
139 .get_or_init(|| Mutex::new(HashMap::new()))
140 .lock()
141 .unwrap();
142 if let Some(answer) = probed.get(&node) {
143 return answer.clone();
144 }
145 #[cfg(target_arch = "aarch64")]
146 if tegra::available() {
147 let served: HardwareEncoders = tegra::served()
148 .into_iter()
149 .map(|c| (c, "tegra", Formats::default()))
150 .collect();
151 let names: Vec<&str> = served.iter().map(|(c, ..)| c.display()).collect();
152 println!(
153 "[pixelflux] Render node {node} encodes {} on tegra.",
154 names.join(", ")
155 );
156 probed.insert(node, Ok(served.clone()));
157 return Ok(served);
158 }
159 let driver = crate::get_gpu_driver(node);
160 let (backend, codecs) = if crate::driver_selects_nvenc(&driver) {
161 ("nvenc", nvenc::probe_codecs(node))
162 } else {
163 ("vaapi", vaapi::probe_codecs(node))
164 };
165 let answer = match codecs {
166 Ok(codecs) => Ok(codecs
167 .into_iter()
168 .map(|(codec, formats): vaapi::Served| (codec, backend, formats))
169 .collect()),
170 Err(e) => {
171 eprintln!("[pixelflux] No hardware encoder on render node {node} ({backend}): {e}");
172 Err((backend, e))
173 }
174 };
175 if answer.as_ref().map_or(true, Vec::is_empty) && v4l2m2m::available() {
176 let codecs = v4l2m2m::served();
180 let served: HardwareEncoders = codecs
181 .iter()
182 .map(|&c| (c, "v4l2m2m", Formats::default()))
183 .collect();
184 let names: Vec<&str> = codecs.iter().map(|c| c.display()).collect();
185 println!(
186 "[pixelflux] A stateful V4L2 M2M encoder serves {}.",
187 names.join(", ")
188 );
189 probed.insert(node, Ok(served.clone()));
190 return Ok(served);
191 }
192 if let Ok(served) = &answer
193 && !served.is_empty()
194 {
195 let names: Vec<&str> = served.iter().map(|(codec, ..)| codec.display()).collect();
196 println!(
197 "[pixelflux] Render node {node} encodes {} on {backend}.",
198 names.join(", ")
199 );
200 }
201 if !matches!(&answer, Err((_, e)) if e == nvenc::SESSIONS_TAKEN) {
202 probed.insert(node, answer.clone());
203 }
204 answer
205}
206
207pub fn software_fullcolor(codec: Codec) -> bool {
211 match software_encoder(codec) {
212 Some(enc) => {
213 matches!(enc.library, "x264" | "x265")
214 || (codec == Codec::Vp9 && enc.library == "libvpx" && vpx::encodes_444())
215 }
216 None => false,
217 }
218}
219
220pub fn software_ten_bit(codec: Codec) -> bool {
225 match software_encoder(codec) {
226 Some(enc) => match (codec, enc.library) {
227 #[cfg(feature = "gpl")]
228 (Codec::H264, "x264") => software::H264EncoderWrapper::ten_bit(),
229 (Codec::H265, "x265") => hevc::ten_bit(),
230 (Codec::Vp9, "libvpx") => vpx::encodes_ten_bit(),
231 (Codec::Av1, _) => true,
232 _ => false,
233 },
234 None => false,
235 }
236}
237
238pub fn software_formats(codec: Codec) -> Formats {
240 let ten_bit = software_ten_bit(codec);
241 let fullcolor = software_fullcolor(codec);
242 Formats {
243 fullcolor,
244 ten_bit: [ten_bit, ten_bit && fullcolor],
245 }
246}
247
248pub fn software_library(codec: Codec) -> &'static str {
251 software_encoder(codec).map_or("none", |enc| enc.library)
252}
253
254pub fn software_encoder(codec: Codec) -> Option<SoftwareEncoder> {
264 static ENCODES: OnceLock<[bool; 5]> = OnceLock::new();
265 let library = match codec {
266 Codec::Jpeg => return None,
267 Codec::H264 => {
268 if cfg!(feature = "gpl") {
269 "x264"
270 } else {
271 "openh264"
272 }
273 }
274 Codec::H265 => {
275 if cfg!(feature = "gpl") {
276 "x265"
277 } else {
278 "kvazaar"
279 }
280 }
281 Codec::Vp8 | Codec::Vp9 => "libvpx",
282 Codec::Av1 => "svt-av1",
283 };
284 let encodes =
285 ENCODES.get_or_init(|| Codec::VIDEO.map(|c| c == Codec::H264 || encodes_in_child(c)));
286 encodes[Codec::VIDEO.iter().position(|&c| c == codec).unwrap()]
287 .then_some(SoftwareEncoder { library })
288}
289
290fn encodes_in_child(codec: Codec) -> bool {
295 survives_in_child(|| {
296 let Ok(pool) = rayon::ThreadPoolBuilder::new()
301 .num_threads(1)
302 .use_current_thread()
303 .build()
304 else {
305 return;
306 };
307 pool.install(|| {
308 let settings = RustCaptureSettings {
309 width: 256,
310 height: 128,
311 ..Default::default()
312 };
313 let Ok(mut session) = software_session(&settings, codec, false) else {
314 return;
315 };
316 let frame = vec![0u8; 256 * 128 * 4];
317 for n in 0..64 {
318 let packet = session.encode_host(
319 &frame,
320 256 * 4,
321 false,
322 n,
323 settings.video_crf as u32,
324 n == 0,
325 );
326 if !matches!(packet, Ok(p) if p.is_empty()) {
327 break;
328 }
329 }
330 })
331 })
332}
333
334fn survives_in_child(f: impl FnOnce()) -> bool {
342 let mut live = svtav1::LIFECYCLE.lock().unwrap_or_else(|e| e.into_inner());
343 while *live > 0 {
344 live = svtav1::RELEASED
345 .wait(live)
346 .unwrap_or_else(|e| e.into_inner());
347 }
348 let pid = unsafe { libc::fork() };
349 drop(live);
350 match pid {
351 0 => {
352 unsafe {
353 libc::signal(libc::SIGALRM, libc::SIG_DFL);
354 libc::alarm(60);
355 let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
356 if null >= 0 {
357 libc::dup2(null, 1);
358 libc::dup2(null, 2);
359 }
360 }
361 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
362 unsafe { libc::_exit(0) }
363 }
364 pid if pid > 0 => {
365 let mut status = 0;
366 while unsafe { libc::waitpid(pid, &mut status, 0) } < 0 {
367 if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
368 return true;
369 }
370 }
371 !libc::WIFSIGNALED(status)
372 }
373 _ => true,
374 }
375}
376
377pub(crate) const HELD_KEY_BUDGET_S: f64 = 1.0;
388
389pub(crate) const HELD_REFRESH_LIMIT_S: f64 = 10.0;
397
398pub(crate) fn held_key_retry(crf: u32, len: usize, cap: usize) -> Option<u32> {
402 (len > cap && cap > 0)
403 .then(|| (crf + (6.0 * (len as f64 / cap as f64).log2()).ceil() as u32).min(51))
404}
405
406pub(crate) fn held_key_start(crf: u32, last: Option<(usize, u32)>, cap: usize) -> u32 {
413 match last {
414 Some((len, q)) if q.abs_diff(crf) <= 12 => {
415 let at_crf = len as f64 * 2f64.powf((q as f64 - crf as f64) / 6.0);
416 held_key_retry(crf, at_crf.round() as usize, cap).unwrap_or(crf)
417 }
418 _ => crf,
419 }
420}
421
422pub(crate) const QP_HYSTERESIS_LIMIT: u32 = 60;
430
431pub fn reference_frames(settings: &RustCaptureSettings) -> u32 {
460 match settings.video_reference_frames {
461 0 => reference::REFERENCE_FRAMES,
462 n => n.min(reference::REFERENCE_FRAMES),
463 }
464}
465
466pub fn vbv_bits(bitrate_bps: u32, fps: f64, keyframe_interval_s: f64, multiplier: f64) -> u32 {
467 let frame_bits = bitrate_bps as f64 / fps.max(1.0);
468 let mult = if multiplier > 0.0 {
469 multiplier
470 } else if keyframe_interval_s > 0.0 {
471 3.0
472 } else {
473 1.5
474 };
475 (frame_bits * mult).round().max(1.0).min(u32::MAX as f64) as u32
476}
477
478pub fn colorspace_desc(fullcolor: bool, full_range: bool) -> &'static str {
483 match (fullcolor, full_range) {
484 (true, true) => "I444 (Full Range)",
485 (true, false) => "I444 (Limited Range)",
486 (false, true) => "I420 (Full Range)",
487 (false, false) => "I420 (Limited Range)",
488 }
489}
490
491pub fn rate_desc(settings: &RustCaptureSettings, fixed: Option<&str>) -> String {
496 match fixed {
497 Some(mode) => format!("{mode} {}", settings.video_bitrate_kbps),
498 None if settings.video_cbr_mode => format!("CBR {}", settings.video_bitrate_kbps),
499 None => format!("CRF: {}", settings.video_crf),
500 }
501}
502
503pub fn paint_over_desc(
513 settings: &RustCaptureSettings,
514 fixed: Option<&str>,
515 holds: bool,
516) -> Option<String> {
517 if !settings.use_paint_over_quality {
518 return None;
519 }
520 if !settings.codec.is_video() {
521 return (settings.paint_over_jpeg_quality > settings.jpeg_quality).then(|| {
522 format!(
523 "PaintOver Q: {} (Trigger: {}f)",
524 settings.paint_over_jpeg_quality, settings.paint_over_trigger_frames
525 )
526 });
527 }
528 let burst = settings.video_paintover_burst_frames;
529 let at_paint_over = if settings.video_cbr_mode {
530 holds
531 } else if settings.video_paintover_crf < settings.video_crf {
532 fixed.is_none()
533 } else {
534 return None;
535 };
536 Some(if at_paint_over {
537 format!(
538 "PaintOver CRF: {} (Burst: {burst}f)",
539 settings.video_paintover_crf
540 )
541 } else {
542 format!("PaintOver Burst: {burst}f")
543 })
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
553 fn the_rate_field_names_what_the_backend_applies() {
554 let crf = RustCaptureSettings {
555 codec: Codec::H264,
556 video_crf: 25,
557 video_bitrate_kbps: 8000,
558 ..Default::default()
559 };
560 let cbr = RustCaptureSettings {
561 video_cbr_mode: true,
562 ..crf.clone()
563 };
564 assert_eq!(rate_desc(&crf, None), "CRF: 25");
565 assert_eq!(rate_desc(&cbr, None), "CBR 8000");
566 for settings in [&crf, &cbr] {
567 assert_eq!(rate_desc(settings, Some("VBR")), "VBR 8000");
568 assert_eq!(rate_desc(settings, Some("CBR")), "CBR 8000");
569 }
570 }
571
572 #[test]
578 fn the_paint_over_field_names_what_paint_over_does() {
579 let crf = RustCaptureSettings {
580 codec: Codec::H264,
581 video_crf: 25,
582 video_paintover_crf: 18,
583 video_paintover_burst_frames: 5,
584 use_paint_over_quality: true,
585 video_streaming_mode: false,
586 ..Default::default()
587 };
588 let cbr = RustCaptureSettings {
589 video_cbr_mode: true,
590 ..crf.clone()
591 };
592 let turbo = |s: &RustCaptureSettings| RustCaptureSettings {
593 video_streaming_mode: true,
594 ..s.clone()
595 };
596 for settings in [&crf, &turbo(&crf)] {
597 assert_eq!(
598 paint_over_desc(settings, None, true).as_deref(),
599 Some("PaintOver CRF: 18 (Burst: 5f)")
600 );
601 assert_eq!(
602 paint_over_desc(settings, None, false).as_deref(),
603 Some("PaintOver CRF: 18 (Burst: 5f)")
604 );
605 assert_eq!(
606 paint_over_desc(settings, Some("VBR"), false).as_deref(),
607 Some("PaintOver Burst: 5f")
608 );
609 }
610 for settings in [&cbr, &turbo(&cbr)] {
611 assert_eq!(
612 paint_over_desc(settings, None, true).as_deref(),
613 Some("PaintOver CRF: 18 (Burst: 5f)")
614 );
615 assert_eq!(
616 paint_over_desc(settings, None, false).as_deref(),
617 Some("PaintOver Burst: 5f")
618 );
619 assert_eq!(
620 paint_over_desc(settings, Some("CBR"), false).as_deref(),
621 Some("PaintOver Burst: 5f")
622 );
623 }
624 assert_eq!(
625 paint_over_desc(
626 &RustCaptureSettings {
627 use_paint_over_quality: false,
628 ..crf.clone()
629 },
630 None,
631 true
632 ),
633 None
634 );
635 assert_eq!(
636 paint_over_desc(
637 &RustCaptureSettings {
638 use_paint_over_quality: false,
639 ..cbr.clone()
640 },
641 None,
642 false
643 ),
644 None
645 );
646 assert_eq!(
647 paint_over_desc(
648 &RustCaptureSettings {
649 video_paintover_crf: 25,
650 ..crf.clone()
651 },
652 None,
653 true
654 ),
655 None
656 );
657 assert_eq!(
658 paint_over_desc(
659 &RustCaptureSettings {
660 video_paintover_crf: 25,
661 ..cbr.clone()
662 },
663 None,
664 true
665 )
666 .as_deref(),
667 Some("PaintOver CRF: 25 (Burst: 5f)")
668 );
669 let jpeg = RustCaptureSettings {
670 codec: Codec::Jpeg,
671 jpeg_quality: 40,
672 paint_over_jpeg_quality: 90,
673 paint_over_trigger_frames: 15,
674 ..crf.clone()
675 };
676 assert_eq!(
677 paint_over_desc(&jpeg, None, false).as_deref(),
678 Some("PaintOver Q: 90 (Trigger: 15f)")
679 );
680 assert_eq!(
681 paint_over_desc(&turbo(&jpeg), None, false).as_deref(),
682 Some("PaintOver Q: 90 (Trigger: 15f)")
683 );
684 assert_eq!(
685 paint_over_desc(
686 &RustCaptureSettings {
687 paint_over_jpeg_quality: 40,
688 ..jpeg.clone()
689 },
690 None,
691 false
692 ),
693 None
694 );
695 assert_eq!(
696 paint_over_desc(
697 &RustCaptureSettings {
698 use_paint_over_quality: false,
699 ..jpeg
700 },
701 None,
702 false
703 ),
704 None
705 );
706 }
707
708 #[test]
711 fn a_held_key_past_its_budget_is_coarsened_by_the_overshoot() {
712 assert_eq!(held_key_retry(18, 1000, 1000), None);
713 assert_eq!(held_key_retry(18, 2000, 1000), Some(24));
714 assert_eq!(held_key_retry(18, 3000, 1000), Some(28));
715 assert_eq!(held_key_retry(40, 64_000, 1000), Some(51));
716 assert_eq!(held_key_retry(18, 5000, 0), None, "no budget, no retry");
717 }
718
719 #[test]
723 fn a_held_key_starts_where_the_last_one_says_it_fits() {
724 assert_eq!(held_key_start(18, None, 1000), 18);
725 assert_eq!(held_key_start(18, Some((800, 18)), 1000), 18);
726 assert_eq!(held_key_start(18, Some((2000, 18)), 1000), 24);
727 assert_eq!(
728 held_key_start(18, Some((1000, 24)), 1000),
729 24,
730 "a retried key's own index"
731 );
732 assert_eq!(
733 held_key_start(18, Some((100, 40)), 1000),
734 18,
735 "too far to say"
736 );
737 }
738
739 #[test]
742 fn a_session_lands_on_a_video_codec_the_host_serves() {
743 let mut settings = RustCaptureSettings {
744 width: 256,
745 height: 128,
746 codec: Codec::Av1,
747 use_cpu: true,
748 ..Default::default()
749 };
750 let encoder = select_frame_encoder(
751 &mut settings,
752 FrameSource::Host { rgba: false },
753 None,
754 "test",
755 );
756 println!("asked for av1, landed on {}", settings.codec.display());
757 assert_ne!(
758 settings.codec,
759 Codec::Jpeg,
760 "this build encodes video in software"
761 );
762 assert!(software_encoder(settings.codec).is_some());
763 assert!(encoder.is_some() || settings.codec == Codec::H264);
765 }
766
767 #[test]
771 fn a_codec_without_a_path_falls_through_the_served_video_codecs() {
772 let software: Vec<Codec> = SOFTWARE_ORDER
773 .iter()
774 .copied()
775 .filter(|&codec| {
776 codec != Codec::Av1 && codec != Codec::H264 && software_encoder(codec).is_some()
777 })
778 .collect();
779 assert_eq!(fallback_codecs(Codec::Av1, &[], false), software);
780 let mut h264_first = vec![Codec::H264];
781 h264_first.extend(software.iter().copied());
782 assert_eq!(
783 fallback_codecs(Codec::Av1, &[], true),
784 h264_first,
785 "one x264 session leads the software rungs"
786 );
787 assert_eq!(
788 fallback_codecs(Codec::Av1, &[Codec::H264], false),
789 h264_first,
790 "an engine's H.264 leads either way"
791 );
792 assert!(!fallback_codecs(Codec::Av1, &[Codec::Av1], true).contains(&Codec::Av1));
793 let engine = [Codec::H264, Codec::H265, Codec::Av1];
795 assert_eq!(
796 &fallback_codecs(Codec::Vp8, &engine, true)[..3],
797 &[Codec::Av1, Codec::H265, Codec::H264]
798 );
799 }
800
801 #[test]
805 fn the_software_probe_prints_nothing() {
806 if std::env::var_os("PIXELFLUX_PROBE_CHILD").is_some() {
807 for codec in Codec::VIDEO {
808 encodes_in_child(codec);
809 }
810 return;
811 }
812 let out = std::process::Command::new(std::env::current_exe().unwrap())
813 .args([
814 "--exact",
815 "encoders::tests::the_software_probe_prints_nothing",
816 "--nocapture",
817 "--test-threads=1",
818 ])
819 .env("PIXELFLUX_PROBE_CHILD", "1")
820 .output()
821 .unwrap();
822 assert!(out.status.success() && String::from_utf8_lossy(&out.stdout).contains("1 passed"));
823 assert_eq!(
824 String::from_utf8_lossy(&out.stderr),
825 "",
826 "the probes wrote to the host's stderr"
827 );
828 }
829
830 #[test]
835 fn a_session_takes_the_answer_its_node_probe_settled() {
836 let settled = (0..300)
837 .find_map(|_| match probe_node(99) {
838 Err((_, e)) if e == nvenc::SESSIONS_TAKEN => {
839 std::thread::sleep(std::time::Duration::from_millis(100));
840 None
841 }
842 Err((backend, e)) => {
843 Some(format!("{} VP8 did not open: {e}", backend.to_uppercase()))
844 }
845 Ok(_) => Some("render node 99 has no VP8 engine".to_string()),
846 })
847 .expect("the device had no NVENC session to spare for 30 s");
848 let report = crate::report::StreamReport::new("x11");
849 let _scope = crate::report::enter(&report);
850 let mut settings = RustCaptureSettings {
851 width: 256,
852 height: 128,
853 codec: Codec::Vp8,
854 encode_node_index: 99,
855 ..Default::default()
856 };
857 let encoder = select_frame_encoder(
858 &mut settings,
859 FrameSource::Host { rgba: false },
860 None,
861 "test",
862 );
863 assert!(
864 encoder.is_some_and(|enc| !enc.is_hardware()),
865 "VP8 comes up in software"
866 );
867 assert_eq!(report.info().encoder_reason, settled);
868 }
869
870 #[test]
873 fn software_fullcolor_follows_the_library() {
874 assert_eq!(
875 software_fullcolor(Codec::H264),
876 software_library(Codec::H264) == "x264"
877 );
878 assert_eq!(
879 software_fullcolor(Codec::H265),
880 software_library(Codec::H265) == "x265"
881 );
882 assert_eq!(
883 software_fullcolor(Codec::Vp9),
884 software_library(Codec::Vp9) == "libvpx" && vpx::encodes_444()
885 );
886 assert!(!software_fullcolor(Codec::Av1) && !software_fullcolor(Codec::Vp8));
887 }
888
889 #[test]
891 fn a_child_survives_unless_a_signal_takes_it() {
892 assert!(survives_in_child(|| {}));
893 assert!(survives_in_child(|| unsafe { libc::_exit(3) }));
894 assert!(!survives_in_child(|| unsafe {
895 libc::raise(libc::SIGILL);
896 }));
897 }
898
899 #[test]
905 fn the_av1_probe_survives_sessions_opening_beside_it() {
906 use std::sync::atomic::{AtomicBool, Ordering};
907 let settings = RustCaptureSettings {
908 width: 64,
909 height: 64,
910 target_fps: 30.0,
911 codec: Codec::Av1,
912 ..Default::default()
913 };
914 let stop = AtomicBool::new(false);
915 let probing = AtomicBool::new(false);
916 std::thread::scope(|s| {
917 for _ in 0..4 {
918 s.spawn(|| {
919 while !stop.load(Ordering::Relaxed) {
920 if probing.load(Ordering::Acquire) {
921 std::thread::sleep(std::time::Duration::from_millis(1));
922 continue;
923 }
924 drop(software_session(&settings, Codec::Av1, false));
925 }
926 });
927 }
928 let found = (0..4).all(|_| {
929 probing.store(true, Ordering::Release);
930 let found = encodes_in_child(Codec::Av1);
931 probing.store(false, Ordering::Release);
932 found
933 });
934 stop.store(true, Ordering::Relaxed);
935 assert!(found);
936 });
937 }
938
939 #[test]
943 fn the_av1_probe_survives_threads_starting_beside_it() {
944 use std::sync::atomic::{AtomicBool, Ordering};
945 let stop = AtomicBool::new(false);
946 std::thread::scope(|s| {
947 for _ in 0..16 {
948 s.spawn(|| {
949 while !stop.load(Ordering::Relaxed) {
950 let _ = std::thread::spawn(|| {}).join();
951 }
952 });
953 }
954 let found = (0..200).all(|_| encodes_in_child(Codec::Av1));
955 stop.store(true, Ordering::Relaxed);
956 assert!(found);
957 });
958 }
959
960 #[test]
963 fn software_encoders_follow_the_build() {
964 use rayon::prelude::*;
965 let _: u32 = (0..8u32).into_par_iter().sum();
966 let h264 = software_encoder(Codec::H264).expect("H.264 is always served");
967 assert_eq!(
968 h264.library,
969 if cfg!(feature = "gpl") {
970 "x264"
971 } else {
972 "openh264"
973 }
974 );
975 assert_eq!(software_library(Codec::H264), h264.library);
976 assert_eq!(software_library(Codec::Jpeg), "none");
977 assert_eq!(software_encoder(Codec::Jpeg), None);
978 assert_eq!(
979 software_library(Codec::H265),
980 if cfg!(feature = "gpl") {
981 "x265"
982 } else {
983 "kvazaar"
984 }
985 );
986 assert_eq!(software_library(Codec::Vp8), "libvpx");
987 assert_eq!(software_library(Codec::Vp9), "libvpx");
988 assert_eq!(software_library(Codec::Av1), "svt-av1");
989 assert_eq!(software_fullcolor(Codec::H264), cfg!(feature = "gpl"));
990 assert_eq!(software_fullcolor(Codec::H265), cfg!(feature = "gpl"));
991 assert!(!software_fullcolor(Codec::Vp8) && !software_fullcolor(Codec::Av1));
992 assert_eq!(software_fullcolor(Codec::Vp9), vpx::encodes_444());
993 }
994
995 #[test]
1001 fn a_session_reports_the_range_it_converted_at() {
1002 assert_eq!(colorspace_desc(true, true), "I444 (Full Range)");
1003 assert_eq!(colorspace_desc(true, false), "I444 (Limited Range)");
1004 assert_eq!(colorspace_desc(false, true), "I420 (Full Range)");
1005 assert_eq!(colorspace_desc(false, false), "I420 (Limited Range)");
1006
1007 let mut settings = RustCaptureSettings {
1008 codec: Codec::H264,
1009 video_fullcolor: false,
1010 ..Default::default()
1011 };
1012 assert!(
1013 !session_full_range(None, &settings),
1014 "striped 4:2:0 converts at limited range"
1015 );
1016 assert_eq!(
1017 colorspace_desc(
1018 session_fullcolor(None, &settings),
1019 session_full_range(None, &settings)
1020 ),
1021 "I420 (Limited Range)"
1022 );
1023
1024 settings.video_fullcolor = true;
1025 assert_eq!(
1026 session_full_range(None, &settings),
1027 software_fullcolor(Codec::H264),
1028 "striped 4:4:4 converts at full range where the build carries it"
1029 );
1030 }
1031}
1032
1033#[allow(clippy::large_enum_variant)]
1037pub enum FrameEncoder {
1038 Nvenc(NvencEncoder),
1039 Vaapi(vaapi::VaapiEncoder),
1041 Vpx(vpx::VpxEncoder),
1043 Hevc(hevc::HevcEncoder),
1045 Av1(svtav1::SvtAv1Encoder),
1047 #[cfg(target_arch = "aarch64")]
1049 Tegra(tegra::TegraEncoder),
1050 V4l2m2m(v4l2m2m::V4l2M2mEncoder),
1052}
1053
1054macro_rules! each {
1056 ($self:expr, $enc:ident => $e:expr) => {
1057 match $self {
1058 FrameEncoder::Nvenc($enc) => $e,
1059 FrameEncoder::Vaapi($enc) => $e,
1060 FrameEncoder::Vpx($enc) => $e,
1061 FrameEncoder::Hevc($enc) => $e,
1062 FrameEncoder::Av1($enc) => $e,
1063 #[cfg(target_arch = "aarch64")]
1064 FrameEncoder::Tegra($enc) => $e,
1065 FrameEncoder::V4l2m2m($enc) => $e,
1066 }
1067 };
1068}
1069
1070impl FrameEncoder {
1071 pub fn codec(&self) -> Codec {
1073 each!(self, enc => enc.codec())
1074 }
1075
1076 pub fn is_hardware(&self) -> bool {
1078 !matches!(
1079 self,
1080 FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)
1081 )
1082 }
1083
1084 pub fn fixed_rate_control(&self) -> Option<&'static str> {
1088 match self {
1089 #[cfg(target_arch = "aarch64")]
1090 FrameEncoder::Tegra(_) => Some("CBR"),
1091 FrameEncoder::V4l2m2m(_) => Some("VBR"),
1092 _ => None,
1093 }
1094 }
1095
1096 pub fn backend_name(&self) -> &'static str {
1098 match self {
1099 FrameEncoder::Nvenc(_) => "NVENC",
1100 FrameEncoder::Vaapi(_) => "VAAPI",
1101 FrameEncoder::Vpx(enc) => enc.library(),
1102 FrameEncoder::Hevc(enc) => enc.library(),
1103 FrameEncoder::Av1(enc) => enc.library(),
1104 #[cfg(target_arch = "aarch64")]
1105 FrameEncoder::Tegra(_) => "TEGRA",
1106 FrameEncoder::V4l2m2m(_) => "V4L2M2M",
1107 }
1108 }
1109
1110 pub fn is_fullcolor(&self) -> bool {
1112 each!(self, enc => enc.is_fullcolor())
1113 }
1114
1115 pub fn bit_depth(&self) -> u32 {
1118 match self {
1119 FrameEncoder::Nvenc(enc) => enc.bit_depth(),
1120 FrameEncoder::Vaapi(enc) => enc.bit_depth(),
1121 FrameEncoder::Vpx(enc) => enc.bit_depth(),
1122 FrameEncoder::Hevc(enc) => enc.bit_depth(),
1123 FrameEncoder::Av1(enc) => enc.bit_depth(),
1124 _ => 8,
1125 }
1126 }
1127
1128 pub fn is_full_range(&self) -> bool {
1131 each!(self, enc => enc.is_full_range())
1132 }
1133
1134 pub fn reconfigure_rate(&mut self, settings: &RustCaptureSettings) -> Result<(), String> {
1137 match self {
1138 FrameEncoder::Nvenc(enc) => {
1139 enc.reconfigure_rate(settings);
1140 Ok(())
1141 }
1142 FrameEncoder::Vaapi(enc) => enc.reconfigure_rate(settings),
1143 FrameEncoder::Vpx(enc) => enc.reconfigure_rate(settings),
1144 FrameEncoder::Hevc(enc) => enc.reconfigure_rate(settings),
1145 FrameEncoder::Av1(enc) => enc.reconfigure_rate(settings),
1146 #[cfg(target_arch = "aarch64")]
1147 FrameEncoder::Tegra(enc) => enc.reconfigure_rate(settings),
1148 FrameEncoder::V4l2m2m(enc) => enc.reconfigure_rate(settings),
1149 }
1150 }
1151
1152 pub fn encode_host(
1155 &mut self,
1156 pixels: &[u8],
1157 stride: usize,
1158 rgba: bool,
1159 frame_number: u64,
1160 qp: u32,
1161 force_idr: bool,
1162 ) -> Result<Vec<u8>, String> {
1163 match self {
1164 FrameEncoder::Nvenc(enc) => {
1165 enc.encode_cpu_packed(pixels, stride, rgba, frame_number, qp, force_idr)
1166 }
1167 FrameEncoder::Vaapi(enc) => {
1168 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1169 }
1170 FrameEncoder::Vpx(enc) => {
1171 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1172 }
1173 FrameEncoder::Hevc(enc) => {
1174 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1175 }
1176 FrameEncoder::Av1(enc) => {
1177 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1178 }
1179 #[cfg(target_arch = "aarch64")]
1180 FrameEncoder::Tegra(enc) => {
1181 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1182 }
1183 FrameEncoder::V4l2m2m(enc) => {
1184 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1185 }
1186 }
1187 }
1188
1189 pub fn hash_next_upload(&mut self, rows: u32) -> bool {
1193 match self {
1194 FrameEncoder::Nvenc(enc) => enc.hash_next_upload(rows),
1195 _ => false,
1196 }
1197 }
1198
1199 pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
1201 match self {
1202 FrameEncoder::Nvenc(enc) => enc.take_upload_hashes(),
1203 _ => None,
1204 }
1205 }
1206
1207 pub fn last_quality(&self) -> Option<u32> {
1211 match self {
1212 FrameEncoder::Nvenc(enc) => enc.last_quality(),
1213 FrameEncoder::Vaapi(enc) => enc.last_quality(),
1214 FrameEncoder::Vpx(enc) => enc.last_quality(),
1215 FrameEncoder::Hevc(enc) => enc.last_quality(),
1216 FrameEncoder::Av1(enc) => enc.last_quality(),
1217 _ => None,
1218 }
1219 }
1220
1221 pub fn last_size(&self) -> Option<usize> {
1227 match self {
1228 FrameEncoder::Nvenc(enc) => enc.last_size(),
1229 FrameEncoder::Vaapi(enc) => enc.last_size(),
1230 FrameEncoder::Vpx(enc) => enc.last_size(),
1231 FrameEncoder::Hevc(enc) => enc.last_size(),
1232 _ => None,
1233 }
1234 }
1235
1236 pub fn measure(&mut self) {
1239 if let FrameEncoder::Vaapi(enc) = self {
1240 enc.measure();
1241 }
1242 }
1243
1244 pub fn measures(&self) -> bool {
1247 match self {
1248 FrameEncoder::Vaapi(enc) => enc.measures(),
1249 _ => false,
1250 }
1251 }
1252
1253 pub fn last_psnr(&self) -> Option<f32> {
1258 match self {
1259 FrameEncoder::Vaapi(enc) => enc.last_psnr(),
1260 _ => None,
1261 }
1262 }
1263
1264 pub fn band_size(&self) -> Option<usize> {
1268 match self {
1269 FrameEncoder::Nvenc(enc) => enc.band_size(),
1270 FrameEncoder::Vpx(enc) => enc.band_size(),
1271 _ => None,
1272 }
1273 }
1274
1275 pub fn holds_quantizer(&self) -> bool {
1284 match self {
1285 FrameEncoder::Nvenc(_) => true,
1286 FrameEncoder::Vpx(enc) => enc.holds_quantizer(),
1287 FrameEncoder::Vaapi(enc) => !enc.is_cbr(),
1288 FrameEncoder::Hevc(enc) => enc.holds_quantizer(),
1289 FrameEncoder::Av1(enc) => enc.holds_quantizer(),
1290 #[cfg(target_arch = "aarch64")]
1291 FrameEncoder::Tegra(_) => false,
1292 FrameEncoder::V4l2m2m(_) => false,
1293 }
1294 }
1295
1296 pub fn reopens_on_quality(&self) -> bool {
1300 matches!(self, FrameEncoder::Hevc(_) | FrameEncoder::Av1(_))
1301 }
1302
1303 pub fn cleans_up_with_key(&self) -> bool {
1311 !matches!(self, FrameEncoder::Vpx(enc) if enc.codec() == Codec::Vp8)
1312 }
1313
1314 pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
1322 match self {
1323 FrameEncoder::Nvenc(enc) => enc.hold_quantizer(crf, band),
1324 FrameEncoder::Vaapi(enc) => enc.hold_quantizer(crf),
1325 FrameEncoder::Vpx(enc) => enc.hold_quantizer(crf, band),
1326 FrameEncoder::Hevc(enc) => enc.hold_quantizer(crf),
1327 FrameEncoder::Av1(enc) => enc.hold_quantizer(crf),
1328 #[cfg(target_arch = "aarch64")]
1329 FrameEncoder::Tegra(_) => {}
1330 FrameEncoder::V4l2m2m(_) => {}
1331 }
1332 }
1333
1334 pub fn delivered_units(
1339 &self,
1340 data: Vec<u8>,
1341 encoded: u16,
1342 ) -> Vec<(Vec<u8>, u16, reference::Reference)> {
1343 #[cfg(target_arch = "aarch64")]
1344 if let FrameEncoder::Tegra(enc) = self {
1345 match enc.delivered_units() {
1346 [] => {}
1347 &[(id, reference, _)] => return vec![(data, id, reference)],
1348 units => {
1349 let mut start = 0;
1350 return units
1351 .iter()
1352 .map(|&(id, reference, end)| {
1353 let unit = data[start..end].to_vec();
1354 start = end;
1355 (unit, id, reference)
1356 })
1357 .collect();
1358 }
1359 }
1360 }
1361 vec![(data, encoded, self.last_reference())]
1362 }
1363
1364 #[cfg_attr(not(target_arch = "aarch64"), allow(unused_variables))]
1368 pub fn push_held(&mut self, frame_number: u64) -> Result<Vec<u8>, String> {
1369 match self {
1370 #[cfg(target_arch = "aarch64")]
1371 FrameEncoder::Tegra(enc) => enc.push_held(frame_number),
1372 _ => Ok(Vec::new()),
1373 }
1374 }
1375
1376 pub fn holds_frame(&self) -> bool {
1377 match self {
1378 #[cfg(target_arch = "aarch64")]
1379 FrameEncoder::Tegra(enc) => enc.holds_frame(),
1380 _ => false,
1381 }
1382 }
1383
1384 pub fn last_reference(&self) -> reference::Reference {
1385 each!(self, enc => enc.last_reference())
1386 }
1387
1388 pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
1392 each!(self, enc => enc.invalidate_reference(frame_id))
1393 }
1394
1395 pub fn take_report(&mut self, report: reference::ReferenceReport) -> bool {
1399 match (self, report) {
1400 (FrameEncoder::Nvenc(enc), report) => enc.take_report(report),
1401 (FrameEncoder::Vaapi(enc), report) => enc.take_report(report),
1402 (FrameEncoder::Vpx(enc), reference::ReferenceReport::Held(id)) => {
1403 enc.acknowledge_reference(id, true);
1404 true
1405 }
1406 (FrameEncoder::Vpx(enc), reference::ReferenceReport::Sent(id)) => {
1407 enc.acknowledge_reference(id, false);
1408 true
1409 }
1410 (FrameEncoder::Av1(enc), reference::ReferenceReport::Held(id)) => {
1411 enc.acknowledge_reference(id, true);
1412 true
1413 }
1414 (FrameEncoder::Av1(enc), reference::ReferenceReport::Sent(id)) => {
1415 enc.acknowledge_reference(id, false);
1416 true
1417 }
1418 (enc, reference::ReferenceReport::Lost(frame_id)) => enc.invalidate_reference(frame_id),
1419 _ => true,
1420 }
1421 }
1422
1423 pub fn encode_dmabuf(
1425 &mut self,
1426 dmabuf: &Dmabuf,
1427 frame_number: u64,
1428 qp: u32,
1429 force_idr: bool,
1430 ) -> Result<Vec<u8>, String> {
1431 match self {
1432 FrameEncoder::Nvenc(enc) => enc.encode(dmabuf, frame_number, qp, force_idr),
1433 FrameEncoder::Vaapi(enc) => enc.encode_dmabuf(dmabuf, frame_number, qp, force_idr),
1434 _ => Err(format!(
1435 "the {} session takes host frames",
1436 self.backend_name()
1437 )),
1438 }
1439 }
1440
1441 pub fn encode_dmabuf_after_blit(
1444 &mut self,
1445 dmabuf: &Dmabuf,
1446 frame_number: u64,
1447 qp: u32,
1448 force_idr: bool,
1449 ) -> Result<Vec<u8>, String> {
1450 match self {
1451 FrameEncoder::Nvenc(enc) => enc.encode_after_blit(dmabuf, frame_number, qp, force_idr),
1452 _ => Err(format!(
1453 "the {} session waits on no blit semaphore",
1454 self.backend_name()
1455 )),
1456 }
1457 }
1458
1459 pub fn blit_semaphore_fd(&mut self) -> Option<Result<std::os::fd::OwnedFd, String>> {
1463 match self {
1464 FrameEncoder::Nvenc(enc) => Some(enc.blit_semaphore_fd()),
1465 _ => None,
1466 }
1467 }
1468}
1469
1470pub fn software_session(
1472 settings: &RustCaptureSettings,
1473 codec: Codec,
1474 rgba: bool,
1475) -> Result<FrameEncoder, String> {
1476 Ok(match codec {
1477 Codec::Vp8 | Codec::Vp9 => FrameEncoder::Vpx(vpx::VpxEncoder::new(settings, codec, rgba)?),
1478 Codec::H265 => FrameEncoder::Hevc(hevc::HevcEncoder::new(settings, rgba)?),
1479 Codec::Av1 => FrameEncoder::Av1(svtav1::SvtAv1Encoder::new(settings, rgba)?),
1480 Codec::H264 | Codec::Jpeg => {
1481 return Err(format!("{} takes the striped path", codec.display()));
1482 }
1483 })
1484}
1485
1486pub fn chroma_name(fullcolor: bool) -> &'static str {
1488 if fullcolor { "4:4:4" } else { "4:2:0" }
1489}
1490
1491pub fn driver_name(driver: &str) -> &str {
1493 match driver.rsplit('/').next() {
1494 Some(name) if !name.is_empty() => name,
1495 _ => "unknown",
1496 }
1497}
1498
1499#[derive(Clone, Copy)]
1502pub enum FrameSource {
1503 Dmabuf { egl_display: *const c_void },
1504 Host { rgba: bool },
1505}
1506
1507pub fn select_frame_encoder(
1525 settings: &mut RustCaptureSettings,
1526 source: FrameSource,
1527 prior: Option<FrameEncoder>,
1528 tag: &str,
1529) -> Option<FrameEncoder> {
1530 let requested = settings.codec;
1531 if !requested.is_video() {
1532 return None;
1533 }
1534 if let Some(enc) = select_for_codec(settings, source, prior, tag) {
1535 return Some(enc);
1536 }
1537 let FrameSource::Host { rgba } = source else {
1540 return None;
1541 };
1542 if requested == Codec::H264 {
1543 return None;
1544 }
1545 let hardware: Vec<Codec> = if settings.use_cpu || settings.encode_node_index == -1 {
1546 Vec::new()
1547 } else {
1548 hardware_encoders(settings.encode_node_index)
1549 .into_iter()
1550 .map(|(codec, ..)| codec)
1551 .collect()
1552 };
1553 eprintln!(
1554 "[{tag}] No {} path on this host; trying the video codecs it serves.",
1555 requested.display()
1556 );
1557 for codec in fallback_codecs(requested, &hardware, settings.video_fullframe) {
1558 settings.codec = codec;
1559 if let Some(enc) = select_for_codec(settings, source, None, tag) {
1560 return Some(enc);
1561 }
1562 if codec == Codec::H264
1564 && settings.video_fullframe
1565 && software_encoder(Codec::H264).is_some()
1566 {
1567 return None;
1568 }
1569 }
1570 settings.codec = Codec::H264;
1573 if software_encoder(Codec::H264).is_some() {
1574 return software_fallback(settings, rgba, tag);
1575 }
1576 eprintln!("[{tag}] No video encoder on this host. Encoding JPEG instead.");
1577 settings.codec = Codec::Jpeg;
1578 None
1579}
1580
1581const HARDWARE_ORDER: [Codec; 5] = [Codec::Av1, Codec::H265, Codec::Vp9, Codec::H264, Codec::Vp8];
1584
1585const SOFTWARE_ORDER: [Codec; 5] = [Codec::H264, Codec::Av1, Codec::Vp8, Codec::H265, Codec::Vp9];
1588
1589fn fallback_codecs(requested: Codec, hardware: &[Codec], fullframe: bool) -> Vec<Codec> {
1595 let mut codecs: Vec<Codec> = HARDWARE_ORDER
1596 .iter()
1597 .copied()
1598 .filter(|&codec| codec != requested && hardware.contains(&codec))
1599 .collect();
1600 codecs.extend(SOFTWARE_ORDER.iter().copied().filter(|&codec| {
1601 codec != requested
1602 && (codec != Codec::H264 || fullframe)
1603 && !hardware.contains(&codec)
1604 && software_encoder(codec).is_some()
1605 }));
1606 codecs
1607}
1608
1609fn hardware_lacks_ten_bit(served: &HardwareEncoders, settings: &RustCaptureSettings) -> bool {
1614 let codec = settings.codec;
1615 if settings.video_bit_depth < 10 || !software_ten_bit(codec) {
1616 return false;
1617 }
1618 served
1619 .iter()
1620 .find(|&&(c, ..)| c == codec)
1621 .is_some_and(|&(_, _, formats)| {
1622 let fullcolor = settings.video_fullcolor && codec.fullcolor() && formats.fullcolor;
1623 !formats.ten_bit[fullcolor as usize]
1624 })
1625}
1626
1627pub(crate) fn ten_bit_is_softwares(settings: &RustCaptureSettings) -> bool {
1630 probe_node(settings.encode_node_index.max(0))
1631 .is_ok_and(|served| hardware_lacks_ten_bit(&served, settings))
1632}
1633
1634fn select_for_codec(
1637 settings: &mut RustCaptureSettings,
1638 source: FrameSource,
1639 prior: Option<FrameEncoder>,
1640 tag: &str,
1641) -> Option<FrameEncoder> {
1642 let codec = settings.codec;
1643 let software_forced = settings.use_cpu || settings.encode_node_index == -1;
1644 #[cfg(target_arch = "aarch64")]
1645 if let (false, Some(_), FrameSource::Host { rgba }) =
1646 (software_forced, tegra::coded_fourcc(codec), source)
1647 {
1648 if tegra::available() {
1653 drop(prior);
1654 if tegra::served().contains(&codec) {
1655 match tegra::TegraEncoder::new(codec, settings, rgba) {
1656 Ok(enc) => {
1657 println!(
1658 "[{tag}] Encoder: TEGRA {} {} on the vendor V4L2 encoder.",
1659 codec.display(),
1660 chroma_name(enc.is_fullcolor())
1661 );
1662 return Some(FrameEncoder::Tegra(enc));
1663 }
1664 Err(e) => {
1665 eprintln!("[{tag}] Failed to init the Tegra encoder: {e}");
1666 crate::report::encoder_reason(&format!(
1667 "Tegra {} did not open: {e}",
1668 codec.display()
1669 ));
1670 }
1671 }
1672 } else {
1673 crate::report::encoder_reason(&format!(
1674 "the Tegra encoder has no {} engine",
1675 codec.display()
1676 ));
1677 }
1678 return software_fallback(settings, rgba, tag);
1679 }
1680 }
1681 if !software_forced {
1682 let node = settings.encode_node_index.max(0);
1683 let driver = crate::get_gpu_driver(node);
1684 crate::log::debug!("[{tag}] Encode node {node}, driver {driver}.");
1685 let settled = match probe_node(node) {
1688 Err((backend, e)) if e != nvenc::SESSIONS_TAKEN => Some(format!(
1689 "{} {} did not open: {e}",
1690 backend.to_uppercase(),
1691 codec.display()
1692 )),
1693 Ok(served)
1694 if !served.iter().any(|&(c, backend, ..)| {
1695 c == codec && matches!(backend, "nvenc" | "vaapi")
1696 }) =>
1697 {
1698 Some(format!(
1699 "render node {node} has no {} engine",
1700 codec.display()
1701 ))
1702 }
1703 Ok(served) if hardware_lacks_ten_bit(&served, settings) => Some(format!(
1704 "render node {node} encodes no 10-bit {}, which the software encoder does",
1705 codec.display()
1706 )),
1707 _ => None,
1708 };
1709 if let Some(reason) = settled {
1710 drop(prior);
1711 crate::report::encoder_reason(&reason);
1712 } else if crate::driver_selects_nvenc(&driver) {
1713 if let Some(FrameEncoder::Nvenc(mut enc)) = prior {
1714 match enc.reconfigure_resolution(settings) {
1715 Ok(resized) => {
1716 if resized {
1717 crate::log::debug!("[{tag}] NVENC session reconfigured in place.");
1718 }
1719 crate::report::hardware_encoder(
1720 enc.device_name(),
1721 driver_name(&driver),
1722 node,
1723 );
1724 return Some(FrameEncoder::Nvenc(enc));
1725 }
1726 Err(e) => eprintln!(
1727 "[{tag}] NVENC in-place reconfigure unavailable ({e}); rebuilding."
1728 ),
1729 }
1730 }
1731 let egl_display = match source {
1732 FrameSource::Dmabuf { egl_display } => egl_display,
1733 FrameSource::Host { .. } => std::ptr::null(),
1734 };
1735 match NvencEncoder::new(settings, egl_display) {
1736 Ok(enc) => {
1737 println!(
1738 "[{tag}] Encoder: NVENC {} {} {}-bit on {} (render node {node}, {} driver), {}.",
1739 codec.display(),
1740 chroma_name(enc.is_fullcolor()),
1741 enc.bit_depth(),
1742 enc.device_name(),
1743 driver_name(&driver),
1744 enc.split_summary()
1745 );
1746 crate::report::hardware_encoder(enc.device_name(), driver_name(&driver), node);
1747 return Some(FrameEncoder::Nvenc(enc));
1748 }
1749 Err(e) => {
1750 eprintln!("[{tag}] Failed to init NVENC {}: {e}", codec.display());
1751 crate::report::encoder_reason(&format!(
1752 "NVENC {} did not open: {e}",
1753 codec.display()
1754 ));
1755 }
1756 }
1757 } else {
1758 drop(prior);
1762 let input = match source {
1763 FrameSource::Dmabuf { .. } => vaapi::Input::Dmabuf,
1764 FrameSource::Host { rgba } => vaapi::Input::Host { rgba },
1765 };
1766 match vaapi::VaapiEncoder::new(settings, codec, input) {
1767 Ok(enc) => {
1768 println!(
1769 "[{tag}] Encoder: VAAPI {} {} {}-bit on {} surfaces (render node {node}, {} driver).",
1770 codec.display(),
1771 chroma_name(enc.is_fullcolor()),
1772 enc.bit_depth(),
1773 enc.surface_format_name(),
1774 driver_name(&driver)
1775 );
1776 crate::report::hardware_encoder("", driver_name(&driver), node);
1777 return Some(FrameEncoder::Vaapi(enc));
1778 }
1779 Err(e) => {
1780 eprintln!("[{tag}] Failed to init VAAPI {}: {e}", codec.display());
1781 crate::report::encoder_reason(&format!(
1782 "VAAPI {} did not open: {e}",
1783 codec.display()
1784 ));
1785 }
1786 }
1787 }
1788 if let (Some(_), FrameSource::Host { rgba }) = (v4l2m2m::coded_fourcc(codec), source)
1793 && v4l2m2m::encodes(codec, settings.width, settings.height)
1794 {
1795 match v4l2m2m::V4l2M2mEncoder::new(codec, settings, rgba) {
1796 Ok(enc) => {
1797 println!(
1798 "[{tag}] Encoder: V4L2M2M {} {} on a stateful M2M node.",
1799 codec.display(),
1800 chroma_name(enc.is_fullcolor())
1801 );
1802 return Some(FrameEncoder::V4l2m2m(enc));
1803 }
1804 Err(e) => eprintln!("[{tag}] Failed to init the V4L2 M2M encoder: {e}"),
1805 }
1806 }
1807 } else {
1808 crate::log::debug!("[{tag}] Software encoding selected (use_cpu or encode_node_index -1).");
1809 crate::report::encoder_reason("software encoding selected");
1810 }
1811 let FrameSource::Host { rgba } = source else {
1812 return None;
1813 };
1814 software_fallback(settings, rgba, tag)
1815}
1816
1817fn software_fallback(
1820 settings: &mut RustCaptureSettings,
1821 rgba: bool,
1822 tag: &str,
1823) -> Option<FrameEncoder> {
1824 let codec = settings.codec;
1825 if codec == Codec::H264 {
1826 println!(
1827 "[{tag}] Encoder: software {} ({}) {} {}-bit.",
1828 codec.display(),
1829 software_library(Codec::H264),
1830 chroma_name(session_fullcolor(None, settings)),
1831 session_bit_depth(None, settings)
1832 );
1833 return None;
1834 }
1835 let session = software_encoder(codec)
1836 .ok_or_else(|| {
1837 format!(
1838 "the {} encoder does not run on this machine",
1839 codec.display()
1840 )
1841 })
1842 .and_then(|_| software_session(settings, codec, rgba));
1843 match session {
1844 Ok(enc) => {
1845 println!(
1846 "[{tag}] Encoder: software {} ({}) {} {}-bit.",
1847 codec.display(),
1848 enc.backend_name(),
1849 chroma_name(enc.is_fullcolor()),
1850 enc.bit_depth()
1851 );
1852 Some(enc)
1853 }
1854 Err(e) => {
1855 eprintln!("[{tag}] No {} encoder available: {e}.", codec.display());
1856 None
1857 }
1858 }
1859}
1860
1861#[cfg(test)]
1862mod software_tests {
1863 use super::codec::{FRAME_DELTA, FRAME_KEY, WIRE_VIDEO, parse_video_type};
1869 use super::*;
1870 use crate::webcam::convert::I420View;
1871 use crate::webcam::decode::{ColorTags, Decoder, VideoDecoder};
1872 use std::cell::Cell;
1873 use std::sync::{Mutex, MutexGuard};
1874
1875 const W: usize = 320;
1876 const H: usize = 240;
1877
1878 const SOFTWARE: [Codec; 4] = [Codec::H265, Codec::Vp8, Codec::Vp9, Codec::Av1];
1880
1881 static ONE_AV1: Mutex<()> = Mutex::new(());
1886
1887 thread_local! {
1888 static AV1_DEPTH: Cell<u32> = const { Cell::new(0) };
1889 }
1890
1891 pub(super) struct Turn {
1893 _guard: Option<MutexGuard<'static, ()>>,
1894 counted: bool,
1895 }
1896
1897 impl Drop for Turn {
1898 fn drop(&mut self) {
1899 if self.counted {
1900 AV1_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
1901 }
1902 }
1903 }
1904
1905 pub(super) fn turn(codec: Codec) -> Turn {
1907 let counted = codec == Codec::Av1;
1908 let first = counted
1909 && AV1_DEPTH.with(|depth| {
1910 let held = depth.get();
1911 depth.set(held + 1);
1912 held == 0
1913 });
1914 Turn {
1915 _guard: first.then(|| ONE_AV1.lock().unwrap_or_else(|e| e.into_inner())),
1916 counted,
1917 }
1918 }
1919
1920 pub(super) struct Session {
1922 encoder: FrameEncoder,
1923 _turn: Turn,
1924 }
1925
1926 impl std::ops::Deref for Session {
1927 type Target = FrameEncoder;
1928 fn deref(&self) -> &FrameEncoder {
1929 &self.encoder
1930 }
1931 }
1932
1933 impl std::ops::DerefMut for Session {
1934 fn deref_mut(&mut self) -> &mut FrameEncoder {
1935 &mut self.encoder
1936 }
1937 }
1938
1939 pub(super) fn settings(codec: Codec) -> RustCaptureSettings {
1940 RustCaptureSettings {
1941 width: W as i32,
1942 height: H as i32,
1943 target_fps: 30.0,
1944 codec,
1945 video_crf: 25,
1946 use_cpu: true,
1947 ..Default::default()
1948 }
1949 }
1950
1951 pub(super) fn session(codec: Codec, s: &RustCaptureSettings, rgba: bool) -> Session {
1952 let _turn = turn(codec);
1953 let encoder = software_session(s, codec, rgba)
1954 .unwrap_or_else(|e| panic!("{codec:?} software session: {e}"));
1955 Session { encoder, _turn }
1956 }
1957
1958 fn pipeline_depth(codec: Codec) -> usize {
1963 let s = settings(codec);
1964 let mut enc = session(codec, &s, false);
1965 for t in 0..8usize {
1966 let out = enc
1967 .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
1968 .unwrap_or_default();
1969 if !out.is_empty() {
1970 return t;
1971 }
1972 }
1973 panic!("{codec:?}: no packet after eight frames");
1974 }
1975
1976 fn lockstep_codecs() -> Vec<Codec> {
1981 SOFTWARE
1982 .into_iter()
1983 .filter(|&codec| {
1984 let depth = pipeline_depth(codec);
1985 if depth > 0 {
1986 println!("[pipeline] {codec:?}: {depth} frames deep, frame-id checks skipped");
1987 }
1988 depth == 0
1989 })
1990 .collect()
1991 }
1992
1993 pub(super) fn frame(t: usize) -> Vec<u8> {
1996 let mut f = vec![0u8; W * H * 4];
1997 let (bx, by) = ((t * 9) % (W - 40), (t * 5) % (H - 30));
1998 for y in 0..H {
1999 for x in 0..W {
2000 let i = (y * W + x) * 4;
2001 let g = ((x * 255) / W) as u8;
2002 let cell = (x / 8 + y / 12) % 3 == 0 && x % 8 < 6 && y % 12 < 9;
2003 let (b, gr, r) = if x >= bx && x < bx + 40 && y >= by && y < by + 30 {
2004 (40, 220, 250)
2005 } else if cell {
2006 (30, 30, 30)
2007 } else {
2008 (g, 200 - g / 2, 120)
2009 };
2010 f[i] = b;
2011 f[i + 1] = gr;
2012 f[i + 2] = r;
2013 f[i + 3] = 255;
2014 }
2015 }
2016 f
2017 }
2018
2019 fn noise(t: usize) -> Vec<u8> {
2021 let mut f = vec![255u8; W * H * 4];
2022 let mut s = (t as u32).wrapping_mul(2654435761).wrapping_add(7);
2023 for px in f.as_chunks_mut::<4>().0 {
2024 s ^= s << 13;
2025 s ^= s >> 17;
2026 s ^= s << 5;
2027 px[0] = (s >> 24) as u8;
2028 px[1] = (s >> 16) as u8;
2029 px[2] = (s >> 8) as u8;
2030 }
2031 f
2032 }
2033
2034 fn luma_psnr(decoded: &I420View<'_>, bgra: &[u8]) -> f64 {
2037 assert_eq!((decoded.width, decoded.height), (W, H));
2038 let mut mse = 0f64;
2039 for y in 0..H {
2040 for x in 0..W {
2041 let i = (y * W + x) * 4;
2042 let (b, g, r) = (bgra[i] as f64, bgra[i + 1] as f64, bgra[i + 2] as f64);
2043 let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2044 let d = decoded.y[y * decoded.y_stride + x] as f64 - luma;
2045 mse += d * d;
2046 }
2047 }
2048 mse /= (W * H) as f64;
2049 if mse <= 0.0 {
2050 99.0
2051 } else {
2052 10.0 * (255.0 * 255.0 / mse).log10()
2053 }
2054 }
2055
2056 fn decode_one(dec: &mut VideoDecoder, packet: &[u8]) -> bool {
2057 dec.decode(&packet[VIDEO_HEADER_LEN..])
2058 .unwrap_or_else(|e| panic!("decode: {e:?}"))
2059 }
2060
2061 fn takes_a_live_rate(enc: &FrameEncoder) -> bool {
2064 match enc.backend_name() {
2065 "libvpx" => true,
2066 "svt-av1" => codec_sys::svtav1::HAS_EVENTS,
2067 _ => false,
2068 }
2069 }
2070
2071 #[test]
2074 fn software_frames_decode_back_to_the_source() {
2075 for codec in lockstep_codecs() {
2076 let s = settings(codec);
2077 let mut enc = session(codec, &s, false);
2078 let mut dec = VideoDecoder::new(codec).expect("decoder");
2079 for t in 0..6usize {
2080 let src = frame(t);
2081 let out = enc
2082 .encode_host(&src, W * 4, false, t as u64, 25, t == 0)
2083 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2084 assert!(out.len() > VIDEO_HEADER_LEN, "{codec:?} frame {t} is empty");
2085 assert_eq!(out[0], WIRE_VIDEO);
2086 let kind = if t == 0 { FRAME_KEY } else { FRAME_DELTA };
2087 assert_eq!(
2088 parse_video_type(out[1]),
2089 Some((codec, kind)),
2090 "{codec:?} frame {t}"
2091 );
2092 assert_eq!(u16::from_be_bytes([out[2], out[3]]) as usize, t);
2093 assert_eq!(
2094 &out[4..10],
2095 &[0, 0, (W >> 8) as u8, W as u8, (H >> 8) as u8, H as u8]
2096 );
2097 assert!(
2098 decode_one(&mut dec, &out),
2099 "{codec:?} frame {t} decoded nothing"
2100 );
2101 let psnr = luma_psnr(&dec.frame().unwrap(), &src);
2102 assert!(psnr > 28.0, "{codec:?} frame {t}: luma PSNR {psnr:.1} dB");
2103 }
2104 let src = frame(6);
2105 let key = enc
2106 .encode_host(&src, W * 4, false, 6, 25, true)
2107 .expect("forced key");
2108 assert_eq!(
2109 parse_video_type(key[1]),
2110 Some((codec, FRAME_KEY)),
2111 "{codec:?} forced key"
2112 );
2113 let mut fresh = VideoDecoder::new(codec).expect("decoder");
2114 assert!(
2115 decode_one(&mut fresh, &key),
2116 "{codec:?}: a forced key frame must decode alone"
2117 );
2118 assert!(luma_psnr(&fresh.frame().unwrap(), &src) > 28.0);
2119 let next = enc
2120 .encode_host(&frame(7), W * 4, false, 7, 25, false)
2121 .expect("delta after key");
2122 assert_eq!(parse_video_type(next[1]), Some((codec, FRAME_DELTA)));
2123 assert!(decode_one(&mut fresh, &next));
2124 }
2125 }
2126
2127 #[test]
2132 fn a_key_frame_asked_for_mid_stream_starts_a_fresh_decoder() {
2133 for codec in SOFTWARE {
2134 let s = settings(codec);
2135 let mut enc = session(codec, &s, false);
2136 for t in 0..6usize {
2137 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2138 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2139 }
2140 let mut out = enc
2141 .encode_host(&frame(6), W * 4, false, 6, 25, true)
2142 .expect("forced key");
2143 let mut t = 7usize;
2144 while out.get(1).and_then(|&kind| parse_video_type(kind)) != Some((codec, FRAME_KEY)) {
2145 assert!(
2146 t < 22,
2147 "{codec:?}: no key frame came back within fifteen frames of the request"
2148 );
2149 out = enc
2150 .encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2151 .expect("delta");
2152 t += 1;
2153 }
2154 assert_eq!(
2155 u16::from_be_bytes([out[2], out[3]]),
2156 6,
2157 "{codec:?}: the key frame carries the id it was asked for"
2158 );
2159 let mut fresh = VideoDecoder::new(codec).expect("decoder");
2160 assert!(
2161 decode_one(&mut fresh, &out),
2162 "{codec:?}: the key frame must decode alone"
2163 );
2164 assert!(
2165 luma_psnr(&fresh.frame().unwrap(), &frame(6)) > 28.0,
2166 "{codec:?}"
2167 );
2168 }
2169 }
2170
2171 #[test]
2176 fn a_session_that_cannot_invalidate_names_no_reference() {
2177 use super::reference::Reference;
2178 for codec in [Codec::H265, Codec::Av1] {
2179 let s = settings(codec);
2180 let mut enc = session(codec, &s, false);
2181 for t in 0..4usize {
2182 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2183 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2184 assert_eq!(
2185 enc.last_reference(),
2186 Reference::Untracked,
2187 "{codec:?} frame {t}"
2188 );
2189 }
2190 assert!(
2191 !enc.invalidate_reference(2),
2192 "{codec:?}: the refusal is what asks for the key frame"
2193 );
2194 }
2195 for codec in [Codec::Vp8, Codec::Vp9] {
2196 let s = settings(codec);
2197 let mut enc = session(codec, &s, false);
2198 enc.encode_host(&frame(0), W * 4, false, 0, 25, true)
2199 .unwrap();
2200 assert_eq!(enc.last_reference(), Reference::None, "{codec:?}");
2201 enc.encode_host(&frame(1), W * 4, false, 1, 25, false)
2202 .unwrap();
2203 assert_eq!(enc.last_reference(), Reference::Frame(0), "{codec:?}");
2204 assert!(enc.invalidate_reference(1), "{codec:?}");
2205 }
2206 }
2207
2208 #[test]
2213 fn a_constant_rate_av1_session_predicts_past_a_lost_frame() {
2214 use super::reference::Reference;
2215 let mut s = settings(Codec::Av1);
2216 s.video_cbr_mode = true;
2217 s.video_bitrate_kbps = 2000;
2218 let mut enc = session(Codec::Av1, &s, false);
2219 let mut frames: Vec<Vec<u8>> = (0..8usize)
2220 .map(|t| {
2221 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2222 .unwrap()
2223 })
2224 .collect();
2225 if !codec_sys::svtav1::HAS_EVENTS {
2226 assert_eq!(enc.last_reference(), Reference::Untracked);
2227 assert!(!enc.invalidate_reference(5));
2228 return;
2229 }
2230 assert_eq!(enc.last_reference(), Reference::Frame(6));
2231 assert!(enc.invalidate_reference(5));
2234 for t in 8..10usize {
2235 frames.push(
2236 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2237 .unwrap(),
2238 );
2239 assert_eq!(
2240 parse_video_type(frames[t][1]),
2241 Some((Codec::Av1, FRAME_DELTA)),
2242 "frame {t}"
2243 );
2244 assert_eq!(
2245 enc.last_reference(),
2246 Reference::Frame(if t == 8 { 0 } else { 8 }),
2247 "frame {t}"
2248 );
2249 }
2250 let (mut whole, mut lossy) = (
2251 VideoDecoder::new(Codec::Av1).unwrap(),
2252 VideoDecoder::new(Codec::Av1).unwrap(),
2253 );
2254 for (i, f) in frames.iter().enumerate() {
2255 assert!(decode_one(&mut whole, f), "frame {i}");
2256 if !(5..8).contains(&i) {
2257 assert!(decode_one(&mut lossy, f), "frame {i} without frames 5-7");
2258 }
2259 }
2260 let (a, b) = (whole.frame().unwrap(), lossy.frame().unwrap());
2261 let same =
2262 a.y.chunks(a.y_stride)
2263 .zip(b.y.chunks(b.y_stride))
2264 .take(H)
2265 .all(|(x, y)| x[..W] == y[..W]);
2266 assert!(
2267 same,
2268 "the decoder that lost frames 5-7 shows frame 9 unlike the one that saw them"
2269 );
2270 let key = enc
2271 .encode_host(&frame(10), W * 4, false, 10, 25, true)
2272 .unwrap();
2273 assert_eq!(parse_video_type(key[1]), Some((Codec::Av1, FRAME_KEY)));
2274 assert_eq!(enc.last_reference(), Reference::None);
2275 assert!(
2276 decode_one(&mut VideoDecoder::new(Codec::Av1).unwrap(), &key),
2277 "the key frame decodes alone"
2278 );
2279 }
2280
2281 #[test]
2287 fn an_anchor_prediction_leaves_no_block_as_it_stood_in_the_anchor() {
2288 use super::reference::ReferenceReport;
2289 if !codec_sys::svtav1::HAS_EVENTS {
2290 return;
2291 }
2292 let (w, h) = (1280usize, 720usize);
2293 let mut noise = vec![0u8; w * 240 * 4];
2294 let mut seed = 1u64;
2295 for b in noise.iter_mut() {
2296 seed ^= seed << 13;
2297 seed ^= seed >> 7;
2298 seed ^= seed << 17;
2299 *b = seed as u8;
2300 }
2301 let scene = |t: usize| {
2302 let bar = (t * 12) % (w - 160);
2303 let mut f = vec![0u8; w * h * 4];
2304 for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
2305 let (x, y) = (i % w, i / w);
2306 let bgr: [u8; 3] = if y >= 480 {
2307 let n = ((t % 4) * w * 240 + (y - 480) * w + x) * 4 % noise.len();
2308 [noise[n], noise[n + 1], noise[n + 2]]
2309 } else if (96..160).contains(&y) {
2310 [if ((x + t * 4) / 16 + (y - 96) / 16).is_multiple_of(2) {
2311 255
2312 } else {
2313 0
2314 }; 3]
2315 } else if y >= 160 && (bar..bar + 160).contains(&x) {
2316 [0x28, 0x3c, 0xdc]
2317 } else {
2318 [0x78, 0x28, 0x1e]
2319 };
2320 px[..3].copy_from_slice(&bgr);
2321 }
2322 f
2323 };
2324 let mut s = settings(Codec::Av1);
2325 s.width = w as i32;
2326 s.height = h as i32;
2327 s.target_fps = 60.0;
2328 s.video_cbr_mode = true;
2329 s.video_bitrate_kbps = 8000;
2330 let mut enc = session(Codec::Av1, &s, false);
2331 let mut dec = VideoDecoder::new(Codec::Av1).unwrap();
2332 let (mut previous, mut anchored) = (None, 0);
2333 for t in 0..84usize {
2334 if t > 10 && t % 13 == 0 {
2335 assert!(enc.take_report(ReferenceReport::Lost(t as u16 - 4)));
2336 }
2337 let src = scene(t);
2338 let out = enc
2339 .encode_host(&src, w * 4, false, t as u64, 25, t == 0)
2340 .unwrap();
2341 let id = u16::from_be_bytes([out[2], out[3]]);
2342 let reference = u16::from_be_bytes([out[10], out[11]]);
2343 assert_eq!(id as usize, t);
2344 if reference != id && Some(reference) != previous {
2345 anchored += 1;
2346 }
2347 previous = Some(id);
2348 assert!(decode_one(&mut dec, &out), "frame {id}");
2349 let pic = dec.frame().unwrap();
2350 for (bx, by) in (0..480)
2351 .step_by(64)
2352 .flat_map(|y| (0..w).step_by(64).map(move |x| (x, y)))
2353 {
2354 let (mut sum, mut n) = (0f64, 0);
2355 for y in (by..(by + 64).min(480)).step_by(2) {
2356 for x in (bx..bx + 64).step_by(2) {
2357 let i = (y * w + x) * 4;
2358 let (b, g, r) = (src[i] as f64, src[i + 1] as f64, src[i + 2] as f64);
2359 let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2360 sum += (pic.y[y * pic.y_stride + x] as f64 - luma).abs();
2361 n += 1;
2362 }
2363 }
2364 let off = sum / n as f64;
2365 assert!(
2366 off <= 20.0,
2367 "frame {id} (from {reference}): the block at ({bx}, {by}) is {off:.1} off its source"
2368 );
2369 }
2370 }
2371 assert!(anchored >= 5, "{anchored} frames predicted from an anchor");
2372 }
2373
2374 #[test]
2377 fn host_byte_order_is_honored() {
2378 for codec in lockstep_codecs() {
2379 let s = settings(codec);
2380 let mut means = Vec::new();
2381 for rgba in [false, true] {
2382 let mut px = [0u8; 4];
2383 if rgba {
2384 px[0] = 220
2385 } else {
2386 px[2] = 220
2387 }
2388 px[3] = 255;
2389 let src: Vec<u8> = px.repeat(W * H);
2390 let mut enc = session(codec, &s, rgba);
2391 let out = enc
2392 .encode_host(&src, W * 4, rgba, 0, 20, true)
2393 .expect("encode");
2394 let mut dec = VideoDecoder::new(codec).expect("decoder");
2395 assert!(decode_one(&mut dec, &out));
2396 let f = dec.frame().unwrap();
2397 let cw = f.chroma_width();
2398 let ch = f.chroma_height();
2399 let v: f64 = (0..ch)
2400 .flat_map(|y| (0..cw).map(move |x| (x, y)))
2401 .map(|(x, y)| f.v[y * f.uv_stride + x] as f64)
2402 .sum::<f64>()
2403 / (cw * ch) as f64;
2404 means.push(v);
2405 }
2406 assert!(
2407 means[0] > 180.0,
2408 "{codec:?}: red must land high in V, got {:.0}",
2409 means[0]
2410 );
2411 assert!(
2412 (means[0] - means[1]).abs() < 6.0,
2413 "{codec:?}: BGRA {:.0} vs RGBA {:.0}",
2414 means[0],
2415 means[1]
2416 );
2417 }
2418 }
2419
2420 #[test]
2423 fn quantizer_and_bitrate_reach_the_encoder() {
2424 for codec in SOFTWARE {
2425 let run = |crf: i32| -> usize {
2426 let mut s = settings(codec);
2427 s.video_crf = crf;
2428 let mut enc = session(codec, &s, false);
2429 (0..12usize)
2430 .map(|t| {
2431 enc.encode_host(&frame(t), W * 4, false, t as u64, crf as u32, t == 0)
2432 .unwrap()
2433 .len()
2434 })
2435 .sum()
2436 };
2437 let (fine, coarse) = (run(15), run(45));
2438 assert!(
2439 coarse * 2 < fine,
2440 "{codec:?}: crf 45 = {coarse} bytes vs crf 15 = {fine}"
2441 );
2442
2443 const KBPS: i32 = 800;
2444 let mut s = settings(codec);
2445 s.video_cbr_mode = true;
2446 s.video_bitrate_kbps = KBPS;
2447 let mut enc = session(codec, &s, false);
2448 let mut bytes = 0usize;
2449 for t in 0..90usize {
2450 let out = enc
2451 .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2452 .unwrap();
2453 if t >= 30 && out.len() > VIDEO_HEADER_LEN {
2457 bytes += out.len() - VIDEO_HEADER_LEN;
2458 }
2459 }
2460 let kbps = bytes as f64 * 8.0 * 30.0 / 60.0 / 1000.0;
2461 println!("{codec:?} CBR {KBPS} kbps on noise: {kbps:.0} kbps");
2462 assert!(
2463 kbps > KBPS as f64 * 0.5 && kbps < KBPS as f64 * 1.6,
2464 "{codec:?}: {kbps:.0} kbps"
2465 );
2466 }
2467 }
2468
2469 #[test]
2474 fn live_quality_and_rate_changes_keep_the_stream_decodable() {
2475 for codec in lockstep_codecs() {
2476 let mut s = settings(codec);
2477 s.video_crf = 40;
2478 let mut enc = session(codec, &s, false);
2479 let mut dec = VideoDecoder::new(codec).expect("decoder");
2480 let coarse = enc
2481 .encode_host(&frame(0), W * 4, false, 0, 40, true)
2482 .unwrap();
2483 assert!(decode_one(&mut dec, &coarse));
2484 let fine = enc
2485 .encode_host(&frame(1), W * 4, false, 1, 15, false)
2486 .unwrap();
2487 let live = enc.backend_name() == "libvpx";
2488 let kind = if live { FRAME_DELTA } else { FRAME_KEY };
2489 assert_eq!(
2490 parse_video_type(fine[1]),
2491 Some((codec, kind)),
2492 "{codec:?}: a quality change {}",
2493 if live {
2494 "is live"
2495 } else {
2496 "re-opens with a key frame"
2497 }
2498 );
2499 assert!(decode_one(&mut dec, &fine));
2500 let held = enc
2501 .encode_host(&frame(2), W * 4, false, 2, 40, false)
2502 .unwrap();
2503 assert_eq!(
2504 parse_video_type(held[1]),
2505 Some((codec, FRAME_DELTA)),
2506 "{codec:?}: a single decrease waits out the hysteresis"
2507 );
2508 assert!(decode_one(&mut dec, &held));
2509 s.target_fps = 15.0;
2510 enc.reconfigure_rate(&s).expect("rate reconfigure");
2511 let after = enc
2512 .encode_host(&frame(3), W * 4, false, 3, 15, false)
2513 .unwrap();
2514 let kind = if enc.backend_name() == "libvpx" {
2515 FRAME_DELTA
2516 } else {
2517 FRAME_KEY
2518 };
2519 assert_eq!(
2520 parse_video_type(after[1]),
2521 Some((codec, kind)),
2522 "{codec:?}: a frame-rate change"
2523 );
2524 assert!(decode_one(&mut dec, &after));
2525 assert!(luma_psnr(&dec.frame().unwrap(), &frame(3)) > 28.0);
2526 }
2527 }
2528
2529 #[test]
2532 fn a_quality_change_reaches_a_running_session() {
2533 for codec in lockstep_codecs() {
2534 let mut s = settings(codec);
2535 s.video_crf = 40;
2536 let mut enc = session(codec, &s, false);
2537 let (mut spent, mut changed) = ([0usize; 2], None);
2538 for t in 0..12usize {
2539 let crf = if t < 6 { 40 } else { 15 };
2540 let out = enc
2541 .encode_host(&frame(t), W * 4, false, t as u64, crf, t == 0)
2542 .unwrap();
2543 if t == 6 {
2544 changed = parse_video_type(out[1]);
2545 }
2546 if (1..6).contains(&t) {
2547 spent[0] += out.len();
2548 } else if t >= 7 {
2549 spent[1] += out.len();
2550 }
2551 }
2552 assert!(
2553 spent[1] * 2 > spent[0] * 3,
2554 "{codec:?}: bytes over five deltas before and after {spent:?}"
2555 );
2556 let kind = if enc.backend_name() == "libvpx" {
2557 FRAME_DELTA
2558 } else {
2559 FRAME_KEY
2560 };
2561 assert_eq!(
2562 changed,
2563 Some((codec, kind)),
2564 "{codec:?}: the frame after the change"
2565 );
2566 }
2567 }
2568
2569 #[test]
2572 fn a_bitrate_change_reaches_a_running_session() {
2573 for codec in lockstep_codecs() {
2574 let mut s = settings(codec);
2575 s.video_cbr_mode = true;
2576 s.video_bitrate_kbps = 1500;
2577 let mut enc = session(codec, &s, false);
2578 let mut dec = VideoDecoder::new(codec).expect("decoder");
2579 let (mut spent, mut changed) = ([0usize; 2], None);
2580 for t in 0..60usize {
2581 if t == 30 {
2582 s.video_bitrate_kbps = 4500;
2583 enc.reconfigure_rate(&s).expect("rate reconfigure");
2584 }
2585 let out = enc
2586 .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2587 .unwrap();
2588 if t == 30 {
2589 changed = parse_video_type(out[1]);
2590 }
2591 if out.len() > VIDEO_HEADER_LEN {
2592 assert!(decode_one(&mut dec, &out), "{codec:?} frame {t}");
2593 }
2594 if (15..30).contains(&t) {
2595 spent[0] += out.len();
2596 } else if t >= 45 {
2597 spent[1] += out.len();
2598 }
2599 }
2600 assert!(
2601 spent[1] > spent[0] * 2,
2602 "{codec:?}: bytes over fifteen frames before and after {spent:?}"
2603 );
2604 let kind = if takes_a_live_rate(&enc) {
2605 FRAME_DELTA
2606 } else {
2607 FRAME_KEY
2608 };
2609 assert_eq!(
2610 changed,
2611 Some((codec, kind)),
2612 "{codec:?}: the frame after the change"
2613 );
2614 }
2615 }
2616
2617 #[test]
2622 fn sessions_declare_the_matrix_they_convert_with() {
2623 for codec in lockstep_codecs() {
2624 let mut s = settings(codec);
2625 let mut enc = session(codec, &s, false);
2626 let out = enc
2627 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2628 .expect("encode");
2629 let mut dec = VideoDecoder::new(codec).expect("decoder");
2630 assert!(decode_one(&mut dec, &out));
2631 let want = if codec == Codec::Vp8 {
2632 ColorTags::BT470BG_LIMITED
2633 } else {
2634 ColorTags::BT709_LIMITED
2635 };
2636 assert_eq!(dec.color_tags(), Some(want), "{codec:?}");
2637 if software_fullcolor(codec) {
2638 s.video_fullcolor = true;
2639 let mut enc = session(codec, &s, false);
2640 assert!(enc.is_fullcolor(), "{codec:?} carries the 4:4:4 request");
2641 let out = enc
2642 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2643 .expect("encode");
2644 let mut dec = VideoDecoder::new(codec).expect("decoder");
2645 assert!(decode_one(&mut dec, &out));
2646 let want = if codec == Codec::Vp9 {
2647 ColorTags::BT709_LIMITED
2648 } else {
2649 ColorTags::BT709_FULL
2650 };
2651 assert_eq!(dec.color_tags(), Some(want), "{codec:?} 4:4:4");
2652 }
2653 }
2654 }
2655
2656 #[test]
2659 fn fullcolor_follows_the_software_encoder() {
2660 for codec in lockstep_codecs() {
2661 let mut s = settings(codec);
2662 s.video_fullcolor = true;
2663 let mut enc = session(codec, &s, false);
2664 assert_eq!(enc.is_fullcolor(), software_fullcolor(codec), "{codec:?}");
2665 let out = enc
2666 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2667 .unwrap();
2668 let mut dec = VideoDecoder::new(codec).expect("decoder");
2669 assert!(decode_one(&mut dec, &out));
2670 }
2671 }
2672
2673 #[test]
2676 fn ten_bit_follows_the_software_encoder() {
2677 for codec in lockstep_codecs() {
2678 for fullcolor in [false, true] {
2679 let mut s = settings(codec);
2680 s.video_bit_depth = 10;
2681 s.video_fullcolor = fullcolor;
2682 let mut enc = session(codec, &s, false);
2683 let want = if software_ten_bit(codec) { 10 } else { 8 };
2684 assert_eq!(enc.bit_depth(), want, "{codec:?}");
2685 assert_eq!(
2686 enc.is_fullcolor(),
2687 fullcolor && software_fullcolor(codec),
2688 "{codec:?}"
2689 );
2690 assert_eq!(
2691 software_formats(codec).ten_bit,
2692 [want == 10, want == 10 && software_fullcolor(codec)],
2693 "{codec:?}"
2694 );
2695 for t in 0..3 {
2696 let out = enc
2697 .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2698 .unwrap();
2699 assert!(t > 0 || out.len() > VIDEO_HEADER_LEN, "{codec:?} key frame");
2700 }
2701 }
2702 }
2703 }
2704
2705 #[test]
2710 fn decoded_chroma_is_neutral_on_a_tile_that_averages_to_gray() {
2711 const N: usize = 128;
2712 let bgra = chroma_siting::bgra(N, N);
2713 let settings = RustCaptureSettings {
2714 width: N as i32,
2715 height: N as i32,
2716 target_fps: 30.0,
2717 video_crf: 20,
2718 ..Default::default()
2719 };
2720 for codec in SOFTWARE {
2721 let mut enc = session(codec, &settings, false);
2722 let out = drain_first(&mut enc, &bgra, N * 4, 20);
2723 assert!(!out.is_empty(), "{codec:?} encoded nothing");
2724 let mut dec = VideoDecoder::new(codec).expect("decoder");
2725 assert!(
2726 dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
2727 "{codec:?} decoded nothing"
2728 );
2729 let worst = chroma_siting::worst(&dec.frame().expect("frame"));
2730 println!("[chroma-siting] {codec:?}: worst |C-128| {worst:.1}");
2731 assert!(
2732 worst <= 8.0,
2733 "{codec:?} sites chroma {worst:.1} off neutral"
2734 );
2735 }
2736 }
2737
2738 #[test]
2744 fn the_chart_decodes_to_the_color_that_was_painted() {
2745 const N: usize = 256;
2746 let bgra = chroma_siting::chart_bgra(N, N / 2);
2747 let settings = RustCaptureSettings {
2748 width: N as i32,
2749 height: (N / 2) as i32,
2750 target_fps: 30.0,
2751 video_crf: 20,
2752 ..Default::default()
2753 };
2754 for codec in SOFTWARE {
2755 let mut enc = session(codec, &settings, false);
2756 let out = drain_first(&mut enc, &bgra, N * 4, 20);
2757 let mut dec = VideoDecoder::new(codec).expect("decoder");
2758 assert!(
2759 dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
2760 "{codec:?}"
2761 );
2762 let declared = codec != Codec::Vp8;
2763 let (k, other, other_name) = if declared {
2764 (chroma_siting::BT709, chroma_siting::BT601, "BT.601")
2765 } else {
2766 (chroma_siting::BT601, chroma_siting::BT709, "BT.709")
2767 };
2768 let frame = dec.frame().expect("frame");
2769 let worst = chroma_siting::chart_error(&frame, k);
2770 let under_other = chroma_siting::chart_error(&frame, other);
2771 println!(
2772 "[chart] {codec:?}: worst |dRGB| {worst:.1} against the declared matrix, {under_other:.1} against {other_name}"
2773 );
2774 assert!(
2775 worst <= 12.0,
2776 "{codec:?} paints {worst:.1} off the chart against the matrix it declares, and {under_other:.1} against {other_name}: {}",
2777 if under_other < worst {
2778 "it converted with that one and declared the other"
2779 } else {
2780 "neither matrix explains it"
2781 }
2782 );
2783 }
2784 }
2785
2786 pub(super) fn drain_first(
2789 enc: &mut FrameEncoder,
2790 bgra: &[u8],
2791 stride: usize,
2792 qp: u32,
2793 ) -> Vec<u8> {
2794 for t in 0..8u64 {
2795 let out = enc
2796 .encode_host(bgra, stride, false, t, qp, t == 0)
2797 .unwrap_or_else(|e| panic!("encode: {e}"));
2798 if !out.is_empty() {
2799 return out;
2800 }
2801 }
2802 Vec::new()
2803 }
2804}
2805
2806#[cfg(test)]
2808pub(crate) mod chroma_siting {
2809 pub const TILE: [[u8; 3]; 4] = [[0, 0, 128], [0, 255, 0], [128, 128, 255], [255, 0, 0]];
2814
2815 pub fn bgra(w: usize, h: usize) -> Vec<u8> {
2817 let mut buf = vec![255u8; w * h * 4];
2818 for y in 0..h {
2819 for x in 0..w {
2820 let p = TILE[(y % 2) * 2 + (x % 2)];
2821 buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
2822 }
2823 }
2824 buf
2825 }
2826
2827 pub const BT709: (f64, f64) = (0.2126, 0.0722);
2829
2830 pub const BT601: (f64, f64) = (0.299, 0.114);
2833
2834 pub fn ycbcr(rgb: [f64; 3], k: (f64, f64)) -> [f64; 3] {
2836 let ([r, g, b], (kr, kb)) = (rgb, k);
2837 let y = kr * r + (1.0 - kr - kb) * g + kb * b;
2838 [
2839 16.0 + 219.0 * y / 255.0,
2840 128.0 + 224.0 * (b - y) / (2.0 * (1.0 - kb) * 255.0),
2841 128.0 + 224.0 * (r - y) / (2.0 * (1.0 - kr) * 255.0),
2842 ]
2843 }
2844
2845 pub fn chroma(rgb: [f64; 3], k: (f64, f64)) -> (f64, f64) {
2847 let c = ycbcr(rgb, k);
2848 (c[1], c[2])
2849 }
2850
2851 pub fn rgb(ycc: [f64; 3], k: (f64, f64)) -> [f64; 3] {
2854 let (kr, kb) = k;
2855 let (y, cb, cr) = (
2856 (ycc[0] - 16.0) / 219.0,
2857 (ycc[1] - 128.0) / 224.0,
2858 (ycc[2] - 128.0) / 224.0,
2859 );
2860 let r = y + 2.0 * (1.0 - kr) * cr;
2861 let b = y + 2.0 * (1.0 - kb) * cb;
2862 let g = (y - kr * r - kb * b) / (1.0 - kr - kb);
2863 [r, g, b].map(|c| (c * 255.0).clamp(0.0, 255.0))
2864 }
2865
2866 pub const CHART: [[u8; 3]; 8] = [
2869 [255, 255, 255],
2870 [128, 128, 128],
2871 [0, 0, 0],
2872 [255, 0, 0],
2873 [0, 255, 0],
2874 [0, 0, 255],
2875 [255, 255, 0],
2876 [0, 255, 255],
2877 ];
2878
2879 pub fn chart_bgra(w: usize, h: usize) -> Vec<u8> {
2881 let mut buf = vec![255u8; w * h * 4];
2882 for y in 0..h {
2883 for x in 0..w {
2884 let p = CHART[(x * CHART.len() / w).min(CHART.len() - 1)];
2885 buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
2886 }
2887 }
2888 buf
2889 }
2890
2891 pub fn chart_error(f: &crate::webcam::convert::I420View<'_>, k: (f64, f64)) -> f64 {
2896 let cols = CHART.len();
2897 let mut worst = 0.0f64;
2898 for (i, want) in CHART.iter().enumerate() {
2899 let (x0, x1) = (f.width * i / cols, f.width * (i + 1) / cols);
2900 let (lo, hi) = (x0 + (x1 - x0) / 4, x1 - (x1 - x0) / 4);
2901 let (mut acc, mut n) = ([0.0f64; 3], 0.0f64);
2902 for y in f.height / 4..f.height * 3 / 4 {
2903 for x in lo..hi {
2904 let ycc = [
2905 f64::from(f.y[y * f.y_stride + x]),
2906 f64::from(f.u[(y / 2) * f.uv_stride + x / 2]),
2907 f64::from(f.v[(y / 2) * f.uv_stride + x / 2]),
2908 ];
2909 let got = rgb(ycc, k);
2910 for c in 0..3 {
2911 acc[c] += got[c];
2912 }
2913 n += 1.0;
2914 }
2915 }
2916 for c in 0..3 {
2917 worst = worst.max((acc[c] / n - f64::from(want[c])).abs());
2918 }
2919 }
2920 worst
2921 }
2922
2923 pub fn worst(f: &crate::webcam::convert::I420View<'_>) -> f64 {
2925 let mut worst = 0.0f64;
2926 for r in 0..f.chroma_height() {
2927 for c in 0..f.chroma_width() {
2928 let i = r * f.uv_stride + c;
2929 worst = worst.max((f64::from(f.u[i]) - 128.0).hypot(f64::from(f.v[i]) - 128.0));
2930 }
2931 }
2932 worst
2933 }
2934
2935 #[test]
2938 fn the_tile_separates_the_sitings() {
2939 let c: Vec<(f64, f64)> = TILE
2940 .iter()
2941 .map(|&p| chroma(p.map(f64::from), BT709))
2942 .collect();
2943 let mean = |of: &[usize]| {
2944 let (u, v) = of
2945 .iter()
2946 .fold((0.0, 0.0), |(u, v), &i| (u + c[i].0, v + c[i].1));
2947 let n = of.len() as f64;
2948 (u / n - 128.0).hypot(v / n - 128.0)
2949 };
2950 let all = mean(&[0, 1, 2, 3]);
2951 assert!(
2952 all < 0.5,
2953 "the whole tile must average to neutral chroma, off by {all:.1}"
2954 );
2955 for part in [
2956 vec![0],
2957 vec![1],
2958 vec![2],
2959 vec![3],
2960 vec![0, 1],
2961 vec![2, 3],
2962 vec![0, 2],
2963 vec![1, 3],
2964 ] {
2965 let d = mean(&part);
2966 assert!(
2967 d > 40.0,
2968 "pixels {part:?} average to chroma only {d:.1} from neutral"
2969 );
2970 }
2971 }
2972}
2973
2974pub fn session_fullcolor(encoder: Option<&FrameEncoder>, settings: &RustCaptureSettings) -> bool {
2978 match encoder {
2979 Some(enc) => enc.is_fullcolor(),
2980 None => settings.video_fullcolor && software_fullcolor(settings.codec),
2981 }
2982}
2983
2984pub fn session_bit_depth(encoder: Option<&FrameEncoder>, settings: &RustCaptureSettings) -> u32 {
2988 match encoder {
2989 Some(enc) => enc.bit_depth(),
2990 None => software::stripe_bit_depth(settings),
2991 }
2992}
2993
2994pub fn session_full_range(encoder: Option<&FrameEncoder>, settings: &RustCaptureSettings) -> bool {
2999 match encoder {
3000 Some(enc) => enc.is_full_range(),
3001 None => session_fullcolor(None, settings),
3002 }
3003}
3004
3005#[cfg(test)]
3006mod hardware_encoder_tests {
3007 use super::*;
3010
3011 #[test]
3014 #[ignore]
3015 fn gpu_hardware_encoders_serve_h264_once_probed() {
3016 let served = hardware_encoders(0);
3017 assert!(
3018 served.iter().any(|(codec, ..)| *codec == Codec::H264),
3019 "node 0 serves {served:?}"
3020 );
3021 assert!(
3022 served
3023 .iter()
3024 .all(|(_, backend, ..)| matches!(*backend, "nvenc" | "vaapi"))
3025 );
3026 assert_eq!(hardware_encoders(0), served);
3027 }
3028}