1pub mod bits;
13pub(crate) mod blit_semaphore;
15pub mod codec;
17pub(crate) mod dmabuf_identity;
19pub mod frame_rate;
21pub mod hevc;
23pub mod nvenc;
26#[cfg(any(feature = "openh264", test))]
29pub mod oh264;
30#[cfg(any(feature = "openh264", test))]
32pub mod oh264_refs;
33pub mod overlay;
35pub mod reference;
36pub mod session;
38pub mod software;
41pub mod sps;
44pub mod svtav1;
46#[cfg(target_arch = "aarch64")]
51pub mod tegra;
52pub mod v4l2m2m;
56pub mod vaapi;
58pub mod vpx;
60
61pub use codec::*;
62
63use std::collections::HashMap;
64use std::ffi::c_void;
65use std::sync::{Mutex, OnceLock};
66
67use nvenc::NvencEncoder;
68use smithay::backend::allocator::dmabuf::Dmabuf;
69
70use crate::CaptureSettings;
71
72#[cfg(not(any(feature = "gpl", feature = "openh264")))]
73compile_error!(
74 "pixelflux needs a software H.264 encoder: enable the `gpl` feature (libx264, the default) or `openh264`."
75);
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct SoftwareEncoder {
81 pub library: &'static str,
82}
83
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct Formats {
89 pub fullcolor: bool,
90 pub ten_bit: [bool; 2],
91}
92
93impl Formats {
94 pub fn names(&self) -> Vec<String> {
96 let mut names = Vec::new();
97 for (fullcolor, chroma) in [(false, "420"), (true, "444")] {
98 for (ten_bit, depth) in [(false, 8), (true, 10)] {
99 let carried = match (fullcolor, ten_bit) {
100 (false, false) => true,
101 (true, false) => self.fullcolor,
102 (_, true) => self.ten_bit[usize::from(fullcolor)],
103 };
104 if carried {
105 names.push(format!("{chroma}-{depth}"));
106 }
107 }
108 }
109 names
110 }
111}
112
113pub type HardwareEncoders = Vec<(Codec, &'static str, Formats)>;
116
117pub fn hardware_encoders(encode_node_index: i32) -> HardwareEncoders {
127 probe_node(encode_node_index).unwrap_or_default()
128}
129
130type ProbeAnswer = Result<HardwareEncoders, (&'static str, String)>;
133
134fn probe_node(encode_node_index: i32) -> ProbeAnswer {
138 static PROBED: OnceLock<Mutex<HashMap<i32, ProbeAnswer>>> = OnceLock::new();
139 let node = encode_node_index.max(0);
140 let mut probed = PROBED
141 .get_or_init(|| Mutex::new(HashMap::new()))
142 .lock()
143 .unwrap();
144 if let Some(answer) = probed.get(&node) {
145 return answer.clone();
146 }
147 #[cfg(target_arch = "aarch64")]
148 if tegra::available() {
149 let served: HardwareEncoders = tegra::served()
150 .into_iter()
151 .map(|c| (c, "tegra", Formats::default()))
152 .collect();
153 let names: Vec<&str> = served.iter().map(|(c, ..)| c.display()).collect();
154 println!(
155 "[pixelflux] Render node {node} encodes {} on tegra.",
156 names.join(", ")
157 );
158 probed.insert(node, Ok(served.clone()));
159 return Ok(served);
160 }
161 let driver = crate::get_gpu_driver(node);
162 let (backend, codecs) = if crate::driver_selects_nvenc(&driver) {
163 ("nvenc", nvenc::probe_codecs(node))
164 } else {
165 ("vaapi", vaapi::probe_codecs(node))
166 };
167 let answer = match codecs {
168 Ok(codecs) => Ok(codecs
169 .into_iter()
170 .map(|(codec, formats): vaapi::Served| (codec, backend, formats))
171 .collect()),
172 Err(e) => {
173 eprintln!("[pixelflux] No hardware encoder on render node {node} ({backend}): {e}");
174 Err((backend, e))
175 }
176 };
177 if answer.as_ref().map_or(true, Vec::is_empty) && v4l2m2m::available() {
178 let codecs = v4l2m2m::served();
182 let served: HardwareEncoders = codecs
183 .iter()
184 .map(|&c| (c, "v4l2m2m", Formats::default()))
185 .collect();
186 let names: Vec<&str> = codecs.iter().map(|c| c.display()).collect();
187 println!(
188 "[pixelflux] A stateful V4L2 M2M encoder serves {}.",
189 names.join(", ")
190 );
191 probed.insert(node, Ok(served.clone()));
192 return Ok(served);
193 }
194 if let Ok(served) = &answer
195 && !served.is_empty()
196 {
197 let names: Vec<&str> = served.iter().map(|(codec, ..)| codec.display()).collect();
198 println!(
199 "[pixelflux] Render node {node} encodes {} on {backend}.",
200 names.join(", ")
201 );
202 }
203 if !matches!(&answer, Err((_, e)) if e == nvenc::SESSIONS_TAKEN) {
204 probed.insert(node, answer.clone());
205 }
206 answer
207}
208
209pub fn software_fullcolor(codec: Codec) -> bool {
213 match software_encoder(codec) {
214 Some(enc) => {
215 matches!(enc.library, "x264" | "x265")
216 || (codec == Codec::Vp9 && enc.library == "libvpx" && vpx::encodes_444())
217 }
218 None => false,
219 }
220}
221
222pub fn software_ten_bit(codec: Codec) -> bool {
227 match software_encoder(codec) {
228 Some(enc) => match (codec, enc.library) {
229 #[cfg(feature = "gpl")]
230 (Codec::H264, "x264") => software::H264EncoderWrapper::ten_bit(),
231 (Codec::H265, "x265") => hevc::ten_bit(),
232 (Codec::Vp9, "libvpx") => vpx::encodes_ten_bit(),
233 (Codec::Av1, _) => true,
234 _ => false,
235 },
236 None => false,
237 }
238}
239
240pub fn software_formats(codec: Codec) -> Formats {
242 let ten_bit = software_ten_bit(codec);
243 let fullcolor = software_fullcolor(codec);
244 Formats {
245 fullcolor,
246 ten_bit: [ten_bit, ten_bit && fullcolor],
247 }
248}
249
250pub fn software_library(codec: Codec) -> &'static str {
253 software_encoder(codec).map_or("none", |enc| enc.library)
254}
255
256pub fn software_encoder(codec: Codec) -> Option<SoftwareEncoder> {
266 static ENCODES: OnceLock<[bool; 5]> = OnceLock::new();
267 let library = match codec {
268 Codec::Jpeg => return None,
269 Codec::H264 => {
270 if cfg!(feature = "gpl") {
271 "x264"
272 } else {
273 "openh264"
274 }
275 }
276 Codec::H265 => {
277 if cfg!(feature = "gpl") {
278 "x265"
279 } else {
280 "kvazaar"
281 }
282 }
283 Codec::Vp8 | Codec::Vp9 => "libvpx",
284 Codec::Av1 => "svt-av1",
285 };
286 let encodes =
287 ENCODES.get_or_init(|| Codec::VIDEO.map(|c| c == Codec::H264 || encodes_in_child(c)));
288 encodes[Codec::VIDEO.iter().position(|&c| c == codec).unwrap()]
289 .then_some(SoftwareEncoder { library })
290}
291
292fn encodes_in_child(codec: Codec) -> bool {
297 survives_in_child(|| {
298 let Ok(pool) = rayon::ThreadPoolBuilder::new()
303 .num_threads(1)
304 .use_current_thread()
305 .build()
306 else {
307 return;
308 };
309 pool.install(|| {
310 let settings = CaptureSettings {
311 width: 256,
312 height: 128,
313 ..Default::default()
314 };
315 let Ok(mut session) = software_session(&settings, codec, false) else {
316 return;
317 };
318 let frame = vec![0u8; 256 * 128 * 4];
319 for n in 0..64 {
320 let packet = session.encode_host(
321 &frame,
322 256 * 4,
323 false,
324 n,
325 settings.video_crf as u32,
326 n == 0,
327 );
328 if !matches!(packet, Ok(p) if p.is_empty()) {
329 break;
330 }
331 }
332 })
333 })
334}
335
336fn survives_in_child(f: impl FnOnce()) -> bool {
344 let mut live = svtav1::LIFECYCLE.lock().unwrap_or_else(|e| e.into_inner());
345 while *live > 0 {
346 live = svtav1::RELEASED
347 .wait(live)
348 .unwrap_or_else(|e| e.into_inner());
349 }
350 let pid = unsafe { libc::fork() };
351 drop(live);
352 match pid {
353 0 => {
354 unsafe {
355 libc::signal(libc::SIGALRM, libc::SIG_DFL);
356 libc::alarm(60);
357 let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY);
358 if null >= 0 {
359 libc::dup2(null, 1);
360 libc::dup2(null, 2);
361 }
362 }
363 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
364 unsafe { libc::_exit(0) }
365 }
366 pid if pid > 0 => {
367 let mut status = 0;
368 while unsafe { libc::waitpid(pid, &mut status, 0) } < 0 {
369 if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
370 return true;
371 }
372 }
373 !libc::WIFSIGNALED(status)
374 }
375 _ => true,
376 }
377}
378
379pub(crate) const HELD_KEY_BUDGET_S: f64 = 1.0;
390
391pub(crate) const HELD_REFRESH_LIMIT_S: f64 = 10.0;
399
400pub(crate) fn held_key_retry(crf: u32, len: usize, cap: usize) -> Option<u32> {
404 (len > cap && cap > 0)
405 .then(|| (crf + (6.0 * (len as f64 / cap as f64).log2()).ceil() as u32).min(51))
406}
407
408pub(crate) fn held_key_start(crf: u32, last: Option<(usize, u32)>, cap: usize) -> u32 {
415 match last {
416 Some((len, q)) if q.abs_diff(crf) <= 12 => {
417 let at_crf = len as f64 * 2f64.powf((f64::from(q) - f64::from(crf)) / 6.0);
418 held_key_retry(crf, at_crf.round() as usize, cap).unwrap_or(crf)
419 }
420 _ => crf,
421 }
422}
423
424pub(crate) const QP_HYSTERESIS_LIMIT: u32 = 60;
432
433pub const CAPPED_VBV_FRAMES: f64 = software::CBR_VBV_FRAMES;
438
439pub fn reference_frames(settings: &CaptureSettings) -> u32 {
468 match settings.video_reference_frames {
469 0 => reference::REFERENCE_FRAMES,
470 n => n.min(reference::REFERENCE_FRAMES),
471 }
472}
473
474pub fn vbv_bits(bitrate_bps: u32, fps: f64, keyframe_interval_s: f64, multiplier: f64) -> u32 {
475 let frame_bits = f64::from(bitrate_bps) / fps.max(1.0);
476 let mult = if multiplier > 0.0 {
477 multiplier
478 } else if keyframe_interval_s > 0.0 {
479 3.0
480 } else {
481 1.5
482 };
483 (frame_bits * mult)
484 .round()
485 .max(1.0)
486 .min(f64::from(u32::MAX)) as u32
487}
488
489pub fn colorspace_desc(fullcolor: bool, full_range: bool) -> &'static str {
494 match (fullcolor, full_range) {
495 (true, true) => "I444 (Full Range)",
496 (true, false) => "I444 (Limited Range)",
497 (false, true) => "I420 (Full Range)",
498 (false, false) => "I420 (Limited Range)",
499 }
500}
501
502pub fn rate_desc(settings: &CaptureSettings, fixed: Option<&str>, capped: bool) -> String {
507 match fixed {
508 Some(mode) => format!("{mode} {}", settings.video_bitrate_kbps),
509 None if settings.video_cbr_mode => format!("CBR {}", settings.video_bitrate_kbps),
510 None if capped => format!(
511 "CRF: {} (Peak {})",
512 settings.video_crf, settings.video_bitrate_kbps
513 ),
514 None => format!("CRF: {}", settings.video_crf),
515 }
516}
517
518pub fn paint_over_desc(
528 settings: &CaptureSettings,
529 fixed: Option<&str>,
530 holds: bool,
531) -> Option<String> {
532 if !settings.cleans_up() {
533 return None;
534 }
535 if !settings.codec.is_video() {
536 return (settings.paint_over_jpeg_quality > settings.jpeg_quality).then(|| {
537 format!(
538 "PaintOver Q: {} (Trigger: {}f)",
539 settings.paint_over_jpeg_quality,
540 settings.paint_over_trigger()
541 )
542 });
543 }
544 let burst = settings.video_paintover_burst_frames;
545 let at_paint_over = if settings.video_cbr_mode {
546 holds
547 } else if settings.video_paintover_crf < settings.video_crf {
548 fixed.is_none()
549 } else {
550 return None;
551 };
552 Some(if at_paint_over {
553 format!(
554 "PaintOver CRF: {} (Burst: {burst}f)",
555 settings.video_paintover_crf
556 )
557 } else {
558 format!("PaintOver Burst: {burst}f")
559 })
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
570 fn the_rate_field_names_what_the_backend_applies() {
571 let crf = CaptureSettings {
572 codec: Codec::H264,
573 video_crf: 25,
574 video_bitrate_kbps: 8000,
575 ..Default::default()
576 };
577 let cbr = CaptureSettings {
578 video_cbr_mode: true,
579 ..crf.clone()
580 };
581 assert_eq!(rate_desc(&crf, None, false), "CRF: 25");
582 assert_eq!(rate_desc(&crf, None, true), "CRF: 25 (Peak 8000)");
583 assert_eq!(rate_desc(&cbr, None, false), "CBR 8000");
584 for settings in [&crf, &cbr] {
585 assert_eq!(rate_desc(settings, Some("VBR"), false), "VBR 8000");
586 assert_eq!(rate_desc(settings, Some("CBR"), false), "CBR 8000");
587 }
588 }
589
590 #[test]
593 fn a_session_reports_whether_it_takes_its_peak() {
594 let capped = CaptureSettings {
595 codec: Codec::H264,
596 width: 64,
597 height: 48,
598 target_fps: 30.0,
599 video_crf: 25,
600 video_bitrate_kbps: 4000,
601 video_vbv_multiplier: CAPPED_VBV_FRAMES,
602 ..Default::default()
603 };
604 assert_eq!(session_capped(None, &capped), cfg!(feature = "gpl"));
605 let cbr = CaptureSettings {
606 video_cbr_mode: true,
607 ..capped.clone()
608 };
609 assert!(!session_capped(None, &cbr));
610 let vpx = |codec| CaptureSettings {
611 codec,
612 ..capped.clone()
613 };
614 for codec in [Codec::Vp8, Codec::Vp9] {
615 let enc = FrameEncoder::Vpx(vpx::VpxEncoder::new(&vpx(codec), codec, false).unwrap());
616 assert!(session_capped(Some(&enc), &vpx(codec)), "{codec:?}");
617 }
618 let hevc = vpx(Codec::H265);
619 let enc = FrameEncoder::Hevc(hevc::HevcEncoder::new(&hevc, false).unwrap());
620 assert!(!session_capped(Some(&enc), &hevc));
621 }
622
623 #[test]
629 fn the_paint_over_field_names_what_paint_over_does() {
630 let crf = CaptureSettings {
631 codec: Codec::H264,
632 video_crf: 25,
633 video_paintover_crf: 18,
634 video_paintover_burst_frames: 5,
635 use_paint_over_quality: true,
636 video_streaming_mode: false,
637 ..Default::default()
638 };
639 let cbr = CaptureSettings {
640 video_cbr_mode: true,
641 ..crf.clone()
642 };
643 let turbo = |s: &CaptureSettings| CaptureSettings {
644 video_streaming_mode: true,
645 ..s.clone()
646 };
647 for settings in [&crf, &turbo(&crf)] {
648 assert_eq!(
649 paint_over_desc(settings, None, true).as_deref(),
650 Some("PaintOver CRF: 18 (Burst: 5f)")
651 );
652 assert_eq!(
653 paint_over_desc(settings, None, false).as_deref(),
654 Some("PaintOver CRF: 18 (Burst: 5f)")
655 );
656 assert_eq!(
657 paint_over_desc(settings, Some("VBR"), false).as_deref(),
658 Some("PaintOver Burst: 5f")
659 );
660 }
661 for settings in [&cbr, &turbo(&cbr)] {
662 assert_eq!(
663 paint_over_desc(settings, None, true).as_deref(),
664 Some("PaintOver CRF: 18 (Burst: 5f)")
665 );
666 assert_eq!(
667 paint_over_desc(settings, None, false).as_deref(),
668 Some("PaintOver Burst: 5f")
669 );
670 assert_eq!(
671 paint_over_desc(settings, Some("CBR"), false).as_deref(),
672 Some("PaintOver Burst: 5f")
673 );
674 }
675 assert_eq!(
676 paint_over_desc(
677 &CaptureSettings {
678 use_paint_over_quality: false,
679 ..crf.clone()
680 },
681 None,
682 true
683 ),
684 None
685 );
686 let cbr_off = CaptureSettings {
688 use_paint_over_quality: false,
689 ..cbr.clone()
690 };
691 assert_eq!(
692 paint_over_desc(&cbr_off, None, false),
693 paint_over_desc(&cbr, None, false)
694 );
695 assert_eq!(
696 paint_over_desc(&cbr_off, None, true),
697 paint_over_desc(&cbr, None, true)
698 );
699 assert_eq!(
700 paint_over_desc(
701 &CaptureSettings {
702 video_paintover_crf: 25,
703 ..crf.clone()
704 },
705 None,
706 true
707 ),
708 None
709 );
710 assert_eq!(
711 paint_over_desc(
712 &CaptureSettings {
713 video_paintover_crf: 25,
714 ..cbr
715 },
716 None,
717 true
718 )
719 .as_deref(),
720 Some("PaintOver CRF: 25 (Burst: 5f)")
721 );
722 let jpeg = CaptureSettings {
723 codec: Codec::Jpeg,
724 jpeg_quality: 40,
725 paint_over_jpeg_quality: 90,
726 paint_over_trigger_frames: 15,
727 ..crf
728 };
729 assert_eq!(
730 paint_over_desc(&jpeg, None, false).as_deref(),
731 Some("PaintOver Q: 90 (Trigger: 15f)")
732 );
733 assert_eq!(
734 paint_over_desc(&turbo(&jpeg), None, false).as_deref(),
735 Some("PaintOver Q: 90 (Trigger: 15f)")
736 );
737 assert_eq!(
738 paint_over_desc(
739 &CaptureSettings {
740 paint_over_jpeg_quality: 40,
741 ..jpeg.clone()
742 },
743 None,
744 false
745 ),
746 None
747 );
748 assert_eq!(
749 paint_over_desc(
750 &CaptureSettings {
751 use_paint_over_quality: false,
752 ..jpeg
753 },
754 None,
755 false
756 ),
757 None
758 );
759 }
760
761 #[test]
764 fn a_held_key_past_its_budget_is_coarsened_by_the_overshoot() {
765 assert_eq!(held_key_retry(18, 1000, 1000), None);
766 assert_eq!(held_key_retry(18, 2000, 1000), Some(24));
767 assert_eq!(held_key_retry(18, 3000, 1000), Some(28));
768 assert_eq!(held_key_retry(40, 64_000, 1000), Some(51));
769 assert_eq!(held_key_retry(18, 5000, 0), None, "no budget, no retry");
770 }
771
772 #[test]
776 fn a_held_key_starts_where_the_last_one_says_it_fits() {
777 assert_eq!(held_key_start(18, None, 1000), 18);
778 assert_eq!(held_key_start(18, Some((800, 18)), 1000), 18);
779 assert_eq!(held_key_start(18, Some((2000, 18)), 1000), 24);
780 assert_eq!(
781 held_key_start(18, Some((1000, 24)), 1000),
782 24,
783 "a retried key's own index"
784 );
785 assert_eq!(
786 held_key_start(18, Some((100, 40)), 1000),
787 18,
788 "too far to say"
789 );
790 }
791
792 #[test]
795 fn a_session_lands_on_a_video_codec_the_host_serves() {
796 let mut settings = CaptureSettings {
797 width: 256,
798 height: 128,
799 codec: Codec::Av1,
800 use_cpu: true,
801 ..Default::default()
802 };
803 let encoder = select_frame_encoder(
804 &mut settings,
805 FrameSource::Host { rgba: false },
806 None,
807 "test",
808 );
809 println!("asked for av1, landed on {}", settings.codec.display());
810 assert_ne!(
811 settings.codec,
812 Codec::Jpeg,
813 "this build encodes video in software"
814 );
815 assert!(software_encoder(settings.codec).is_some());
816 assert!(encoder.is_some() || settings.codec == Codec::H264);
818 }
819
820 #[test]
824 fn a_codec_without_a_path_falls_through_the_served_video_codecs() {
825 let software: Vec<Codec> = SOFTWARE_ORDER
826 .iter()
827 .copied()
828 .filter(|&codec| codec != Codec::Av1 && software_encoder(codec).is_some())
829 .collect();
830 assert_eq!(software.first(), Some(&Codec::H264));
831 assert_eq!(
832 fallback_codecs(Codec::Av1, &[]),
833 software,
834 "software H.264 leads the software rungs"
835 );
836 assert_eq!(
837 fallback_codecs(Codec::Av1, &[Codec::H264]),
838 software,
839 "an engine's H.264 leads either way"
840 );
841 assert!(!fallback_codecs(Codec::Av1, &[Codec::Av1]).contains(&Codec::Av1));
842 let engine = [Codec::H264, Codec::H265, Codec::Av1];
844 assert_eq!(
845 &fallback_codecs(Codec::Vp8, &engine)[..3],
846 &[Codec::Av1, Codec::H265, Codec::H264]
847 );
848 }
849
850 #[test]
854 fn the_software_probe_prints_nothing() {
855 if std::env::var_os("PIXELFLUX_PROBE_CHILD").is_some() {
856 for codec in Codec::VIDEO {
857 encodes_in_child(codec);
858 }
859 return;
860 }
861 let out = std::process::Command::new(std::env::current_exe().unwrap())
862 .args([
863 "--exact",
864 "encoders::tests::the_software_probe_prints_nothing",
865 "--nocapture",
866 "--test-threads=1",
867 ])
868 .env("PIXELFLUX_PROBE_CHILD", "1")
869 .output()
870 .unwrap();
871 assert!(out.status.success() && String::from_utf8_lossy(&out.stdout).contains("1 passed"));
872 assert_eq!(
873 String::from_utf8_lossy(&out.stderr),
874 "",
875 "the probes wrote to the host's stderr"
876 );
877 }
878
879 #[test]
884 fn a_session_takes_the_answer_its_node_probe_settled() {
885 let settled = (0..300)
886 .find_map(|_| match probe_node(99) {
887 Err((_, e)) if e == nvenc::SESSIONS_TAKEN => {
888 std::thread::sleep(std::time::Duration::from_millis(100));
889 None
890 }
891 Err((backend, e)) => {
892 Some(format!("{} VP8 did not open: {e}", backend.to_uppercase()))
893 }
894 Ok(_) => Some("render node 99 has no VP8 engine".to_string()),
895 })
896 .expect("the device had no NVENC session to spare for 30 s");
897 let report = crate::report::StreamReport::new("x11");
898 let _scope = crate::report::enter(&report);
899 let mut settings = CaptureSettings {
900 width: 256,
901 height: 128,
902 codec: Codec::Vp8,
903 encode_node_index: 99,
904 ..Default::default()
905 };
906 let encoder = select_frame_encoder(
907 &mut settings,
908 FrameSource::Host { rgba: false },
909 None,
910 "test",
911 );
912 assert!(
913 encoder.is_some_and(|enc| !enc.is_hardware()),
914 "VP8 comes up in software"
915 );
916 assert_eq!(report.info().encoder_reason, settled);
917 }
918
919 #[test]
922 fn software_fullcolor_follows_the_library() {
923 assert_eq!(
924 software_fullcolor(Codec::H264),
925 software_library(Codec::H264) == "x264"
926 );
927 assert_eq!(
928 software_fullcolor(Codec::H265),
929 software_library(Codec::H265) == "x265"
930 );
931 assert_eq!(
932 software_fullcolor(Codec::Vp9),
933 software_library(Codec::Vp9) == "libvpx" && vpx::encodes_444()
934 );
935 assert!(!software_fullcolor(Codec::Av1) && !software_fullcolor(Codec::Vp8));
936 }
937
938 #[test]
940 fn a_child_survives_unless_a_signal_takes_it() {
941 assert!(survives_in_child(|| {}));
942 assert!(survives_in_child(|| unsafe { libc::_exit(3) }));
943 assert!(!survives_in_child(|| unsafe {
944 libc::raise(libc::SIGILL);
945 }));
946 }
947
948 #[test]
954 fn the_av1_probe_survives_sessions_opening_beside_it() {
955 use std::sync::atomic::{AtomicBool, Ordering};
956 let settings = CaptureSettings {
957 width: 64,
958 height: 64,
959 target_fps: 30.0,
960 codec: Codec::Av1,
961 ..Default::default()
962 };
963 let stop = AtomicBool::new(false);
964 let probing = AtomicBool::new(false);
965 std::thread::scope(|s| {
966 for _ in 0..4 {
967 s.spawn(|| {
968 while !stop.load(Ordering::Relaxed) {
969 if probing.load(Ordering::Acquire) {
970 std::thread::sleep(std::time::Duration::from_millis(1));
971 continue;
972 }
973 drop(software_session(&settings, Codec::Av1, false));
974 }
975 });
976 }
977 let found = (0..4).all(|_| {
978 probing.store(true, Ordering::Release);
979 let found = encodes_in_child(Codec::Av1);
980 probing.store(false, Ordering::Release);
981 found
982 });
983 stop.store(true, Ordering::Relaxed);
984 assert!(found);
985 });
986 }
987
988 #[test]
992 fn the_av1_probe_survives_threads_starting_beside_it() {
993 use std::sync::atomic::{AtomicBool, Ordering};
994 let stop = AtomicBool::new(false);
995 std::thread::scope(|s| {
996 for _ in 0..16 {
997 s.spawn(|| {
998 while !stop.load(Ordering::Relaxed) {
999 let _ = std::thread::spawn(|| {}).join();
1000 }
1001 });
1002 }
1003 let found = (0..200).all(|_| encodes_in_child(Codec::Av1));
1004 stop.store(true, Ordering::Relaxed);
1005 assert!(found);
1006 });
1007 }
1008
1009 #[test]
1012 fn software_encoders_follow_the_build() {
1013 use rayon::prelude::*;
1014 let _: u32 = (0..8u32).into_par_iter().sum();
1015 let h264 = software_encoder(Codec::H264).expect("H.264 is always served");
1016 assert_eq!(
1017 h264.library,
1018 if cfg!(feature = "gpl") {
1019 "x264"
1020 } else {
1021 "openh264"
1022 }
1023 );
1024 assert_eq!(software_library(Codec::H264), h264.library);
1025 assert_eq!(software_library(Codec::Jpeg), "none");
1026 assert_eq!(software_encoder(Codec::Jpeg), None);
1027 assert_eq!(
1028 software_library(Codec::H265),
1029 if cfg!(feature = "gpl") {
1030 "x265"
1031 } else {
1032 "kvazaar"
1033 }
1034 );
1035 assert_eq!(software_library(Codec::Vp8), "libvpx");
1036 assert_eq!(software_library(Codec::Vp9), "libvpx");
1037 assert_eq!(software_library(Codec::Av1), "svt-av1");
1038 assert_eq!(software_fullcolor(Codec::H264), cfg!(feature = "gpl"));
1039 assert_eq!(software_fullcolor(Codec::H265), cfg!(feature = "gpl"));
1040 assert!(!software_fullcolor(Codec::Vp8) && !software_fullcolor(Codec::Av1));
1041 assert_eq!(software_fullcolor(Codec::Vp9), vpx::encodes_444());
1042 }
1043
1044 #[test]
1050 fn a_session_reports_the_range_it_converted_at() {
1051 assert_eq!(colorspace_desc(true, true), "I444 (Full Range)");
1052 assert_eq!(colorspace_desc(true, false), "I444 (Limited Range)");
1053 assert_eq!(colorspace_desc(false, true), "I420 (Full Range)");
1054 assert_eq!(colorspace_desc(false, false), "I420 (Limited Range)");
1055
1056 let mut settings = CaptureSettings {
1057 codec: Codec::H264,
1058 video_fullcolor: false,
1059 ..Default::default()
1060 };
1061 assert!(
1062 !session_full_range(None, &settings),
1063 "software H.264 at 4:2:0 converts at limited range"
1064 );
1065 assert_eq!(
1066 colorspace_desc(
1067 session_fullcolor(None, &settings),
1068 session_full_range(None, &settings)
1069 ),
1070 "I420 (Limited Range)"
1071 );
1072
1073 settings.video_fullcolor = true;
1074 assert_eq!(
1075 session_full_range(None, &settings),
1076 software_fullcolor(Codec::H264),
1077 "software H.264 at 4:4:4 converts at full range where the build carries it"
1078 );
1079 }
1080}
1081
1082#[allow(clippy::large_enum_variant)]
1086pub enum FrameEncoder {
1087 Nvenc(NvencEncoder),
1088 Vaapi(vaapi::VaapiEncoder),
1090 Vpx(vpx::VpxEncoder),
1092 Hevc(hevc::HevcEncoder),
1094 Av1(svtav1::SvtAv1Encoder),
1096 #[cfg(target_arch = "aarch64")]
1098 Tegra(tegra::TegraEncoder),
1099 V4l2m2m(v4l2m2m::V4l2M2mEncoder),
1101}
1102
1103macro_rules! each {
1105 ($self:expr, $enc:ident => $e:expr) => {
1106 match $self {
1107 FrameEncoder::Nvenc($enc) => $e,
1108 FrameEncoder::Vaapi($enc) => $e,
1109 FrameEncoder::Vpx($enc) => $e,
1110 FrameEncoder::Hevc($enc) => $e,
1111 FrameEncoder::Av1($enc) => $e,
1112 #[cfg(target_arch = "aarch64")]
1113 FrameEncoder::Tegra($enc) => $e,
1114 FrameEncoder::V4l2m2m($enc) => $e,
1115 }
1116 };
1117}
1118
1119impl FrameEncoder {
1120 pub fn codec(&self) -> Codec {
1122 each!(self, enc => enc.codec())
1123 }
1124
1125 pub fn is_hardware(&self) -> bool {
1127 !matches!(
1128 self,
1129 FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)
1130 )
1131 }
1132
1133 pub fn fixed_rate_control(&self) -> Option<&'static str> {
1137 match self {
1138 #[cfg(target_arch = "aarch64")]
1139 FrameEncoder::Tegra(_) => Some("CBR"),
1140 FrameEncoder::V4l2m2m(_) => Some("VBR"),
1141 _ => None,
1142 }
1143 }
1144
1145 pub fn backend_name(&self) -> &'static str {
1147 match self {
1148 FrameEncoder::Nvenc(_) => "NVENC",
1149 FrameEncoder::Vaapi(_) => "VAAPI",
1150 FrameEncoder::Vpx(enc) => enc.library(),
1151 FrameEncoder::Hevc(enc) => enc.library(),
1152 FrameEncoder::Av1(enc) => enc.library(),
1153 #[cfg(target_arch = "aarch64")]
1154 FrameEncoder::Tegra(_) => "TEGRA",
1155 FrameEncoder::V4l2m2m(_) => "V4L2M2M",
1156 }
1157 }
1158
1159 pub fn dmabuf_planes(&self) -> usize {
1163 match self {
1164 FrameEncoder::Vaapi(_) => 4,
1165 _ => 1,
1166 }
1167 }
1168
1169 pub fn is_fullcolor(&self) -> bool {
1171 each!(self, enc => enc.is_fullcolor())
1172 }
1173
1174 pub fn bit_depth(&self) -> u32 {
1177 match self {
1178 FrameEncoder::Nvenc(enc) => enc.bit_depth(),
1179 FrameEncoder::Vaapi(enc) => enc.bit_depth(),
1180 FrameEncoder::Vpx(enc) => enc.bit_depth(),
1181 FrameEncoder::Hevc(enc) => enc.bit_depth(),
1182 FrameEncoder::Av1(enc) => enc.bit_depth(),
1183 _ => 8,
1184 }
1185 }
1186
1187 pub fn is_full_range(&self) -> bool {
1190 each!(self, enc => enc.is_full_range())
1191 }
1192
1193 pub fn reconfigure_rate(&mut self, settings: &CaptureSettings) -> Result<(), String> {
1196 match self {
1197 FrameEncoder::Nvenc(enc) => {
1198 enc.reconfigure_rate(settings);
1199 Ok(())
1200 }
1201 FrameEncoder::Vaapi(enc) => enc.reconfigure_rate(settings),
1202 FrameEncoder::Vpx(enc) => enc.reconfigure_rate(settings),
1203 FrameEncoder::Hevc(enc) => enc.reconfigure_rate(settings),
1204 FrameEncoder::Av1(enc) => enc.reconfigure_rate(settings),
1205 #[cfg(target_arch = "aarch64")]
1206 FrameEncoder::Tegra(enc) => enc.reconfigure_rate(settings),
1207 FrameEncoder::V4l2m2m(enc) => enc.reconfigure_rate(settings),
1208 }
1209 }
1210
1211 pub fn encode_host(
1214 &mut self,
1215 pixels: &[u8],
1216 stride: usize,
1217 rgba: bool,
1218 frame_number: u64,
1219 qp: u32,
1220 force_idr: bool,
1221 ) -> Result<Vec<u8>, String> {
1222 match self {
1223 FrameEncoder::Nvenc(enc) => {
1224 enc.encode_cpu_packed(pixels, stride, rgba, frame_number, qp, force_idr)
1225 }
1226 FrameEncoder::Vaapi(enc) => {
1227 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1228 }
1229 FrameEncoder::Vpx(enc) => {
1230 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1231 }
1232 FrameEncoder::Hevc(enc) => {
1233 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1234 }
1235 FrameEncoder::Av1(enc) => {
1236 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1237 }
1238 #[cfg(target_arch = "aarch64")]
1239 FrameEncoder::Tegra(enc) => {
1240 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1241 }
1242 FrameEncoder::V4l2m2m(enc) => {
1243 enc.encode_host(pixels, stride, rgba, frame_number, qp, force_idr)
1244 }
1245 }
1246 }
1247
1248 pub fn encode_client_host(
1253 &mut self,
1254 pixels: &[u8],
1255 stride: usize,
1256 rgba: bool,
1257 frame_number: u64,
1258 qp: u32,
1259 force_idr: bool,
1260 ) -> Result<Vec<u8>, String> {
1261 match self {
1262 FrameEncoder::Nvenc(enc) => {
1263 enc.encode_client_packed(pixels, stride, rgba, frame_number, qp, force_idr)
1264 }
1265 _ => self.encode_host(pixels, stride, rgba, frame_number, qp, force_idr),
1266 }
1267 }
1268
1269 pub fn hash_next_upload(&mut self, rows: u32) -> bool {
1273 match self {
1274 FrameEncoder::Nvenc(enc) => enc.hash_next_upload(rows),
1275 _ => false,
1276 }
1277 }
1278
1279 pub fn take_upload_hashes(&mut self) -> Option<Vec<u64>> {
1281 match self {
1282 FrameEncoder::Nvenc(enc) => enc.take_upload_hashes(),
1283 _ => None,
1284 }
1285 }
1286
1287 pub fn last_quality(&self) -> Option<u32> {
1291 match self {
1292 FrameEncoder::Nvenc(enc) => enc.last_quality(),
1293 FrameEncoder::Vaapi(enc) => enc.last_quality(),
1294 FrameEncoder::Vpx(enc) => enc.last_quality(),
1295 FrameEncoder::Hevc(enc) => enc.last_quality(),
1296 FrameEncoder::Av1(enc) => enc.last_quality(),
1297 _ => None,
1298 }
1299 }
1300
1301 pub fn last_size(&self) -> Option<usize> {
1307 match self {
1308 FrameEncoder::Nvenc(enc) => enc.last_size(),
1309 FrameEncoder::Vaapi(enc) => enc.last_size(),
1310 FrameEncoder::Vpx(enc) => enc.last_size(),
1311 FrameEncoder::Hevc(enc) => enc.last_size(),
1312 _ => None,
1313 }
1314 }
1315
1316 pub fn measure(&mut self) {
1319 if let FrameEncoder::Vaapi(enc) = self {
1320 enc.measure();
1321 }
1322 }
1323
1324 pub fn measures(&self) -> bool {
1327 match self {
1328 FrameEncoder::Vaapi(enc) => enc.measures(),
1329 _ => false,
1330 }
1331 }
1332
1333 pub fn last_psnr(&self) -> Option<f32> {
1338 match self {
1339 FrameEncoder::Vaapi(enc) => enc.last_psnr(),
1340 _ => None,
1341 }
1342 }
1343
1344 pub fn cap(&mut self, bytes: usize) {
1351 match self {
1352 FrameEncoder::Nvenc(enc) => enc.cap(bytes),
1353 FrameEncoder::Vaapi(enc) => enc.cap(bytes),
1354 FrameEncoder::Vpx(enc) => enc.cap(bytes),
1355 _ => {}
1356 }
1357 }
1358
1359 pub fn follows_cap(&self) -> bool {
1364 match self {
1365 FrameEncoder::Vaapi(enc) => enc.follows_cap(),
1366 FrameEncoder::Vpx(enc) => enc.follows_cap(),
1367 _ => false,
1368 }
1369 }
1370
1371 pub fn reach_finer(&self) -> u32 {
1377 match self {
1378 FrameEncoder::Hevc(enc) => enc.reach_finer(),
1379 _ => 0,
1380 }
1381 }
1382
1383 pub fn ends_cleanup_on_key(&self) -> bool {
1390 match self {
1391 FrameEncoder::Vaapi(enc) => enc.ends_cleanup_on_key(),
1392 _ => false,
1393 }
1394 }
1395
1396 pub fn refit(&self) -> Option<f64> {
1399 match self {
1400 FrameEncoder::Nvenc(enc) => enc.refit(),
1401 _ => None,
1402 }
1403 }
1404
1405 pub fn take_lost_cleanup(&mut self) -> bool {
1409 match self {
1410 FrameEncoder::Nvenc(enc) => enc.take_lost_cleanup(),
1411 _ => false,
1412 }
1413 }
1414
1415 pub fn band_size(&self) -> Option<usize> {
1419 match self {
1420 FrameEncoder::Nvenc(enc) => enc.band_size(),
1421 FrameEncoder::Vpx(enc) => enc.band_size(),
1422 _ => None,
1423 }
1424 }
1425
1426 pub fn refines(&self) -> Option<crate::ledger::Refiner> {
1434 use crate::ledger::Refiner;
1435 match self {
1436 FrameEncoder::Nvenc(enc) => {
1437 enc.refines().then_some(Refiner::finer(nvenc::REFINE_FINER))
1438 }
1439 FrameEncoder::Vpx(enc) => {
1440 (enc.codec() == Codec::Vp8).then_some(Refiner::finer(vpx::REFINE_FINER))
1441 }
1442 FrameEncoder::Hevc(enc) => enc.refines().then(|| Refiner::finer(enc.refine_finer())),
1443 FrameEncoder::Vaapi(enc) => enc.refines(),
1444 _ => None,
1445 }
1446 }
1447
1448 pub fn constant_blocks(&mut self, constant: &[bool]) {
1455 match self {
1456 FrameEncoder::Vpx(enc) => enc.constant_blocks(constant),
1457 FrameEncoder::Hevc(enc) => enc.constant_blocks(constant),
1458 FrameEncoder::Av1(enc) => enc.constant_blocks(constant),
1459 _ => {}
1460 }
1461 }
1462
1463 pub fn refine_blocks(&mut self, refinement: &crate::ledger::Refinement) {
1466 match self {
1467 FrameEncoder::Nvenc(enc) => enc.refine_blocks(refinement),
1468 FrameEncoder::Vpx(enc) => enc.refine_blocks(refinement),
1469 FrameEncoder::Hevc(enc) => enc.refine_blocks(refinement),
1470 FrameEncoder::Vaapi(enc) => enc.refine_blocks(refinement),
1471 _ => {}
1472 }
1473 }
1474
1475 pub fn holds_quantizer(&self) -> bool {
1484 match self {
1485 FrameEncoder::Nvenc(_) => true,
1486 FrameEncoder::Vpx(enc) => enc.holds_quantizer(),
1487 FrameEncoder::Vaapi(enc) => !enc.is_cbr(),
1488 FrameEncoder::Hevc(enc) => enc.holds_quantizer(),
1489 FrameEncoder::Av1(enc) => enc.holds_quantizer(),
1490 #[cfg(target_arch = "aarch64")]
1491 FrameEncoder::Tegra(_) => false,
1492 FrameEncoder::V4l2m2m(_) => false,
1493 }
1494 }
1495
1496 pub fn capped(&self) -> bool {
1501 match self {
1502 FrameEncoder::Nvenc(enc) => enc.capped(),
1503 FrameEncoder::Vpx(enc) => enc.capped(),
1504 FrameEncoder::Av1(enc) => enc.capped(),
1505 FrameEncoder::Vaapi(_) | FrameEncoder::Hevc(_) => false,
1506 #[cfg(target_arch = "aarch64")]
1507 FrameEncoder::Tegra(_) => false,
1508 FrameEncoder::V4l2m2m(_) => false,
1509 }
1510 }
1511
1512 pub fn rate_bound(&self, cbr: bool) -> bool {
1519 cbr || match self {
1520 FrameEncoder::Nvenc(enc) => enc.peak_bound(),
1521 FrameEncoder::Vpx(enc) => enc.peak_bound(),
1522 FrameEncoder::Av1(enc) => enc.peak_bound(),
1523 _ => false,
1524 }
1525 }
1526
1527 pub fn coded_capped(&self) -> bool {
1532 match self {
1533 FrameEncoder::Nvenc(enc) => enc.coded_capped(),
1534 FrameEncoder::Vpx(enc) => enc.coded_capped(),
1535 FrameEncoder::Av1(enc) => enc.coded_capped(),
1536 _ => false,
1537 }
1538 }
1539
1540 pub fn idle(&mut self) {
1544 match self {
1545 FrameEncoder::Nvenc(enc) => enc.idle(),
1546 FrameEncoder::Vpx(enc) => enc.idle(),
1547 FrameEncoder::Av1(enc) => enc.idle(),
1548 _ => {}
1549 }
1550 }
1551
1552 pub fn defer_blocks(&mut self, plan: Option<&crate::ledger::BlockPlan>) {
1557 if let FrameEncoder::Nvenc(enc) = self {
1558 enc.defer_blocks(plan);
1559 }
1560 }
1561
1562 pub fn reopens_on_quality(&self) -> bool {
1567 match self {
1568 FrameEncoder::Hevc(_) => true,
1569 FrameEncoder::Av1(enc) => enc.reopens_on_quality(),
1570 _ => false,
1571 }
1572 }
1573
1574 pub fn cleans_up_with_key(&self) -> bool {
1582 !matches!(self, FrameEncoder::Vpx(enc) if enc.codec() == Codec::Vp8)
1583 }
1584
1585 pub fn hold_quantizer(&mut self, crf: u32, band: Option<(f64, f64)>) {
1593 match self {
1594 FrameEncoder::Nvenc(enc) => enc.hold_quantizer(crf, band),
1595 FrameEncoder::Vaapi(enc) => enc.hold_quantizer(crf),
1596 FrameEncoder::Vpx(enc) => enc.hold_quantizer(crf, band),
1597 FrameEncoder::Hevc(enc) => enc.hold_quantizer(crf),
1598 FrameEncoder::Av1(enc) => enc.hold_quantizer(crf),
1599 #[cfg(target_arch = "aarch64")]
1600 FrameEncoder::Tegra(_) => {}
1601 FrameEncoder::V4l2m2m(_) => {}
1602 }
1603 }
1604
1605 pub fn delivered_units(
1610 &self,
1611 data: Vec<u8>,
1612 encoded: u16,
1613 ) -> Vec<(Vec<u8>, u16, reference::Reference)> {
1614 #[cfg(target_arch = "aarch64")]
1615 if let FrameEncoder::Tegra(enc) = self {
1616 match enc.delivered_units() {
1617 [] => {}
1618 &[(id, reference, _)] => return vec![(data, id, reference)],
1619 units => {
1620 let mut start = 0;
1621 return units
1622 .iter()
1623 .map(|&(id, reference, end)| {
1624 let unit = data[start..end].to_vec();
1625 start = end;
1626 (unit, id, reference)
1627 })
1628 .collect();
1629 }
1630 }
1631 }
1632 vec![(data, encoded, self.last_reference())]
1633 }
1634
1635 #[cfg_attr(not(target_arch = "aarch64"), allow(unused_variables))]
1639 pub fn push_held(&mut self, frame_number: u64) -> Result<Vec<u8>, String> {
1640 match self {
1641 #[cfg(target_arch = "aarch64")]
1642 FrameEncoder::Tegra(enc) => enc.push_held(frame_number),
1643 _ => Ok(Vec::new()),
1644 }
1645 }
1646
1647 pub fn holds_frame(&self) -> bool {
1648 match self {
1649 #[cfg(target_arch = "aarch64")]
1650 FrameEncoder::Tegra(enc) => enc.holds_frame(),
1651 _ => false,
1652 }
1653 }
1654
1655 pub fn last_reference(&self) -> reference::Reference {
1656 each!(self, enc => enc.last_reference())
1657 }
1658
1659 pub fn invalidate_reference(&mut self, frame_id: u16) -> bool {
1663 each!(self, enc => enc.invalidate_reference(frame_id))
1664 }
1665
1666 pub fn take_report(&mut self, report: reference::ReferenceReport) -> bool {
1670 match (self, report) {
1671 (FrameEncoder::Nvenc(enc), report) => enc.take_report(report),
1672 (FrameEncoder::Vaapi(enc), report) => enc.take_report(report),
1673 (FrameEncoder::Vpx(enc), reference::ReferenceReport::Held(id)) => {
1674 enc.acknowledge_reference(id, true);
1675 true
1676 }
1677 (FrameEncoder::Vpx(enc), reference::ReferenceReport::Sent(id)) => {
1678 enc.acknowledge_reference(id, false);
1679 true
1680 }
1681 (FrameEncoder::Av1(enc), reference::ReferenceReport::Held(id)) => {
1682 enc.acknowledge_reference(id, true);
1683 true
1684 }
1685 (FrameEncoder::Av1(enc), reference::ReferenceReport::Sent(id)) => {
1686 enc.acknowledge_reference(id, false);
1687 true
1688 }
1689 (
1690 enc,
1691 reference::ReferenceReport::Lost(frame_id)
1692 | reference::ReferenceReport::Withheld(frame_id),
1693 ) => enc.invalidate_reference(frame_id),
1694 _ => true,
1695 }
1696 }
1697
1698 pub fn encode_dmabuf(
1700 &mut self,
1701 dmabuf: &Dmabuf,
1702 frame_number: u64,
1703 qp: u32,
1704 force_idr: bool,
1705 ) -> Result<Vec<u8>, String> {
1706 match self {
1707 FrameEncoder::Nvenc(enc) => enc.encode(dmabuf, frame_number, qp, force_idr),
1708 FrameEncoder::Vaapi(enc) => enc.encode_dmabuf(dmabuf, frame_number, qp, force_idr),
1709 _ => Err(format!(
1710 "the {} session takes host frames",
1711 self.backend_name()
1712 )),
1713 }
1714 }
1715
1716 pub fn encode_dmabuf_after_blit(
1719 &mut self,
1720 dmabuf: &Dmabuf,
1721 frame_number: u64,
1722 qp: u32,
1723 force_idr: bool,
1724 ) -> Result<Vec<u8>, String> {
1725 match self {
1726 FrameEncoder::Nvenc(enc) => enc.encode_after_blit(dmabuf, frame_number, qp, force_idr),
1727 _ => Err(format!(
1728 "the {} session waits on no blit semaphore",
1729 self.backend_name()
1730 )),
1731 }
1732 }
1733
1734 pub fn blit_semaphore_fd(&mut self) -> Option<Result<std::os::fd::OwnedFd, String>> {
1738 match self {
1739 FrameEncoder::Nvenc(enc) => Some(enc.blit_semaphore_fd()),
1740 _ => None,
1741 }
1742 }
1743}
1744
1745pub fn software_session(
1747 settings: &CaptureSettings,
1748 codec: Codec,
1749 rgba: bool,
1750) -> Result<FrameEncoder, String> {
1751 Ok(match codec {
1752 Codec::Vp8 | Codec::Vp9 => FrameEncoder::Vpx(vpx::VpxEncoder::new(settings, codec, rgba)?),
1753 Codec::H265 => FrameEncoder::Hevc(hevc::HevcEncoder::new(settings, rgba)?),
1754 Codec::Av1 => FrameEncoder::Av1(svtav1::SvtAv1Encoder::new(settings, rgba)?),
1755 Codec::H264 | Codec::Jpeg => {
1756 return Err(format!("{} takes the CPU path", codec.display()));
1757 }
1758 })
1759}
1760
1761pub fn chroma_name(fullcolor: bool) -> &'static str {
1763 if fullcolor { "4:4:4" } else { "4:2:0" }
1764}
1765
1766pub fn driver_name(driver: &str) -> &str {
1768 match driver.rsplit('/').next() {
1769 Some(name) if !name.is_empty() => name,
1770 _ => "unknown",
1771 }
1772}
1773
1774#[derive(Clone, Copy)]
1777pub enum FrameSource {
1778 Dmabuf { egl_display: *const c_void },
1779 Host { rgba: bool },
1780}
1781
1782fn threads_desc(threads: i32) -> String {
1784 if threads == 1 {
1785 "1 thread".to_string()
1786 } else {
1787 format!("{threads} threads")
1788 }
1789}
1790
1791pub fn select_frame_encoder(
1808 settings: &mut CaptureSettings,
1809 source: FrameSource,
1810 prior: Option<FrameEncoder>,
1811 tag: &str,
1812) -> Option<FrameEncoder> {
1813 let requested = settings.codec;
1814 if !requested.is_video() {
1815 return None;
1816 }
1817 if let Some(enc) = select_for_codec(settings, source, prior, tag) {
1818 return Some(enc);
1819 }
1820 let FrameSource::Host { .. } = source else {
1823 return None;
1824 };
1825 if requested == Codec::H264 {
1826 return None;
1827 }
1828 let hardware: Vec<Codec> = if settings.use_cpu || settings.encode_node_index == -1 {
1829 Vec::new()
1830 } else {
1831 hardware_encoders(settings.encode_node_index)
1832 .into_iter()
1833 .map(|(codec, ..)| codec)
1834 .collect()
1835 };
1836 eprintln!(
1837 "[{tag}] No {} path on this host; trying the video codecs it serves.",
1838 requested.display()
1839 );
1840 for codec in fallback_codecs(requested, &hardware) {
1841 settings.codec = codec;
1842 if let Some(enc) = select_for_codec(settings, source, None, tag) {
1843 return Some(enc);
1844 }
1845 if codec == Codec::H264 && software_encoder(Codec::H264).is_some() {
1847 return None;
1848 }
1849 }
1850 eprintln!("[{tag}] No video encoder on this host. Encoding JPEG instead.");
1851 settings.codec = Codec::Jpeg;
1852 None
1853}
1854
1855const HARDWARE_ORDER: [Codec; 5] = [Codec::Av1, Codec::H265, Codec::Vp9, Codec::H264, Codec::Vp8];
1858
1859const SOFTWARE_ORDER: [Codec; 5] = [Codec::H264, Codec::Av1, Codec::Vp8, Codec::H265, Codec::Vp9];
1862
1863fn fallback_codecs(requested: Codec, hardware: &[Codec]) -> Vec<Codec> {
1867 let mut codecs: Vec<Codec> = HARDWARE_ORDER
1868 .iter()
1869 .copied()
1870 .filter(|&codec| codec != requested && hardware.contains(&codec))
1871 .collect();
1872 codecs.extend(SOFTWARE_ORDER.iter().copied().filter(|&codec| {
1873 codec != requested && !hardware.contains(&codec) && software_encoder(codec).is_some()
1874 }));
1875 codecs
1876}
1877
1878fn hardware_lacks_ten_bit(served: &HardwareEncoders, settings: &CaptureSettings) -> bool {
1883 let codec = settings.codec;
1884 if settings.video_bit_depth < 10 || !software_ten_bit(codec) {
1885 return false;
1886 }
1887 served
1888 .iter()
1889 .find(|&&(c, ..)| c == codec)
1890 .is_some_and(|&(_, _, formats)| {
1891 let fullcolor = settings.video_fullcolor && codec.fullcolor() && formats.fullcolor;
1892 !formats.ten_bit[usize::from(fullcolor)]
1893 })
1894}
1895
1896pub(crate) fn ten_bit_is_softwares(settings: &CaptureSettings) -> bool {
1899 probe_node(settings.encode_node_index.max(0))
1900 .is_ok_and(|served| hardware_lacks_ten_bit(&served, settings))
1901}
1902
1903pub(crate) fn engine_may_serve(settings: &CaptureSettings) -> bool {
1908 if settings.use_cpu || settings.encode_node_index == -1 {
1909 return false;
1910 }
1911 #[cfg(target_arch = "aarch64")]
1912 if tegra::available() {
1913 return tegra::served().contains(&settings.codec);
1914 }
1915 match probe_node(settings.encode_node_index) {
1916 Ok(served) => {
1917 served.iter().any(|&(c, ..)| c == settings.codec)
1918 && !hardware_lacks_ten_bit(&served, settings)
1919 }
1920 Err((_, e)) => e == nvenc::SESSIONS_TAKEN,
1921 }
1922}
1923
1924fn select_for_codec(
1927 settings: &mut CaptureSettings,
1928 source: FrameSource,
1929 prior: Option<FrameEncoder>,
1930 tag: &str,
1931) -> Option<FrameEncoder> {
1932 let codec = settings.codec;
1933 let software_forced = settings.use_cpu || settings.encode_node_index == -1;
1934 #[cfg(target_arch = "aarch64")]
1935 if let (false, Some(_), FrameSource::Host { rgba }) =
1936 (software_forced, tegra::coded_fourcc(codec), source)
1937 {
1938 if tegra::available() {
1943 drop(prior);
1944 if tegra::served().contains(&codec) {
1945 match tegra::TegraEncoder::new(codec, settings, rgba) {
1946 Ok(enc) => {
1947 println!(
1948 "[{tag}] Encoder: TEGRA {} {} on the vendor V4L2 encoder.",
1949 codec.display(),
1950 chroma_name(enc.is_fullcolor())
1951 );
1952 return Some(FrameEncoder::Tegra(enc));
1953 }
1954 Err(e) => {
1955 eprintln!("[{tag}] Failed to init the Tegra encoder: {e}");
1956 crate::report::encoder_reason(&format!(
1957 "Tegra {} did not open: {e}",
1958 codec.display()
1959 ));
1960 }
1961 }
1962 } else {
1963 crate::report::encoder_reason(&format!(
1964 "the Tegra encoder has no {} engine",
1965 codec.display()
1966 ));
1967 }
1968 return software_fallback(settings, rgba, tag);
1969 }
1970 }
1971 if !software_forced {
1972 let node = settings.encode_node_index.max(0);
1973 let driver = crate::get_gpu_driver(node);
1974 crate::log::debug!("[{tag}] Encode node {node}, driver {driver}.");
1975 let settled = match probe_node(node) {
1978 Err((backend, e)) if e != nvenc::SESSIONS_TAKEN => Some(format!(
1979 "{} {} did not open: {e}",
1980 backend.to_uppercase(),
1981 codec.display()
1982 )),
1983 Ok(served)
1984 if !served.iter().any(|&(c, backend, ..)| {
1985 c == codec && matches!(backend, "nvenc" | "vaapi")
1986 }) =>
1987 {
1988 Some(format!(
1989 "render node {node} has no {} engine",
1990 codec.display()
1991 ))
1992 }
1993 Ok(served) if hardware_lacks_ten_bit(&served, settings) => Some(format!(
1994 "render node {node} encodes no 10-bit {}, which the software encoder does",
1995 codec.display()
1996 )),
1997 _ => None,
1998 };
1999 if let Some(reason) = settled {
2000 drop(prior);
2001 crate::report::encoder_reason(&reason);
2002 } else if crate::driver_selects_nvenc(&driver) {
2003 if let Some(FrameEncoder::Nvenc(mut enc)) = prior {
2004 match enc.reconfigure_resolution(settings) {
2005 Ok(resized) => {
2006 if resized {
2007 crate::log::debug!("[{tag}] NVENC session reconfigured in place.");
2008 }
2009 crate::report::hardware_encoder(
2010 enc.device_name(),
2011 driver_name(&driver),
2012 node,
2013 );
2014 return Some(FrameEncoder::Nvenc(enc));
2015 }
2016 Err(e) => eprintln!(
2017 "[{tag}] NVENC in-place reconfigure unavailable ({e}); rebuilding."
2018 ),
2019 }
2020 }
2021 let egl_display = match source {
2022 FrameSource::Dmabuf { egl_display } => egl_display,
2023 FrameSource::Host { .. } => std::ptr::null(),
2024 };
2025 match NvencEncoder::new(settings, egl_display) {
2026 Ok(enc) => {
2027 println!(
2028 "[{tag}] Encoder: NVENC {} {} {}-bit on {} (render node {node}, {} driver), {}.",
2029 codec.display(),
2030 chroma_name(enc.is_fullcolor()),
2031 enc.bit_depth(),
2032 enc.device_name(),
2033 driver_name(&driver),
2034 enc.split_summary()
2035 );
2036 crate::report::hardware_encoder(enc.device_name(), driver_name(&driver), node);
2037 return Some(FrameEncoder::Nvenc(enc));
2038 }
2039 Err(e) => {
2040 eprintln!("[{tag}] Failed to init NVENC {}: {e}", codec.display());
2041 crate::report::encoder_reason(&format!(
2042 "NVENC {} did not open: {e}",
2043 codec.display()
2044 ));
2045 }
2046 }
2047 } else {
2048 drop(prior);
2052 let input = match source {
2053 FrameSource::Dmabuf { .. } => vaapi::Input::Dmabuf,
2054 FrameSource::Host { rgba } => vaapi::Input::Host { rgba },
2055 };
2056 match vaapi::VaapiEncoder::new(settings, codec, input) {
2057 Ok(enc) => {
2058 println!(
2059 "[{tag}] Encoder: VAAPI {} {} {}-bit on {} surfaces (render node {node}, {} driver).",
2060 codec.display(),
2061 chroma_name(enc.is_fullcolor()),
2062 enc.bit_depth(),
2063 enc.surface_format_name(),
2064 driver_name(&driver)
2065 );
2066 crate::report::hardware_encoder("", driver_name(&driver), node);
2067 return Some(FrameEncoder::Vaapi(enc));
2068 }
2069 Err(e) => {
2070 eprintln!("[{tag}] Failed to init VAAPI {}: {e}", codec.display());
2071 crate::report::encoder_reason(&format!(
2072 "VAAPI {} did not open: {e}",
2073 codec.display()
2074 ));
2075 }
2076 }
2077 }
2078 if let (Some(_), FrameSource::Host { rgba }) = (v4l2m2m::coded_fourcc(codec), source)
2083 && v4l2m2m::encodes(codec, settings.width, settings.height)
2084 {
2085 match v4l2m2m::V4l2M2mEncoder::new(codec, settings, rgba) {
2086 Ok(enc) => {
2087 println!(
2088 "[{tag}] Encoder: V4L2M2M {} {} on a stateful M2M node.",
2089 codec.display(),
2090 chroma_name(enc.is_fullcolor())
2091 );
2092 return Some(FrameEncoder::V4l2m2m(enc));
2093 }
2094 Err(e) => eprintln!("[{tag}] Failed to init the V4L2 M2M encoder: {e}"),
2095 }
2096 }
2097 } else {
2098 crate::log::debug!("[{tag}] Software encoding selected (use_cpu or encode_node_index -1).");
2099 crate::report::encoder_reason("software encoding selected");
2100 }
2101 let FrameSource::Host { rgba } = source else {
2102 return None;
2103 };
2104 software_fallback(settings, rgba, tag)
2105}
2106
2107fn software_fallback(
2111 settings: &mut CaptureSettings,
2112 rgba: bool,
2113 tag: &str,
2114) -> Option<FrameEncoder> {
2115 let codec = settings.codec;
2116 if codec == Codec::H264 {
2117 println!(
2118 "[{tag}] Encoder: software {} ({}) {} {}-bit, {}.",
2119 codec.display(),
2120 software_library(Codec::H264),
2121 chroma_name(session_fullcolor(None, settings)),
2122 session_bit_depth(None, settings),
2123 threads_desc(session::h264_fullframe_threads(settings))
2124 );
2125 return None;
2126 }
2127 let session = software_encoder(codec)
2128 .ok_or_else(|| {
2129 format!(
2130 "the {} encoder does not run on this machine",
2131 codec.display()
2132 )
2133 })
2134 .and_then(|_| software_session(settings, codec, rgba));
2135 match session {
2136 Ok(enc) => {
2137 println!(
2138 "[{tag}] Encoder: software {} ({}) {} {}-bit, {}.",
2139 codec.display(),
2140 enc.backend_name(),
2141 chroma_name(enc.is_fullcolor()),
2142 enc.bit_depth(),
2143 threads_desc(session::encode_threads(settings))
2144 );
2145 Some(enc)
2146 }
2147 Err(e) => {
2148 eprintln!("[{tag}] No {} encoder available: {e}.", codec.display());
2149 None
2150 }
2151 }
2152}
2153
2154#[cfg(test)]
2155mod software_tests {
2156 use super::codec::{FRAME_DELTA, FRAME_KEY, WIRE_VIDEO, parse_video_type};
2162 use super::*;
2163 use crate::webcam::convert::I420View;
2164 use crate::webcam::decode::{ColorTags, Decoder, VideoDecoder};
2165 use std::cell::Cell;
2166 use std::sync::{Mutex, MutexGuard};
2167
2168 const W: usize = 320;
2169 const H: usize = 240;
2170
2171 const SOFTWARE: [Codec; 4] = [Codec::H265, Codec::Vp8, Codec::Vp9, Codec::Av1];
2173
2174 static ONE_AV1: Mutex<()> = Mutex::new(());
2179
2180 thread_local! {
2181 static AV1_DEPTH: Cell<u32> = const { Cell::new(0) };
2182 }
2183
2184 pub(super) struct Turn {
2186 _guard: Option<MutexGuard<'static, ()>>,
2187 counted: bool,
2188 }
2189
2190 impl Drop for Turn {
2191 fn drop(&mut self) {
2192 if self.counted {
2193 AV1_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
2194 }
2195 }
2196 }
2197
2198 pub(super) fn turn(codec: Codec) -> Turn {
2200 let counted = codec == Codec::Av1;
2201 let first = counted
2202 && AV1_DEPTH.with(|depth| {
2203 let held = depth.get();
2204 depth.set(held + 1);
2205 held == 0
2206 });
2207 Turn {
2208 _guard: first.then(|| ONE_AV1.lock().unwrap_or_else(|e| e.into_inner())),
2209 counted,
2210 }
2211 }
2212
2213 pub(super) struct Session {
2215 encoder: FrameEncoder,
2216 _turn: Turn,
2217 }
2218
2219 impl std::ops::Deref for Session {
2220 type Target = FrameEncoder;
2221 fn deref(&self) -> &FrameEncoder {
2222 &self.encoder
2223 }
2224 }
2225
2226 impl std::ops::DerefMut for Session {
2227 fn deref_mut(&mut self) -> &mut FrameEncoder {
2228 &mut self.encoder
2229 }
2230 }
2231
2232 pub(super) fn settings(codec: Codec) -> CaptureSettings {
2233 CaptureSettings {
2234 width: W as i32,
2235 height: H as i32,
2236 target_fps: 30.0,
2237 codec,
2238 video_crf: 25,
2239 use_cpu: true,
2240 ..Default::default()
2241 }
2242 }
2243
2244 pub(super) fn session(codec: Codec, s: &CaptureSettings, rgba: bool) -> Session {
2245 let _turn = turn(codec);
2246 let encoder = software_session(s, codec, rgba)
2247 .unwrap_or_else(|e| panic!("{codec:?} software session: {e}"));
2248 Session { encoder, _turn }
2249 }
2250
2251 fn pipeline_depth(codec: Codec) -> usize {
2256 let s = settings(codec);
2257 let mut enc = session(codec, &s, false);
2258 for t in 0..8usize {
2259 let out = enc
2260 .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2261 .unwrap_or_default();
2262 if !out.is_empty() {
2263 return t;
2264 }
2265 }
2266 panic!("{codec:?}: no packet after eight frames");
2267 }
2268
2269 fn lockstep_codecs() -> Vec<Codec> {
2274 SOFTWARE
2275 .into_iter()
2276 .filter(|&codec| {
2277 let depth = pipeline_depth(codec);
2278 if depth > 0 {
2279 println!("[pipeline] {codec:?}: {depth} frames deep, frame-id checks skipped");
2280 }
2281 depth == 0
2282 })
2283 .collect()
2284 }
2285
2286 pub(super) fn frame(t: usize) -> Vec<u8> {
2289 let mut f = vec![0u8; W * H * 4];
2290 let (bx, by) = ((t * 9) % (W - 40), (t * 5) % (H - 30));
2291 for y in 0..H {
2292 for x in 0..W {
2293 let i = (y * W + x) * 4;
2294 let g = ((x * 255) / W) as u8;
2295 let cell = (x / 8 + y / 12) % 3 == 0 && x % 8 < 6 && y % 12 < 9;
2296 let (b, gr, r) = if x >= bx && x < bx + 40 && y >= by && y < by + 30 {
2297 (40, 220, 250)
2298 } else if cell {
2299 (30, 30, 30)
2300 } else {
2301 (g, 200 - g / 2, 120)
2302 };
2303 f[i] = b;
2304 f[i + 1] = gr;
2305 f[i + 2] = r;
2306 f[i + 3] = 255;
2307 }
2308 }
2309 f
2310 }
2311
2312 fn noise(t: usize) -> Vec<u8> {
2314 let mut f = vec![255u8; W * H * 4];
2315 let mut s = (t as u32).wrapping_mul(2654435761).wrapping_add(7);
2316 for px in f.as_chunks_mut::<4>().0 {
2317 s ^= s << 13;
2318 s ^= s >> 17;
2319 s ^= s << 5;
2320 px[0] = (s >> 24) as u8;
2321 px[1] = (s >> 16) as u8;
2322 px[2] = (s >> 8) as u8;
2323 }
2324 f
2325 }
2326
2327 fn luma_psnr(decoded: &I420View<'_>, bgra: &[u8]) -> f64 {
2330 assert_eq!((decoded.width, decoded.height), (W, H));
2331 let mut mse = 0f64;
2332 for y in 0..H {
2333 for x in 0..W {
2334 let i = (y * W + x) * 4;
2335 let (b, g, r) = (
2336 f64::from(bgra[i]),
2337 f64::from(bgra[i + 1]),
2338 f64::from(bgra[i + 2]),
2339 );
2340 let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2341 let d = f64::from(decoded.y[y * decoded.y_stride + x]) - luma;
2342 mse += d * d;
2343 }
2344 }
2345 mse /= (W * H) as f64;
2346 if mse <= 0.0 {
2347 99.0
2348 } else {
2349 10.0 * (255.0 * 255.0 / mse).log10()
2350 }
2351 }
2352
2353 fn decode_one(dec: &mut VideoDecoder, packet: &[u8]) -> bool {
2354 dec.decode(&packet[VIDEO_HEADER_LEN..])
2355 .unwrap_or_else(|e| panic!("decode: {e:?}"))
2356 }
2357
2358 fn takes_a_live_rate(enc: &FrameEncoder) -> bool {
2361 match enc.backend_name() {
2362 "libvpx" => true,
2363 "svt-av1" => codec_sys::svtav1::HAS_EVENTS,
2364 _ => false,
2365 }
2366 }
2367
2368 #[test]
2371 fn software_frames_decode_back_to_the_source() {
2372 for codec in lockstep_codecs() {
2373 let s = settings(codec);
2374 let mut enc = session(codec, &s, false);
2375 let mut dec = VideoDecoder::new(codec).expect("decoder");
2376 for t in 0..6usize {
2377 let src = frame(t);
2378 let out = enc
2379 .encode_host(&src, W * 4, false, t as u64, 25, t == 0)
2380 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2381 assert!(out.len() > VIDEO_HEADER_LEN, "{codec:?} frame {t} is empty");
2382 assert_eq!(out[0], WIRE_VIDEO);
2383 let kind = if t == 0 { FRAME_KEY } else { FRAME_DELTA };
2384 assert_eq!(
2385 parse_video_type(out[1]),
2386 Some((codec, kind)),
2387 "{codec:?} frame {t}"
2388 );
2389 assert_eq!(u16::from_be_bytes([out[2], out[3]]) as usize, t);
2390 assert_eq!(
2391 &out[4..10],
2392 &[0, 0, (W >> 8) as u8, W as u8, (H >> 8) as u8, H as u8]
2393 );
2394 assert!(
2395 decode_one(&mut dec, &out),
2396 "{codec:?} frame {t} decoded nothing"
2397 );
2398 let psnr = luma_psnr(&dec.frame().unwrap(), &src);
2399 assert!(psnr > 28.0, "{codec:?} frame {t}: luma PSNR {psnr:.1} dB");
2400 }
2401 let src = frame(6);
2402 let key = enc
2403 .encode_host(&src, W * 4, false, 6, 25, true)
2404 .expect("forced key");
2405 assert_eq!(
2406 parse_video_type(key[1]),
2407 Some((codec, FRAME_KEY)),
2408 "{codec:?} forced key"
2409 );
2410 let mut fresh = VideoDecoder::new(codec).expect("decoder");
2411 assert!(
2412 decode_one(&mut fresh, &key),
2413 "{codec:?}: a forced key frame must decode alone"
2414 );
2415 assert!(luma_psnr(&fresh.frame().unwrap(), &src) > 28.0);
2416 let next = enc
2417 .encode_host(&frame(7), W * 4, false, 7, 25, false)
2418 .expect("delta after key");
2419 assert_eq!(parse_video_type(next[1]), Some((codec, FRAME_DELTA)));
2420 assert!(decode_one(&mut fresh, &next));
2421 }
2422 }
2423
2424 #[test]
2429 fn a_key_frame_asked_for_mid_stream_starts_a_fresh_decoder() {
2430 for codec in SOFTWARE {
2431 let s = settings(codec);
2432 let mut enc = session(codec, &s, false);
2433 for t in 0..6usize {
2434 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2435 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2436 }
2437 let mut out = enc
2438 .encode_host(&frame(6), W * 4, false, 6, 25, true)
2439 .expect("forced key");
2440 let mut t = 7usize;
2441 while out.get(1).and_then(|&kind| parse_video_type(kind)) != Some((codec, FRAME_KEY)) {
2442 assert!(
2443 t < 22,
2444 "{codec:?}: no key frame came back within fifteen frames of the request"
2445 );
2446 out = enc
2447 .encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2448 .expect("delta");
2449 t += 1;
2450 }
2451 assert_eq!(
2452 u16::from_be_bytes([out[2], out[3]]),
2453 6,
2454 "{codec:?}: the key frame carries the id it was asked for"
2455 );
2456 let mut fresh = VideoDecoder::new(codec).expect("decoder");
2457 assert!(
2458 decode_one(&mut fresh, &out),
2459 "{codec:?}: the key frame must decode alone"
2460 );
2461 assert!(
2462 luma_psnr(&fresh.frame().unwrap(), &frame(6)) > 28.0,
2463 "{codec:?}"
2464 );
2465 }
2466 }
2467
2468 #[test]
2473 fn a_session_that_cannot_invalidate_names_no_reference() {
2474 use super::reference::Reference;
2475 for codec in [Codec::H265, Codec::Av1] {
2476 let s = settings(codec);
2477 let mut enc = session(codec, &s, false);
2478 for t in 0..4usize {
2479 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2480 .unwrap_or_else(|e| panic!("{codec:?} encode {t}: {e}"));
2481 assert_eq!(
2482 enc.last_reference(),
2483 Reference::Untracked,
2484 "{codec:?} frame {t}"
2485 );
2486 }
2487 assert!(
2488 !enc.invalidate_reference(2),
2489 "{codec:?}: the refusal is what asks for the key frame"
2490 );
2491 }
2492 for codec in [Codec::Vp8, Codec::Vp9] {
2493 let s = settings(codec);
2494 let mut enc = session(codec, &s, false);
2495 enc.encode_host(&frame(0), W * 4, false, 0, 25, true)
2496 .unwrap();
2497 assert_eq!(enc.last_reference(), Reference::None, "{codec:?}");
2498 enc.encode_host(&frame(1), W * 4, false, 1, 25, false)
2499 .unwrap();
2500 assert_eq!(enc.last_reference(), Reference::Frame(0), "{codec:?}");
2501 assert!(enc.invalidate_reference(1), "{codec:?}");
2502 }
2503 }
2504
2505 #[test]
2510 fn a_constant_rate_av1_session_predicts_past_a_lost_frame() {
2511 use super::reference::Reference;
2512 let mut s = settings(Codec::Av1);
2513 s.video_cbr_mode = true;
2514 s.video_bitrate_kbps = 2000;
2515 let mut enc = session(Codec::Av1, &s, false);
2516 let mut frames: Vec<Vec<u8>> = (0..8usize)
2517 .map(|t| {
2518 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2519 .unwrap()
2520 })
2521 .collect();
2522 if !codec_sys::svtav1::HAS_EVENTS {
2523 assert_eq!(enc.last_reference(), Reference::Untracked);
2524 assert!(!enc.invalidate_reference(5));
2525 return;
2526 }
2527 assert_eq!(enc.last_reference(), Reference::Frame(6));
2528 assert!(enc.invalidate_reference(5));
2531 for t in 8..10usize {
2532 frames.push(
2533 enc.encode_host(&frame(t), W * 4, false, t as u64, 25, false)
2534 .unwrap(),
2535 );
2536 assert_eq!(
2537 parse_video_type(frames[t][1]),
2538 Some((Codec::Av1, FRAME_DELTA)),
2539 "frame {t}"
2540 );
2541 assert_eq!(
2542 enc.last_reference(),
2543 Reference::Frame(if t == 8 { 0 } else { 8 }),
2544 "frame {t}"
2545 );
2546 }
2547 let (mut whole, mut lossy) = (
2548 VideoDecoder::new(Codec::Av1).unwrap(),
2549 VideoDecoder::new(Codec::Av1).unwrap(),
2550 );
2551 for (i, f) in frames.iter().enumerate() {
2552 assert!(decode_one(&mut whole, f), "frame {i}");
2553 if !(5..8).contains(&i) {
2554 assert!(decode_one(&mut lossy, f), "frame {i} without frames 5-7");
2555 }
2556 }
2557 let (a, b) = (whole.frame().unwrap(), lossy.frame().unwrap());
2558 let same =
2559 a.y.chunks(a.y_stride)
2560 .zip(b.y.chunks(b.y_stride))
2561 .take(H)
2562 .all(|(x, y)| x[..W] == y[..W]);
2563 assert!(
2564 same,
2565 "the decoder that lost frames 5-7 shows frame 9 unlike the one that saw them"
2566 );
2567 let key = enc
2568 .encode_host(&frame(10), W * 4, false, 10, 25, true)
2569 .unwrap();
2570 assert_eq!(parse_video_type(key[1]), Some((Codec::Av1, FRAME_KEY)));
2571 assert_eq!(enc.last_reference(), Reference::None);
2572 assert!(
2573 decode_one(&mut VideoDecoder::new(Codec::Av1).unwrap(), &key),
2574 "the key frame decodes alone"
2575 );
2576 }
2577
2578 #[test]
2584 fn an_anchor_prediction_leaves_no_block_as_it_stood_in_the_anchor() {
2585 use super::reference::ReferenceReport;
2586 if !codec_sys::svtav1::HAS_EVENTS {
2587 return;
2588 }
2589 let (w, h) = (1280usize, 720usize);
2590 let mut noise = vec![0u8; w * 240 * 4];
2591 let mut seed = 1u64;
2592 for b in noise.iter_mut() {
2593 seed ^= seed << 13;
2594 seed ^= seed >> 7;
2595 seed ^= seed << 17;
2596 *b = seed as u8;
2597 }
2598 let scene = |t: usize| {
2599 let bar = (t * 12) % (w - 160);
2600 let mut f = vec![0u8; w * h * 4];
2601 for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
2602 let (x, y) = (i % w, i / w);
2603 let bgr: [u8; 3] = if y >= 480 {
2604 let n = ((t % 4) * w * 240 + (y - 480) * w + x) * 4 % noise.len();
2605 [noise[n], noise[n + 1], noise[n + 2]]
2606 } else if (96..160).contains(&y) {
2607 [if ((x + t * 4) / 16 + (y - 96) / 16).is_multiple_of(2) {
2608 255
2609 } else {
2610 0
2611 }; 3]
2612 } else if y >= 160 && (bar..bar + 160).contains(&x) {
2613 [0x28, 0x3c, 0xdc]
2614 } else {
2615 [0x78, 0x28, 0x1e]
2616 };
2617 px[..3].copy_from_slice(&bgr);
2618 }
2619 f
2620 };
2621 let mut s = settings(Codec::Av1);
2622 s.width = w as i32;
2623 s.height = h as i32;
2624 s.target_fps = 60.0;
2625 s.video_cbr_mode = true;
2626 s.video_bitrate_kbps = 8000;
2627 let mut enc = session(Codec::Av1, &s, false);
2628 let mut dec = VideoDecoder::new(Codec::Av1).unwrap();
2629 let (mut previous, mut anchored) = (None, 0);
2630 for t in 0..84usize {
2631 if t > 10 && t % 13 == 0 {
2632 assert!(enc.take_report(ReferenceReport::Lost(t as u16 - 4)));
2633 }
2634 let src = scene(t);
2635 let out = enc
2636 .encode_host(&src, w * 4, false, t as u64, 25, t == 0)
2637 .unwrap();
2638 let id = u16::from_be_bytes([out[2], out[3]]);
2639 let reference = u16::from_be_bytes([out[10], out[11]]);
2640 assert_eq!(id as usize, t);
2641 if reference != id && Some(reference) != previous {
2642 anchored += 1;
2643 }
2644 previous = Some(id);
2645 assert!(decode_one(&mut dec, &out), "frame {id}");
2646 let pic = dec.frame().unwrap();
2647 for (bx, by) in (0..480)
2648 .step_by(64)
2649 .flat_map(|y| (0..w).step_by(64).map(move |x| (x, y)))
2650 {
2651 let (mut sum, mut n) = (0f64, 0);
2652 for y in (by..(by + 64).min(480)).step_by(2) {
2653 for x in (bx..bx + 64).step_by(2) {
2654 let i = (y * w + x) * 4;
2655 let (b, g, r) = (
2656 f64::from(src[i]),
2657 f64::from(src[i + 1]),
2658 f64::from(src[i + 2]),
2659 );
2660 let luma = 16.0 + (0.2126 * r + 0.7152 * g + 0.0722 * b) * 219.0 / 255.0;
2661 sum += (f64::from(pic.y[y * pic.y_stride + x]) - luma).abs();
2662 n += 1;
2663 }
2664 }
2665 let off = sum / f64::from(n);
2666 assert!(
2667 off <= 20.0,
2668 "frame {id} (from {reference}): the block at ({bx}, {by}) is {off:.1} off its source"
2669 );
2670 }
2671 }
2672 assert!(anchored >= 5, "{anchored} frames predicted from an anchor");
2673 }
2674
2675 #[test]
2678 fn host_byte_order_is_honored() {
2679 for codec in lockstep_codecs() {
2680 let s = settings(codec);
2681 let mut means = Vec::new();
2682 for rgba in [false, true] {
2683 let mut px = [0u8; 4];
2684 if rgba {
2685 px[0] = 220
2686 } else {
2687 px[2] = 220
2688 }
2689 px[3] = 255;
2690 let src: Vec<u8> = px.repeat(W * H);
2691 let mut enc = session(codec, &s, rgba);
2692 let out = enc
2693 .encode_host(&src, W * 4, rgba, 0, 20, true)
2694 .expect("encode");
2695 let mut dec = VideoDecoder::new(codec).expect("decoder");
2696 assert!(decode_one(&mut dec, &out));
2697 let f = dec.frame().unwrap();
2698 let cw = f.chroma_width();
2699 let ch = f.chroma_height();
2700 let v: f64 = (0..ch)
2701 .flat_map(|y| (0..cw).map(move |x| (x, y)))
2702 .map(|(x, y)| f64::from(f.v[y * f.uv_stride + x]))
2703 .sum::<f64>()
2704 / (cw * ch) as f64;
2705 means.push(v);
2706 }
2707 assert!(
2708 means[0] > 180.0,
2709 "{codec:?}: red must land high in V, got {:.0}",
2710 means[0]
2711 );
2712 assert!(
2713 (means[0] - means[1]).abs() < 6.0,
2714 "{codec:?}: BGRA {:.0} vs RGBA {:.0}",
2715 means[0],
2716 means[1]
2717 );
2718 }
2719 }
2720
2721 #[test]
2724 fn quantizer_and_bitrate_reach_the_encoder() {
2725 for codec in SOFTWARE {
2726 let run = |crf: i32| -> usize {
2727 let mut s = settings(codec);
2728 s.video_crf = crf;
2729 let mut enc = session(codec, &s, false);
2730 (0..12usize)
2731 .map(|t| {
2732 enc.encode_host(&frame(t), W * 4, false, t as u64, crf as u32, t == 0)
2733 .unwrap()
2734 .len()
2735 })
2736 .sum()
2737 };
2738 let (fine, coarse) = (run(15), run(45));
2739 assert!(
2740 coarse * 2 < fine,
2741 "{codec:?}: crf 45 = {coarse} bytes vs crf 15 = {fine}"
2742 );
2743
2744 const KBPS: i32 = 800;
2745 let mut s = settings(codec);
2746 s.video_cbr_mode = true;
2747 s.video_bitrate_kbps = KBPS;
2748 let mut enc = session(codec, &s, false);
2749 let mut bytes = 0usize;
2750 for t in 0..90usize {
2751 let out = enc
2752 .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2753 .unwrap();
2754 if t >= 30 && out.len() > VIDEO_HEADER_LEN {
2758 bytes += out.len() - VIDEO_HEADER_LEN;
2759 }
2760 }
2761 let kbps = bytes as f64 * 8.0 * 30.0 / 60.0 / 1000.0;
2762 println!("{codec:?} CBR {KBPS} kbps on noise: {kbps:.0} kbps");
2763 assert!(
2764 kbps > f64::from(KBPS) * 0.5 && kbps < f64::from(KBPS) * 1.6,
2765 "{codec:?}: {kbps:.0} kbps"
2766 );
2767 }
2768 }
2769
2770 #[test]
2775 fn live_quality_and_rate_changes_keep_the_stream_decodable() {
2776 for codec in lockstep_codecs() {
2777 let mut s = settings(codec);
2778 s.video_crf = 40;
2779 let mut enc = session(codec, &s, false);
2780 let mut dec = VideoDecoder::new(codec).expect("decoder");
2781 let coarse = enc
2782 .encode_host(&frame(0), W * 4, false, 0, 40, true)
2783 .unwrap();
2784 assert!(decode_one(&mut dec, &coarse));
2785 let fine = enc
2786 .encode_host(&frame(1), W * 4, false, 1, 15, false)
2787 .unwrap();
2788 let live = enc.backend_name() == "libvpx";
2789 let kind = if live { FRAME_DELTA } else { FRAME_KEY };
2790 assert_eq!(
2791 parse_video_type(fine[1]),
2792 Some((codec, kind)),
2793 "{codec:?}: a quality change {}",
2794 if live {
2795 "is live"
2796 } else {
2797 "re-opens with a key frame"
2798 }
2799 );
2800 assert!(decode_one(&mut dec, &fine));
2801 let held = enc
2802 .encode_host(&frame(2), W * 4, false, 2, 40, false)
2803 .unwrap();
2804 assert_eq!(
2805 parse_video_type(held[1]),
2806 Some((codec, FRAME_DELTA)),
2807 "{codec:?}: a single decrease waits out the hysteresis"
2808 );
2809 assert!(decode_one(&mut dec, &held));
2810 s.target_fps = 15.0;
2811 enc.reconfigure_rate(&s).expect("rate reconfigure");
2812 let after = enc
2813 .encode_host(&frame(3), W * 4, false, 3, 15, false)
2814 .unwrap();
2815 let kind = if enc.backend_name() == "libvpx" {
2816 FRAME_DELTA
2817 } else {
2818 FRAME_KEY
2819 };
2820 assert_eq!(
2821 parse_video_type(after[1]),
2822 Some((codec, kind)),
2823 "{codec:?}: a frame-rate change"
2824 );
2825 assert!(decode_one(&mut dec, &after));
2826 assert!(luma_psnr(&dec.frame().unwrap(), &frame(3)) > 28.0);
2827 }
2828 }
2829
2830 #[test]
2833 fn a_quality_change_reaches_a_running_session() {
2834 for codec in lockstep_codecs() {
2835 let mut s = settings(codec);
2836 s.video_crf = 40;
2837 let mut enc = session(codec, &s, false);
2838 let (mut spent, mut changed) = ([0usize; 2], None);
2839 for t in 0..12usize {
2840 let crf = if t < 6 { 40 } else { 15 };
2841 let out = enc
2842 .encode_host(&frame(t), W * 4, false, t as u64, crf, t == 0)
2843 .unwrap();
2844 if t == 6 {
2845 changed = parse_video_type(out[1]);
2846 }
2847 if (1..6).contains(&t) {
2848 spent[0] += out.len();
2849 } else if t >= 7 {
2850 spent[1] += out.len();
2851 }
2852 }
2853 assert!(
2854 spent[1] * 2 > spent[0] * 3,
2855 "{codec:?}: bytes over five deltas before and after {spent:?}"
2856 );
2857 let kind = if enc.backend_name() == "libvpx" {
2858 FRAME_DELTA
2859 } else {
2860 FRAME_KEY
2861 };
2862 assert_eq!(
2863 changed,
2864 Some((codec, kind)),
2865 "{codec:?}: the frame after the change"
2866 );
2867 }
2868 }
2869
2870 #[test]
2873 fn a_bitrate_change_reaches_a_running_session() {
2874 for codec in lockstep_codecs() {
2875 let mut s = settings(codec);
2876 s.video_cbr_mode = true;
2877 s.video_bitrate_kbps = 1500;
2878 let mut enc = session(codec, &s, false);
2879 let mut dec = VideoDecoder::new(codec).expect("decoder");
2880 let (mut spent, mut changed) = ([0usize; 2], None);
2881 for t in 0..60usize {
2882 if t == 30 {
2883 s.video_bitrate_kbps = 4500;
2884 enc.reconfigure_rate(&s).expect("rate reconfigure");
2885 }
2886 let out = enc
2887 .encode_host(&noise(t), W * 4, false, t as u64, 25, t == 0)
2888 .unwrap();
2889 if t == 30 {
2890 changed = parse_video_type(out[1]);
2891 }
2892 if out.len() > VIDEO_HEADER_LEN {
2893 assert!(decode_one(&mut dec, &out), "{codec:?} frame {t}");
2894 }
2895 if (15..30).contains(&t) {
2896 spent[0] += out.len();
2897 } else if t >= 45 {
2898 spent[1] += out.len();
2899 }
2900 }
2901 assert!(
2902 spent[1] > spent[0] * 2,
2903 "{codec:?}: bytes over fifteen frames before and after {spent:?}"
2904 );
2905 let kind = if takes_a_live_rate(&enc) {
2906 FRAME_DELTA
2907 } else {
2908 FRAME_KEY
2909 };
2910 assert_eq!(
2911 changed,
2912 Some((codec, kind)),
2913 "{codec:?}: the frame after the change"
2914 );
2915 }
2916 }
2917
2918 #[test]
2923 fn sessions_declare_the_matrix_they_convert_with() {
2924 for codec in lockstep_codecs() {
2925 let mut s = settings(codec);
2926 let mut enc = session(codec, &s, false);
2927 let out = enc
2928 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2929 .expect("encode");
2930 let mut dec = VideoDecoder::new(codec).expect("decoder");
2931 assert!(decode_one(&mut dec, &out));
2932 let want = if codec == Codec::Vp8 {
2933 ColorTags::BT470BG_LIMITED
2934 } else {
2935 ColorTags::BT709_LIMITED
2936 };
2937 assert_eq!(dec.color_tags(), Some(want), "{codec:?}");
2938 if software_fullcolor(codec) {
2939 s.video_fullcolor = true;
2940 let mut enc = session(codec, &s, false);
2941 assert!(enc.is_fullcolor(), "{codec:?} carries the 4:4:4 request");
2942 let out = enc
2943 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2944 .expect("encode");
2945 let mut dec = VideoDecoder::new(codec).expect("decoder");
2946 assert!(decode_one(&mut dec, &out));
2947 let want = if codec == Codec::Vp9 {
2948 ColorTags::BT709_LIMITED
2949 } else {
2950 ColorTags::BT709_FULL
2951 };
2952 assert_eq!(dec.color_tags(), Some(want), "{codec:?} 4:4:4");
2953 }
2954 }
2955 }
2956
2957 #[test]
2960 fn fullcolor_follows_the_software_encoder() {
2961 for codec in lockstep_codecs() {
2962 let mut s = settings(codec);
2963 s.video_fullcolor = true;
2964 let mut enc = session(codec, &s, false);
2965 assert_eq!(enc.is_fullcolor(), software_fullcolor(codec), "{codec:?}");
2966 let out = enc
2967 .encode_host(&frame(0), W * 4, false, 0, 25, true)
2968 .unwrap();
2969 let mut dec = VideoDecoder::new(codec).expect("decoder");
2970 assert!(decode_one(&mut dec, &out));
2971 }
2972 }
2973
2974 #[test]
2977 fn ten_bit_follows_the_software_encoder() {
2978 for codec in lockstep_codecs() {
2979 for fullcolor in [false, true] {
2980 let mut s = settings(codec);
2981 s.video_bit_depth = 10;
2982 s.video_fullcolor = fullcolor;
2983 let mut enc = session(codec, &s, false);
2984 let want = if software_ten_bit(codec) { 10 } else { 8 };
2985 assert_eq!(enc.bit_depth(), want, "{codec:?}");
2986 assert_eq!(
2987 enc.is_fullcolor(),
2988 fullcolor && software_fullcolor(codec),
2989 "{codec:?}"
2990 );
2991 assert_eq!(
2992 software_formats(codec).ten_bit,
2993 [want == 10, want == 10 && software_fullcolor(codec)],
2994 "{codec:?}"
2995 );
2996 for t in 0..3 {
2997 let out = enc
2998 .encode_host(&frame(t), W * 4, false, t as u64, 25, t == 0)
2999 .unwrap();
3000 assert!(t > 0 || out.len() > VIDEO_HEADER_LEN, "{codec:?} key frame");
3001 }
3002 }
3003 }
3004 }
3005
3006 #[test]
3011 fn decoded_chroma_is_neutral_on_a_tile_that_averages_to_gray() {
3012 const N: usize = 128;
3013 let bgra = chroma_siting::bgra(N, N);
3014 let settings = CaptureSettings {
3015 width: N as i32,
3016 height: N as i32,
3017 target_fps: 30.0,
3018 video_crf: 20,
3019 ..Default::default()
3020 };
3021 for codec in SOFTWARE {
3022 let mut enc = session(codec, &settings, false);
3023 let out = drain_first(&mut enc, &bgra, N * 4, 20);
3024 assert!(!out.is_empty(), "{codec:?} encoded nothing");
3025 let mut dec = VideoDecoder::new(codec).expect("decoder");
3026 assert!(
3027 dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
3028 "{codec:?} decoded nothing"
3029 );
3030 let worst = chroma_siting::worst(&dec.frame().expect("frame"));
3031 println!("[chroma-siting] {codec:?}: worst |C-128| {worst:.1}");
3032 assert!(
3033 worst <= 8.0,
3034 "{codec:?} sites chroma {worst:.1} off neutral"
3035 );
3036 }
3037 }
3038
3039 #[test]
3045 fn the_chart_decodes_to_the_color_that_was_painted() {
3046 const N: usize = 256;
3047 let bgra = chroma_siting::chart_bgra(N, N / 2);
3048 let settings = CaptureSettings {
3049 width: N as i32,
3050 height: (N / 2) as i32,
3051 target_fps: 30.0,
3052 video_crf: 20,
3053 ..Default::default()
3054 };
3055 for codec in SOFTWARE {
3056 let mut enc = session(codec, &settings, false);
3057 let out = drain_first(&mut enc, &bgra, N * 4, 20);
3058 let mut dec = VideoDecoder::new(codec).expect("decoder");
3059 assert!(
3060 dec.decode(&out[VIDEO_HEADER_LEN..]).unwrap_or(false),
3061 "{codec:?}"
3062 );
3063 let declared = codec != Codec::Vp8;
3064 let (k, other, other_name) = if declared {
3065 (chroma_siting::BT709, chroma_siting::BT601, "BT.601")
3066 } else {
3067 (chroma_siting::BT601, chroma_siting::BT709, "BT.709")
3068 };
3069 let frame = dec.frame().expect("frame");
3070 let worst = chroma_siting::chart_error(&frame, k);
3071 let under_other = chroma_siting::chart_error(&frame, other);
3072 println!(
3073 "[chart] {codec:?}: worst |dRGB| {worst:.1} against the declared matrix, {under_other:.1} against {other_name}"
3074 );
3075 assert!(
3076 worst <= 12.0,
3077 "{codec:?} paints {worst:.1} off the chart against the matrix it declares, and {under_other:.1} against {other_name}: {}",
3078 if under_other < worst {
3079 "it converted with that one and declared the other"
3080 } else {
3081 "neither matrix explains it"
3082 }
3083 );
3084 }
3085 }
3086
3087 pub(super) fn drain_first(
3090 enc: &mut FrameEncoder,
3091 bgra: &[u8],
3092 stride: usize,
3093 qp: u32,
3094 ) -> Vec<u8> {
3095 for t in 0..8u64 {
3096 let out = enc
3097 .encode_host(bgra, stride, false, t, qp, t == 0)
3098 .unwrap_or_else(|e| panic!("encode: {e}"));
3099 if !out.is_empty() {
3100 return out;
3101 }
3102 }
3103 Vec::new()
3104 }
3105}
3106
3107#[cfg(test)]
3110pub(crate) fn scrolled_text(w: usize, h: usize, t: usize) -> Vec<u8> {
3111 let mut f = vec![18u8; w * h * 4];
3112 for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3113 let (x, y) = (i % w, i / w + t * 4);
3114 let cell =
3115 ((x / 8) as u32).wrapping_mul(2654435761) ^ ((y / 12) as u32).wrapping_mul(40503);
3116 if x % 8 < 7 && y % 12 < 10 && (cell >> (((y % 12) * 3 + x % 8) % 29)) & 1 == 1 {
3117 px.copy_from_slice(&[224, 224, 224, 255]);
3118 }
3119 }
3120 f
3121}
3122
3123#[cfg(test)]
3126pub(crate) mod mixed_screen {
3127 use crate::webcam::decode::{Decoder as _, VideoDecoder};
3128
3129 pub const W: usize = 328;
3133 pub const H: usize = 244;
3134
3135 const BOX: usize = 64;
3138 const BX: usize = W - BOX - 20;
3139 const BY: usize = H - BOX - 12;
3140
3141 pub fn in_box(x: usize, y: usize) -> bool {
3143 (BX..BX + BOX).contains(&x) && (BY..BY + BOX).contains(&y)
3144 }
3145
3146 pub fn picture(t: usize) -> Vec<u8> {
3149 let mut f = vec![0u8; W * H * 4];
3150 for (i, px) in f.as_chunks_mut::<4>().0.iter_mut().enumerate() {
3151 let (x, y) = (i % W, i / W);
3152 let text = (x / 7 + y / 11) % 3 == 0 && x % 7 < 5 && y % 11 < 8;
3153 let v = if in_box(x, y) {
3154 ((x * 7 + y * 13 + t * 41) % 251) as u8
3155 } else if text {
3156 30
3157 } else {
3158 235
3159 };
3160 px.copy_from_slice(&[v, v, v, 255]);
3161 }
3162 f
3163 }
3164
3165 pub fn constant() -> Vec<bool> {
3168 let (cols, rows) = (W.div_ceil(16), H.div_ceil(16));
3169 let map: Vec<bool> = (0..rows)
3170 .flat_map(|row| {
3171 (0..cols).map(move |col| {
3172 let (x0, y0) = (col * 16, row * 16);
3173 !(x0 + 16 > BX && x0 < BX + BOX && y0 + 16 > BY && y0 < BY + BOX)
3174 })
3175 })
3176 .collect();
3177 assert!(map.iter().filter(|&&c| !c).count() < map.len() / 2);
3178 map
3179 }
3180
3181 pub fn luma(dec: &VideoDecoder) -> Vec<u8> {
3183 let f = dec.frame().expect("a decoded picture");
3184 f.y.chunks(f.y_stride)
3185 .take(H)
3186 .flat_map(|row| row[..W].iter().copied())
3187 .collect()
3188 }
3189
3190 pub fn distance(decoded: &[u8], source: &[u8], boxed: bool) -> f64 {
3193 let (mut sum, mut n) = (0.0, 0usize);
3194 for y in 0..H {
3195 for x in 0..W {
3196 if in_box(x, y) == boxed {
3197 sum +=
3198 (f64::from(decoded[y * W + x]) - f64::from(source[(y * W + x) * 4])).abs();
3199 n += 1;
3200 }
3201 }
3202 }
3203 sum / n as f64
3204 }
3205
3206 pub fn assert_decodes_alike(
3210 codec: super::Codec,
3211 label: &str,
3212 mut plain: impl FnMut(&[u8], u64) -> Vec<u8>,
3213 mut mapped: impl FnMut(&[u8], u64, &[bool]) -> Vec<u8>,
3214 ) {
3215 let map = constant();
3216 let mut plain_dec = VideoDecoder::new(codec).expect("decoder");
3217 let mut mapped_dec = VideoDecoder::new(codec).expect("decoder");
3218 let mut source = Vec::new();
3219 for n in 0..8u64 {
3220 source = picture(n as usize);
3221 let a = plain(&source, n);
3222 let b = mapped(&source, n, &map);
3223 assert!(
3224 plain_dec
3225 .decode(&a[super::codec::VIDEO_HEADER_LEN..])
3226 .expect("decode")
3227 );
3228 assert!(
3229 mapped_dec
3230 .decode(&b[super::codec::VIDEO_HEADER_LEN..])
3231 .expect("decode")
3232 );
3233 }
3234 let (pa, pb) = (luma(&plain_dec), luma(&mapped_dec));
3235 for boxed in [true, false] {
3236 let (da, db) = (distance(&pa, &source, boxed), distance(&pb, &source, boxed));
3237 assert!(
3238 db <= da + 1.0,
3239 "{label} {}: {db:.2} luma levels from the source with the map, {da:.2} without",
3240 if boxed {
3241 "in the box"
3242 } else {
3243 "on the still screen"
3244 }
3245 );
3246 }
3247 }
3248}
3249
3250#[cfg(test)]
3252pub(crate) mod chroma_siting {
3253 pub const TILE: [[u8; 3]; 4] = [[0, 0, 128], [0, 255, 0], [128, 128, 255], [255, 0, 0]];
3258
3259 pub fn bgra(w: usize, h: usize) -> Vec<u8> {
3261 let mut buf = vec![255u8; w * h * 4];
3262 for y in 0..h {
3263 for x in 0..w {
3264 let p = TILE[(y % 2) * 2 + (x % 2)];
3265 buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
3266 }
3267 }
3268 buf
3269 }
3270
3271 pub const BT709: (f64, f64) = (0.2126, 0.0722);
3273
3274 pub const BT601: (f64, f64) = (0.299, 0.114);
3277
3278 pub fn ycbcr(rgb: [f64; 3], k: (f64, f64)) -> [f64; 3] {
3280 let ([r, g, b], (kr, kb)) = (rgb, k);
3281 let y = kr * r + (1.0 - kr - kb) * g + kb * b;
3282 [
3283 16.0 + 219.0 * y / 255.0,
3284 128.0 + 224.0 * (b - y) / (2.0 * (1.0 - kb) * 255.0),
3285 128.0 + 224.0 * (r - y) / (2.0 * (1.0 - kr) * 255.0),
3286 ]
3287 }
3288
3289 pub fn chroma(rgb: [f64; 3], k: (f64, f64)) -> (f64, f64) {
3291 let c = ycbcr(rgb, k);
3292 (c[1], c[2])
3293 }
3294
3295 pub fn rgb(ycc: [f64; 3], k: (f64, f64)) -> [f64; 3] {
3298 let (kr, kb) = k;
3299 let (y, cb, cr) = (
3300 (ycc[0] - 16.0) / 219.0,
3301 (ycc[1] - 128.0) / 224.0,
3302 (ycc[2] - 128.0) / 224.0,
3303 );
3304 let r = y + 2.0 * (1.0 - kr) * cr;
3305 let b = y + 2.0 * (1.0 - kb) * cb;
3306 let g = (y - kr * r - kb * b) / (1.0 - kr - kb);
3307 [r, g, b].map(|c| (c * 255.0).clamp(0.0, 255.0))
3308 }
3309
3310 pub const CHART: [[u8; 3]; 8] = [
3313 [255, 255, 255],
3314 [128, 128, 128],
3315 [0, 0, 0],
3316 [255, 0, 0],
3317 [0, 255, 0],
3318 [0, 0, 255],
3319 [255, 255, 0],
3320 [0, 255, 255],
3321 ];
3322
3323 pub fn chart_bgra(w: usize, h: usize) -> Vec<u8> {
3325 let mut buf = vec![255u8; w * h * 4];
3326 for y in 0..h {
3327 for x in 0..w {
3328 let p = CHART[(x * CHART.len() / w).min(CHART.len() - 1)];
3329 buf[(y * w + x) * 4..][..3].copy_from_slice(&[p[2], p[1], p[0]]);
3330 }
3331 }
3332 buf
3333 }
3334
3335 pub fn chart_error(f: &crate::webcam::convert::I420View<'_>, k: (f64, f64)) -> f64 {
3340 let cols = CHART.len();
3341 let mut worst = 0.0f64;
3342 for (i, want) in CHART.iter().enumerate() {
3343 let (x0, x1) = (f.width * i / cols, f.width * (i + 1) / cols);
3344 let (lo, hi) = (x0 + (x1 - x0) / 4, x1 - (x1 - x0) / 4);
3345 let (mut acc, mut n) = ([0.0f64; 3], 0.0f64);
3346 for y in f.height / 4..f.height * 3 / 4 {
3347 for x in lo..hi {
3348 let ycc = [
3349 f64::from(f.y[y * f.y_stride + x]),
3350 f64::from(f.u[(y / 2) * f.uv_stride + x / 2]),
3351 f64::from(f.v[(y / 2) * f.uv_stride + x / 2]),
3352 ];
3353 let got = rgb(ycc, k);
3354 for c in 0..3 {
3355 acc[c] += got[c];
3356 }
3357 n += 1.0;
3358 }
3359 }
3360 for c in 0..3 {
3361 worst = worst.max((acc[c] / n - f64::from(want[c])).abs());
3362 }
3363 }
3364 worst
3365 }
3366
3367 pub fn worst(f: &crate::webcam::convert::I420View<'_>) -> f64 {
3369 let mut worst = 0.0f64;
3370 for r in 0..f.chroma_height() {
3371 for c in 0..f.chroma_width() {
3372 let i = r * f.uv_stride + c;
3373 worst = worst.max((f64::from(f.u[i]) - 128.0).hypot(f64::from(f.v[i]) - 128.0));
3374 }
3375 }
3376 worst
3377 }
3378
3379 #[test]
3382 fn the_tile_separates_the_sitings() {
3383 let c: Vec<(f64, f64)> = TILE
3384 .iter()
3385 .map(|&p| chroma(p.map(f64::from), BT709))
3386 .collect();
3387 let mean = |of: &[usize]| {
3388 let (u, v) = of
3389 .iter()
3390 .fold((0.0, 0.0), |(u, v), &i| (u + c[i].0, v + c[i].1));
3391 let n = of.len() as f64;
3392 (u / n - 128.0).hypot(v / n - 128.0)
3393 };
3394 let all = mean(&[0, 1, 2, 3]);
3395 assert!(
3396 all < 0.5,
3397 "the whole tile must average to neutral chroma, off by {all:.1}"
3398 );
3399 for part in [
3400 vec![0],
3401 vec![1],
3402 vec![2],
3403 vec![3],
3404 vec![0, 1],
3405 vec![2, 3],
3406 vec![0, 2],
3407 vec![1, 3],
3408 ] {
3409 let d = mean(&part);
3410 assert!(
3411 d > 40.0,
3412 "pixels {part:?} average to chroma only {d:.1} from neutral"
3413 );
3414 }
3415 }
3416}
3417
3418pub fn session_fullcolor(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3422 match encoder {
3423 Some(enc) => enc.is_fullcolor(),
3424 None => settings.video_fullcolor && software_fullcolor(settings.codec),
3425 }
3426}
3427
3428pub fn session_bit_depth(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> u32 {
3432 match encoder {
3433 Some(enc) => enc.bit_depth(),
3434 None => software::h264_bit_depth(settings),
3435 }
3436}
3437
3438pub fn session_capped(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3442 match encoder {
3443 Some(enc) => enc.capped(),
3444 None => {
3445 cfg!(feature = "gpl")
3446 && settings.codec == Codec::H264
3447 && session::RateSettings::new(settings).capped
3448 }
3449 }
3450}
3451
3452pub fn session_full_range(encoder: Option<&FrameEncoder>, settings: &CaptureSettings) -> bool {
3457 match encoder {
3458 Some(enc) => enc.is_full_range(),
3459 None => session_fullcolor(None, settings),
3460 }
3461}
3462
3463#[cfg(test)]
3464mod hardware_encoder_tests {
3465 use super::*;
3468
3469 #[test]
3472 #[ignore]
3473 fn gpu_hardware_encoders_serve_h264_once_probed() {
3474 let served = hardware_encoders(0);
3475 assert!(
3476 served.iter().any(|(codec, ..)| *codec == Codec::H264),
3477 "node 0 serves {served:?}"
3478 );
3479 assert!(
3480 served
3481 .iter()
3482 .all(|(_, backend, ..)| matches!(*backend, "nvenc" | "vaapi"))
3483 );
3484 assert_eq!(hardware_encoders(0), served);
3485 }
3486}