use ez_ffmpeg::stream_info::{find_subtitle_stream_info, find_video_stream_info, StreamInfo};
use ez_ffmpeg::{FfmpegContext, Input, Output};
mod common;
use common::{tmp_path_in, wait_with_watchdog};
fn tmp_path(name: &str) -> String {
tmp_path_in("ez_ffmpeg_streamcopy_deadlock_tests", name)
}
fn sparse_srt_fixture(name: &str, start_s: u32) -> String {
let path = tmp_path(name);
std::fs::write(
&path,
format!(
"1\n00:00:{:02},000 --> 00:00:{:02},000\nlate cue\n\n",
start_s,
start_s + 1
),
)
.unwrap();
path
}
fn build_fixture(mkv: &str, secs: u32, sub_start_s: u32) -> bool {
let srt = sparse_srt_fixture("deadlock_in.srt", sub_start_s);
let ctx = match FfmpegContext::builder()
.input(Input::from(format!("color=c=black:s=320x240:r=30:d={secs}")).set_format("lavfi"))
.input(Input::from(srt.as_str()))
.output(
Output::from(mkv)
.set_video_codec("mpeg4")
.set_subtitle_codec("copy"),
)
.build()
{
Ok(c) => c,
Err(e) => {
eprintln!("skipping: fixture build failed (codec unavailable?): {e:?}");
return false;
}
};
let scheduler = match ctx.start() {
Ok(s) => s,
Err(e) => {
eprintln!("skipping: fixture start failed (codec unavailable?): {e:?}");
return false;
}
};
if let Err(e) = wait_with_watchdog(scheduler, 60, "build video+subtitle fixture") {
eprintln!("skipping: fixture run failed (codec unavailable?): {e:?}");
return false;
}
find_video_stream_info(mkv).ok().flatten().is_some()
&& find_subtitle_stream_info(mkv).ok().flatten().is_some()
}
#[test]
fn streamcopy_plus_late_encoder_does_not_deadlock() {
let mkv = tmp_path("deadlock_fixture.mkv");
let out = tmp_path("deadlock_out.mkv");
if !build_fixture(&mkv, 4, 2) {
eprintln!("streamcopy_deadlock: fixture unavailable, skipping");
return;
}
let scheduler = FfmpegContext::builder()
.input(Input::from(mkv.as_str()))
.output(
Output::from(out.as_str())
.set_video_codec("copy")
.set_subtitle_codec("subrip"),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 30, "streamcopy + late encoder");
assert!(result.is_ok(), "streamcopy+encode job failed: {result:?}");
assert!(
find_video_stream_info(&out).ok().flatten().is_some(),
"output must contain the copied video stream"
);
assert!(
find_subtitle_stream_info(&out).ok().flatten().is_some(),
"output must contain the re-encoded subtitle stream alongside the copy"
);
let probe = tmp_path("deadlock_probe.mp4");
let scheduler = FfmpegContext::builder()
.input(Input::from(out.as_str()))
.output(
Output::from(probe.as_str())
.add_stream_map("0:v")
.set_video_codec("mpeg4"),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 60, "re-encode copied video to count frames");
assert!(result.is_ok(), "probe re-encode failed: {result:?}");
let frames = match find_video_stream_info(&probe)
.expect("failed to probe re-encoded video")
.expect("re-encoded output must contain the video stream")
{
StreamInfo::Video { nb_frames, .. } => nb_frames,
other => panic!("expected video stream info, got {other:?}"),
};
assert!(
frames >= 100,
"the copied 4s/30fps video must carry ~120 frames (not truncated), got {frames}"
);
}