mod common;
use common::tmp_path_in;
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::RecvTimeoutError;
use std::sync::Arc;
use std::time::{Duration, Instant};
use ez_ffmpeg::{FfmpegContext, Input, Output};
const SUBDIR: &str = "ez_ffmpeg_start_panic_tests";
const GUARD_PANIC_MSG: &str = "test logger: panicking on the demux skip warning";
struct SignalOnUnwind(Arc<AtomicBool>);
impl Drop for SignalOnUnwind {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
struct PanicOnSkipWarning {
writes: Arc<AtomicUsize>,
panic_reached: Arc<AtomicBool>,
}
impl log::Log for PanicOnSkipWarning {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if !record
.args()
.to_string()
.contains("does not need to be sent")
{
return;
}
let deadline = Instant::now() + Duration::from_secs(10);
while self.writes.load(Ordering::SeqCst) == 0 {
assert!(
Instant::now() < deadline,
"mux worker never reached its first write; scenario broken"
);
std::thread::sleep(Duration::from_millis(2));
}
let _signal = SignalOnUnwind(self.panic_reached.clone());
panic!("{GUARD_PANIC_MSG}");
}
fn flush(&self) {}
}
#[test]
fn start_panic_after_workers_spawned_joins_workers_and_propagates() {
let fixture = tmp_path_in(SUBDIR, "src.mp4");
{
let running = FfmpegContext::builder()
.input(Input::from("testsrc=size=320x240:rate=30:duration=1").set_format("lavfi"))
.output(
Output::from(fixture.as_str())
.set_video_codec("mpeg4")
.set_max_video_frames(30),
)
.build()
.expect("fixture context must build")
.start()
.expect("fixture job must start");
running.wait().expect("fixture encode must finish");
}
let writes = Arc::new(AtomicUsize::new(0));
let release = Arc::new(AtomicBool::new(false));
let panic_reached = Arc::new(AtomicBool::new(false));
log::set_boxed_logger(Box::new(PanicOnSkipWarning {
writes: writes.clone(),
panic_reached: panic_reached.clone(),
}))
.expect("logger installs once");
log::set_max_level(log::LevelFilter::Warn);
let writes_cb = writes.clone();
let release_cb = release.clone();
let fixture_for_job = fixture.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
FfmpegContext::builder()
.input(Input::from(fixture_for_job.as_str()))
.input(Input::from(fixture_for_job.as_str()))
.output(
Output::new_by_write_callback(move |data: &[u8]| -> i32 {
writes_cb.fetch_add(1, Ordering::SeqCst);
while !release_cb.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(5));
}
data.len() as i32
})
.set_format("mpegts")
.add_stream_map_with_copy("0:v"),
)
.build()
.expect("two-input copy context must build")
.start()
}));
let verdict = match outcome {
Ok(_) => "completed-without-panic".to_string(),
Err(payload) => payload
.downcast_ref::<String>()
.map(|s| s.as_str())
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("non-string panic payload")
.to_string(),
};
let _ = tx.send(verdict);
});
let deadline = Instant::now() + Duration::from_secs(30);
while !panic_reached.load(Ordering::SeqCst) {
assert!(
Instant::now() < deadline,
"the demux skip warning never fired (scenario broken — did the \
message change?)"
);
std::thread::sleep(Duration::from_millis(5));
}
match rx.recv_timeout(Duration::from_secs(2)) {
Err(RecvTimeoutError::Timeout) => { }
Ok(v) => panic!(
"start()'s unwind completed (\"{v}\") while the muxer worker was \
still parked inside the write callback: the failure guard did \
not join the workers before the context dropped"
),
Err(RecvTimeoutError::Disconnected) => {
unreachable!("worker thread always sends before exiting")
}
}
release.store(true, Ordering::SeqCst);
match rx.recv_timeout(Duration::from_secs(60)) {
Ok(v) if v == GUARD_PANIC_MSG => { }
Ok(v) => panic!(
"start() ended with an unexpected outcome \"{v}\" instead of the \
logger's panic (scenario broken)"
),
Err(RecvTimeoutError::Timeout) => panic!(
"start() panic did not unwind within 60s of releasing the \
writer: the failure guard hung joining a worker"
),
Err(RecvTimeoutError::Disconnected) => {
unreachable!("worker thread always sends before exiting")
}
}
}