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 arc_swap::ArcSwapOption;
13use ffmpeg_next as ffmpeg;
14use thiserror::Error as ThisError;
15
16use super::video_layer::{
17 self, LayerGeometry, MAX_DIMENSION, VideoFit, VideoInputId, VideoLayer, VideoLayerError,
18 VideoRect,
19};
20use crate::{
21 buffer::MediaBuffer,
22 bus::{Bus, BusEvent},
23 color::Color,
24 control::{ControlMsg, ControlReceiver, drain_control},
25 element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
26 error::Result,
27 pad::SrcPad,
28 pool::{UnboundObjectPool, UnboundObjectPoolRef},
29 schedule::PeriodicSchedule,
30};
31
32const OUTPUT_POOL_SIZE: usize = 4;
33const CONTROL_POLL_INTERVAL: Duration = Duration::from_millis(5);
34
35#[derive(Debug, Clone, Copy)]
38pub struct VideoCompositorOptions {
39 pub width: u32,
41 pub height: u32,
43 pub frame_rate: ffmpeg::Rational,
45 pub background: Color,
47}
48
49impl Default for VideoCompositorOptions {
50 fn default() -> Self {
51 Self {
52 width: 1920,
53 height: 1080,
54 frame_rate: ffmpeg::Rational::new(30, 1),
55 background: Color::BLACK,
56 }
57 }
58}
59
60#[derive(Debug, ThisError)]
62pub enum SwVideoCompositorError {
63 #[error("ffmpeg error: {0}")]
65 Ffmpeg(#[from] ffmpeg::Error),
66
67 #[error(
69 "invalid output dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
70 )]
71 InvalidOutputDimensions {
72 width: u32,
74 height: u32,
76 },
77
78 #[error("invalid frame rate {0}; numerator and denominator must both be positive")]
80 InvalidFrameRate(ffmpeg::Rational),
81
82 #[error(
84 "invalid layer dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
85 )]
86 InvalidLayerDimensions {
87 width: u32,
89 height: u32,
91 },
92
93 #[error("layer opacity must be finite and between 0.0 and 1.0, got {0}")]
95 InvalidOpacity(f32),
96
97 #[error("input frame has invalid dimensions {width}x{height}")]
99 InvalidInputDimensions {
100 width: u32,
102 height: u32,
104 },
105
106 #[error("scaled layer would exceed {MAX_DIMENSION}px: {width}x{height}")]
108 ScaledLayerTooLarge {
109 width: u32,
111 height: u32,
113 },
114
115 #[error("the compositor input has been removed")]
117 SourceRemoved,
118
119 #[error(
121 "SwVideoCompositorInputSink only accepts decoded Video frames, got a {0}; link it after a decoder or video source"
122 )]
123 UnsupportedBuffer(&'static str),
124
125 #[error("SwVideoCompositor doesn't support seeking a live composition")]
127 SeekUnsupported,
128}
129
130struct VideoInput {
131 id: VideoInputId,
132 latest_frame: ArcSwapOption<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
136 layer: Mutex<VideoLayer>,
139}
140
141struct CompositorShared {
142 inputs: Mutex<HashMap<Arc<str>, Arc<VideoInput>>>,
143 next_input_id: AtomicU64,
144}
145
146#[derive(Clone)]
150pub struct SwVideoCompositorHandle {
151 shared: Weak<CompositorShared>,
152}
153
154pub struct SwVideoCompositorInput {
158 pub sink: Box<dyn Sink>,
160 pub layer: SwVideoLayerHandle,
162}
163
164impl SwVideoCompositorHandle {
165 pub fn add_source(
169 &self,
170 name: impl Into<String>,
171 layer: VideoLayer,
172 ) -> std::result::Result<Option<SwVideoCompositorInput>, SwVideoCompositorError> {
173 validate_layer(layer)?;
174 let Some(shared) = self.shared.upgrade() else {
175 return Ok(None);
176 };
177 let name: Arc<str> = name.into().into();
178 let id = VideoInputId(shared.next_input_id.fetch_add(1, Ordering::Relaxed));
179 let input = Arc::new(VideoInput {
180 id,
181 latest_frame: ArcSwapOption::empty(),
182 layer: Mutex::new(layer),
183 });
184 shared
185 .inputs
186 .lock()
187 .unwrap()
188 .insert(name.clone(), input.clone());
189
190 Ok(Some(SwVideoCompositorInput {
191 sink: Box::new(SwVideoCompositorInputSink {
192 name: name.clone(),
193 pp_log: element_pp_log(ElementType::SwVideoCompositor, &name, None),
194 shared: self.shared.clone(),
195 input: Arc::downgrade(&input),
196 }),
197 layer: SwVideoLayerHandle {
198 id,
199 name,
200 input: Arc::downgrade(&input),
201 },
202 }))
203 }
204
205 pub fn remove_source(&self, name: &str) {
208 if let Some(shared) = self.shared.upgrade() {
209 shared.inputs.lock().unwrap().remove(name);
210 }
211 }
212
213 pub fn source_count(&self) -> usize {
217 self.shared
218 .upgrade()
219 .map(|shared| shared.inputs.lock().unwrap().len())
220 .unwrap_or(0)
221 }
222}
223
224#[derive(Clone)]
227pub struct SwVideoLayerHandle {
228 id: VideoInputId,
229 name: Arc<str>,
230 input: Weak<VideoInput>,
231}
232
233impl SwVideoLayerHandle {
234 pub fn id(&self) -> VideoInputId {
236 self.id
237 }
238
239 pub fn name(&self) -> Arc<str> {
241 self.name.clone()
242 }
243
244 pub fn layer(&self) -> Option<VideoLayer> {
246 self.input
247 .upgrade()
248 .map(|input| *input.layer.lock().unwrap())
249 }
250
251 pub fn set_layer(&self, layer: VideoLayer) -> std::result::Result<(), SwVideoCompositorError> {
255 validate_layer(layer)?;
256 self.update(|current| *current = layer)
257 }
258
259 pub fn set_rect(&self, rect: VideoRect) -> std::result::Result<(), SwVideoCompositorError> {
261 validate_rect(rect)?;
262 self.update(|layer| layer.rect = rect)
263 }
264
265 pub fn set_opacity(&self, opacity: f32) -> std::result::Result<(), SwVideoCompositorError> {
267 validate_opacity(opacity)?;
268 self.update(|layer| layer.opacity = opacity)
269 }
270
271 pub fn set_z_index(&self, z_index: i32) -> std::result::Result<(), SwVideoCompositorError> {
273 self.update(|layer| layer.z_index = z_index)
274 }
275
276 pub fn set_visible(&self, visible: bool) -> std::result::Result<(), SwVideoCompositorError> {
278 self.update(|layer| layer.visible = visible)
279 }
280
281 pub fn set_fit(&self, fit: VideoFit) -> std::result::Result<(), SwVideoCompositorError> {
283 self.update(|layer| layer.fit = fit)
284 }
285
286 fn update(
287 &self,
288 update: impl FnOnce(&mut VideoLayer),
289 ) -> std::result::Result<(), SwVideoCompositorError> {
290 let input = self
291 .input
292 .upgrade()
293 .ok_or(SwVideoCompositorError::SourceRemoved)?;
294 update(&mut input.layer.lock().unwrap());
295 Ok(())
296 }
297}
298
299pub struct SwVideoCompositorInputSink {
304 pp_log: PpLog,
305 name: Arc<str>,
306 shared: Weak<CompositorShared>,
307 input: Weak<VideoInput>,
308}
309
310impl SwVideoCompositorInputSink {
311 fn detach(&self) {
312 let (Some(shared), Some(input)) = (self.shared.upgrade(), self.input.upgrade()) else {
313 return;
314 };
315 let mut inputs = shared.inputs.lock().unwrap();
316 let is_current = inputs
317 .get(&self.name)
318 .is_some_and(|current| Arc::ptr_eq(current, &input));
319 if is_current {
320 inputs.remove(&self.name);
321 }
322 }
323}
324
325impl Element for SwVideoCompositorInputSink {
326 fn name(&self) -> Arc<str> {
327 self.name.clone()
328 }
329
330 fn element_type(&self) -> ElementType {
331 ElementType::SwVideoCompositor
332 }
333
334 fn pp_log(&self) -> &PpLog {
335 &self.pp_log
336 }
337
338 fn pp_log_mut(&mut self) -> &mut PpLog {
339 &mut self.pp_log
340 }
341}
342
343impl Sink for SwVideoCompositorInputSink {
344 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
345 let Some(input) = self.input.upgrade() else {
346 return Ok(());
347 };
348 match buf {
349 MediaBuffer::Video(frame) => {
350 input.latest_frame.store(Some(frame));
351 Ok(())
352 }
353 MediaBuffer::Eos => {
354 self.detach();
355 Ok(())
356 }
357 MediaBuffer::Packet(_) => {
358 Err(SwVideoCompositorError::UnsupportedBuffer("Packet").into())
359 }
360 MediaBuffer::Audio(_) => Err(SwVideoCompositorError::UnsupportedBuffer("Audio").into()),
361 }
362 }
363
364 fn control(&mut self, msg: ControlMsg) -> Result<()> {
365 match msg {
366 ControlMsg::Stop => self.detach(),
367 ControlMsg::Seek(_) => {
368 if let Some(input) = self.input.upgrade() {
369 input.latest_frame.store(None);
370 }
371 }
372 ControlMsg::Pause | ControlMsg::Resume => {}
373 }
374 Ok(())
375 }
376}
377
378#[derive(Clone)]
379struct InputSnapshot {
380 id: VideoInputId,
381 layer: VideoLayer,
382 frame: Option<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>>,
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
386struct ScaleDefinition {
387 source_format: ffmpeg::format::Pixel,
388 source_width: u32,
389 source_height: u32,
390 target_width: u32,
391 target_height: u32,
392}
393
394struct InputScaler {
395 definition: Option<ScaleDefinition>,
396 context: Option<ffmpeg::software::scaling::Context>,
397 output: ffmpeg::frame::Video,
398}
399
400impl InputScaler {
401 fn new() -> Self {
402 Self {
403 definition: None,
404 context: None,
405 output: ffmpeg::frame::Video::empty(),
406 }
407 }
408
409 fn scale(
410 &mut self,
411 source: &ffmpeg::frame::Video,
412 width: u32,
413 height: u32,
414 ) -> std::result::Result<&ffmpeg::frame::Video, ffmpeg::Error> {
415 let definition = ScaleDefinition {
416 source_format: source.format(),
417 source_width: source.width(),
418 source_height: source.height(),
419 target_width: width,
420 target_height: height,
421 };
422 if self.definition != Some(definition) {
423 self.context = Some(ffmpeg::software::scaling::Context::get(
424 definition.source_format,
425 definition.source_width,
426 definition.source_height,
427 ffmpeg::format::Pixel::BGRA,
428 width,
429 height,
430 ffmpeg::software::scaling::Flags::BILINEAR,
431 )?);
432 self.output = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height);
433 self.definition = Some(definition);
434 }
435 self.context
436 .as_mut()
437 .expect("created with a changed definition or retained from a matching one")
438 .run(source, &mut self.output)?;
439 Ok(&self.output)
440 }
441}
442
443pub struct SwVideoCompositor {
453 pp_log: PpLog,
454 name: Arc<str>,
455 shared: Arc<CompositorShared>,
456 options: VideoCompositorOptions,
457 frame_interval: Duration,
458 frame_index: i64,
459 scalers: HashMap<VideoInputId, InputScaler>,
460 output_pool: UnboundObjectPool<ffmpeg::frame::Video>,
461 pad: SrcPad,
462}
463
464unsafe impl Send for SwVideoCompositor {}
468
469impl SwVideoCompositor {
470 pub fn new(
472 name: impl Into<String>,
473 options: VideoCompositorOptions,
474 ) -> std::result::Result<(Self, SwVideoCompositorHandle), SwVideoCompositorError> {
475 validate_output_options(options)?;
476 let name: Arc<str> = name.into().into();
477 let pp_log = element_pp_log(ElementType::SwVideoCompositor, &name, None);
478 let shared = Arc::new(CompositorShared {
479 inputs: Mutex::new(HashMap::new()),
480 next_input_id: AtomicU64::new(1),
481 });
482 let frame_interval = Duration::from_secs_f64(
483 options.frame_rate.denominator() as f64 / options.frame_rate.numerator() as f64,
484 );
485 let (width, height) = (options.width, options.height);
486 let output_pool = UnboundObjectPool::new(
487 OUTPUT_POOL_SIZE,
488 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
489 |_| {},
490 );
491 pp_info!(
492 pp_log: &pp_log,
493 "created: {}x{}, frame_rate={}, format=BGRA",
494 width,
495 height,
496 options.frame_rate
497 );
498 Ok((
499 Self {
500 name: name.clone(),
501 pp_log,
502 shared: shared.clone(),
503 options,
504 frame_interval,
505 frame_index: 0,
506 scalers: HashMap::new(),
507 output_pool,
508 pad: SrcPad::new(format!("{name}_src")),
509 },
510 SwVideoCompositorHandle {
511 shared: Arc::downgrade(&shared),
512 },
513 ))
514 }
515
516 pub fn format(&self) -> ffmpeg::format::Pixel {
518 ffmpeg::format::Pixel::BGRA
519 }
520
521 pub fn width(&self) -> u32 {
523 self.options.width
524 }
525
526 pub fn height(&self) -> u32 {
528 self.options.height
529 }
530
531 pub fn frame_rate(&self) -> ffmpeg::Rational {
533 self.options.frame_rate
534 }
535
536 pub fn time_base(&self) -> ffmpeg::Rational {
538 ffmpeg::Rational::new(
539 self.options.frame_rate.denominator(),
540 self.options.frame_rate.numerator(),
541 )
542 }
543
544 fn snapshots(&self) -> Vec<InputSnapshot> {
545 let inputs: Vec<_> = self
546 .shared
547 .inputs
548 .lock()
549 .unwrap()
550 .values()
551 .cloned()
552 .collect();
553 inputs
554 .into_iter()
555 .map(|input| InputSnapshot {
556 id: input.id,
557 layer: *input.layer.lock().unwrap(),
558 frame: input.latest_frame.load_full(),
559 })
560 .collect()
561 }
562
563 fn compose_frame(
564 &mut self,
565 ) -> std::result::Result<UnboundObjectPoolRef<ffmpeg::frame::Video>, SwVideoCompositorError>
566 {
567 let mut snapshots = self.snapshots();
568 let active: HashSet<_> = snapshots.iter().map(|snapshot| snapshot.id).collect();
569 self.scalers.retain(|id, _| active.contains(id));
570 snapshots.sort_by(|left, right| {
571 left.layer
572 .z_index
573 .cmp(&right.layer.z_index)
574 .then_with(|| left.id.cmp(&right.id))
575 });
576
577 let mut output = self.output_pool.get();
578 fill_background(&mut output, self.options.background);
579 for snapshot in snapshots {
580 if !snapshot.layer.visible || snapshot.layer.opacity == 0.0 {
581 continue;
582 }
583 let Some(frame) = snapshot.frame else {
584 continue;
585 };
586 let geometry = layer_geometry(
587 frame.width(),
588 frame.height(),
589 snapshot.layer.rect,
590 snapshot.layer.fit,
591 )?;
592 let scaled = self
593 .scalers
594 .entry(snapshot.id)
595 .or_insert_with(InputScaler::new)
596 .scale(&frame, geometry.image_width, geometry.image_height)
597 .map_err(SwVideoCompositorError::from)?;
598 blend_bgra(&mut output, scaled, geometry, snapshot.layer.opacity);
599 }
600 output.set_pts(Some(self.frame_index));
601 self.frame_index += 1;
602 Ok(output)
603 }
604
605 fn push_frame(&mut self, bus: &Bus) -> std::result::Result<(), SwVideoCompositorError> {
606 let output = self.compose_frame()?;
607 if let Err(error) = self.pad.push(MediaBuffer::Video(Arc::new(output))) {
608 bus.post(
609 &self.pp_log,
610 BusEvent::Error {
611 element_type: ElementType::SwVideoCompositor,
612 name: self.name.clone(),
613 error,
614 },
615 );
616 }
617 Ok(())
618 }
619}
620
621impl Element for SwVideoCompositor {
622 fn name(&self) -> Arc<str> {
623 self.name.clone()
624 }
625
626 fn element_type(&self) -> ElementType {
627 ElementType::SwVideoCompositor
628 }
629
630 fn pp_log(&self) -> &PpLog {
631 &self.pp_log
632 }
633
634 fn pp_log_mut(&mut self) -> &mut PpLog {
635 &mut self.pp_log
636 }
637}
638
639impl Source for SwVideoCompositor {
640 fn src_pads(&mut self) -> &mut [SrcPad] {
641 std::slice::from_mut(&mut self.pad)
642 }
643}
644
645impl SourceElement for SwVideoCompositor {
646 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
647 pp_info!(self, "started");
648 let mut schedule = PeriodicSchedule::new(self.frame_interval, Instant::now());
649 loop {
650 let outcome = drain_control(control, self, bus)?;
651 if outcome.stopped {
652 pp_info!(self, "stopped");
653 return Ok(());
654 }
655 if outcome.paused_for > Duration::ZERO {
656 schedule.resume_after_pause(outcome.paused_for, Instant::now());
657 }
658
659 let now = Instant::now();
660 if !schedule.is_due(now) {
661 thread::sleep(schedule.remaining(now).min(CONTROL_POLL_INTERVAL));
662 continue;
663 }
664
665 self.push_frame(bus)?;
666 schedule.advance_after_tick(Instant::now());
667 }
668 }
669
670 fn seek(&mut self, _target: Duration) -> Result<Duration> {
671 Err(SwVideoCompositorError::SeekUnsupported.into())
672 }
673}
674
675fn validate_output_options(
676 options: VideoCompositorOptions,
677) -> std::result::Result<(), SwVideoCompositorError> {
678 if options.width == 0
679 || options.height == 0
680 || options.width > MAX_DIMENSION
681 || options.height > MAX_DIMENSION
682 {
683 return Err(SwVideoCompositorError::InvalidOutputDimensions {
684 width: options.width,
685 height: options.height,
686 });
687 }
688 if options.frame_rate.numerator() <= 0 || options.frame_rate.denominator() <= 0 {
689 return Err(SwVideoCompositorError::InvalidFrameRate(options.frame_rate));
690 }
691 Ok(())
692}
693
694fn map_layer_error(error: VideoLayerError) -> SwVideoCompositorError {
700 match error {
701 VideoLayerError::InvalidDimensions { width, height } => {
702 SwVideoCompositorError::InvalidLayerDimensions { width, height }
703 }
704 VideoLayerError::InvalidOpacity(opacity) => SwVideoCompositorError::InvalidOpacity(opacity),
705 VideoLayerError::InvalidInputDimensions { width, height } => {
706 SwVideoCompositorError::InvalidInputDimensions { width, height }
707 }
708 VideoLayerError::ScaledLayerTooLarge { width, height } => {
709 SwVideoCompositorError::ScaledLayerTooLarge { width, height }
710 }
711 }
712}
713
714fn validate_layer(layer: VideoLayer) -> std::result::Result<(), SwVideoCompositorError> {
715 video_layer::validate_layer(layer).map_err(map_layer_error)
716}
717
718fn validate_rect(rect: VideoRect) -> std::result::Result<(), SwVideoCompositorError> {
719 video_layer::validate_rect(rect).map_err(map_layer_error)
720}
721
722fn validate_opacity(opacity: f32) -> std::result::Result<(), SwVideoCompositorError> {
723 video_layer::validate_opacity(opacity).map_err(map_layer_error)
724}
725
726fn layer_geometry(
727 source_width: u32,
728 source_height: u32,
729 rect: VideoRect,
730 fit: VideoFit,
731) -> std::result::Result<LayerGeometry, SwVideoCompositorError> {
732 video_layer::layer_geometry(source_width, source_height, rect, fit).map_err(map_layer_error)
733}
734
735fn fill_background(frame: &mut ffmpeg::frame::Video, color: Color) {
736 let width = frame.width() as usize;
737 let height = frame.height() as usize;
738 let stride = frame.stride(0);
739 let data = frame.data_mut(0);
740 for row in 0..height {
741 for pixel in data[row * stride..row * stride + width * 4]
742 .as_chunks_mut::<4>()
743 .0
744 {
745 *pixel = [color.blue, color.green, color.red, 255];
746 }
747 }
748}
749
750fn blend_bgra(
751 destination: &mut ffmpeg::frame::Video,
752 source: &ffmpeg::frame::Video,
753 geometry: LayerGeometry,
754 opacity: f32,
755) {
756 let output_width = i64::from(destination.width());
757 let output_height = i64::from(destination.height());
758 let clip_left = i64::from(geometry.clip.x).max(0);
759 let clip_top = i64::from(geometry.clip.y).max(0);
760 let clip_right =
761 (i64::from(geometry.clip.x) + i64::from(geometry.clip.width)).min(output_width);
762 let clip_bottom =
763 (i64::from(geometry.clip.y) + i64::from(geometry.clip.height)).min(output_height);
764 let left = geometry.image_x.max(clip_left);
765 let top = geometry.image_y.max(clip_top);
766 let right = (geometry.image_x + i64::from(geometry.image_width)).min(clip_right);
767 let bottom = (geometry.image_y + i64::from(geometry.image_height)).min(clip_bottom);
768 if left >= right || top >= bottom {
769 return;
770 }
771
772 let source_stride = source.stride(0);
773 let destination_stride = destination.stride(0);
774 let source_data = source.data(0);
775 let destination_data = destination.data_mut(0);
776 for output_y in top..bottom {
777 let source_y = (output_y - geometry.image_y) as usize;
778 let destination_y = output_y as usize;
779 for output_x in left..right {
780 let source_x = (output_x - geometry.image_x) as usize;
781 let destination_x = output_x as usize;
782 let source_offset = source_y * source_stride + source_x * 4;
783 let destination_offset = destination_y * destination_stride + destination_x * 4;
784 let source_pixel = &source_data[source_offset..source_offset + 4];
785 let destination_pixel =
786 &mut destination_data[destination_offset..destination_offset + 4];
787 let alpha = (f32::from(source_pixel[3]) / 255.0) * opacity;
788 let inverse = 1.0 - alpha;
789 for channel in 0..3 {
790 destination_pixel[channel] = (f32::from(source_pixel[channel]) * alpha
791 + f32::from(destination_pixel[channel]) * inverse)
792 .round()
793 .clamp(0.0, 255.0) as u8;
794 }
795 destination_pixel[3] = 255;
796 }
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use std::{
803 sync::{
804 Mutex as StdMutex,
805 atomic::{AtomicBool, Ordering as AtomicOrdering},
806 },
807 thread,
808 };
809
810 use super::*;
811
812 struct CapturingSink {
813 pp_log: PpLog,
814 received: Arc<StdMutex<Vec<MediaBuffer>>>,
815 }
816
817 impl Element for CapturingSink {
818 fn name(&self) -> Arc<str> {
819 "capture".into()
820 }
821
822 fn element_type(&self) -> ElementType {
823 ElementType::Other
824 }
825
826 fn pp_log(&self) -> &PpLog {
827 &self.pp_log
828 }
829
830 fn pp_log_mut(&mut self) -> &mut PpLog {
831 &mut self.pp_log
832 }
833 }
834
835 impl Sink for CapturingSink {
836 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
837 self.received.lock().unwrap().push(buf);
838 Ok(())
839 }
840
841 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
842 Ok(())
843 }
844 }
845
846 fn options(width: u32, height: u32) -> VideoCompositorOptions {
847 VideoCompositorOptions {
848 width,
849 height,
850 frame_rate: ffmpeg::Rational::new(30, 1),
851 background: Color::BLACK,
852 }
853 }
854
855 fn solid_frame(
856 width: u32,
857 height: u32,
858 color: Color,
859 ) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
860 let pool = UnboundObjectPool::new(
861 0,
862 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
863 |_| {},
864 );
865 let mut frame = pool.get();
866 fill_background(&mut frame, color);
867 Arc::new(frame)
868 }
869
870 fn pixel(frame: &ffmpeg::frame::Video, x: usize, y: usize) -> [u8; 4] {
871 let offset = y * frame.stride(0) + x * 4;
872 frame.data(0)[offset..offset + 4].try_into().unwrap()
873 }
874
875 fn input(
876 handle: &SwVideoCompositorHandle,
877 name: &str,
878 layer: VideoLayer,
879 ) -> (Box<dyn Sink>, SwVideoLayerHandle) {
880 let input = handle.add_source(name, layer).unwrap().unwrap();
881 (input.sink, input.layer)
882 }
883
884 #[test]
885 fn composes_inputs_in_z_order_and_preserves_output_contract() {
886 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(4, 4)).unwrap();
887 let mut background = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
888 background.fit = VideoFit::Stretch;
889 let (mut red_sink, _) = input(&handle, "red", background);
890 let mut overlay = VideoLayer::new(VideoRect::new(1, 1, 2, 2));
891 overlay.z_index = 1;
892 overlay.fit = VideoFit::Stretch;
893 let (mut blue_sink, _) = input(&handle, "blue", overlay);
894 red_sink
895 .consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(255, 0, 0))))
896 .unwrap();
897 blue_sink
898 .consume(MediaBuffer::Video(solid_frame(2, 2, Color::new(0, 0, 255))))
899 .unwrap();
900
901 let frame = compositor.compose_frame().unwrap();
902 assert_eq!(frame.format(), ffmpeg::format::Pixel::BGRA);
903 assert_eq!((frame.width(), frame.height()), (4, 4));
904 assert_eq!(frame.pts(), Some(0));
905 assert_eq!(pixel(&frame, 0, 0), [0, 0, 255, 255]);
906 assert_eq!(pixel(&frame, 1, 1), [255, 0, 0, 255]);
907 }
908
909 #[test]
910 fn layer_handle_moves_blends_and_hides_a_live_source() {
911 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(3, 1)).unwrap();
912 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
913 let (mut sink, layer_handle) = input(&handle, "white", layer);
914 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::WHITE)))
915 .unwrap();
916
917 layer_handle.set_rect(VideoRect::new(1, 0, 1, 1)).unwrap();
918 layer_handle.set_opacity(0.5).unwrap();
919 let blended = compositor.compose_frame().unwrap();
920 assert_eq!(pixel(&blended, 0, 0), [0, 0, 0, 255]);
921 assert_eq!(pixel(&blended, 1, 0), [128, 128, 128, 255]);
922
923 layer_handle.set_visible(false).unwrap();
924 let hidden = compositor.compose_frame().unwrap();
925 assert_eq!(pixel(&hidden, 1, 0), [0, 0, 0, 255]);
926 assert_eq!(hidden.pts(), Some(1));
927 }
928
929 #[test]
930 fn input_keeps_only_the_latest_frame() {
931 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
932 let (mut sink, _) = input(
933 &handle,
934 "latest",
935 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
936 );
937 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
938 .unwrap();
939 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 255, 0))))
940 .unwrap();
941
942 let frame = compositor.compose_frame().unwrap();
943 assert_eq!(pixel(&frame, 0, 0), [0, 255, 0, 255]);
944 }
945
946 #[test]
947 fn frame_replacement_and_composition_run_concurrently() {
948 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
949 let (mut sink, _) = input(&handle, "live", VideoLayer::new(VideoRect::new(0, 0, 1, 1)));
950 let red = solid_frame(1, 1, Color::new(255, 0, 0));
951 let green = solid_frame(1, 1, Color::new(0, 255, 0));
952 let done = Arc::new(AtomicBool::new(false));
953 let producer_done = done.clone();
954 let producer = thread::spawn(move || {
955 for index in 0..2_000 {
956 let frame = if index % 2 == 0 {
957 red.clone()
958 } else {
959 green.clone()
960 };
961 sink.consume(MediaBuffer::Video(frame)).unwrap();
962 }
963 sink.consume(MediaBuffer::Video(green)).unwrap();
966 producer_done.store(true, AtomicOrdering::Release);
967 });
968
969 while !done.load(AtomicOrdering::Acquire) {
970 let frame = compositor.compose_frame().unwrap();
971 assert!(matches!(
972 pixel(&frame, 0, 0),
973 [0, 0, 0, 255] | [0, 0, 255, 255] | [0, 255, 0, 255]
974 ));
975 }
976 producer.join().unwrap();
977 let final_frame = compositor.compose_frame().unwrap();
978 assert_eq!(pixel(&final_frame, 0, 0), [0, 255, 0, 255]);
979 }
980
981 #[test]
982 fn replacing_a_name_invalidates_old_sink_and_layer_handle() {
983 let (mut compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
984 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
985 let (mut old_sink, old_layer) = input(&handle, "camera", layer);
986 let (mut new_sink, _) = input(&handle, "camera", layer);
987 assert!(matches!(
988 old_layer.set_visible(false),
989 Err(SwVideoCompositorError::SourceRemoved)
990 ));
991 old_sink
992 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
993 .unwrap();
994 new_sink
995 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 0, 255))))
996 .unwrap();
997
998 let frame = compositor.compose_frame().unwrap();
999 assert_eq!(pixel(&frame, 0, 0), [255, 0, 0, 255]);
1000 assert_eq!(handle.source_count(), 1);
1001 }
1002
1003 #[test]
1004 fn stop_removes_only_the_current_registration() {
1005 let (_compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1006 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
1007 let (mut old_sink, _) = input(&handle, "camera", layer);
1008 let (_new_sink, _) = input(&handle, "camera", layer);
1009 old_sink.control(ControlMsg::Stop).unwrap();
1010 assert_eq!(handle.source_count(), 1);
1011
1012 handle.remove_source("camera");
1013 assert_eq!(handle.source_count(), 0);
1014 }
1015
1016 #[test]
1017 fn contain_and_cover_preserve_aspect_ratio() {
1018 let rect = VideoRect::new(10, 20, 100, 100);
1019 let contain = layer_geometry(160, 90, rect, VideoFit::Contain).unwrap();
1020 assert_eq!((contain.image_width, contain.image_height), (100, 56));
1021 assert_eq!((contain.image_x, contain.image_y), (10, 42));
1022
1023 let cover = layer_geometry(160, 90, rect, VideoFit::Cover).unwrap();
1024 assert_eq!((cover.image_width, cover.image_height), (178, 100));
1025 assert_eq!((cover.image_x, cover.image_y), (-29, 20));
1026 }
1027
1028 #[test]
1029 fn rejects_invalid_layers_and_non_video_buffers() {
1030 let (_compositor, handle) = SwVideoCompositor::new("compositor", options(1, 1)).unwrap();
1031 let invalid = VideoLayer {
1032 opacity: 1.5,
1033 ..VideoLayer::new(VideoRect::new(0, 0, 1, 1))
1034 };
1035 assert!(matches!(
1036 handle.add_source("invalid", invalid),
1037 Err(SwVideoCompositorError::InvalidOpacity(1.5))
1038 ));
1039
1040 let (mut sink, _) = input(
1041 &handle,
1042 "valid",
1043 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
1044 );
1045 let error = sink
1046 .consume(MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty())))
1047 .unwrap_err();
1048 assert!(matches!(
1049 error,
1050 crate::Error::SwVideoCompositorError(SwVideoCompositorError::UnsupportedBuffer(
1051 "Packet"
1052 ))
1053 ));
1054 }
1055
1056 #[test]
1057 fn pushes_fixed_format_frames_with_contiguous_pts() {
1058 let (mut compositor, _) = SwVideoCompositor::new("compositor", options(2, 2)).unwrap();
1059 let received = Arc::new(StdMutex::new(Vec::new()));
1060 compositor.src_pads()[0].link(Box::new(CapturingSink {
1061 received: received.clone(),
1062 pp_log: element_pp_log(ElementType::Other, "capture", None),
1063 }));
1064 let (bus, _) = Bus::new();
1065 compositor.push_frame(&bus).unwrap();
1066 compositor.push_frame(&bus).unwrap();
1067
1068 let received = received.lock().unwrap();
1069 let pts: Vec<_> = received
1070 .iter()
1071 .filter_map(|buffer| match buffer {
1072 MediaBuffer::Video(frame) => Some(frame.pts()),
1073 _ => None,
1074 })
1075 .collect();
1076 assert_eq!(pts, vec![Some(0), Some(1)]);
1077 }
1078
1079 struct TimestampSink {
1080 pp_log: PpLog,
1081 tx: crossbeam_channel::Sender<Instant>,
1082 }
1083
1084 impl Element for TimestampSink {
1085 fn name(&self) -> Arc<str> {
1086 "timestamp-recorder".into()
1087 }
1088 fn element_type(&self) -> ElementType {
1089 ElementType::Other
1090 }
1091 fn pp_log(&self) -> &PpLog {
1092 &self.pp_log
1093 }
1094 fn pp_log_mut(&mut self) -> &mut PpLog {
1095 &mut self.pp_log
1096 }
1097 }
1098
1099 impl Sink for TimestampSink {
1100 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1101 if matches!(buf, MediaBuffer::Video(_)) {
1102 let _ = self.tx.send(Instant::now());
1103 }
1104 Ok(())
1105 }
1106 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1107 Ok(())
1108 }
1109 }
1110
1111 #[test]
1124 fn resuming_after_a_pause_preserves_output_phase() {
1125 use crate::pipeline::Pipeline;
1126
1127 let (tx, rx) = crossbeam_channel::unbounded();
1128 let sink = TimestampSink {
1129 tx,
1130 pp_log: element_pp_log(ElementType::Other, "timestamp-recorder", None),
1131 };
1132 let (compositor, _handle) = SwVideoCompositor::new(
1133 "compositor",
1134 VideoCompositorOptions {
1135 frame_rate: ffmpeg::Rational::new(10, 1),
1136 ..options(2, 2)
1137 },
1138 )
1139 .unwrap();
1140
1141 let pipeline = Pipeline::new("phase-test", compositor, |source, ctx| {
1142 let branch = ctx.branch().to(Box::new(sink))?;
1143 ctx.attach(source, 0, branch)?;
1144 Ok(())
1145 })
1146 .expect("test pipeline wiring must succeed");
1147
1148 pipeline.run().unwrap();
1149 for _ in 0..2 {
1152 rx.recv_timeout(Duration::from_millis(500))
1153 .expect("expected steady frames before pausing");
1154 }
1155 pipeline.pause();
1156 thread::sleep(Duration::from_millis(500));
1157
1158 let resumed_at = Instant::now();
1159 pipeline.resume();
1160 let first_after_resume = rx
1161 .recv_timeout(Duration::from_millis(500))
1162 .expect("expected a frame after resume");
1163 pipeline.stop();
1164 pipeline.bus().log_events();
1165
1166 let gap = first_after_resume.saturating_duration_since(resumed_at);
1167 assert!(
1168 gap >= Duration::from_millis(50),
1169 "expected the post-pause frame to land close to a full 100ms \
1170 interval after resume (phase preserved from before the \
1171 pause), not almost immediately (phase reset to the resume \
1172 instant): got {gap:?}"
1173 );
1174 }
1175}