Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 111 additions & 9 deletions crates/enc-avfoundation/src/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use cidre::{cm::SampleTimingInfo, objc::Obj, *};
use ffmpeg::frame;
use std::{ops::Sub, path::PathBuf, time::Duration};
use tracing::{debug, error, info};
use tracing::{debug, error, info, trace};

// before pausing at all, subtract 0.
// on pause, record last frame time.
Expand All @@ -24,6 +24,9 @@
// elapsed_duration: cm::Time,
video_frames_appended: usize,
audio_frames_appended: usize,
last_timestamp: Option<Duration>,
last_video_pts: Option<Duration>,
last_audio_pts: Option<Duration>,
}

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -204,6 +207,9 @@
is_paused: false,
video_frames_appended: 0,
audio_frames_appended: 0,
last_timestamp: None,
last_video_pts: None,
last_audio_pts: None,
})
}

Expand All @@ -227,15 +233,41 @@
self.most_recent_frame = Some((frame.clone(), timestamp));

if let Some(pause_timestamp) = self.pause_timestamp {
self.timestamp_offset += timestamp - pause_timestamp;
self.pause_timestamp = None;
if let Some(gap) = timestamp.checked_sub(pause_timestamp) {
self.timestamp_offset += gap;
self.pause_timestamp = None;
}
}

let mut pts_duration = timestamp
.checked_sub(self.timestamp_offset)
.unwrap_or(Duration::ZERO);

if let Some(last_pts) = self.last_video_pts {
if pts_duration <= last_pts {
let frame_duration = self.video_frame_duration();
let adjusted_pts = last_pts + frame_duration;

trace!(
?timestamp,
?last_pts,
adjusted_pts = ?adjusted_pts,
frame_duration_ns = frame_duration.as_nanos(),
"Monotonic video pts correction",
);

if let Some(new_offset) = timestamp.checked_sub(adjusted_pts) {
self.timestamp_offset = new_offset;
}

pts_duration = adjusted_pts;
}
}

self.last_video_pts = Some(pts_duration);

let mut timing = frame.timing_info(0).unwrap();
timing.pts = cm::Time::new(
timestamp.sub(self.timestamp_offset).as_millis() as i64,
1_000,
);
timing.pts = cm::Time::new(pts_duration.as_millis() as i64, 1_000);
let frame = frame.copy_with_new_timing(&[timing]).unwrap();

self.video_input
Expand All @@ -244,6 +276,7 @@
.and_then(|v| v.then_some(()).ok_or(QueueVideoFrameError::Failed))?;

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

Ok(())
}
Expand All @@ -259,6 +292,13 @@
return Ok(());
}

if let Some(pause_timestamp) = self.pause_timestamp {
if let Some(gap) = timestamp.checked_sub(pause_timestamp) {
self.timestamp_offset += gap;
self.pause_timestamp = None;
}
}

let Some(audio_input) = &mut self.audio_input else {
return Err(QueueAudioFrameError::NoAudioInput);
};
Expand Down Expand Up @@ -297,8 +337,37 @@
let format_desc =
cm::AudioFormatDesc::with_asbd(&audio_desc).map_err(QueueAudioFrameError::Setup)?;

let mut pts_duration = timestamp
.checked_sub(self.timestamp_offset)
.unwrap_or(Duration::ZERO);

if let Some(last_pts) = self.last_audio_pts {
if pts_duration <= last_pts {
let frame_duration = Self::audio_frame_duration(&frame);
let adjusted_pts = last_pts + frame_duration;

trace!(
?timestamp,
?last_pts,
adjusted_pts = ?adjusted_pts,
frame_duration_ns = frame_duration.as_nanos(),
samples = frame.samples(),
sample_rate = frame.rate(),
"Monotonic audio pts correction",
);

if let Some(new_offset) = timestamp.checked_sub(adjusted_pts) {
self.timestamp_offset = new_offset;
}

pts_duration = adjusted_pts;
}
}

self.last_audio_pts = Some(pts_duration);

let pts = cm::Time::new(
(timestamp.sub(self.timestamp_offset).as_secs_f64() * frame.rate() as f64) as i64,
(pts_duration.as_secs_f64() * frame.rate() as f64) as i64,
frame.rate() as i32,
);

Expand All @@ -322,16 +391,49 @@
.and_then(|v| v.then_some(()).ok_or(QueueAudioFrameError::Failed))?;

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

Ok(())
}

fn video_frame_duration(&self) -> Duration {
let fps_num = self.config.frame_rate.0;
let fps_den = self.config.frame_rate.1;

if fps_num <= 0 {
return Duration::from_millis(1);
}

let numerator = fps_den.unsigned_abs() as u128 * 1_000_000_000u128;
let denominator = fps_num as u128;
let nanos = (numerator / denominator).max(1);

Duration::from_nanos(nanos as u64)
}

fn audio_frame_duration(frame: &frame::Audio) -> Duration {
let rate = frame.rate();

if rate <= 0 {

Check failure on line 417 in crates/enc-avfoundation/src/mp4.rs

View workflow job for this annotation

GitHub Actions / Clippy

this comparison involving the minimum or maximum element for this type contains a case that is always true or always false

error: this comparison involving the minimum or maximum element for this type contains a case that is always true or always false --> crates/enc-avfoundation/src/mp4.rs:417:12 | 417 | if rate <= 0 { | ^^^^^^^^^ | = help: because `0` is the minimum value for this type, the case where the two sides are not equal never occurs, consider using `rate == 0` instead = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons = note: `#[deny(clippy::absurd_extreme_comparisons)]` on by default
return Duration::from_millis(1);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let samples = frame.samples() as u128;
if samples == 0 {
return Duration::from_nanos(1);
}

let nanos = (samples * 1_000_000_000u128) / rate as u128;

Duration::from_nanos(nanos.max(1) as u64)
}

pub fn pause(&mut self) {
if self.is_paused || !self.is_writing {
return;
}

let Some((_, timestamp)) = self.most_recent_frame else {
let Some(timestamp) = self.last_timestamp else {
return;
};

Expand Down
Loading