Skip to content

Commit 3a172c0

Browse files
feat: Ensure monotonic PTS for video and audio frames (fixes instant mode pausing) (#1240)
* featL Ensure monotonic PTS for video and audio frames * CodeRabbit suggestion
1 parent 598d31d commit 3a172c0

File tree

1 file changed

+111
-9
lines changed
  • crates/enc-avfoundation/src

1 file changed

+111
-9
lines changed

crates/enc-avfoundation/src/mp4.rs

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use cap_media_info::{AudioInfo, VideoInfo};
22
use cidre::{cm::SampleTimingInfo, objc::Obj, *};
33
use ffmpeg::frame;
44
use std::{ops::Sub, path::PathBuf, time::Duration};
5-
use tracing::{debug, error, info};
5+
use tracing::{debug, error, info, trace};
66

77
// before pausing at all, subtract 0.
88
// on pause, record last frame time.
@@ -24,6 +24,9 @@ pub struct MP4Encoder {
2424
// elapsed_duration: cm::Time,
2525
video_frames_appended: usize,
2626
audio_frames_appended: usize,
27+
last_timestamp: Option<Duration>,
28+
last_video_pts: Option<Duration>,
29+
last_audio_pts: Option<Duration>,
2730
}
2831

2932
#[derive(thiserror::Error, Debug)]
@@ -204,6 +207,9 @@ impl MP4Encoder {
204207
is_paused: false,
205208
video_frames_appended: 0,
206209
audio_frames_appended: 0,
210+
last_timestamp: None,
211+
last_video_pts: None,
212+
last_audio_pts: None,
207213
})
208214
}
209215

@@ -227,15 +233,41 @@ impl MP4Encoder {
227233
self.most_recent_frame = Some((frame.clone(), timestamp));
228234

229235
if let Some(pause_timestamp) = self.pause_timestamp {
230-
self.timestamp_offset += timestamp - pause_timestamp;
231-
self.pause_timestamp = None;
236+
if let Some(gap) = timestamp.checked_sub(pause_timestamp) {
237+
self.timestamp_offset += gap;
238+
self.pause_timestamp = None;
239+
}
232240
}
233241

242+
let mut pts_duration = timestamp
243+
.checked_sub(self.timestamp_offset)
244+
.unwrap_or(Duration::ZERO);
245+
246+
if let Some(last_pts) = self.last_video_pts {
247+
if pts_duration <= last_pts {
248+
let frame_duration = self.video_frame_duration();
249+
let adjusted_pts = last_pts + frame_duration;
250+
251+
trace!(
252+
?timestamp,
253+
?last_pts,
254+
adjusted_pts = ?adjusted_pts,
255+
frame_duration_ns = frame_duration.as_nanos(),
256+
"Monotonic video pts correction",
257+
);
258+
259+
if let Some(new_offset) = timestamp.checked_sub(adjusted_pts) {
260+
self.timestamp_offset = new_offset;
261+
}
262+
263+
pts_duration = adjusted_pts;
264+
}
265+
}
266+
267+
self.last_video_pts = Some(pts_duration);
268+
234269
let mut timing = frame.timing_info(0).unwrap();
235-
timing.pts = cm::Time::new(
236-
timestamp.sub(self.timestamp_offset).as_millis() as i64,
237-
1_000,
238-
);
270+
timing.pts = cm::Time::new(pts_duration.as_millis() as i64, 1_000);
239271
let frame = frame.copy_with_new_timing(&[timing]).unwrap();
240272

241273
self.video_input
@@ -244,6 +276,7 @@ impl MP4Encoder {
244276
.and_then(|v| v.then_some(()).ok_or(QueueVideoFrameError::Failed))?;
245277

246278
self.video_frames_appended += 1;
279+
self.last_timestamp = Some(timestamp);
247280

248281
Ok(())
249282
}
@@ -259,6 +292,13 @@ impl MP4Encoder {
259292
return Ok(());
260293
}
261294

295+
if let Some(pause_timestamp) = self.pause_timestamp {
296+
if let Some(gap) = timestamp.checked_sub(pause_timestamp) {
297+
self.timestamp_offset += gap;
298+
self.pause_timestamp = None;
299+
}
300+
}
301+
262302
let Some(audio_input) = &mut self.audio_input else {
263303
return Err(QueueAudioFrameError::NoAudioInput);
264304
};
@@ -297,8 +337,37 @@ impl MP4Encoder {
297337
let format_desc =
298338
cm::AudioFormatDesc::with_asbd(&audio_desc).map_err(QueueAudioFrameError::Setup)?;
299339

340+
let mut pts_duration = timestamp
341+
.checked_sub(self.timestamp_offset)
342+
.unwrap_or(Duration::ZERO);
343+
344+
if let Some(last_pts) = self.last_audio_pts {
345+
if pts_duration <= last_pts {
346+
let frame_duration = Self::audio_frame_duration(&frame);
347+
let adjusted_pts = last_pts + frame_duration;
348+
349+
trace!(
350+
?timestamp,
351+
?last_pts,
352+
adjusted_pts = ?adjusted_pts,
353+
frame_duration_ns = frame_duration.as_nanos(),
354+
samples = frame.samples(),
355+
sample_rate = frame.rate(),
356+
"Monotonic audio pts correction",
357+
);
358+
359+
if let Some(new_offset) = timestamp.checked_sub(adjusted_pts) {
360+
self.timestamp_offset = new_offset;
361+
}
362+
363+
pts_duration = adjusted_pts;
364+
}
365+
}
366+
367+
self.last_audio_pts = Some(pts_duration);
368+
300369
let pts = cm::Time::new(
301-
(timestamp.sub(self.timestamp_offset).as_secs_f64() * frame.rate() as f64) as i64,
370+
(pts_duration.as_secs_f64() * frame.rate() as f64) as i64,
302371
frame.rate() as i32,
303372
);
304373

@@ -322,16 +391,49 @@ impl MP4Encoder {
322391
.and_then(|v| v.then_some(()).ok_or(QueueAudioFrameError::Failed))?;
323392

324393
self.audio_frames_appended += 1;
394+
self.last_timestamp = Some(timestamp);
325395

326396
Ok(())
327397
}
328398

399+
fn video_frame_duration(&self) -> Duration {
400+
let fps_num = self.config.frame_rate.0;
401+
let fps_den = self.config.frame_rate.1;
402+
403+
if fps_num <= 0 {
404+
return Duration::from_millis(1);
405+
}
406+
407+
let numerator = fps_den.unsigned_abs() as u128 * 1_000_000_000u128;
408+
let denominator = fps_num as u128;
409+
let nanos = (numerator / denominator).max(1);
410+
411+
Duration::from_nanos(nanos as u64)
412+
}
413+
414+
fn audio_frame_duration(frame: &frame::Audio) -> Duration {
415+
let rate = frame.rate();
416+
417+
if rate == 0 {
418+
return Duration::from_millis(1);
419+
}
420+
421+
let samples = frame.samples() as u128;
422+
if samples == 0 {
423+
return Duration::from_nanos(1);
424+
}
425+
426+
let nanos = (samples * 1_000_000_000u128) / rate as u128;
427+
428+
Duration::from_nanos(nanos.max(1) as u64)
429+
}
430+
329431
pub fn pause(&mut self) {
330432
if self.is_paused || !self.is_writing {
331433
return;
332434
}
333435

334-
let Some((_, timestamp)) = self.most_recent_frame else {
436+
let Some(timestamp) = self.last_timestamp else {
335437
return;
336438
};
337439

0 commit comments

Comments
 (0)