mod common;
use common::{tmp_path_in, wait_with_watchdog};
use ez_ffmpeg::core::scheduler::ffmpeg_scheduler::Running;
use ez_ffmpeg::{FfmpegContext, FfmpegScheduler, Input, Output};
use std::sync::mpsc::RecvTimeoutError;
use std::sync::OnceLock;
use std::time::Duration;
const SUBDIR: &str = "ez_ffmpeg_start_failure_tests";
fn fixture_mp4() -> &'static str {
static FIXTURE: OnceLock<String> = OnceLock::new();
static ATTEMPT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
if let Some(path) = FIXTURE.get() {
return path;
}
let attempt = ATTEMPT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = tmp_path_in(SUBDIR, &format!("src_{attempt}.mp4"));
let running = FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(path.as_str())
.set_video_codec("mpeg4")
.set_max_video_frames(30),
)
.build()
.expect("fixture context must build")
.start()
.expect("fixture job must start");
let result = wait_with_watchdog(running, 60, "mpeg4 fixture encode");
assert!(result.is_ok(), "fixture encode failed: {result:?}");
if let Err(loser) = FIXTURE.set(path) {
let _ = std::fs::remove_file(loser);
}
FIXTURE
.get()
.expect("a completed fixture candidate was published")
}
fn start_failure_with_watchdog<F>(scenario: &str, secs: u64, build_and_start: F) -> String
where
F: FnOnce() -> ez_ffmpeg::error::Result<FfmpegScheduler<Running>> + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let outcome = match build_and_start() {
Ok(running) => {
drop(running);
None
}
Err(e) => Some(format!("{e}")),
};
let _ = tx.send(outcome);
});
match rx.recv_timeout(Duration::from_secs(secs)) {
Ok(Some(msg)) => msg,
Ok(None) => panic!("scenario `{scenario}`: start() succeeded but must fail"),
Err(RecvTimeoutError::Timeout) => panic!(
"scenario `{scenario}` did not finish within {secs}s: start() hung \
instead of joining the spawned workers and returning Err"
),
Err(RecvTimeoutError::Disconnected) => {
panic!("scenario `{scenario}`: start() thread panicked before reporting")
}
}
}
fn two_output_bad_bsf_start(
fixture: &str,
out_ok: String,
out_bad: String,
) -> ez_ffmpeg::error::Result<FfmpegScheduler<Running>> {
FfmpegContext::builder()
.input(Input::from(fixture))
.output(Output::from(out_ok.as_str()).add_stream_map_with_copy("0:v"))
.output(
Output::from(out_bad.as_str())
.add_stream_map_with_copy("0:v")
.set_video_bsf("definitely_not_a_bsf"),
)
.build()
.expect("two-output copy context must build; the failure under test comes from start()")
.start()
}
#[test]
fn mux_init_bsf_failure_fails_start_and_joins_healthy_mux_worker() {
let fixture = fixture_mp4();
let out_ok = tmp_path_in(SUBDIR, "bsf_join_ok.mp4");
let out_bad = tmp_path_in(SUBDIR, "bsf_join_bad.mp4");
let msg = start_failure_with_watchdog("two-output bad bsf", 30, move || {
two_output_bad_bsf_start(fixture, out_ok, out_bad)
});
assert!(
msg.contains("bitstream filter"),
"error should identify the bitstream filter failure, got: {msg}"
);
}
#[test]
fn mux_init_bsf_failure_stress_fresh_contexts() {
let fixture = fixture_mp4();
for i in 0..12 {
let out_ok = tmp_path_in(SUBDIR, &format!("bsf_stress_{i}_ok.mp4"));
let out_bad = tmp_path_in(SUBDIR, &format!("bsf_stress_{i}_bad.mp4"));
let scenario = format!("bad bsf stress iteration {i}");
let msg = start_failure_with_watchdog(&scenario, 30, move || {
two_output_bad_bsf_start(fixture, out_ok, out_bad)
});
assert!(
msg.contains("bitstream filter"),
"iteration {i}: expected the bitstream filter error, got: {msg}"
);
}
}
#[test]
fn enc_init_bad_encoder_opt_fails_start_and_joins_live_mux_worker() {
let fixture = fixture_mp4();
let out_ok = tmp_path_in(SUBDIR, "enc_opt_ok.mp4");
let out_bad = tmp_path_in(SUBDIR, "enc_opt_bad.mp4");
let msg = start_failure_with_watchdog("bad encoder option value", 30, move || {
FfmpegContext::builder()
.input(Input::from(fixture))
.output(Output::from(out_ok.as_str()).add_stream_map_with_copy("0:v"))
.output(
Output::from(out_bad.as_str())
.set_video_codec("mpeg4")
.set_video_codec_opt("b", "not_a_number")
.set_max_video_frames(30),
)
.build()
.expect("copy+encode context must build; the failure under test comes from start()")
.start()
});
assert!(
msg.contains("encoder"),
"error should come from the encoder option path, got: {msg}"
);
}
#[test]
fn enc_init_bad_encoder_opt_with_live_sibling_encoder_worker() {
let out_ok = tmp_path_in(SUBDIR, "enc_sibling_ok.mp4");
let out_bad = tmp_path_in(SUBDIR, "enc_sibling_bad.mp4");
let msg = start_failure_with_watchdog("bad encoder option, encoder sibling", 30, move || {
FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(out_ok.as_str())
.set_video_codec("mpeg4")
.set_max_video_frames(30),
)
.output(
Output::from(out_bad.as_str())
.set_video_codec("mpeg4")
.set_video_codec_opt("b", "not_a_number")
.set_max_video_frames(30),
)
.build()
.expect("encode context must build; the failure under test comes from start()")
.start()
});
assert!(
msg.contains("encoder"),
"error should come from the encoder option path, got: {msg}"
);
}
#[test]
fn dec_init_bad_decoder_opt_fails_start_and_joins_spawned_workers() {
let out = tmp_path_in(SUBDIR, "dec_opt_out.mp4");
let msg = start_failure_with_watchdog("bad decoder option value", 30, move || {
FfmpegContext::builder()
.input(
Input::from("testsrc=size=320x240:rate=30:duration=1")
.set_format("lavfi")
.set_video_codec_opt("threads", "not_a_number"),
)
.output(
Output::from(out.as_str())
.set_video_codec("mpeg4")
.set_max_video_frames(30),
)
.build()
.expect("decode context must build; the failure under test comes from start()")
.start()
});
assert!(
msg.contains("decoder"),
"error should come from the decoder option path, got: {msg}"
);
}