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 control::{ControlReceiver, drain_control},
15 element::{Element, ElementType, Source, SourceElement, element_pp_log},
16 pad::SrcPad,
17 pool::UnboundObjectPool,
18 schedule::PeriodicSchedule,
19};
20
21#[derive(Debug, ThisError)]
24pub enum TestVideoSourceError {
25 #[error("TestVideoSource doesn't support seeking a generated stream")]
26 SeekUnsupported,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct TestVideoOptions {
32 pub width: u32,
33 pub height: u32,
34 pub framerate: ffmpeg::Rational,
43}
44
45impl Default for TestVideoOptions {
46 fn default() -> Self {
47 Self {
48 width: 640,
49 height: 480,
50 framerate: ffmpeg::Rational::new(30, 1),
51 }
52 }
53}
54
55pub struct TestVideoSource {
95 pp_log: PpLog,
96 name: Arc<str>,
97 options: TestVideoOptions,
98 pad: SrcPad,
99 frame_index: i64,
100 frame_interval: Duration,
106 pool: UnboundObjectPool<ffmpeg::frame::Video>,
111}
112
113impl TestVideoSource {
114 pub fn new(name: impl Into<String>, options: TestVideoOptions) -> Self {
115 let name: Arc<str> = name.into().into();
116 let pp_log = element_pp_log(ElementType::TestVideoSource, &name, None);
117 let pad = SrcPad::new(format!("{name}_src"));
118 pp_info!(
119 pp_log: &pp_log,
120 "created: {}x{}, framerate={}",
121 options.width,
122 options.height,
123 options.framerate
124 );
125 let (width, height) = (options.width, options.height);
126 let pool = UnboundObjectPool::new(
127 0,
128 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUV420P, width, height),
129 |_| {},
130 );
131 let frame_interval = if options.framerate.numerator() > 0 {
133 Duration::from_secs_f64(
134 options.framerate.denominator() as f64 / options.framerate.numerator() as f64,
135 )
136 } else {
137 Duration::ZERO
138 };
139 Self {
140 name,
141 pp_log,
142 options,
143 pad,
144 frame_index: 0,
145 frame_interval,
146 pool,
147 }
148 }
149
150 pub fn time_base(&self) -> ffmpeg::Rational {
153 ffmpeg::Rational::new(
154 self.options.framerate.denominator(),
155 self.options.framerate.numerator(),
156 )
157 }
158
159 fn generate_frame(&mut self) -> crate::pool::UnboundObjectPoolRef<ffmpeg::frame::Video> {
164 let mut frame = self.pool.get();
165
166 let offset = self.frame_index;
167 let width = self.options.width as usize;
168 let y_stride = frame.stride(0);
169 let y_height = frame.plane_height(0) as usize;
170 {
171 let y_plane = frame.data_mut(0);
172 for row in 0..y_height {
173 for col in 0..width {
174 y_plane[row * y_stride + col] =
175 ((col as i64 + row as i64 + offset) % 256) as u8;
176 }
177 }
178 }
179 for plane in [1usize, 2usize] {
180 frame.data_mut(plane).fill(128);
181 }
182
183 frame.set_pts(Some(self.frame_index));
184 self.frame_index += 1;
185 frame
186 }
187}
188
189impl Element for TestVideoSource {
190 fn name(&self) -> Arc<str> {
191 self.name.clone()
192 }
193
194 fn element_type(&self) -> ElementType {
195 ElementType::TestVideoSource
196 }
197
198 fn pp_log(&self) -> &crate::pp_log::PpLog {
199 &self.pp_log
200 }
201
202 fn pp_log_mut(&mut self) -> &mut crate::pp_log::PpLog {
203 &mut self.pp_log
204 }
205}
206
207impl Source for TestVideoSource {
208 fn src_pads(&mut self) -> &mut [SrcPad] {
209 std::slice::from_mut(&mut self.pad)
210 }
211}
212
213impl SourceElement for TestVideoSource {
214 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> crate::error::Result<()> {
215 pp_info!(self, "started");
216 let mut schedule = PeriodicSchedule::new(self.frame_interval, Instant::now());
217 loop {
218 let outcome = drain_control(control, self, bus)?;
219 if outcome.stopped {
220 pp_info!(self, "stopped");
221 return Ok(());
222 }
223 if outcome.paused_for > Duration::ZERO {
224 schedule.resume_after_pause(outcome.paused_for, Instant::now());
225 }
226 thread::sleep(schedule.remaining(Instant::now()));
227
228 let frame = self.generate_frame();
229 if let Err(error) = self.pad.push(MediaBuffer::Video(Arc::new(frame))) {
234 bus.post(
235 &self.pp_log,
236 BusEvent::Error {
237 element_type: ElementType::TestVideoSource,
238 name: self.name.clone(),
239 error,
240 },
241 );
242 }
243 schedule.advance_after_tick(Instant::now());
249 }
250 }
251
252 fn seek(&mut self, _target: std::time::Duration) -> crate::error::Result<std::time::Duration> {
253 Err(TestVideoSourceError::SeekUnsupported.into())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use std::{sync::Mutex, thread, time::Duration};
260
261 use crate::pp_log::PpLog;
262
263 use super::*;
264 use crate::{control::ControlMsg, element::Sink, pipeline::Pipeline};
265
266 type VideoObservation = (ffmpeg::format::Pixel, u32, u32, Option<i64>);
267 type RecordedFrames = Arc<Mutex<Vec<VideoObservation>>>;
268
269 struct RecordingSink {
273 pp_log: PpLog,
274 seen: RecordedFrames,
275 }
276
277 impl Element for RecordingSink {
278 fn name(&self) -> Arc<str> {
279 "recorder".into()
280 }
281 fn element_type(&self) -> ElementType {
282 ElementType::Other
283 }
284 fn pp_log(&self) -> &PpLog {
285 &self.pp_log
286 }
287 fn pp_log_mut(&mut self) -> &mut PpLog {
288 &mut self.pp_log
289 }
290 }
291
292 impl Sink for RecordingSink {
293 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
294 if let MediaBuffer::Video(frame) = buf {
295 self.seen.lock().unwrap().push((
296 frame.format(),
297 frame.width(),
298 frame.height(),
299 frame.pts(),
300 ));
301 }
302 Ok(())
303 }
304 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
305 Ok(())
306 }
307 }
308
309 #[test]
310 fn generates_correctly_sized_yuv420p_frames_with_increasing_pts() {
311 let seen = Arc::new(Mutex::new(Vec::new()));
312 let sink = RecordingSink {
313 seen: seen.clone(),
314 pp_log: element_pp_log(ElementType::Other, "recorder", None),
315 };
316 let source = TestVideoSource::new(
317 "test-video",
318 TestVideoOptions {
319 width: 16,
320 height: 16,
321 framerate: ffmpeg::Rational::new(30, 1),
322 },
323 );
324
325 let pipeline = Pipeline::new("test", source, |source, ctx| {
326 let branch = ctx.branch().to(Box::new(sink))?;
327 ctx.attach(source, 0, branch)?;
328 Ok(())
329 })
330 .expect("test pipeline wiring must succeed");
331
332 pipeline.run();
333 thread::sleep(Duration::from_millis(200));
337 pipeline.stop();
338
339 pipeline.bus().log_events();
342
343 let frames = seen.lock().unwrap();
344 assert!(!frames.is_empty(), "expected at least one generated frame");
345 for window in frames.windows(2) {
346 let (format, width, height, pts) = window[0];
347 assert_eq!(format, ffmpeg::format::Pixel::YUV420P);
348 assert_eq!((width, height), (16, 16));
349 assert!(
350 window[1].3 > pts,
351 "expected pts to strictly increase frame over frame, got {:?} then {:?}",
352 pts,
353 window[1].3
354 );
355 }
356 }
357
358 #[test]
359 fn seek_is_explicitly_unsupported() {
360 let mut source = TestVideoSource::new("test-video", TestVideoOptions::default());
361 assert!(source.seek(Duration::from_secs(1)).is_err());
362 }
363
364 struct TimestampSink {
369 pp_log: PpLog,
370 seen: Arc<Mutex<Vec<Instant>>>,
371 }
372
373 impl Element for TimestampSink {
374 fn name(&self) -> Arc<str> {
375 "timestamp-recorder".into()
376 }
377 fn element_type(&self) -> ElementType {
378 ElementType::Other
379 }
380 fn pp_log(&self) -> &PpLog {
381 &self.pp_log
382 }
383 fn pp_log_mut(&mut self) -> &mut PpLog {
384 &mut self.pp_log
385 }
386 }
387
388 impl Sink for TimestampSink {
389 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
390 if matches!(buf, MediaBuffer::Video(_)) {
391 self.seen.lock().unwrap().push(Instant::now());
392 }
393 Ok(())
394 }
395 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
396 Ok(())
397 }
398 }
399
400 #[test]
409 fn resuming_after_a_pause_does_not_dump_a_burst_of_catch_up_frames() {
410 let seen = Arc::new(Mutex::new(Vec::new()));
411 let sink = TimestampSink {
412 seen: seen.clone(),
413 pp_log: element_pp_log(ElementType::Other, "timestamp-recorder", None),
414 };
415 let source = TestVideoSource::new(
416 "test-video",
417 TestVideoOptions {
418 width: 16,
419 height: 16,
420 framerate: ffmpeg::Rational::new(50, 1), },
422 );
423
424 let pipeline = Pipeline::new("pause-resume-test", source, |source, ctx| {
425 let branch = ctx.branch().to(Box::new(sink))?;
426 ctx.attach(source, 0, branch)?;
427 Ok(())
428 })
429 .expect("test pipeline wiring must succeed");
430
431 pipeline.run();
432 thread::sleep(Duration::from_millis(60));
433 pipeline.pause();
434 thread::sleep(Duration::from_millis(400));
435
436 let resumed_at = Instant::now();
437 pipeline.resume();
438 thread::sleep(Duration::from_millis(120));
439 pipeline.stop();
440 pipeline.bus().log_events();
441
442 let after_resume = seen
443 .lock()
444 .unwrap()
445 .iter()
446 .filter(|&&t| t >= resumed_at)
447 .count();
448 assert!(
453 after_resume <= 12,
454 "expected a steady framerate after resume, not a burst of catch-up frames: \
455 {after_resume} frames arrived within 120ms of resuming"
456 );
457 }
458
459 struct SlowFirstFrameSink {
460 pp_log: PpLog,
461 tx: crossbeam_channel::Sender<Instant>,
462 slow_duration: Duration,
463 delayed: bool,
464 }
465
466 impl Element for SlowFirstFrameSink {
467 fn name(&self) -> Arc<str> {
468 "slow-sink".into()
469 }
470 fn element_type(&self) -> ElementType {
471 ElementType::Other
472 }
473 fn pp_log(&self) -> &PpLog {
474 &self.pp_log
475 }
476 fn pp_log_mut(&mut self) -> &mut PpLog {
477 &mut self.pp_log
478 }
479 }
480
481 impl Sink for SlowFirstFrameSink {
482 fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
483 if matches!(buf, MediaBuffer::Video(_)) {
484 if !self.delayed {
485 self.delayed = true;
486 thread::sleep(self.slow_duration);
487 }
488 let _ = self.tx.send(Instant::now());
493 }
494 Ok(())
495 }
496 fn control(&mut self, _msg: ControlMsg) -> crate::error::Result<()> {
497 Ok(())
498 }
499 }
500
501 #[test]
515 fn a_slow_sink_does_not_cause_a_burst_of_catch_up_frames() {
516 let (tx, rx) = crossbeam_channel::unbounded();
517 let sink = SlowFirstFrameSink {
518 tx,
519 slow_duration: Duration::from_millis(300),
520 delayed: false,
521 pp_log: element_pp_log(ElementType::Other, "slow-sink", None),
522 };
523 let source = TestVideoSource::new(
524 "test-video",
525 TestVideoOptions {
526 width: 16,
527 height: 16,
528 framerate: ffmpeg::Rational::new(20, 1), },
530 );
531
532 let pipeline = Pipeline::new("slow-sink-test", source, |source, ctx| {
533 let branch = ctx.branch().to(Box::new(sink))?;
534 ctx.attach(source, 0, branch)?;
535 Ok(())
536 })
537 .expect("test pipeline wiring must succeed");
538
539 pipeline.run();
540 let slow_done = rx
541 .recv_timeout(Duration::from_secs(1))
542 .expect("expected the first (slow) frame to finish");
543 let after_slow = rx
544 .recv_timeout(Duration::from_millis(500))
545 .expect("expected the frame right after the slow one");
546 let steady = rx
547 .recv_timeout(Duration::from_millis(500))
548 .expect("expected a third frame at steady cadence");
549 pipeline.stop();
550 pipeline.bus().log_events();
551
552 let immediate_gap = after_slow.saturating_duration_since(slow_done);
553 assert!(
554 immediate_gap >= Duration::from_millis(25),
555 "expected the frame right after the slow one to wait a steady \
556 ~50ms interval, not follow immediately just because the slow \
557 sink had finally caught up: got {immediate_gap:?}"
558 );
559
560 let gap = steady.saturating_duration_since(after_slow);
561 assert!(
562 gap >= Duration::from_millis(25),
563 "expected steady ~50ms cadence once the slow sink caught up, not a \
564 burst of catch-up frames immediately following it: got {gap:?}"
565 );
566 }
567}