1use std::{
2 collections::{HashMap, HashSet},
3 sync::{
4 Arc, Mutex, Weak,
5 atomic::{AtomicU64, Ordering},
6 },
7 thread,
8 time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_info};
12use crate::rate::FrameRate;
13use arc_swap::ArcSwapOption;
14use ffmpeg_next::{self as ffmpeg, ffi};
15use thiserror::Error as ThisError;
16
17use super::video_layer::{
18 self, LayerGeometry, MAX_DIMENSION, VideoFit, VideoInputId, VideoLayer, VideoLayerError,
19 VideoRect, VideoSourceRect,
20};
21use crate::{
22 buffer::{MediaBuffer, picture_id, picture_is_referenced, release_picture},
23 bus::{Bus, BusEvent},
24 color::Color,
25 contract::{InputContract, MediaKind, MemoryDomain, OutputContract, PortContract},
26 control::{ControlMsg, ControlReceiver, drain_control},
27 element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
28 error::Result,
29 pad::SrcPad,
30 pool::{UnboundObjectPool, UnboundObjectPoolRef},
31 schedule::PeriodicSchedule,
32};
33
34const OUTPUT_POOL_SIZE: usize = 4;
35const CONTROL_POLL_INTERVAL: Duration = Duration::from_millis(5);
36
37#[derive(Debug, Clone, Copy)]
40pub struct VideoCompositorOptions {
41 pub width: u32,
43 pub height: u32,
45 pub frame_rate: ffmpeg::Rational,
47 pub background: Color,
49}
50
51impl Default for VideoCompositorOptions {
52 fn default() -> Self {
53 Self {
54 width: 1920,
55 height: 1080,
56 frame_rate: ffmpeg::Rational::new(30, 1),
57 background: Color::BLACK,
58 }
59 }
60}
61
62#[derive(Debug, ThisError)]
64pub enum SwVideoCompositorError {
65 #[error("ffmpeg error: {0}")]
67 Ffmpeg(#[from] ffmpeg::Error),
68
69 #[error("failed to reference the previous composite (code {0})")]
72 FrameRef(i32),
73
74 #[error(
76 "invalid output dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
77 )]
78 InvalidOutputDimensions {
79 width: u32,
81 height: u32,
83 },
84
85 #[error("invalid frame rate {0}; numerator and denominator must both be positive")]
87 InvalidFrameRate(ffmpeg::Rational),
88
89 #[error(
91 "invalid layer dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
92 )]
93 InvalidLayerDimensions {
94 width: u32,
96 height: u32,
98 },
99
100 #[error("layer opacity must be finite and between 0.0 and 1.0, got {0}")]
102 InvalidOpacity(f32),
103
104 #[error("layer source region has invalid dimensions {width}x{height}")]
107 InvalidSourceRegion {
108 width: u32,
110 height: u32,
112 },
113
114 #[error("input frame has invalid dimensions {width}x{height}")]
116 InvalidInputDimensions {
117 width: u32,
119 height: u32,
121 },
122
123 #[error("scaled layer would exceed {MAX_DIMENSION}px: {width}x{height}")]
125 ScaledLayerTooLarge {
126 width: u32,
128 height: u32,
130 },
131
132 #[error("the compositor input has been removed")]
134 SourceRemoved,
135
136 #[error(
138 "SwVideoCompositorInputSink only accepts decoded Video frames, got a {0}; link it after a decoder or video source"
139 )]
140 UnsupportedBuffer(&'static str),
141
142 #[error("SwVideoCompositor doesn't support seeking a live composition")]
144 SeekUnsupported,
145}
146
147struct VideoInput {
148 id: VideoInputId,
149 latest_frame: ArcSwapOption<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
153 layer: Mutex<VideoLayer>,
156}
157
158struct CompositorShared {
159 inputs: Mutex<HashMap<Arc<str>, Arc<VideoInput>>>,
160 next_input_id: AtomicU64,
161 frame_rate: Arc<FrameRate>,
164}
165
166#[derive(Clone)]
170pub struct SwVideoCompositorHandle {
171 shared: Weak<CompositorShared>,
172}
173
174pub struct SwVideoCompositorInput {
178 pub sink: Box<dyn Sink>,
180 pub layer: SwVideoLayerHandle,
182}
183
184impl SwVideoCompositorHandle {
185 pub fn add_source(
189 &self,
190 name: impl Into<String>,
191 layer: VideoLayer,
192 ) -> std::result::Result<Option<SwVideoCompositorInput>, SwVideoCompositorError> {
193 validate_layer(layer)?;
194 let Some(shared) = self.shared.upgrade() else {
195 return Ok(None);
196 };
197 let name: Arc<str> = name.into().into();
198 let id = VideoInputId(shared.next_input_id.fetch_add(1, Ordering::Relaxed));
199 let input = Arc::new(VideoInput {
200 id,
201 latest_frame: ArcSwapOption::empty(),
202 layer: Mutex::new(layer),
203 });
204 shared
205 .inputs
206 .lock()
207 .unwrap()
208 .insert(name.clone(), input.clone());
209
210 Ok(Some(SwVideoCompositorInput {
211 sink: Box::new(SwVideoCompositorInputSink {
212 name: name.clone(),
213 pp_log: element_pp_log(ElementType::SwVideoCompositor, &name, None),
214 shared: self.shared.clone(),
215 input: Arc::downgrade(&input),
216 }),
217 layer: SwVideoLayerHandle {
218 id,
219 name,
220 input: Arc::downgrade(&input),
221 },
222 }))
223 }
224
225 pub fn remove_source(&self, name: &str) {
228 if let Some(shared) = self.shared.upgrade() {
229 shared.inputs.lock().unwrap().remove(name);
230 }
231 }
232
233 pub fn set_frame_rate(&self, frame_rate: ffmpeg::Rational) -> bool {
246 self.shared
247 .upgrade()
248 .is_some_and(|shared| shared.frame_rate.set(frame_rate))
249 }
250
251 pub fn frame_rate(&self) -> Option<ffmpeg::Rational> {
256 Some(self.shared.upgrade()?.frame_rate.get())
257 }
258
259 pub fn source_count(&self) -> usize {
260 self.shared
261 .upgrade()
262 .map(|shared| shared.inputs.lock().unwrap().len())
263 .unwrap_or(0)
264 }
265}
266
267#[derive(Clone)]
270pub struct SwVideoLayerHandle {
271 id: VideoInputId,
272 name: Arc<str>,
273 input: Weak<VideoInput>,
274}
275
276impl SwVideoLayerHandle {
277 pub fn id(&self) -> VideoInputId {
279 self.id
280 }
281
282 pub fn name(&self) -> Arc<str> {
284 self.name.clone()
285 }
286
287 pub fn layer(&self) -> Option<VideoLayer> {
289 self.input
290 .upgrade()
291 .map(|input| *input.layer.lock().unwrap())
292 }
293
294 pub fn set_layer(&self, layer: VideoLayer) -> std::result::Result<(), SwVideoCompositorError> {
298 validate_layer(layer)?;
299 self.update(|current| *current = layer)
300 }
301
302 pub fn set_rect(&self, rect: VideoRect) -> std::result::Result<(), SwVideoCompositorError> {
304 validate_rect(rect)?;
305 self.update(|layer| layer.rect = rect)
306 }
307
308 pub fn set_opacity(&self, opacity: f32) -> std::result::Result<(), SwVideoCompositorError> {
310 validate_opacity(opacity)?;
311 self.update(|layer| layer.opacity = opacity)
312 }
313
314 pub fn set_z_index(&self, z_index: i32) -> std::result::Result<(), SwVideoCompositorError> {
316 self.update(|layer| layer.z_index = z_index)
317 }
318
319 pub fn set_visible(&self, visible: bool) -> std::result::Result<(), SwVideoCompositorError> {
321 self.update(|layer| layer.visible = visible)
322 }
323
324 pub fn set_fit(&self, fit: VideoFit) -> std::result::Result<(), SwVideoCompositorError> {
326 self.update(|layer| layer.fit = fit)
327 }
328
329 pub fn set_source(
335 &self,
336 source: Option<VideoSourceRect>,
337 ) -> std::result::Result<(), SwVideoCompositorError> {
338 video_layer::validate_source(source).map_err(map_layer_error)?;
339 self.update(|layer| layer.source = source)
340 }
341
342 fn update(
343 &self,
344 update: impl FnOnce(&mut VideoLayer),
345 ) -> std::result::Result<(), SwVideoCompositorError> {
346 let input = self
347 .input
348 .upgrade()
349 .ok_or(SwVideoCompositorError::SourceRemoved)?;
350 update(&mut input.layer.lock().unwrap());
351 Ok(())
352 }
353}
354
355pub struct SwVideoCompositorInputSink {
360 pp_log: PpLog,
361 name: Arc<str>,
362 shared: Weak<CompositorShared>,
363 input: Weak<VideoInput>,
364}
365
366impl SwVideoCompositorInputSink {
367 fn detach(&self) {
368 let (Some(shared), Some(input)) = (self.shared.upgrade(), self.input.upgrade()) else {
369 return;
370 };
371 let mut inputs = shared.inputs.lock().unwrap();
372 let is_current = inputs
373 .get(&self.name)
374 .is_some_and(|current| Arc::ptr_eq(current, &input));
375 if is_current {
376 inputs.remove(&self.name);
377 }
378 }
379}
380
381impl Element for SwVideoCompositorInputSink {
382 fn name(&self) -> Arc<str> {
383 self.name.clone()
384 }
385
386 fn element_type(&self) -> ElementType {
387 ElementType::SwVideoCompositor
388 }
389
390 fn pp_log(&self) -> &PpLog {
391 &self.pp_log
392 }
393
394 fn pp_log_mut(&mut self) -> &mut PpLog {
395 &mut self.pp_log
396 }
397}
398
399impl Sink for SwVideoCompositorInputSink {
400 fn input_contract(&self) -> InputContract {
403 InputContract::Fixed(PortContract::frame(
404 MediaKind::VideoFrame,
405 MemoryDomain::System,
406 ))
407 }
408
409 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
410 let Some(input) = self.input.upgrade() else {
411 return Ok(());
412 };
413 match buf {
414 MediaBuffer::Video(frame) => {
415 input.latest_frame.store(Some(frame));
416 Ok(())
417 }
418 MediaBuffer::Eos => {
419 self.detach();
420 Ok(())
421 }
422 MediaBuffer::Packet(_) => {
423 Err(SwVideoCompositorError::UnsupportedBuffer("Packet").into())
424 }
425 MediaBuffer::Audio(_) => Err(SwVideoCompositorError::UnsupportedBuffer("Audio").into()),
426 }
427 }
428
429 fn control(&mut self, msg: ControlMsg) -> Result<()> {
430 match msg {
431 ControlMsg::Stop => self.detach(),
432 ControlMsg::Flush => {
433 if let Some(input) = self.input.upgrade() {
434 input.latest_frame.store(None);
435 }
436 }
437 ControlMsg::Pause
438 | ControlMsg::Resume
439 | ControlMsg::Seek(_)
440 | ControlMsg::CheckSeek(_)
441 | ControlMsg::Preroll(_) => {}
442 }
443 Ok(())
444 }
445}
446
447#[derive(Clone)]
448struct InputSnapshot {
449 id: VideoInputId,
450 layer: VideoLayer,
451 frame: Option<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>>,
452}
453
454impl InputSnapshot {
455 fn same_as(&self, other: &Self) -> bool {
463 self.id == other.id
464 && self.layer == other.layer
465 && match (&self.frame, &other.frame) {
466 (Some(drawn), Some(now)) => picture_id(drawn) == picture_id(now),
467 (None, None) => true,
468 _ => false,
469 }
470 }
471}
472
473struct Composed {
491 inputs: Vec<InputSnapshot>,
492 frame: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
493}
494
495impl Composed {
496 fn matches(&self, inputs: &[InputSnapshot]) -> bool {
497 self.inputs.len() == inputs.len()
498 && std::iter::zip(&self.inputs, inputs).all(|(drawn, now)| drawn.same_as(now))
499 }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503struct ScaleDefinition {
504 source_format: ffmpeg::format::Pixel,
505 source_width: u32,
506 source_height: u32,
507 target_width: u32,
508 target_height: u32,
509}
510
511struct InputScaler {
512 definition: Option<ScaleDefinition>,
513 context: Option<ffmpeg::software::scaling::Context>,
514 output: ffmpeg::frame::Video,
515}
516
517impl InputScaler {
518 fn new() -> Self {
519 Self {
520 definition: None,
521 context: None,
522 output: ffmpeg::frame::Video::empty(),
523 }
524 }
525
526 fn scale(
527 &mut self,
528 frame: &ffmpeg::frame::Video,
529 region: VideoSourceRect,
530 width: u32,
531 height: u32,
532 ) -> std::result::Result<&ffmpeg::frame::Video, ffmpeg::Error> {
533 let source = cropped(frame, region)?;
534 let source = source.as_ref().unwrap_or(frame);
535 let definition = ScaleDefinition {
536 source_format: source.format(),
537 source_width: source.width(),
538 source_height: source.height(),
539 target_width: width,
540 target_height: height,
541 };
542 if self.definition != Some(definition) {
543 self.context = Some(ffmpeg::software::scaling::Context::get(
544 definition.source_format,
545 definition.source_width,
546 definition.source_height,
547 ffmpeg::format::Pixel::BGRA,
548 width,
549 height,
550 ffmpeg::software::scaling::Flags::BILINEAR,
551 )?);
552 self.output = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height);
553 self.definition = Some(definition);
554 }
555 self.context
556 .as_mut()
557 .expect("created with a changed definition or retained from a matching one")
558 .run(source, &mut self.output)?;
559 Ok(&self.output)
560 }
561}
562
563pub struct SwVideoCompositor {
573 pp_log: PpLog,
574 name: Arc<str>,
575 shared: Arc<CompositorShared>,
576 options: VideoCompositorOptions,
577 frame_index: i64,
578 scalers: HashMap<VideoInputId, InputScaler>,
579 output_pool: UnboundObjectPool<ffmpeg::frame::Video>,
580 repeat_pool: UnboundObjectPool<ffmpeg::frame::Video>,
584 composed: Option<Composed>,
586 retired: Vec<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>>,
597 pad: SrcPad,
598}
599
600unsafe impl Send for SwVideoCompositor {}
604
605impl SwVideoCompositor {
606 pub fn new(
608 name: impl Into<String>,
609 options: VideoCompositorOptions,
610 ) -> std::result::Result<(Self, SwVideoCompositorHandle), SwVideoCompositorError> {
611 validate_output_options(options)?;
612 let name: Arc<str> = name.into().into();
613 let pp_log = element_pp_log(ElementType::SwVideoCompositor, &name, None);
614 let shared = Arc::new(CompositorShared {
615 inputs: Mutex::new(HashMap::new()),
616 next_input_id: AtomicU64::new(1),
617 frame_rate: FrameRate::new(options.frame_rate),
618 });
619 let (width, height) = (options.width, options.height);
620 let output_pool = UnboundObjectPool::new(
621 OUTPUT_POOL_SIZE,
622 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
623 |_| {},
624 );
625 pp_info!(
626 pp_log: &pp_log,
627 "created: {}x{}, frame_rate={}, format=BGRA",
628 width,
629 height,
630 options.frame_rate
631 );
632 Ok((
633 Self {
634 name: name.clone(),
635 pp_log,
636 shared: shared.clone(),
637 options,
638 frame_index: 0,
639 scalers: HashMap::new(),
640 output_pool,
641 repeat_pool: UnboundObjectPool::new(
642 0,
643 ffmpeg::frame::Video::empty,
644 release_picture,
645 ),
646 composed: None,
647 retired: Vec::new(),
648 pad: SrcPad::with_contract(
649 format!("{name}_src"),
650 OutputContract::Fixed(PortContract::frame(
651 MediaKind::VideoFrame,
652 MemoryDomain::System,
653 )),
654 ),
655 },
656 SwVideoCompositorHandle {
657 shared: Arc::downgrade(&shared),
658 },
659 ))
660 }
661
662 pub fn format(&self) -> ffmpeg::format::Pixel {
664 ffmpeg::format::Pixel::BGRA
665 }
666
667 pub fn width(&self) -> u32 {
669 self.options.width
670 }
671
672 pub fn height(&self) -> u32 {
674 self.options.height
675 }
676
677 pub fn frame_rate(&self) -> ffmpeg::Rational {
680 self.shared.frame_rate.get()
681 }
682
683 pub fn time_base(&self) -> ffmpeg::Rational {
686 self.frame_rate().invert()
687 }
688
689 fn snapshots(&self) -> Vec<InputSnapshot> {
690 let inputs: Vec<_> = self
691 .shared
692 .inputs
693 .lock()
694 .unwrap()
695 .values()
696 .cloned()
697 .collect();
698 inputs
699 .into_iter()
700 .map(|input| InputSnapshot {
701 id: input.id,
702 layer: *input.layer.lock().unwrap(),
703 frame: input.latest_frame.load_full(),
704 })
705 .collect()
706 }
707
708 fn compose_frame(
714 &mut self,
715 ) -> std::result::Result<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>, SwVideoCompositorError>
716 {
717 let mut snapshots = self.snapshots();
718 let active: HashSet<_> = snapshots.iter().map(|snapshot| snapshot.id).collect();
719 self.scalers.retain(|id, _| active.contains(id));
720 snapshots.sort_by(|left, right| {
721 left.layer
722 .z_index
723 .cmp(&right.layer.z_index)
724 .then_with(|| left.id.cmp(&right.id))
725 });
726
727 if self
730 .composed
731 .as_ref()
732 .is_some_and(|composed| composed.matches(&snapshots))
733 {
734 return self.repeat_frame();
735 }
736 let kept = snapshots.clone();
737
738 if let Some(previous) = self.composed.take() {
743 self.retired.push(previous.frame);
744 }
745 self.retired
746 .retain(|picture| picture_is_referenced(picture));
747
748 let mut output = self.output_pool.get();
749 fill_background(&mut output, self.options.background);
750 for snapshot in snapshots {
751 if !snapshot.layer.visible || snapshot.layer.opacity == 0.0 {
752 continue;
753 }
754 let Some(frame) = snapshot.frame else {
755 continue;
756 };
757 let Some(source) =
758 video_layer::source_region(snapshot.layer.source, frame.width(), frame.height())
759 else {
760 continue;
763 };
764 let geometry = layer_geometry(
767 source.width,
768 source.height,
769 snapshot.layer.rect,
770 snapshot.layer.fit,
771 )?;
772 let scaled = self
773 .scalers
774 .entry(snapshot.id)
775 .or_insert_with(InputScaler::new)
776 .scale(&frame, source, geometry.image_width, geometry.image_height)
777 .map_err(SwVideoCompositorError::from)?;
778 blend_bgra(&mut output, scaled, geometry, snapshot.layer.opacity);
779 }
780 output.set_pts(Some(self.frame_index));
781 self.frame_index += 1;
782 let output = Arc::new(output);
783 self.composed = Some(Composed {
786 inputs: kept,
787 frame: Arc::clone(&output),
788 });
789 Ok(output)
790 }
791
792 fn repeat_frame(
798 &mut self,
799 ) -> std::result::Result<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>, SwVideoCompositorError>
800 {
801 let mut output = self.repeat_pool.get();
802 let composed = self
803 .composed
804 .as_ref()
805 .expect("only reached with a previous composite");
806 unsafe {
811 let ptr = output.as_mut_ptr();
812 ffi::av_frame_unref(ptr);
813 let code = ffi::av_frame_ref(ptr, composed.frame.as_ptr());
814 if code < 0 {
815 return Err(SwVideoCompositorError::FrameRef(code));
816 }
817 }
818 output.set_pts(Some(self.frame_index));
819 self.frame_index += 1;
820 Ok(Arc::new(output))
821 }
822
823 fn push_frame(&mut self, bus: &Bus) -> std::result::Result<(), SwVideoCompositorError> {
824 let output = self.compose_frame()?;
825 if let Err(error) = self.pad.push(MediaBuffer::Video(output)) {
826 bus.post(
827 &self.pp_log,
828 BusEvent::Error {
829 element_type: ElementType::SwVideoCompositor,
830 name: self.name.clone(),
831 error,
832 },
833 );
834 }
835 Ok(())
836 }
837}
838
839impl Element for SwVideoCompositor {
840 fn name(&self) -> Arc<str> {
841 self.name.clone()
842 }
843
844 fn element_type(&self) -> ElementType {
845 ElementType::SwVideoCompositor
846 }
847
848 fn pp_log(&self) -> &PpLog {
849 &self.pp_log
850 }
851
852 fn pp_log_mut(&mut self) -> &mut PpLog {
853 &mut self.pp_log
854 }
855}
856
857impl Source for SwVideoCompositor {
858 fn src_pads(&mut self) -> &mut [SrcPad] {
859 std::slice::from_mut(&mut self.pad)
860 }
861}
862
863impl SourceElement for SwVideoCompositor {
864 fn is_live(&self) -> bool {
865 true
866 }
867
868 fn is_seekable(&self) -> bool {
869 false
870 }
871
872 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
873 pp_info!(self, "started");
874 let mut schedule = PeriodicSchedule::new(self.shared.frame_rate.interval(), Instant::now());
875 loop {
876 let outcome = drain_control(control, self, bus)?;
877 if outcome.stopped {
878 pp_info!(self, "stopped");
879 return Ok(());
880 }
881 if outcome.paused_for > Duration::ZERO {
882 schedule.resume_after_pause(outcome.paused_for, Instant::now());
883 }
884
885 let interval = self.shared.frame_rate.interval();
888 let now = Instant::now();
889 if schedule.interval() != interval {
890 pp_info!(self, "frame rate is now {}", self.frame_rate());
891 schedule.set_interval(interval, now);
892 }
893 if !schedule.is_due(now) {
894 thread::sleep(schedule.remaining(now).min(CONTROL_POLL_INTERVAL));
895 continue;
896 }
897
898 self.push_frame(bus)?;
899 schedule.advance_after_tick(Instant::now());
900 }
901 }
902
903 fn seek(&mut self, _target: Duration) -> Result<Duration> {
904 Err(SwVideoCompositorError::SeekUnsupported.into())
905 }
906}
907
908fn validate_output_options(
909 options: VideoCompositorOptions,
910) -> std::result::Result<(), SwVideoCompositorError> {
911 if options.width == 0
912 || options.height == 0
913 || options.width > MAX_DIMENSION
914 || options.height > MAX_DIMENSION
915 {
916 return Err(SwVideoCompositorError::InvalidOutputDimensions {
917 width: options.width,
918 height: options.height,
919 });
920 }
921 if options.frame_rate.numerator() <= 0 || options.frame_rate.denominator() <= 0 {
922 return Err(SwVideoCompositorError::InvalidFrameRate(options.frame_rate));
923 }
924 Ok(())
925}
926
927fn cropped(
941 frame: &ffmpeg::frame::Video,
942 region: VideoSourceRect,
943) -> std::result::Result<Option<ffmpeg::frame::Video>, ffmpeg::Error> {
944 if region.x == 0
945 && region.y == 0
946 && region.width == frame.width()
947 && region.height == frame.height()
948 {
949 return Ok(None);
950 }
951 let mut view = ffmpeg::frame::Video::empty();
952 let code = unsafe { ffi::av_frame_ref(view.as_mut_ptr(), frame.as_ptr()) };
955 if code < 0 {
956 return Err(ffmpeg::Error::from(code));
957 }
958 let code = unsafe {
961 let raw = view.as_mut_ptr();
962 (*raw).crop_left = usize::try_from(region.x).unwrap_or(0);
963 (*raw).crop_top = usize::try_from(region.y).unwrap_or(0);
964 (*raw).crop_right = usize::try_from(frame.width() - region.x - region.width).unwrap_or(0);
965 (*raw).crop_bottom =
966 usize::try_from(frame.height() - region.y - region.height).unwrap_or(0);
967 ffi::av_frame_apply_cropping(raw, ffi::AV_FRAME_CROP_UNALIGNED as i32)
968 };
969 if code < 0 {
970 return Err(ffmpeg::Error::from(code));
971 }
972 Ok(Some(view))
973}
974
975fn map_layer_error(error: VideoLayerError) -> SwVideoCompositorError {
981 match error {
982 VideoLayerError::InvalidDimensions { width, height } => {
983 SwVideoCompositorError::InvalidLayerDimensions { width, height }
984 }
985 VideoLayerError::InvalidOpacity(opacity) => SwVideoCompositorError::InvalidOpacity(opacity),
986 VideoLayerError::InvalidInputDimensions { width, height } => {
987 SwVideoCompositorError::InvalidInputDimensions { width, height }
988 }
989 VideoLayerError::ScaledLayerTooLarge { width, height } => {
990 SwVideoCompositorError::ScaledLayerTooLarge { width, height }
991 }
992 VideoLayerError::InvalidSourceRegion { width, height } => {
993 SwVideoCompositorError::InvalidSourceRegion { width, height }
994 }
995 }
996}
997
998fn validate_layer(layer: VideoLayer) -> std::result::Result<(), SwVideoCompositorError> {
999 video_layer::validate_layer(layer).map_err(map_layer_error)
1000}
1001
1002fn validate_rect(rect: VideoRect) -> std::result::Result<(), SwVideoCompositorError> {
1003 video_layer::validate_rect(rect).map_err(map_layer_error)
1004}
1005
1006fn validate_opacity(opacity: f32) -> std::result::Result<(), SwVideoCompositorError> {
1007 video_layer::validate_opacity(opacity).map_err(map_layer_error)
1008}
1009
1010fn layer_geometry(
1011 source_width: u32,
1012 source_height: u32,
1013 rect: VideoRect,
1014 fit: VideoFit,
1015) -> std::result::Result<LayerGeometry, SwVideoCompositorError> {
1016 video_layer::layer_geometry(source_width, source_height, rect, fit).map_err(map_layer_error)
1017}
1018
1019fn fill_background(frame: &mut ffmpeg::frame::Video, color: Color) {
1020 let width = frame.width() as usize;
1021 let height = frame.height() as usize;
1022 let stride = frame.stride(0);
1023 let data = frame.data_mut(0);
1024 for row in 0..height {
1025 for pixel in data[row * stride..row * stride + width * 4]
1026 .as_chunks_mut::<4>()
1027 .0
1028 {
1029 *pixel = [color.blue, color.green, color.red, 255];
1030 }
1031 }
1032}
1033
1034fn blend_bgra(
1035 destination: &mut ffmpeg::frame::Video,
1036 source: &ffmpeg::frame::Video,
1037 geometry: LayerGeometry,
1038 opacity: f32,
1039) {
1040 let output_width = i64::from(destination.width());
1041 let output_height = i64::from(destination.height());
1042 let clip_left = i64::from(geometry.clip.x).max(0);
1043 let clip_top = i64::from(geometry.clip.y).max(0);
1044 let clip_right =
1045 (i64::from(geometry.clip.x) + i64::from(geometry.clip.width)).min(output_width);
1046 let clip_bottom =
1047 (i64::from(geometry.clip.y) + i64::from(geometry.clip.height)).min(output_height);
1048 let left = geometry.image_x.max(clip_left);
1049 let top = geometry.image_y.max(clip_top);
1050 let right = (geometry.image_x + i64::from(geometry.image_width)).min(clip_right);
1051 let bottom = (geometry.image_y + i64::from(geometry.image_height)).min(clip_bottom);
1052 if left >= right || top >= bottom {
1053 return;
1054 }
1055
1056 let source_stride = source.stride(0);
1057 let destination_stride = destination.stride(0);
1058 let source_data = source.data(0);
1059 let destination_data = destination.data_mut(0);
1060 for output_y in top..bottom {
1061 let source_y = (output_y - geometry.image_y) as usize;
1062 let destination_y = output_y as usize;
1063 for output_x in left..right {
1064 let source_x = (output_x - geometry.image_x) as usize;
1065 let destination_x = output_x as usize;
1066 let source_offset = source_y * source_stride + source_x * 4;
1067 let destination_offset = destination_y * destination_stride + destination_x * 4;
1068 let source_pixel = &source_data[source_offset..source_offset + 4];
1069 let destination_pixel =
1070 &mut destination_data[destination_offset..destination_offset + 4];
1071 let alpha = (f32::from(source_pixel[3]) / 255.0) * opacity;
1072 let inverse = 1.0 - alpha;
1073 for channel in 0..3 {
1074 destination_pixel[channel] = (f32::from(source_pixel[channel]) * alpha
1075 + f32::from(destination_pixel[channel]) * inverse)
1076 .round()
1077 .clamp(0.0, 255.0) as u8;
1078 }
1079 destination_pixel[3] = 255;
1080 }
1081 }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use std::{
1087 sync::{
1088 Mutex as StdMutex,
1089 atomic::{AtomicBool, Ordering as AtomicOrdering},
1090 },
1091 thread,
1092 };
1093
1094 use super::*;
1095
1096 struct CapturingSink {
1097 pp_log: PpLog,
1098 received: Arc<StdMutex<Vec<MediaBuffer>>>,
1099 }
1100
1101 impl Element for CapturingSink {
1102 fn name(&self) -> Arc<str> {
1103 "capture".into()
1104 }
1105
1106 fn element_type(&self) -> ElementType {
1107 ElementType::Other
1108 }
1109
1110 fn pp_log(&self) -> &PpLog {
1111 &self.pp_log
1112 }
1113
1114 fn pp_log_mut(&mut self) -> &mut PpLog {
1115 &mut self.pp_log
1116 }
1117 }
1118
1119 impl Sink for CapturingSink {
1120 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1121 self.received.lock().unwrap().push(buf);
1122 Ok(())
1123 }
1124
1125 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1126 Ok(())
1127 }
1128 }
1129
1130 fn options(width: u32, height: u32) -> VideoCompositorOptions {
1131 VideoCompositorOptions {
1132 width,
1133 height,
1134 frame_rate: ffmpeg::Rational::new(30, 1),
1135 background: Color::BLACK,
1136 }
1137 }
1138
1139 fn solid_frame(
1140 width: u32,
1141 height: u32,
1142 color: Color,
1143 ) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
1144 let pool = UnboundObjectPool::new(
1145 0,
1146 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
1147 |_| {},
1148 );
1149 let mut frame = pool.get();
1150 fill_background(&mut frame, color);
1151 Arc::new(frame)
1152 }
1153
1154 fn pixel(frame: &ffmpeg::frame::Video, x: usize, y: usize) -> [u8; 4] {
1155 let offset = y * frame.stride(0) + x * 4;
1156 frame.data(0)[offset..offset + 4].try_into().unwrap()
1157 }
1158
1159 fn input(
1160 handle: &SwVideoCompositorHandle,
1161 name: &str,
1162 layer: VideoLayer,
1163 ) -> (Box<dyn Sink>, SwVideoLayerHandle) {
1164 let input = handle.add_source(name, layer).unwrap().unwrap();
1165 (input.sink, input.layer)
1166 }
1167
1168 fn quadrant_frame(width: u32, height: u32) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
1171 let pool = UnboundObjectPool::new(
1172 0,
1173 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
1174 |_| {},
1175 );
1176 let mut frame = pool.get();
1177 let stride = frame.stride(0);
1178 for y in 0..height as usize {
1179 for x in 0..width as usize {
1180 let bgra = match (x >= width as usize / 2, y >= height as usize / 2) {
1181 (false, false) => [0, 0, 255, 255],
1182 (true, false) => [0, 255, 0, 255],
1183 (false, true) => [255, 0, 0, 255],
1184 (true, true) => [0, 255, 255, 255],
1185 };
1186 let offset = y * stride + x * 4;
1187 frame.data_mut(0)[offset..offset + 4].copy_from_slice(&bgra);
1188 }
1189 }
1190 Arc::new(frame)
1191 }
1192
1193 #[test]
1196 fn a_layer_draws_only_its_source_region() {
1197 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1198 let mut layer = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
1199 layer.fit = VideoFit::Stretch;
1200 layer.source = Some(VideoSourceRect::new(4, 4, 4, 4));
1202 let (mut sink, _) = input(&handle, "input", layer);
1203 sink.consume(MediaBuffer::Video(quadrant_frame(8, 8)))
1204 .unwrap();
1205
1206 let frame = compositor.compose_frame().unwrap();
1207
1208 for (x, y) in [(0, 0), (3, 0), (0, 3), (3, 3)] {
1209 assert_eq!(
1210 pixel(&frame, x, y),
1211 [0, 255, 255, 255],
1212 "every output pixel must come from the cropped quadrant, at {x},{y}"
1213 );
1214 }
1215 }
1216
1217 #[test]
1221 fn a_source_region_outside_the_frame_leaves_the_layer_out() {
1222 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1223 let mut layer = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
1224 layer.fit = VideoFit::Stretch;
1225 layer.source = Some(VideoSourceRect::new(64, 64, 4, 4));
1226 let (mut sink, _) = input(&handle, "input", layer);
1227 sink.consume(MediaBuffer::Video(solid_frame(8, 8, Color::new(255, 0, 0))))
1228 .unwrap();
1229
1230 let frame = compositor.compose_frame().unwrap();
1231
1232 assert_eq!(
1233 pixel(&frame, 0, 0),
1234 [0, 0, 0, 255],
1235 "the background is what a layer with nothing to draw leaves"
1236 );
1237 }
1238
1239 #[test]
1242 fn an_empty_source_region_is_refused() {
1243 let (_compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1244 let (_sink, layer) = input(
1245 &handle,
1246 "input",
1247 VideoLayer::new(VideoRect::new(0, 0, 4, 4)),
1248 );
1249
1250 let refused = layer.set_source(Some(VideoSourceRect::new(0, 0, 4, 0)));
1251
1252 assert!(matches!(
1253 refused,
1254 Err(SwVideoCompositorError::InvalidSourceRegion { .. })
1255 ));
1256 }
1257
1258 #[test]
1259 fn composes_inputs_in_z_order_and_preserves_output_contract() {
1260 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1261 let mut background = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
1262 background.fit = VideoFit::Stretch;
1263 let (mut red_sink, _) = input(&handle, "red", background);
1264 let mut overlay = VideoLayer::new(VideoRect::new(1, 1, 2, 2));
1265 overlay.z_index = 1;
1266 overlay.fit = VideoFit::Stretch;
1267 let (mut blue_sink, _) = input(&handle, "blue", overlay);
1268 red_sink
1269 .consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(255, 0, 0))))
1270 .unwrap();
1271 blue_sink
1272 .consume(MediaBuffer::Video(solid_frame(2, 2, Color::new(0, 0, 255))))
1273 .unwrap();
1274
1275 let frame = compositor.compose_frame().unwrap();
1276 assert_eq!(frame.format(), ffmpeg::format::Pixel::BGRA);
1277 assert_eq!((frame.width(), frame.height()), (4, 4));
1278 assert_eq!(frame.pts(), Some(0));
1279 assert_eq!(pixel(&frame, 0, 0), [0, 0, 255, 255]);
1280 assert_eq!(pixel(&frame, 1, 1), [255, 0, 0, 255]);
1281 }
1282
1283 #[test]
1288 fn a_repeat_still_in_flight_is_never_composed_over() {
1289 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1290 let mut layer = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
1291 layer.fit = VideoFit::Stretch;
1292 let (mut sink, _layer_handle) = input(&handle, "only", layer);
1293 sink.consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(255, 0, 0))))
1294 .unwrap();
1295
1296 drop(compositor.compose_frame().unwrap());
1299 let in_flight = compositor.compose_frame().unwrap();
1300 let showing = picture_id(&in_flight);
1301
1302 for _ in 0..(OUTPUT_POOL_SIZE * 2 + 2) {
1307 sink.consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(0, 255, 0))))
1308 .unwrap();
1309 let composed = compositor.compose_frame().unwrap();
1310 assert_ne!(
1311 picture_id(&composed),
1312 showing,
1313 "composed into the buffer a repeat still in flight is showing"
1314 );
1315 }
1316
1317 assert_eq!(
1318 pixel(&in_flight, 0, 0),
1319 [0, 0, 255, 255],
1320 "the repeat still shows the picture it was published with"
1321 );
1322 }
1323
1324 #[test]
1328 fn an_unchanged_scene_is_composed_once() {
1329 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
1330 let mut layer = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
1331 layer.fit = VideoFit::Stretch;
1332 let (mut sink, layer_handle) = input(&handle, "only", layer);
1333 sink.consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(255, 0, 0))))
1334 .unwrap();
1335
1336 let composed = compositor.compose_frame().unwrap();
1337 let picture = picture_id(&composed);
1338 assert_eq!(composed.pts(), Some(0));
1339
1340 let repeated = compositor.compose_frame().unwrap();
1341 assert_eq!(
1342 picture_id(&repeated),
1343 picture,
1344 "nothing changed, so this is the picture already composed"
1345 );
1346 assert_eq!(
1347 repeated.pts(),
1348 Some(1),
1349 "a repeat carries this tick's timestamp, not the one it points at"
1350 );
1351 assert_eq!(
1352 pixel(&repeated, 0, 0),
1353 [0, 0, 255, 255],
1354 "and it still shows what was composed"
1355 );
1356
1357 layer_handle.set_rect(VideoRect::new(1, 1, 2, 2)).unwrap();
1358 let moved = compositor.compose_frame().unwrap();
1359 assert_ne!(
1360 picture_id(&moved),
1361 picture,
1362 "a moved layer is a different picture and must be composed"
1363 );
1364 assert_eq!(
1365 pixel(&composed, 0, 0),
1366 [0, 0, 255, 255],
1367 "composing again must not draw over the picture still held"
1368 );
1369 }
1370
1371 #[test]
1372 fn layer_handle_moves_blends_and_hides_a_live_source() {
1373 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(3, 1)).unwrap();
1374 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
1375 let (mut sink, layer_handle) = input(&handle, "white", layer);
1376 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::WHITE)))
1377 .unwrap();
1378
1379 layer_handle.set_rect(VideoRect::new(1, 0, 1, 1)).unwrap();
1380 layer_handle.set_opacity(0.5).unwrap();
1381 let blended = compositor.compose_frame().unwrap();
1382 assert_eq!(pixel(&blended, 0, 0), [0, 0, 0, 255]);
1383 assert_eq!(pixel(&blended, 1, 0), [128, 128, 128, 255]);
1384
1385 layer_handle.set_visible(false).unwrap();
1386 let hidden = compositor.compose_frame().unwrap();
1387 assert_eq!(pixel(&hidden, 1, 0), [0, 0, 0, 255]);
1388 assert_eq!(hidden.pts(), Some(1));
1389 }
1390
1391 #[test]
1392 fn input_keeps_only_the_latest_frame() {
1393 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1394 let (mut sink, _) = input(
1395 &handle,
1396 "latest",
1397 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
1398 );
1399 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
1400 .unwrap();
1401 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 255, 0))))
1402 .unwrap();
1403
1404 let frame = compositor.compose_frame().unwrap();
1405 assert_eq!(pixel(&frame, 0, 0), [0, 255, 0, 255]);
1406 }
1407
1408 #[test]
1409 fn frame_replacement_and_composition_run_concurrently() {
1410 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1411 let (mut sink, _) = input(&handle, "live", VideoLayer::new(VideoRect::new(0, 0, 1, 1)));
1412 let red = solid_frame(1, 1, Color::new(255, 0, 0));
1413 let green = solid_frame(1, 1, Color::new(0, 255, 0));
1414 let done = Arc::new(AtomicBool::new(false));
1415 let producer_done = done.clone();
1416 let producer = thread::spawn(move || {
1417 for index in 0..2_000 {
1418 let frame = if index % 2 == 0 {
1419 red.clone()
1420 } else {
1421 green.clone()
1422 };
1423 sink.consume(MediaBuffer::Video(frame)).unwrap();
1424 }
1425 sink.consume(MediaBuffer::Video(green)).unwrap();
1428 producer_done.store(true, AtomicOrdering::Release);
1429 });
1430
1431 while !done.load(AtomicOrdering::Acquire) {
1432 let frame = compositor.compose_frame().unwrap();
1433 assert!(matches!(
1434 pixel(&frame, 0, 0),
1435 [0, 0, 0, 255] | [0, 0, 255, 255] | [0, 255, 0, 255]
1436 ));
1437 }
1438 producer.join().unwrap();
1439 let final_frame = compositor.compose_frame().unwrap();
1440 assert_eq!(pixel(&final_frame, 0, 0), [0, 255, 0, 255]);
1441 }
1442
1443 #[test]
1444 fn replacing_a_name_invalidates_old_sink_and_layer_handle() {
1445 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1446 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
1447 let (mut old_sink, old_layer) = input(&handle, "camera", layer);
1448 let (mut new_sink, _) = input(&handle, "camera", layer);
1449 assert!(matches!(
1450 old_layer.set_visible(false),
1451 Err(SwVideoCompositorError::SourceRemoved)
1452 ));
1453 old_sink
1454 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
1455 .unwrap();
1456 new_sink
1457 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 0, 255))))
1458 .unwrap();
1459
1460 let frame = compositor.compose_frame().unwrap();
1461 assert_eq!(pixel(&frame, 0, 0), [255, 0, 0, 255]);
1462 assert_eq!(handle.source_count(), 1);
1463 }
1464
1465 #[test]
1466 fn stop_removes_only_the_current_registration() {
1467 let (_compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1468 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
1469 let (mut old_sink, _) = input(&handle, "camera", layer);
1470 let (_new_sink, _) = input(&handle, "camera", layer);
1471 old_sink.control(ControlMsg::Stop).unwrap();
1472 assert_eq!(handle.source_count(), 1);
1473
1474 handle.remove_source("camera");
1475 assert_eq!(handle.source_count(), 0);
1476 }
1477
1478 #[test]
1479 fn contain_and_cover_preserve_aspect_ratio() {
1480 let rect = VideoRect::new(10, 20, 100, 100);
1481 let contain = layer_geometry(160, 90, rect, VideoFit::Contain).unwrap();
1482 assert_eq!((contain.image_width, contain.image_height), (100, 56));
1483 assert_eq!((contain.image_x, contain.image_y), (10, 42));
1484
1485 let cover = layer_geometry(160, 90, rect, VideoFit::Cover).unwrap();
1486 assert_eq!((cover.image_width, cover.image_height), (178, 100));
1487 assert_eq!((cover.image_x, cover.image_y), (-29, 20));
1488 }
1489
1490 #[test]
1491 fn rejects_invalid_layers_and_non_video_buffers() {
1492 let (_compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1493 let invalid = VideoLayer {
1494 opacity: 1.5,
1495 ..VideoLayer::new(VideoRect::new(0, 0, 1, 1))
1496 };
1497 assert!(matches!(
1498 handle.add_source("invalid", invalid),
1499 Err(SwVideoCompositorError::InvalidOpacity(1.5))
1500 ));
1501
1502 let (mut sink, _) = input(
1503 &handle,
1504 "valid",
1505 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
1506 );
1507 let error = sink
1508 .consume(MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty())))
1509 .unwrap_err();
1510 assert!(matches!(
1511 error,
1512 crate::Error::SwVideoCompositorError(SwVideoCompositorError::UnsupportedBuffer(
1513 "Packet"
1514 ))
1515 ));
1516 }
1517
1518 #[test]
1519 fn pushes_fixed_format_frames_with_contiguous_pts() {
1520 let (mut compositor, _) = SwVideoCompositor::new("compositor", options(2, 2)).unwrap();
1521 let received = Arc::new(StdMutex::new(Vec::new()));
1522 compositor.src_pads()[0].link(Box::new(CapturingSink {
1523 received: received.clone(),
1524 pp_log: element_pp_log(ElementType::Other, "capture", None),
1525 }));
1526 let (bus, _) = Bus::new();
1527 compositor.push_frame(&bus).unwrap();
1528 compositor.push_frame(&bus).unwrap();
1529
1530 let received = received.lock().unwrap();
1531 let pts: Vec<_> = received
1532 .iter()
1533 .filter_map(|buffer| match buffer {
1534 MediaBuffer::Video(frame) => Some(frame.pts()),
1535 _ => None,
1536 })
1537 .collect();
1538 assert_eq!(pts, vec![Some(0), Some(1)]);
1539 }
1540
1541 struct TimestampSink {
1542 pp_log: PpLog,
1543 tx: crossbeam_channel::Sender<Instant>,
1544 }
1545
1546 impl Element for TimestampSink {
1547 fn name(&self) -> Arc<str> {
1548 "timestamp-recorder".into()
1549 }
1550 fn element_type(&self) -> ElementType {
1551 ElementType::Other
1552 }
1553 fn pp_log(&self) -> &PpLog {
1554 &self.pp_log
1555 }
1556 fn pp_log_mut(&mut self) -> &mut PpLog {
1557 &mut self.pp_log
1558 }
1559 }
1560
1561 impl Sink for TimestampSink {
1562 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1563 if matches!(buf, MediaBuffer::Video(_)) {
1564 let _ = self.tx.send(Instant::now());
1565 }
1566 Ok(())
1567 }
1568 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1569 Ok(())
1570 }
1571 }
1572
1573 #[test]
1586 fn resuming_after_a_pause_preserves_output_phase() {
1587 use crate::pipeline::Pipeline;
1588
1589 let (tx, rx) = crossbeam_channel::unbounded();
1590 let sink = TimestampSink {
1591 tx,
1592 pp_log: element_pp_log(ElementType::Other, "timestamp-recorder", None),
1593 };
1594 let (compositor, _handle) = SwVideoCompositor::new(
1595 "compositor",
1596 VideoCompositorOptions {
1597 frame_rate: ffmpeg::Rational::new(10, 1),
1598 ..options(2, 2)
1599 },
1600 )
1601 .unwrap();
1602
1603 let pipeline = Pipeline::new("phase-test", compositor, |source, ctx| {
1604 let branch = ctx.branch().to(Box::new(sink))?;
1605 ctx.attach(source, 0, branch)?;
1606 Ok(())
1607 })
1608 .expect("test pipeline wiring must succeed");
1609
1610 pipeline.run().unwrap();
1611 for _ in 0..2 {
1614 rx.recv_timeout(Duration::from_millis(500))
1615 .expect("expected steady frames before pausing");
1616 }
1617 pipeline.pause();
1618 thread::sleep(Duration::from_millis(500));
1619
1620 let resumed_at = Instant::now();
1621 pipeline.resume();
1622 let first_after_resume = rx
1623 .recv_timeout(Duration::from_millis(500))
1624 .expect("expected a frame after resume");
1625 pipeline.stop();
1626 pipeline.bus().log_events();
1627
1628 let gap = first_after_resume.saturating_duration_since(resumed_at);
1629 assert!(
1630 gap >= Duration::from_millis(50),
1631 "expected the post-pause frame to land close to a full 100ms \
1632 interval after resume (phase preserved from before the \
1633 pause), not almost immediately (phase reset to the resume \
1634 instant): got {gap:?}"
1635 );
1636 }
1637
1638 #[test]
1641 fn the_frame_rate_can_be_changed_while_it_is_running() {
1642 let (compositor, handle) = SwVideoCompositor::new(
1643 "rate",
1644 VideoCompositorOptions {
1645 width: 64,
1646 height: 64,
1647 frame_rate: ffmpeg::Rational::new(60, 1),
1648 background: Color::BLACK,
1649 },
1650 )
1651 .expect("compositor");
1652
1653 assert_eq!(compositor.frame_rate(), ffmpeg::Rational::new(60, 1));
1654 assert_eq!(compositor.time_base(), ffmpeg::Rational::new(1, 60));
1655
1656 assert!(handle.set_frame_rate(ffmpeg::Rational::new(24, 1)));
1657 assert_eq!(handle.frame_rate(), Some(ffmpeg::Rational::new(24, 1)));
1658 assert_eq!(compositor.frame_rate(), ffmpeg::Rational::new(24, 1));
1660 assert_eq!(compositor.time_base(), ffmpeg::Rational::new(1, 24));
1661
1662 assert!(!handle.set_frame_rate(ffmpeg::Rational::new(0, 1)));
1664 assert_eq!(compositor.frame_rate(), ffmpeg::Rational::new(24, 1));
1665
1666 drop(compositor);
1668 assert!(!handle.set_frame_rate(ffmpeg::Rational::new(30, 1)));
1669 assert_eq!(handle.frame_rate(), None);
1670 }
1671}