use ez_ffmpeg::packet_scanner::PacketScanner;
use ez_ffmpeg::stream_info::StreamInfo;
use ez_ffmpeg::{FfmpegContext, FfmpegScheduler, Input, Output};
use std::time::Duration;
fn tmp_path(name: &str) -> String {
let dir = std::env::temp_dir().join(format!(
"ez_ffmpeg_force_key_frames_tests_{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir.join(name).to_string_lossy().into_owned()
}
fn wait_with_watchdog(
scheduler: FfmpegScheduler<ez_ffmpeg::core::scheduler::ffmpeg_scheduler::Running>,
secs: u64,
scenario: &str,
) -> ez_ffmpeg::error::Result<()> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(scheduler.wait());
});
match rx.recv_timeout(Duration::from_secs(secs)) {
Ok(result) => result,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
panic!("scenario `{scenario}` did not finish within {secs}s (hang)")
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
panic!("scenario `{scenario}`: wait() thread panicked before reporting")
}
}
}
fn encode_video(force: Option<&str>, name: &str) -> String {
let out = tmp_path(name);
let mut output = Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_video_codec_opt("g", "600")
.set_max_video_frames(90);
if let Some(spec) = force {
output = output
.set_force_key_frames(spec)
.expect("force_key_frames spec should parse");
}
let result = wait_with_watchdog(
FfmpegContext::builder()
.input(Input::from("color=c=black:s=320x240:r=30").set_format("lavfi"))
.output(output)
.build()
.unwrap()
.start()
.unwrap(),
60,
name,
);
assert!(result.is_ok(), "video encode `{name}` failed: {result:?}");
out
}
fn keyframe_times(path: &str) -> (Vec<f64>, bool) {
let mut scanner = PacketScanner::open(path).expect("open output for probing");
let time_base = match scanner.video_stream().expect("output has a video stream") {
StreamInfo::Video { time_base, .. } => *time_base,
other => panic!("expected a video stream, got {other:?}"),
};
let mut times = Vec::new();
let mut first_is_keyframe = false;
let mut seen_video = false;
for packet in scanner.packets() {
let info = packet.expect("read packet");
if !info.is_video() {
continue;
}
let secs = info.pts().unwrap_or(0) as f64 * time_base.num as f64 / time_base.den as f64;
if !seen_video {
first_is_keyframe = info.is_keyframe();
seen_video = true;
}
if info.is_keyframe() {
times.push(secs);
}
}
(times, first_is_keyframe)
}
#[test]
fn forces_a_keyframe_at_the_requested_time() {
let control = encode_video(None, "control.mp4");
let forced = encode_video(Some("1"), "forced.mp4");
let (control_kfs, control_first_kf) = keyframe_times(&control);
let (forced_kfs, forced_first_kf) = keyframe_times(&forced);
assert!(control_first_kf, "frame 0 must be a keyframe (control)");
assert!(forced_first_kf, "frame 0 must be a keyframe (forced)");
assert!(
control_kfs.iter().all(|&t| (t - 1.0).abs() > 0.05),
"control run must have no natural keyframe near 1s, got {control_kfs:?}"
);
assert!(
forced_kfs
.iter()
.any(|&t| t > 0.0 && (t - 1.0).abs() <= 0.05),
"force_key_frames(\"1\") must yield a keyframe near 1s, got {forced_kfs:?}"
);
assert!(
forced_kfs.len() > control_kfs.len(),
"forcing must add a keyframe: forced {forced_kfs:?} vs control {control_kfs:?}"
);
}
fn encode_audio(force: Option<&str>, name: &str) -> String {
let out = tmp_path(name);
let mut output = Output::from(out.as_str()).set_audio_codec("pcm_s16le");
if let Some(spec) = force {
output = output
.set_force_key_frames(spec)
.expect("force_key_frames spec should parse");
}
let result = wait_with_watchdog(
FfmpegContext::builder()
.input(
Input::from("sine=frequency=440:duration=2:sample_rate=44100").set_format("lavfi"),
)
.output(output)
.build()
.unwrap()
.start()
.unwrap(),
60,
name,
);
assert!(result.is_ok(), "audio encode `{name}` failed: {result:?}");
out
}
fn audio_packet_count(path: &str) -> usize {
let mut scanner = PacketScanner::open(path).expect("open audio output for probing");
scanner
.packets()
.filter(|p| p.as_ref().map(|i| i.is_audio()).unwrap_or(false))
.count()
}
#[test]
fn set_force_key_frames_is_a_no_op_for_audio_only() {
let plain = encode_audio(None, "audio_plain.wav");
let with_opt = encode_audio(Some("1"), "audio_forced.wav");
let plain_packets = audio_packet_count(&plain);
let with_packets = audio_packet_count(&with_opt);
assert!(
plain_packets > 0,
"baseline audio output should have packets"
);
assert_eq!(
with_packets, plain_packets,
"set_force_key_frames must not change audio output \
({with_packets} vs {plain_packets} packets)"
);
let scanner = PacketScanner::open(&with_opt).expect("open audio output");
assert!(
scanner.video_stream().is_none(),
"audio-only output must have no video stream"
);
assert!(
scanner.audio_stream().is_some(),
"audio-only output must have an audio stream"
);
}