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