1use std::{
2 sync::Arc,
3 thread,
4 time::{Duration, Instant},
5};
6
7use crate::pp_log::{PpLog, pp_info};
8use ffmpeg_next as ffmpeg;
9use thiserror::Error as ThisError;
10
11use crate::{
12 buffer::MediaBuffer,
13 bus::{Bus, BusEvent},
14 contract::{MediaKind, MemoryDomain, OutputContract, PortContract},
15 control::{ControlReceiver, drain_control},
16 element::{Element, ElementType, Source, SourceElement, element_pp_log},
17 pad::SrcPad,
18 pool::UnboundObjectPool,
19 schedule::PeriodicSchedule,
20};
21
22#[derive(Debug, ThisError)]
25pub enum TestVideoSourceError {
26 #[error("TestVideoSource doesn't support seeking a generated stream")]
28 SeekUnsupported,
29}
30
31#[derive(Debug, Clone, Copy)]
33pub struct TestVideoOptions {
34 pub width: u32,
36 pub height: u32,
38 pub framerate: ffmpeg::Rational,
47}
48
49impl Default for TestVideoOptions {
50 fn default() -> Self {
51 Self {
52 width: 640,
53 height: 480,
54 framerate: ffmpeg::Rational::new(30, 1),
55 }
56 }
57}
58
59pub struct TestVideoSource {
99 pp_log: PpLog,
100 name: Arc<str>,
101 options: TestVideoOptions,
102 pad: SrcPad,
103 frame_index: i64,
104 frame_interval: Duration,
110 pool: UnboundObjectPool<ffmpeg::frame::Video>,
115}
116
117impl TestVideoSource {
118 pub fn new(name: impl Into<String>, options: TestVideoOptions) -> Self {
120 let name: Arc<str> = name.into().into();
121 let pp_log = element_pp_log(ElementType::TestVideoSource, &name, None);
122 let pad = SrcPad::with_contract(
123 format!("{name}_src"),
124 OutputContract::Fixed(PortContract::frame(
125 MediaKind::VideoFrame,
126 MemoryDomain::System,
127 )),
128 );
129 pp_info!(
130 pp_log: &pp_log,
131 "created: {}x{}, framerate={}",
132 options.width,
133 options.height,
134 options.framerate
135 );
136 let (width, height) = (options.width, options.height);
137 let pool = UnboundObjectPool::new(
138 0,
139 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, width, height),
140 |_| {},
141 );
142 let frame_interval = if options.framerate.numerator() > 0 {
144 Duration::from_secs_f64(
145 options.framerate.denominator() as f64 / options.framerate.numerator() as f64,
146 )
147 } else {
148 Duration::ZERO
149 };
150 Self {
151 name,
152 pp_log,
153 options,
154 pad,
155 frame_index: 0,
156 frame_interval,
157 pool,
158 }
159 }
160
161 pub fn time_base(&self) -> ffmpeg::Rational {
164 ffmpeg::Rational::new(
165 self.options.framerate.denominator(),
166 self.options.framerate.numerator(),
167 )
168 }
169
170 fn generate_frame(&mut self) -> crate::pool::UnboundObjectPoolRef<ffmpeg::frame::Video> {
175 let mut frame = self.pool.get();
176
177 let offset = self.frame_index;
178 let width = self.options.width as usize;
179 let y_stride = frame.stride(0);
180 let y_height = frame.plane_height(0) as usize;
181 {
182 let y_plane = frame.data_mut(0);
183 for row in 0..y_height {
184 for col in 0..width {
185 y_plane[row * y_stride + col] =
186 ((col as i64 + row as i64 + offset) % 256) as u8;
187 }
188 }
189 }
190 for plane in [1usize, 2usize] {
191 frame.data_mut(plane).fill(128);
192 }
193
194 frame.set_pts(Some(self.frame_index));
195 self.frame_index += 1;
196 frame
197 }
198}
199
200impl Element for TestVideoSource {
201 fn name(&self) -> Arc<str> {
202 self.name.clone()
203 }
204
205 fn element_type(&self) -> ElementType {
206 ElementType::TestVideoSource
207 }
208
209 fn pp_log(&self) -> &crate::pp_log::PpLog {
210 &self.pp_log
211 }
212
213 fn pp_log_mut(&mut self) -> &mut crate::pp_log::PpLog {
214 &mut self.pp_log
215 }
216}
217
218impl Source for TestVideoSource {
219 fn src_pads(&mut self) -> &mut [SrcPad] {
220 std::slice::from_mut(&mut self.pad)
221 }
222}
223
224impl SourceElement for TestVideoSource {
225 fn is_live(&self) -> bool {
226 true
227 }
228
229 fn is_seekable(&self) -> bool {
230 false
231 }
232
233 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> crate::error::Result<()> {
234 pp_info!(self, "started");
235 let mut schedule = PeriodicSchedule::new(self.frame_interval, Instant::now());
236 loop {
237 let outcome = drain_control(control, self, bus)?;
238 if outcome.stopped {
239 pp_info!(self, "stopped");
240 return Ok(());
241 }
242 if outcome.paused_for > Duration::ZERO {
243 schedule.resume_after_pause(outcome.paused_for, Instant::now());
244 }
245 thread::sleep(schedule.remaining(Instant::now()));
246
247 let frame = self.generate_frame();
248 if let Err(error) = self.pad.push(MediaBuffer::Video(Arc::new(frame))) {
253 bus.post(
254 &self.pp_log,
255 BusEvent::Error {
256 element_type: ElementType::TestVideoSource,
257 name: self.name.clone(),
258 error,
259 },
260 );
261 }
262 schedule.advance_after_tick(Instant::now());
268 }
269 }
270
271 fn seek(&mut self, _target: std::time::Duration) -> crate::error::Result<std::time::Duration> {
272 Err(TestVideoSourceError::SeekUnsupported.into())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use std::{sync::Mutex, thread, time::Duration};
279
280 use crate::pp_log::PpLog;
281
282 use super::*;
283 use crate::{control::ControlMsg, element::Sink, pipeline::Pipeline};
284
285 type VideoObservation = (ffmpeg::format::Pixel, u32, u32, Option<i64>);
286 type RecordedFrames = Arc<Mutex<Vec<VideoObservation>>>;
287
288 struct RecordingSink {
292 pp_log: PpLog,
293 seen: RecordedFrames,
294 }
295
296 impl Element for RecordingSink {
297 fn name(&self) -> Arc<str> {
298 "recorder".into()
299 }
300 fn element_type(&self) -> ElementType {
301 ElementType::Other
302 }
303 fn pp_log(&self) -> &PpLog {
304 &self.pp_log
305 }
306 fn pp_log_mut(&mut self) -> &mut PpLog {
307 &mut self.pp_log
308 }
309 }
310
311 impl Sink for RecordingSink {
312 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
313 if let MediaBuffer::Video(frame) = buf {
314 self.seen.lock().unwrap().push((
315 frame.format(),
316 frame.width(),
317 frame.height(),
318 frame.pts(),
319 ));
320 }
321 Ok(())
322 }
323 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
324 Ok(())
325 }
326 }
327
328 #[test]
329 fn generates_correctly_sized_yuv420p_frames_with_increasing_pts() {
330 let seen = Arc::new(Mutex::new(Vec::new()));
331 let sink = RecordingSink {
332 seen: seen.clone(),
333 pp_log: element_pp_log(ElementType::Other, "recorder", None),
334 };
335 let source = TestVideoSource::new(
336 "test-video",
337 TestVideoOptions {
338 width: 16,
339 height: 16,
340 framerate: ffmpeg::Rational::new(30, 1),
341 },
342 );
343
344 let pipeline = Pipeline::new("test", source, |source, ctx| {
345 let branch = ctx.branch().to(Box::new(sink))?;
346 ctx.attach(source, 0, branch)?;
347 Ok(())
348 })
349 .expect("test pipeline wiring must succeed");
350
351 pipeline.run().unwrap();
352 thread::sleep(Duration::from_millis(200));
356 pipeline.stop();
357
358 pipeline.bus().log_events();
361
362 let frames = seen.lock().unwrap();
363 assert!(!frames.is_empty(), "expected at least one generated frame");
364 for window in frames.windows(2) {
365 let (format, width, height, pts) = window[0];
366 assert_eq!(format, ffmpeg::format::Pixel::YUV420P);
367 assert_eq!((width, height), (16, 16));
368 assert!(
369 window[1].3 > pts,
370 "expected pts to strictly increase frame over frame, got {:?} then {:?}",
371 pts,
372 window[1].3
373 );
374 }
375 }
376
377 #[test]
378 fn seek_is_explicitly_unsupported() {
379 let mut source = TestVideoSource::new("test-video", TestVideoOptions::default());
380 assert!(source.seek(Duration::from_secs(1)).is_err());
381 }
382
383 struct TimestampSink {
388 pp_log: PpLog,
389 seen: Arc<Mutex<Vec<Instant>>>,
390 }
391
392 impl Element for TimestampSink {
393 fn name(&self) -> Arc<str> {
394 "timestamp-recorder".into()
395 }
396 fn element_type(&self) -> ElementType {
397 ElementType::Other
398 }
399 fn pp_log(&self) -> &PpLog {
400 &self.pp_log
401 }
402 fn pp_log_mut(&mut self) -> &mut PpLog {
403 &mut self.pp_log
404 }
405 }
406
407 impl Sink for TimestampSink {
408 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
409 if matches!(buf, MediaBuffer::Video(_)) {
410 self.seen.lock().unwrap().push(Instant::now());
411 }
412 Ok(())
413 }
414 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
415 Ok(())
416 }
417 }
418
419 #[test]
428 fn resuming_after_a_pause_does_not_dump_a_burst_of_catch_up_frames() {
429 let seen = Arc::new(Mutex::new(Vec::new()));
430 let sink = TimestampSink {
431 seen: seen.clone(),
432 pp_log: element_pp_log(ElementType::Other, "timestamp-recorder", None),
433 };
434 let source = TestVideoSource::new(
435 "test-video",
436 TestVideoOptions {
437 width: 16,
438 height: 16,
439 framerate: ffmpeg::Rational::new(50, 1), },
441 );
442
443 let pipeline = Pipeline::new("pause-resume-test", source, |source, ctx| {
444 let branch = ctx.branch().to(Box::new(sink))?;
445 ctx.attach(source, 0, branch)?;
446 Ok(())
447 })
448 .expect("test pipeline wiring must succeed");
449
450 pipeline.run().unwrap();
451 thread::sleep(Duration::from_millis(60));
452 pipeline.pause();
453 thread::sleep(Duration::from_millis(400));
454
455 let resumed_at = Instant::now();
456 pipeline.resume();
457 thread::sleep(Duration::from_millis(120));
458 pipeline.stop();
459 pipeline.bus().log_events();
460
461 let after_resume = seen
462 .lock()
463 .unwrap()
464 .iter()
465 .filter(|&&t| t >= resumed_at)
466 .count();
467 assert!(
472 after_resume <= 12,
473 "expected a steady framerate after resume, not a burst of catch-up frames: \
474 {after_resume} frames arrived within 120ms of resuming"
475 );
476 }
477
478 struct SlowFirstFrameSink {
479 pp_log: PpLog,
480 tx: crossbeam_channel::Sender<Instant>,
481 slow_duration: Duration,
482 delayed: bool,
483 }
484
485 impl Element for SlowFirstFrameSink {
486 fn name(&self) -> Arc<str> {
487 "slow-sink".into()
488 }
489 fn element_type(&self) -> ElementType {
490 ElementType::Other
491 }
492 fn pp_log(&self) -> &PpLog {
493 &self.pp_log
494 }
495 fn pp_log_mut(&mut self) -> &mut PpLog {
496 &mut self.pp_log
497 }
498 }
499
500 impl Sink for SlowFirstFrameSink {
501 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
502 if matches!(buf, MediaBuffer::Video(_)) {
503 if !self.delayed {
504 self.delayed = true;
505 thread::sleep(self.slow_duration);
506 }
507 let _ = self.tx.send(Instant::now());
512 }
513 Ok(())
514 }
515 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
516 Ok(())
517 }
518 }
519
520 #[test]
534 fn a_slow_sink_does_not_cause_a_burst_of_catch_up_frames() {
535 let (tx, rx) = crossbeam_channel::unbounded();
536 let sink = SlowFirstFrameSink {
537 tx,
538 slow_duration: Duration::from_millis(300),
539 delayed: false,
540 pp_log: element_pp_log(ElementType::Other, "slow-sink", None),
541 };
542 let source = TestVideoSource::new(
543 "test-video",
544 TestVideoOptions {
545 width: 16,
546 height: 16,
547 framerate: ffmpeg::Rational::new(20, 1), },
549 );
550
551 let pipeline = Pipeline::new("slow-sink-test", source, |source, ctx| {
552 let branch = ctx.branch().to(Box::new(sink))?;
553 ctx.attach(source, 0, branch)?;
554 Ok(())
555 })
556 .expect("test pipeline wiring must succeed");
557
558 pipeline.run().unwrap();
559 let slow_done = rx
560 .recv_timeout(Duration::from_secs(1))
561 .expect("expected the first (slow) frame to finish");
562 let after_slow = rx
563 .recv_timeout(Duration::from_millis(500))
564 .expect("expected the frame right after the slow one");
565 let steady = rx
566 .recv_timeout(Duration::from_millis(500))
567 .expect("expected a third frame at steady cadence");
568 pipeline.stop();
569 pipeline.bus().log_events();
570
571 let immediate_gap = after_slow.saturating_duration_since(slow_done);
572 assert!(
573 immediate_gap >= Duration::from_millis(25),
574 "expected the frame right after the slow one to wait a steady \
575 ~50ms interval, not follow immediately just because the slow \
576 sink had finally caught up: got {immediate_gap:?}"
577 );
578
579 let gap = steady.saturating_duration_since(after_slow);
580 assert!(
581 gap >= Duration::from_millis(25),
582 "expected steady ~50ms cadence once the slow sink caught up, not a \
583 burst of catch-up frames immediately following it: got {gap:?}"
584 );
585 }
586}