Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
49 changes: 49 additions & 0 deletions src/video_argv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@ pub(crate) fn live_capture_argv(
args
}

/// Clamp `PTS`/`DTS` pairs that movenc cannot represent as a signed 32-bit delta.
///
/// A live capture can jump its timeline mid-file (for example across an ad
/// break). FFmpeg corrects the jump from the third post-jump packet, but the
/// first two keep the new PTS and a DTS synthesised from the pre-jump reorder
/// buffer; that difference overflows movenc's int32 check and aborts the remux.
/// Ten minutes at the 90 kHz MPEG-TS timebase is far beyond a real reorder
/// delay, so anything larger is corruption. `NOPTS` is left untouched.
const LIVE_REMUX_PTS_DTS_BSF: &str = "setts=pts='if(eq(DTS,NOPTS),PTS,if(eq(PTS,NOPTS),PTS,DTS+(PTS-DTS)*lt(abs(PTS-DTS),54000000)))'";

/// ffmpeg argv remuxing a stopped live capture into the finished file (stream-copy + faststart). Pure.
/// `page_url` is stamped into `comment`: the live leg never gets `--embed-metadata`, so this is its only provenance.
/// `dest` is the `.part` remux target: ffmpeg can't guess a container from that name, so `-f`
Expand All @@ -391,6 +401,13 @@ pub(crate) fn live_remux_argv(
if repair_timestamps {
argv.extend(["-fflags".to_string(), "+genpts+igndts".to_string()]);
}
// Input-scoped: this runs after FFmpeg's own discontinuity correction and
// before the interleaver, so the pairs it could not fix never reach movenc.
// shortcut: clamps only the >10-minute pairs; remove when FFmpeg flushes
// its reorder buffer on a PTS jump.
if !audio_only {
argv.extend(["-bsf:v".to_string(), LIVE_REMUX_PTS_DTS_BSF.to_string()]);
}
argv.extend(["-i".to_string(), ts_path.to_string_lossy().into_owned()]);
if audio_only {
argv.extend(["-map".to_string(), "0:a?".to_string()]);
Expand Down Expand Up @@ -646,6 +663,38 @@ mod live_remux_argv_tests {
assert_eq!(maps, vec!["0"]);
}

#[test]
fn live_video_remux_clamps_pts_dts_pairs_before_input() {
let args = live_remux_argv(
Path::new("capture.ts"),
Path::new("finished.mp4.part"),
false,
true,
false,
"",
);
let bsf_at = args
.iter()
.position(|arg| arg == "-bsf:v")
.expect("video remuxes must clamp overflowing pts/dts pairs");
let input = args.iter().position(|arg| arg == "-i").unwrap();
assert!(bsf_at < input, "the clamp is an input BSF");
assert_eq!(args[bsf_at + 1], super::LIVE_REMUX_PTS_DTS_BSF);

let audio = live_remux_argv(
Path::new("capture.m4a"),
Path::new("finished.m4a.part"),
true,
true,
false,
"",
);
assert!(
!audio.iter().any(|arg| arg == "-bsf:v"),
"audio-only remuxes have no video stream to clamp"
);
}

#[test]
fn timestamp_repair_remux_narrows_mapping_to_audio_and_video() {
let args = live_remux_argv(
Expand Down
2 changes: 2 additions & 0 deletions src/video_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3909,6 +3909,7 @@ fn live_remux_argv_copies_with_fixup_and_carries_provenance() {
assert!(!argv.windows(2).any(|w| w == ["-map", "0:a?"]));
assert!(argv.windows(2).any(|w| w == ["-c", "copy"]));
assert!(argv.windows(2).any(|w| w == ["-bsf:a", "aac_adtstoasc"]));
assert!(argv.windows(2).any(|w| w[0] == "-bsf:v"));
assert!(argv.windows(2).any(|w| w == ["-f", "mp4"]));
assert!(argv.windows(2).any(|w| w == ["-movflags", "+faststart"]));
assert!(!argv.iter().any(|a| a == "-vn"));
Expand Down Expand Up @@ -3936,6 +3937,7 @@ fn live_remux_argv_copies_with_fixup_and_carries_provenance() {
assert!(argv.windows(2).any(|w| w == ["-map", "0:a?"]));
assert!(argv.windows(2).any(|w| w == ["-f", "mp4"]));
assert!(!argv.iter().any(|a| a == "-bsf:a"));
assert!(!argv.iter().any(|a| a == "-bsf:v"));
}

#[test]
Expand Down
Loading