1use super::Encoded;
9use super::backend::{self, Backend};
10use crate::{Color, Error, Frame, Size};
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Codec {
22 #[default]
25 H264,
26 H265,
28}
29
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum Kind {
35 #[default]
38 Auto,
39 Hardware,
41 Software,
43 Named(String),
46}
47
48#[derive(Clone, Debug)]
54#[non_exhaustive]
55pub struct Config {
56 pub width: u32,
57 pub height: u32,
58 pub framerate: u32,
59 pub bitrate: Option<u64>,
62 pub gop: u32,
65 pub codec: Codec,
67 pub kind: Kind,
68 pub color: Option<Color>,
76}
77
78impl Config {
79 pub fn new(width: u32, height: u32, framerate: u32) -> Self {
82 Self {
83 width,
84 height,
85 framerate,
86 bitrate: None,
87 gop: framerate.saturating_mul(2).max(1),
89 codec: Codec::default(),
90 kind: Kind::Auto,
91 color: None,
92 }
93 }
94
95 pub fn size(&self) -> Size {
97 Size::new(self.width, self.height)
98 }
99
100 pub async fn probe(&self) -> Result<hang::catalog::VideoConfig, Error> {
121 let mut sink = super::Sink::open(self).await?;
124
125 let size = self.size();
127 let i420 = crate::I420::new(
128 size.width,
129 size.height,
130 vec![0x80u8; crate::I420::len(size.width, size.height)],
131 )?;
132 let frame = Frame::new(crate::Surface::I420(i420), moq_net::Timestamp::from_micros(0)?);
133
134 sink.keyframe();
135 let mut encoded = sink.encode(frame).await?;
136 if encoded.is_empty() {
138 encoded = sink.flush().await?;
139 }
140
141 let annexb: Vec<u8> = encoded.iter().flat_map(|frame| frame.payload.iter().copied()).collect();
142 let parsed = match self.codec {
143 Codec::H264 => moq_mux::codec::h264::config(&annexb),
144 Codec::H265 => moq_mux::codec::h265::config(&annexb),
145 };
146 let mut rendition = parsed.map_err(|err| {
147 Error::Codec(anyhow::anyhow!(
148 "{} emitted no usable parameter sets: {err}",
149 sink.name()
150 ))
151 })?;
152
153 rendition.bitrate.get_or_insert(self.resolved_bitrate());
156 rendition.framerate.get_or_insert(self.framerate.into());
157 Ok(rendition)
158 }
159
160 pub(crate) fn resolved_color(&self) -> Color {
165 self.color.unwrap_or_else(|| Color::infer(self.size()))
166 }
167
168 pub(crate) fn resolved_bitrate(&self) -> u64 {
170 self.bitrate
171 .unwrap_or_else(|| default_bitrate(self.size(), self.framerate))
172 }
173}
174
175pub(crate) fn default_bitrate(size: Size, framerate: u32) -> u64 {
178 ((size.pixels() * framerate as u64) as f64 * 0.07) as u64
179}
180
181pub struct Encoder {
185 backend: Box<dyn Backend>,
186 codec: Codec,
187 size: Size,
188 bitrate: u64,
189 color: Color,
192 pending_keyframe: bool,
196}
197
198impl Encoder {
199 pub fn new(config: &Config) -> Result<Self, Error> {
201 if config.framerate == 0 {
205 return Err(Error::InvalidFramerate(0));
206 }
207 let size = config.size();
209 size.validate("encoder")?;
210 size.validate_encodable("encoder", config.framerate)?;
211
212 let backend = backend::open(config)?;
213 Ok(Self {
214 backend,
215 codec: config.codec,
216 size,
217 bitrate: config.resolved_bitrate(),
218 color: config.resolved_color(),
219 pending_keyframe: false,
220 })
221 }
222
223 pub fn name(&self) -> &str {
225 self.backend.name()
226 }
227
228 pub fn size(&self) -> Size {
230 self.size
231 }
232
233 pub fn bitrate(&self) -> u64 {
237 self.bitrate
238 }
239
240 pub fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
255 if bitrate == self.bitrate {
256 return Ok(());
257 }
258 self.backend.set_bitrate(bitrate)?;
259 self.bitrate = bitrate;
262 Ok(())
263 }
264
265 pub fn codec(&self) -> Codec {
268 self.codec
269 }
270
271 pub fn keyframe(&mut self) {
283 self.pending_keyframe = true;
284 }
285
286 pub fn encode(&mut self, frame: &Frame) -> Result<Vec<Encoded>, Error> {
301 let size = frame.size();
304 if size != self.size {
305 return Err(Error::Codec(anyhow::anyhow!(
306 "frame {size} does not match encoder {}",
307 self.size
308 )));
309 }
310 if let Some(color) = frame.surface.color()
320 && color != self.color
321 {
322 static WARN_ONCE: std::sync::Once = std::sync::Once::new();
323 WARN_ONCE.call_once(|| {
324 tracing::warn!(
325 frame = ?color,
326 encoder = ?self.color,
327 "frame color space differs from the one written into the bitstream; set encode::Config::color"
328 );
329 });
330 }
331 let encoded = self.backend.encode(frame, self.pending_keyframe)?;
332 self.pending_keyframe = false;
336 Ok(encoded)
337 }
338
339 pub fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
354 self.backend.flush()
355 }
356
357 pub fn finish(mut self) -> Result<Vec<Encoded>, Error> {
364 self.backend.finish()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 use crate::{I420, Surface};
373
374 fn gray_rgba(width: u32, height: u32) -> Vec<u8> {
376 vec![0x80u8; width as usize * height as usize * 4]
377 }
378
379 fn gray_frame(width: u32, height: u32, index: u64) -> Frame {
382 let surface = Surface::rgba(&gray_rgba(width, height), Size::new(width, height)).unwrap();
383 Frame::new(surface, at(index))
384 }
385
386 fn at(index: u64) -> moq_net::Timestamp {
388 moq_net::Timestamp::from_micros(index * 33_333).unwrap()
389 }
390
391 fn payloads(frames: &[Encoded]) -> Vec<bytes::Bytes> {
393 frames.iter().map(|f| f.payload.clone()).collect()
394 }
395
396 #[test]
397 fn software_encoder_emits_annexb() {
398 let config = Config {
399 kind: Kind::Software,
400 ..Config::new(320, 240, 30)
401 };
402 let mut encoder = Encoder::new(&config).expect("openh264 is vendored, always available");
403 assert_eq!(encoder.name(), "openh264");
404
405 let mut frames = Vec::new();
406 for i in 0..30 {
407 if i == 0 {
408 encoder.keyframe();
409 }
410 frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
411 }
412 frames.extend(encoder.finish().unwrap());
413
414 assert!(!frames.is_empty(), "encoder produced no packets");
415
416 let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
419 assert!(
420 micros.windows(2).all(|w| w[0] < w[1]),
421 "encoded timestamps not strictly increasing: {micros:?}"
422 );
423 assert!(
424 micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
425 "encoded timestamp outside the fed set: {micros:?}"
426 );
427
428 let packets = payloads(&frames);
431 let first = &packets[0];
432 let has_start_code = first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]);
433 assert!(
434 has_start_code,
435 "first packet is not Annex-B: {:02x?}",
436 &first[..first.len().min(8)]
437 );
438 }
439
440 #[test]
442 fn encode_rgba_surface_emits_annexb() {
443 let config = Config {
444 kind: Kind::Software,
445 ..Config::new(320, 240, 30)
446 };
447 let mut encoder = Encoder::new(&config).unwrap();
448
449 let mut frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
450 frames.extend(encoder.finish().unwrap());
451 assert!(!frames.is_empty());
452 let packets = payloads(&frames);
453 assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
454 }
455
456 #[test]
458 fn encode_i420_surface_emits_annexb() {
459 let config = Config {
460 kind: Kind::Software,
461 ..Config::new(320, 240, 30)
462 };
463 let mut encoder = Encoder::new(&config).unwrap();
464
465 let i420 = I420::new(320, 240, vec![0x80u8; I420::len(320, 240)]).unwrap();
467 let frame = Frame::new(Surface::I420(i420), at(0));
468 let mut frames = encoder.encode(&frame).unwrap();
469 frames.extend(encoder.finish().unwrap());
470 assert!(!frames.is_empty());
471 let packets = payloads(&frames);
472 assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
473 }
474
475 #[test]
478 fn encode_rejects_dimension_mismatch() {
479 let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
480 return;
481 };
482 assert!(matches!(encoder.encode(&gray_frame(640, 480, 0)), Err(Error::Codec(_))));
483 }
484
485 #[test]
489 fn encode_rejects_transposed_frame() {
490 let Ok(mut encoder) = Encoder::new(&Config::new(320, 240, 30)) else {
491 return;
492 };
493
494 let transposed = gray_frame(240, 320, 0);
495 assert_eq!(
496 gray_rgba(240, 320).len(),
497 gray_rgba(320, 240).len(),
498 "the byte counts must collide"
499 );
500 assert!(matches!(encoder.encode(&transposed), Err(Error::Codec(_))));
501 }
502
503 #[test]
504 fn new_rejects_zero_framerate() {
505 let config = Config::new(320, 240, 0);
508 assert!(matches!(Encoder::new(&config), Err(Error::InvalidFramerate(0))));
509 }
510
511 #[test]
512 fn unknown_named_encoder_errors() {
513 let config = Config {
514 kind: Kind::Named("definitely_not_a_codec".into()),
515 ..Config::new(320, 240, 30)
516 };
517 assert!(matches!(Encoder::new(&config), Err(Error::NoEncoder(_))));
518 }
519
520 #[cfg(target_os = "macos")]
524 #[test]
525 fn videotoolbox_emits_annexb_keyframe() {
526 let config = Config {
527 kind: Kind::Named("videotoolbox".into()),
528 ..Config::new(320, 240, 30)
529 };
530 let mut encoder = Encoder::new(&config).expect("videotoolbox is available on macOS");
531 assert_eq!(encoder.name(), "videotoolbox");
532
533 let mut frames = Vec::new();
534 for i in 0..10 {
535 if i == 0 {
536 encoder.keyframe();
537 }
538 frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
539 }
540 frames.extend(encoder.finish().unwrap());
541
542 assert!(!frames.is_empty(), "encoder produced no packets");
543 let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
546 assert!(
547 micros.windows(2).all(|w| w[0] < w[1]),
548 "encoded timestamps not strictly increasing: {micros:?}"
549 );
550
551 let packets = payloads(&frames);
552 let first = &packets[0];
553 assert!(
554 first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
555 "first packet is not Annex-B"
556 );
557
558 let types = nal_types(first);
561 assert!(types.contains(&7), "no SPS in first packet: {types:?}");
562 assert!(types.contains(&8), "no PPS in first packet: {types:?}");
563 assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
564 }
565
566 #[cfg(target_os = "macos")]
570 #[test]
571 fn videotoolbox_emits_annexb_keyframe_h265() {
572 let config = Config {
573 codec: Codec::H265,
574 kind: Kind::Named("videotoolbox".into()),
575 ..Config::new(320, 240, 30)
576 };
577 let mut encoder = Encoder::new(&config).expect("videotoolbox HEVC is available on macOS");
578 assert_eq!(encoder.name(), "videotoolbox");
579 assert_eq!(encoder.codec(), Codec::H265);
580
581 let mut frames = Vec::new();
582 for i in 0..10 {
583 if i == 0 {
584 encoder.keyframe();
585 }
586 frames.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
587 }
588 frames.extend(encoder.finish().unwrap());
589
590 assert!(!frames.is_empty(), "encoder produced no packets");
591 let packets = payloads(&frames);
592 let first = &packets[0];
593 assert!(
594 first.starts_with(&[0, 0, 0, 1]) || first.starts_with(&[0, 0, 1]),
595 "first packet is not Annex-B"
596 );
597
598 let types = hevc_nal_types(first);
601 assert!(types.contains(&32), "no VPS in first packet: {types:?}");
602 assert!(types.contains(&33), "no SPS in first packet: {types:?}");
603 assert!(types.contains(&34), "no PPS in first packet: {types:?}");
604 assert!(
605 types.iter().any(|t| (16..=23).contains(t)),
606 "first packet is not an IRAP: {types:?}"
607 );
608 }
609
610 #[cfg(target_os = "macos")]
612 fn hevc_nal_types(annexb: &[u8]) -> Vec<u8> {
613 let mut types = Vec::new();
614 let mut i = 0;
615 while i + 3 < annexb.len() {
616 if annexb[i..i + 3] == [0, 0, 1] {
617 types.push((annexb[i + 3] >> 1) & 0x3f);
618 i += 3;
619 } else {
620 i += 1;
621 }
622 }
623 types
624 }
625
626 #[cfg(target_os = "macos")]
629 #[test]
630 fn videotoolbox_encodes_surface_zero_copy() {
631 let config = Config {
632 kind: Kind::Named("videotoolbox".into()),
633 ..Config::new(320, 240, 30)
634 };
635 let mut encoder = Encoder::new(&config).unwrap();
636
637 let mut frames = Vec::new();
638 for i in 0..10 {
639 if i == 0 {
640 encoder.keyframe();
641 }
642 let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(i));
643 frames.extend(encoder.encode(&frame).unwrap());
644 }
645 frames.extend(encoder.finish().unwrap());
646
647 assert!(!frames.is_empty());
648 let packets = payloads(&frames);
649 let types = nal_types(&packets[0]);
650 assert!(
651 types.contains(&7) && types.contains(&8) && types.contains(&5),
652 "no IDR: {types:?}"
653 );
654 }
655
656 #[cfg(target_os = "macos")]
659 #[test]
660 fn openh264_downloads_surface() {
661 let config = Config {
662 kind: Kind::Software,
663 ..Config::new(320, 240, 30)
664 };
665 let mut encoder = Encoder::new(&config).unwrap();
666
667 encoder.keyframe();
668 let frame = Frame::new(Surface::PixelBuffer(nv12_surface(320, 240)), at(0));
669 let mut frames = encoder.encode(&frame).unwrap();
670 frames.extend(encoder.finish().unwrap());
671
672 assert!(!frames.is_empty());
673 let packets = payloads(&frames);
674 assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
675 }
676
677 #[cfg(target_os = "macos")]
680 fn nv12_surface(width: u32, height: u32) -> crate::frame::macos::PixelBuffer {
681 use std::ptr::{self, NonNull};
682
683 use objc2_core_foundation::CFRetained;
684 use objc2_core_video::{
685 CVPixelBuffer, CVPixelBufferCreate, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
686 CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
687 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
688 };
689
690 let mut raw: *mut CVPixelBuffer = ptr::null_mut();
691 let status = unsafe {
692 CVPixelBufferCreate(
693 None,
694 width as usize,
695 height as usize,
696 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
697 None,
698 NonNull::new(&mut raw).unwrap(),
699 )
700 };
701 assert_eq!(status, 0, "CVPixelBufferCreate failed");
702 let buffer = unsafe { CFRetained::from_raw(NonNull::new(raw).unwrap()) };
703
704 let flags = CVPixelBufferLockFlags(0);
705 assert_eq!(unsafe { CVPixelBufferLockBaseAddress(&buffer, flags) }, 0);
706 for (plane, rows) in [(0usize, height as usize), (1usize, height as usize / 2)] {
707 let base = CVPixelBufferGetBaseAddressOfPlane(&buffer, plane) as *mut u8;
708 let stride = CVPixelBufferGetBytesPerRowOfPlane(&buffer, plane);
709 unsafe { ptr::write_bytes(base, 128, stride * rows) };
710 }
711 unsafe { CVPixelBufferUnlockBaseAddress(&buffer, flags) };
712
713 crate::frame::macos::PixelBuffer::new(buffer, width, height)
714 }
715
716 fn nal_types(annexb: &[u8]) -> Vec<u8> {
719 let mut types = Vec::new();
720 let mut i = 0;
721 while i + 3 < annexb.len() {
722 if annexb[i..i + 3] == [0, 0, 1] {
723 types.push(annexb[i + 3] & 0x1f);
724 i += 3;
725 } else {
726 i += 1;
727 }
728 }
729 types
730 }
731
732 #[cfg(target_os = "windows")]
736 #[test]
737 #[ignore]
738 fn mediafoundation_cpu_rgba() {
739 let config = Config {
740 kind: Kind::Named("mediafoundation".into()),
741 ..Config::new(640, 480, 30)
742 };
743 let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
744 assert_eq!(encoder.name(), "mediafoundation");
745
746 let mut frames = Vec::new();
747 for i in 0..30 {
748 if i == 0 {
749 encoder.keyframe();
750 }
751 frames.extend(encoder.encode(&gray_frame(640, 480, i)).unwrap());
752 }
753 frames.extend(encoder.finish().unwrap());
754
755 assert!(!frames.is_empty(), "encoder produced no packets");
756 let micros: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
759 assert!(
760 micros.windows(2).all(|w| w[0] < w[1]),
761 "encoded timestamps not strictly increasing: {micros:?}"
762 );
763 assert!(
764 micros.iter().all(|&t| t % 33_333 == 0 && t < 30 * 33_333),
765 "encoded timestamp outside the fed set: {micros:?}"
766 );
767
768 let packets = payloads(&frames);
769 let types = nal_types(&packets[0]);
770 assert!(types.contains(&7), "no SPS in first packet: {types:?}");
771 assert!(types.contains(&8), "no PPS in first packet: {types:?}");
772 assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
773 }
774
775 #[cfg(all(target_os = "windows", feature = "capture"))]
779 #[tokio::test]
780 #[ignore]
781 async fn mediafoundation_camera_texture() {
782 let mut camera = crate::capture::open(&crate::capture::Config::default())
783 .await
784 .expect("open default camera");
785 let (w, h) = (camera.width(), camera.height());
786
787 let config = Config {
788 kind: Kind::Named("mediafoundation".into()),
789 ..Config::new(w, h, camera.framerate().unwrap_or(30))
790 };
791 let mut encoder = Encoder::new(&config).expect("hardware H.264 encoder available");
792
793 let mut frames = Vec::new();
794 let mut textures = 0;
795 for i in 0..30 {
796 let surface = camera.read().await.expect("read camera frame");
797 if matches!(surface, Some(Surface::Texture(_))) {
798 textures += 1;
799 }
800 if i == 0 {
801 encoder.keyframe();
802 }
803 let surface = surface.expect("frame, not end of stream");
804 frames.extend(encoder.encode(&Frame::new(surface, at(i))).unwrap());
805 }
806 frames.extend(encoder.finish().unwrap());
807
808 assert!(textures > 0, "capture never produced a GPU texture");
811 assert!(!frames.is_empty(), "encoder produced no packets");
812 let packets = payloads(&frames);
813 let types = nal_types(&packets[0]);
814 assert!(
815 types.contains(&7) && types.contains(&8) && types.contains(&5),
816 "no IDR: {types:?}"
817 );
818 }
819
820 #[test]
824 fn set_bitrate_retunes_software_encoder() {
825 let config = Config {
826 kind: Kind::Software,
827 ..Config::new(320, 240, 30)
828 };
829 let mut encoder = Encoder::new(&config).unwrap();
830
831 let opened = encoder.bitrate();
832 assert_eq!(opened, config.resolved_bitrate());
833
834 encoder.encode(&gray_frame(320, 240, 0)).unwrap();
836
837 let halved = opened / 2;
838 encoder.set_bitrate(halved).unwrap();
839 assert_eq!(encoder.bitrate(), halved);
840
841 let frames = encoder.encode(&gray_frame(320, 240, 1)).unwrap();
843 assert!(!frames.is_empty(), "encoder produced nothing after a retune");
844 let packets = payloads(&frames);
845 assert!(packets[0].starts_with(&[0, 0, 0, 1]) || packets[0].starts_with(&[0, 0, 1]));
846 }
847
848 #[test]
852 fn set_bitrate_before_the_first_frame_is_deferred() {
853 let config = Config {
854 kind: Kind::Software,
855 ..Config::new(320, 240, 30)
856 };
857 let mut encoder = Encoder::new(&config).unwrap();
858
859 let halved = encoder.bitrate() / 2;
860 encoder.set_bitrate(halved).expect("a retune before the first frame");
861 assert_eq!(encoder.bitrate(), halved);
862
863 let frames = encoder.encode(&gray_frame(320, 240, 0)).unwrap();
865 assert!(!frames.is_empty());
866
867 encoder.set_bitrate(halved / 2).unwrap();
869 assert!(encoder.encode(&gray_frame(320, 240, 1)).is_ok());
870 }
871
872 #[test]
875 fn set_bitrate_to_current_is_a_noop() {
876 let config = Config {
877 kind: Kind::Software,
878 ..Config::new(320, 240, 30)
879 };
880 let mut encoder = Encoder::new(&config).unwrap();
881
882 let opened = encoder.bitrate();
883 encoder.set_bitrate(opened).unwrap();
884 assert_eq!(encoder.bitrate(), opened);
885 }
886
887 #[test]
888 fn default_bitrate_scales_with_resolution() {
889 let small = Config::new(320, 240, 30).resolved_bitrate();
890 let large = Config::new(1920, 1080, 30).resolved_bitrate();
891 assert!(large > small);
892 assert!(small > 0);
893 }
894
895 struct Delayed {
900 pending: Option<Encoded>,
901 }
902
903 impl Backend for Delayed {
904 fn encode(&mut self, frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
905 let payload = bytes::Bytes::from(frame.timestamp.as_micros().to_string());
906 let previous = self.pending.replace(Encoded::new(payload, frame.timestamp));
907 Ok(previous.into_iter().collect())
908 }
909
910 fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
911 Ok(self.pending.take().into_iter().collect())
912 }
913
914 fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
915 self.flush()
916 }
917
918 fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
919 Ok(())
920 }
921
922 fn name(&self) -> &str {
923 "delayed"
924 }
925 }
926
927 fn encoder_with(backend: Box<dyn Backend>, config: &Config) -> Encoder {
930 Encoder {
931 backend,
932 codec: config.codec,
933 size: config.size(),
934 bitrate: config.resolved_bitrate(),
935 color: config.resolved_color(),
936 pending_keyframe: false,
937 }
938 }
939
940 struct Recorder(std::sync::Arc<std::sync::Mutex<Vec<bool>>>);
943
944 impl Backend for Recorder {
945 fn encode(&mut self, _frame: &Frame, keyframe: bool) -> Result<Vec<Encoded>, Error> {
946 self.0.lock().unwrap().push(keyframe);
947 Ok(Vec::new())
948 }
949
950 fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
951 Ok(Vec::new())
952 }
953
954 fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
955 Ok(Vec::new())
956 }
957
958 fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
959 Ok(())
960 }
961
962 fn name(&self) -> &str {
963 "recorder"
964 }
965 }
966
967 #[test]
972 fn a_keyframe_request_waits_for_the_next_frame_then_clears() {
973 let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
974 let config = Config::new(320, 240, 30);
975 let mut encoder = encoder_with(Box::new(Recorder(log.clone())), &config);
976
977 encoder.encode(&gray_frame(320, 240, 0)).unwrap();
979
980 encoder.keyframe();
982 encoder.keyframe();
983 encoder.encode(&gray_frame(320, 240, 1)).unwrap();
984
985 encoder.encode(&gray_frame(320, 240, 2)).unwrap();
987
988 assert_eq!(*log.lock().unwrap(), vec![false, true, false]);
989 }
990
991 #[test]
996 fn a_mid_stream_keyframe_request_emits_an_idr() {
997 let config = Config {
998 kind: Kind::Software,
999 ..Config::new(320, 240, 30)
1000 };
1001 let mut encoder = Encoder::new(&Config { gop: 1000, ..config }).unwrap();
1004
1005 let mut per_frame = Vec::new();
1006 for i in 0..6 {
1007 if i == 3 {
1009 encoder.keyframe();
1010 }
1011 let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
1012 let joined: Vec<u8> = encoded.iter().flat_map(|f| f.payload.iter()).copied().collect();
1013 per_frame.push(nal_types(&joined));
1014 }
1015
1016 let asked = &per_frame[3];
1019 assert!(asked.contains(&5), "the requested frame is not an IDR: {asked:?}");
1020 assert!(asked.contains(&7), "no SPS with the requested IDR: {asked:?}");
1021 assert!(asked.contains(&8), "no PPS with the requested IDR: {asked:?}");
1022
1023 for i in [1, 2, 4, 5] {
1027 assert!(
1028 !per_frame[i].contains(&5),
1029 "frame {i} was keyed without being asked: {:?}",
1030 per_frame[i]
1031 );
1032 }
1033 }
1034
1035 struct Failing;
1037
1038 impl Backend for Failing {
1039 fn encode(&mut self, _frame: &Frame, _keyframe: bool) -> Result<Vec<Encoded>, Error> {
1040 Err(Error::Codec(anyhow::anyhow!("no")))
1041 }
1042
1043 fn flush(&mut self) -> Result<Vec<Encoded>, Error> {
1044 Ok(Vec::new())
1045 }
1046
1047 fn finish(&mut self) -> Result<Vec<Encoded>, Error> {
1048 Ok(Vec::new())
1049 }
1050
1051 fn set_bitrate(&mut self, _bitrate: u64) -> Result<(), Error> {
1052 Ok(())
1053 }
1054
1055 fn name(&self) -> &str {
1056 "failing"
1057 }
1058 }
1059
1060 #[test]
1065 fn a_failed_encode_keeps_the_keyframe_request() {
1066 let config = Config::new(320, 240, 30);
1067
1068 let mut encoder = encoder_with(Box::new(Failing), &config);
1069 encoder.keyframe();
1070 assert!(encoder.encode(&gray_frame(320, 240, 0)).is_err());
1071 assert!(encoder.pending_keyframe, "the backend error swallowed the request");
1072
1073 let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
1075 encoder.keyframe();
1076 assert!(encoder.encode(&gray_frame(640, 480, 0)).is_err());
1077 assert!(encoder.pending_keyframe, "the size check swallowed the request");
1078
1079 encoder.encode(&gray_frame(320, 240, 1)).unwrap();
1081 assert!(!encoder.pending_keyframe);
1082 }
1083
1084 #[test]
1090 fn a_buffering_backend_keeps_each_frames_timestamp() {
1091 let config = Config::new(320, 240, 30);
1092 let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
1093
1094 for i in 0..5 {
1098 let encoded = encoder.encode(&gray_frame(320, 240, i)).unwrap();
1099 if i == 0 {
1100 assert!(encoded.is_empty(), "the first frame is still buffered");
1101 continue;
1102 }
1103 assert_eq!(encoded.len(), 1);
1104 assert_eq!(encoded[0].timestamp, at(i - 1));
1105 assert_eq!(&encoded[0].payload[..], at(i - 1).as_micros().to_string().as_bytes());
1108 }
1109
1110 let tail = encoder.finish().unwrap();
1113 assert_eq!(tail.len(), 1);
1114 assert_eq!(tail[0].timestamp, at(4));
1115 assert!(
1116 encoder_with(Box::new(Delayed { pending: None }), &config)
1117 .finish()
1118 .unwrap()
1119 .is_empty()
1120 );
1121 }
1122
1123 #[test]
1131 fn a_flush_empties_a_pipelined_backend_and_leaves_it_running() {
1132 let config = Config::new(320, 240, 30);
1133 let mut encoder = encoder_with(Box::new(Delayed { pending: None }), &config);
1134
1135 let mut group = Vec::new();
1136 for i in 0..3 {
1137 group.extend(encoder.encode(&gray_frame(320, 240, i)).unwrap());
1138 }
1139 group.extend(encoder.flush().unwrap());
1140
1141 let times: Vec<_> = group.iter().map(|packet| packet.timestamp).collect();
1143 assert_eq!(times, vec![at(0), at(1), at(2)], "the group lost or reordered frames");
1144
1145 let mut next = encoder.encode(&gray_frame(320, 240, 3)).unwrap();
1148 assert!(next.is_empty(), "frame 3 is buffered, so nothing comes back yet");
1149 next.extend(encoder.finish().unwrap());
1150 let times: Vec<_> = next.iter().map(|packet| packet.timestamp).collect();
1151 assert_eq!(times, vec![at(3)], "the flush left something behind");
1152 }
1153
1154 #[cfg(target_os = "macos")]
1159 #[test]
1160 fn videotoolbox_sps_declares_the_color_space() {
1161 use super::backend::test_util::{BT601_DESCRIBED, BT709_DESCRIBED, declared_color};
1162
1163 for (size, described) in [
1164 (Size::new(640, 480), BT601_DESCRIBED),
1165 (Size::new(1920, 1080), BT709_DESCRIBED),
1166 ] {
1167 let config = Config {
1168 kind: Kind::Named("videotoolbox".into()),
1169 ..Config::new(size.width, size.height, 30)
1170 };
1171 let mut encoder = Encoder::new(&config).expect("videotoolbox is available on macOS");
1172
1173 let rgba = [255u8, 0, 0, 255].repeat(size.pixels() as usize);
1174 let surface = crate::frame::Surface::rgba(&rgba, size).unwrap();
1175 encoder.keyframe();
1176 let frames = encoder
1177 .encode(&Frame::new(surface, moq_net::Timestamp::from_micros(0).unwrap()))
1178 .unwrap();
1179
1180 let keyframe = frames.first().expect("a keyframe");
1181 assert_eq!(declared_color(&keyframe.payload), Some(described), "{size} SPS");
1182 }
1183 }
1184
1185 #[test]
1190 fn config_color_pins_the_space_a_resize_carried() {
1191 use crate::Color;
1192
1193 let big = Size::new(1280, 720);
1194 let small = Size::new(640, 480);
1195
1196 let rgba = vec![0x80u8; big.pixels() as usize * 4];
1198 let frame = Frame::new(
1199 crate::frame::Surface::rgba(&rgba, big).unwrap(),
1200 moq_net::Timestamp::from_micros(0).unwrap(),
1201 );
1202 let scaled = frame.resize(small).unwrap();
1203 assert_eq!(
1204 scaled.surface.color(),
1205 Some(Color::Bt709Limited),
1206 "resize keeps the space"
1207 );
1208
1209 let config = Config {
1213 kind: Kind::Software,
1214 ..Config::new(small.width, small.height, 30)
1215 };
1216 let mut encoder = Encoder::new(&config).unwrap();
1217 encoder.keyframe();
1218 let frames = encoder.encode(&scaled).expect("a mismatch warns rather than fails");
1219 use super::backend::test_util::{BT601_DESCRIBED, BT709_DESCRIBED, declared_color};
1220 assert_eq!(
1221 declared_color(&frames.first().expect("a keyframe").payload),
1222 Some(BT601_DESCRIBED),
1223 "the inferred label is the wrong one, which is the case Config::color covers"
1224 );
1225
1226 let config = Config {
1228 kind: Kind::Software,
1229 color: Some(Color::Bt709Limited),
1230 ..Config::new(small.width, small.height, 30)
1231 };
1232 let mut encoder = Encoder::new(&config).unwrap();
1233 encoder.keyframe();
1234 let frames = encoder.encode(&scaled).expect("a declared space encodes");
1235
1236 let keyframe = frames.first().expect("a keyframe");
1237 assert_eq!(declared_color(&keyframe.payload), Some(BT709_DESCRIBED));
1238 }
1239}