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