use ez_ffmpeg::stream_info::{find_video_stream_info, StreamInfo};
use ez_ffmpeg::{FfmpegContext, FfmpegScheduler, Input, Output};
use std::process::Command;
use std::time::Duration;
fn tmp_path(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("ez_ffmpeg_bsf_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 software_h264_encoder() -> Option<String> {
let preferred = ["libx264", "libopenh264"];
let encoders = ez_ffmpeg::codec::get_encoders();
preferred
.into_iter()
.find(|name| encoders.iter().any(|e| e.codec_name == *name))
.map(|s| s.to_string())
}
fn ffmpeg_cli_available() -> bool {
Command::new("ffmpeg")
.arg("-version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn make_h264_mp4(path: &str) -> bool {
if ffmpeg_cli_available() {
let ok = Command::new("ffmpeg")
.args([
"-y",
"-v",
"error",
"-f",
"lavfi",
"-i",
"testsrc=size=320x240:rate=30:duration=1",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
path,
])
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok && fixture_is_h264(path) {
return true;
}
}
if let Some(enc) = software_h264_encoder() {
let built = FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(path)
.set_video_codec(enc)
.set_max_video_frames(30),
)
.build();
if let Ok(ctx) = built {
if let Ok(running) = ctx.start() {
if wait_with_watchdog(running, 60, "h264 fixture").is_ok() && fixture_is_h264(path)
{
return true;
}
}
}
}
false
}
fn fixture_is_h264(path: &str) -> bool {
matches!(
find_video_stream_info(path),
Ok(Some(StreamInfo::Video { ref codec_name, .. })) if codec_name == "h264"
)
}
fn contains_annex_b_start_code(bytes: &[u8]) -> bool {
bytes.windows(3).any(|w| w == [0x00, 0x00, 0x01])
}
#[test]
fn stream_copy_h264_mp4_to_mpegts_with_mp4toannexb() {
let fixture = tmp_path("bsf_copy_src.mp4");
if !make_h264_mp4(&fixture) {
eprintln!(
"SKIP stream_copy_h264_mp4_to_mpegts_with_mp4toannexb: \
no H.264 encoder / ffmpeg CLI available to build the fixture"
);
return;
}
let out_ts = tmp_path("bsf_copy_out.ts");
let result = wait_with_watchdog(
FfmpegContext::builder()
.input(Input::from(fixture.as_str()))
.output(
Output::from(out_ts.as_str())
.set_format("mpegts")
.add_stream_map_with_copy("0:v")
.set_video_bsf("h264_mp4toannexb"),
)
.build()
.unwrap()
.start()
.unwrap(),
60,
"h264_mp4toannexb copy",
);
assert!(
result.is_ok(),
"stream-copy with h264_mp4toannexb failed: {result:?}"
);
assert!(
fixture_is_h264(&out_ts),
"MPEG-TS output is not a readable H.264 stream"
);
let bytes = std::fs::read(&out_ts).expect("failed to read MPEG-TS output");
assert!(
contains_annex_b_start_code(&bytes),
"MPEG-TS output has no Annex B start code"
);
}
#[test]
fn encode_path_null_bsf_smoke() {
let out = tmp_path("bsf_encode_null.mp4");
let result = wait_with_watchdog(
FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_video_bsf("null")
.set_max_video_frames(30),
)
.build()
.unwrap()
.start()
.unwrap(),
60,
"encode null bsf",
);
assert!(result.is_ok(), "encode with null BSF failed: {result:?}");
match find_video_stream_info(&out)
.expect("failed to probe output")
.expect("output has no video stream")
{
StreamInfo::Video { nb_frames, .. } => {
assert!(
nb_frames > 0,
"null-BSF re-encode produced no frames (nb_frames={nb_frames})"
);
}
other => panic!("expected a video stream, got {other:?}"),
}
}
#[test]
fn nonexistent_bsf_fails_the_job() {
let out = tmp_path("bsf_bad.mp4");
let result = wait_with_watchdog(
FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_video_bsf("definitely_not_a_bsf")
.set_max_video_frames(30),
)
.build()
.unwrap()
.start()
.unwrap(),
60,
"bad bsf",
);
let err = match result {
Ok(()) => panic!("a nonexistent bitstream filter must fail the job, not succeed"),
Err(e) => e,
};
let msg = format!("{err}");
assert!(
msg.contains("bitstream filter"),
"error should identify the bitstream filter failure, got: {msg}"
);
}