use ez_ffmpeg::stream_info::{find_audio_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_shortest_tests", name)
}
fn lavfi_video_infinite() -> Input {
Input::from("color=c=black:s=320x240:r=30").set_format("lavfi")
}
fn lavfi_video_secs(secs: u32) -> Input {
Input::from(format!("color=c=black:s=320x240:r=30:d={secs}")).set_format("lavfi")
}
fn lavfi_audio_secs(secs: u32) -> Input {
Input::from(format!(
"sine=frequency=440:duration={secs}:sample_rate=44100"
))
.set_format("lavfi")
}
fn video_nb_frames(url: &str) -> i64 {
match find_video_stream_info(url)
.unwrap()
.expect("output has no video stream")
{
StreamInfo::Video { nb_frames, .. } => nb_frames,
other => panic!("expected video stream info, got {other:?}"),
}
}
fn audio_duration_secs(url: &str) -> f64 {
match find_audio_stream_info(url)
.unwrap()
.expect("output has no audio stream")
{
StreamInfo::Audio {
duration,
time_base,
..
} => duration as f64 * time_base.num as f64 / time_base.den as f64,
other => panic!("expected audio stream info, got {other:?}"),
}
}
fn video_duration_secs(url: &str) -> f64 {
match find_video_stream_info(url)
.unwrap()
.expect("output has no video stream")
{
StreamInfo::Video {
duration,
time_base,
..
} => duration as f64 * time_base.num as f64 / time_base.den as f64,
other => panic!("expected video stream info, got {other:?}"),
}
}
#[test]
fn shortest_stops_infinite_video_at_short_audio() {
let out = tmp_path("shortest_infinite_video.mp4");
let scheduler = FfmpegContext::builder()
.input(lavfi_video_infinite())
.input(lavfi_audio_secs(2))
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_audio_codec("aac")
.set_shortest(true),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 60, "shortest stops infinite video");
assert!(result.is_ok(), "-shortest job failed: {result:?}");
let frames = video_nb_frames(&out);
assert!(
(45..=75).contains(&frames),
"video should be truncated to ~2s (~60 frames), got {frames}"
);
let vdur = video_duration_secs(&out);
assert!(
(1.5..=2.6).contains(&vdur),
"video duration should be ~2s, got {vdur:.3}s"
);
}
#[test]
fn shortest_stops_three_streams_with_two_infinite_videos() {
let out = tmp_path("shortest_three_streams.mp4");
let scheduler = FfmpegContext::builder()
.input(lavfi_audio_secs(2))
.input(lavfi_video_infinite())
.input(lavfi_video_infinite())
.output(
Output::from(out.as_str())
.add_stream_map("0:a")
.add_stream_map("1:v")
.add_stream_map("2:v")
.set_video_codec("mpeg4")
.set_audio_codec("aac")
.set_shortest(true),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 60, "shortest stops three streams");
assert!(result.is_ok(), "-shortest 3-stream job failed: {result:?}");
let frames = video_nb_frames(&out);
assert!(
(45..=120).contains(&frames),
"video should be truncated near the 2s audio bound, got {frames}"
);
}
#[test]
fn shortest_truncates_longer_finite_stream() {
let out = tmp_path("shortest_finite_streams.mp4");
let scheduler = FfmpegContext::builder()
.input(lavfi_video_secs(4))
.input(lavfi_audio_secs(1))
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_audio_codec("aac")
.set_shortest(true),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 60, "shortest truncates longer finite stream");
assert!(result.is_ok(), "-shortest job failed: {result:?}");
let frames = video_nb_frames(&out);
assert!(
(20..=45).contains(&frames),
"video should be truncated to ~1s (~30 frames), got {frames}"
);
let adur = audio_duration_secs(&out);
assert!(
(0.8..=1.4).contains(&adur),
"audio (the shortest) should be ~1s, got {adur:.3}s"
);
}
#[test]
fn without_shortest_streams_keep_their_own_length() {
let out = tmp_path("no_shortest_streams.mp4");
let scheduler = FfmpegContext::builder()
.input(lavfi_video_secs(4))
.input(lavfi_audio_secs(1))
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_audio_codec("aac"),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(scheduler, 60, "no -shortest keeps lengths");
assert!(result.is_ok(), "job failed: {result:?}");
let frames = video_nb_frames(&out);
assert!(
frames >= 100,
"without -shortest the 4s video must keep ~120 frames, got {frames}"
);
}
#[test]
fn shortest_with_max_frames_bounds_infinite_streams() {
let out = tmp_path("shortest_max_frames.mp4");
let scheduler = FfmpegContext::builder()
.input(lavfi_video_infinite())
.input(lavfi_video_infinite())
.output(
Output::from(out.as_str())
.add_stream_map("0:v")
.add_stream_map("1:v")
.set_video_codec("mpeg4")
.set_shortest(true)
.set_max_video_frames(30),
)
.build()
.unwrap()
.start()
.unwrap();
let result = wait_with_watchdog(
scheduler,
60,
"shortest + max_video_frames bounds infinite streams",
);
assert!(
result.is_ok(),
"-shortest + max_frames job failed: {result:?}"
);
let frames = video_nb_frames(&out);
assert!(
(20..=45).contains(&frames),
"video should be capped near 30 frames, got {frames}"
);
}