use ez_ffmpeg::{FfmpegContext, Input, Output};
use std::sync::Mutex;
use std::time::Duration;
#[cfg(target_os = "linux")]
use std::time::Instant;
static PROCESS_LOCK: Mutex<()> = Mutex::new(());
#[cfg(target_os = "linux")]
fn process_thread_count() -> usize {
let status = std::fs::read_to_string("/proc/self/status").unwrap();
status
.lines()
.find_map(|l| l.strip_prefix("Threads:"))
.and_then(|v| v.trim().parse().ok())
.expect("Threads: line missing from /proc/self/status")
}
#[cfg(target_os = "linux")]
fn listener_with_small_rcvbuf(rcvbuf: libc::c_int) -> Option<std::net::TcpListener> {
use std::os::unix::io::FromRawFd;
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0);
if fd < 0 {
return None;
}
let set = |opt: libc::c_int, val: libc::c_int| -> bool {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
opt,
&val as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
) == 0
};
if !set(libc::SO_RCVBUF, rcvbuf) || !set(libc::SO_REUSEADDR, 1) {
libc::close(fd);
return None;
}
let addr = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: 0, sin_addr: libc::in_addr {
s_addr: u32::from(std::net::Ipv4Addr::LOCALHOST).to_be(),
},
sin_zero: [0; 8],
};
let bound = libc::bind(
fd,
&addr as *const libc::sockaddr_in as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
) == 0;
if !bound || libc::listen(fd, 1) != 0 {
libc::close(fd);
return None;
}
Some(std::net::TcpListener::from_raw_fd(fd))
}
}
#[cfg(target_os = "linux")]
fn rcvbuf_is_small(fd: std::os::unix::io::RawFd) -> bool {
unsafe {
let mut got: libc::c_int = 0;
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
let ret = libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_RCVBUF,
&mut got as *mut libc::c_int as *mut libc::c_void,
&mut len,
);
ret == 0 && got < 1024 * 1024
}
}
fn make_backpressure_listener() -> (std::net::TcpListener, bool) {
#[cfg(target_os = "linux")]
{
if let Some(l) = listener_with_small_rcvbuf(16 * 1024) {
return (l, true);
}
}
(std::net::TcpListener::bind("127.0.0.1:0").unwrap(), false)
}
#[cfg(target_os = "linux")]
#[test]
fn stop_returns_only_after_all_worker_threads_exited() {
let _lock = PROCESS_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let out = std::env::temp_dir().join(format!(
"ez_ffmpeg_lifecycle_{}_stop.mp4",
std::process::id()
));
let baseline = process_thread_count();
let scheduler = FfmpegContext::builder()
.input(Input::from("color=c=black:s=320x240:r=30").set_format("lavfi"))
.output(Output::from(out.to_string_lossy().as_ref()).set_video_codec("mpeg4"))
.build()
.unwrap()
.start()
.unwrap();
std::thread::sleep(Duration::from_millis(300));
scheduler
.stop()
.expect("graceful stop of a healthy local job must succeed");
let deadline = Instant::now() + Duration::from_millis(50);
let mut count = process_thread_count();
while count > baseline && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(5));
count = process_thread_count();
}
assert!(
count <= baseline,
"stop() returned while {} worker thread(s) were still running",
count - baseline
);
}
#[test]
fn stop_interrupts_muxer_blocked_on_unread_network_output() {
let _lock = PROCESS_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let (listener, listener_small) = make_backpressure_listener();
let addr = listener.local_addr().unwrap();
let (shrunk_tx, shrunk_rx) = std::sync::mpsc::channel::<bool>();
let accept_thread = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let child_small = {
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
listener_small && rcvbuf_is_small(stream.as_raw_fd())
}
#[cfg(not(target_os = "linux"))]
{
let _ = listener_small;
false
}
};
let _ = shrunk_tx.send(child_small);
std::thread::sleep(Duration::from_secs(30));
drop(stream);
});
let scheduler = FfmpegContext::builder()
.input(Input::from("testsrc2=s=1920x1080:r=30").set_format("lavfi"))
.output(
Output::from(format!("tcp://{addr}?send_buffer_size=16384"))
.set_format("mpegts")
.set_video_codec("mpeg4")
.set_video_codec_opt("qscale", "1"),
)
.build()
.unwrap()
.start()
.unwrap();
let buffers_shrunk = shrunk_rx
.recv_timeout(Duration::from_secs(5))
.unwrap_or(false);
std::thread::sleep(Duration::from_secs(4));
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(scheduler.stop());
});
let stopped = rx.recv_timeout(Duration::from_secs(5));
assert!(
stopped.is_ok(),
"stop() must interrupt a muxer blocked in a network write \
(interrupt_callback missing on the output context)"
);
if buffers_shrunk {
assert!(
stopped.unwrap().is_err(),
"a stop() that had to cut a blocked network write must report the error"
);
}
drop(accept_thread); }
#[test]
fn immediate_stop_of_frame_threaded_h264_decode_is_clean() {
let _lock = PROCESS_LOCK.lock().unwrap_or_else(|e| e.into_inner());
for i in 0..512 {
let scheduler = FfmpegContext::builder()
.input(
Input::from("test.mp4")
.set_video_codec_opt("threads", "4")
.set_video_codec_opt("thread_type", "frame"),
)
.output(
Output::from("-")
.set_format("null")
.add_stream_map("0:v")
.set_video_codec("mpeg4"),
)
.build()
.unwrap()
.start()
.unwrap();
std::thread::yield_now();
scheduler
.stop()
.unwrap_or_else(|e| panic!("iteration {i}: clean immediate stop failed: {e}"));
}
}