use ez_ffmpeg::filter::frame_filter::{FrameFilter, FrameFilterError};
use ez_ffmpeg::filter::frame_filter_context::FrameFilterContext;
use ez_ffmpeg::filter::frame_pipeline_builder::FramePipelineBuilder;
use ez_ffmpeg::{AVMediaType, FfmpegContext, FfmpegScheduler, Frame, Input, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
fn tmp_path(name: &str) -> String {
let dir = std::env::temp_dir().join(format!("ez_ffmpeg_cross_graph_{}", 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 cross_graph_builder(out: &str) -> FfmpegContext {
FfmpegContext::builder()
.input(Input::from("color=c=red:s=64x64:r=15:d=0.2").set_format("lavfi"))
.input(Input::from("color=c=blue:s=64x64:r=15:d=0.2").set_format("lavfi"))
.filter_desc("[0:v]hue=s=0[mid]")
.filter_desc("[mid][1:v]overlay[vout]")
.output(
Output::from(out)
.add_stream_map("vout")
.set_video_codec("mpeg4"),
)
.build()
.expect("cross-graph build must not panic or error")
}
#[test]
fn cross_graph_bound_pad_before_demux_pad_builds_without_panic() {
let _ctx = cross_graph_builder(&tmp_path("cross_graph_build.mp4"));
}
#[test]
fn cross_graph_bound_pad_before_demux_pad_runs_to_completion() {
let ctx = cross_graph_builder(&tmp_path("cross_graph_run.mp4"));
let scheduler = ctx.start().expect("start");
wait_with_watchdog(scheduler, 30, "cross-graph overlay run").expect("run to completion");
}
struct FrameCounter {
count: Arc<AtomicUsize>,
}
impl FrameFilter for FrameCounter {
fn media_type(&self) -> AVMediaType {
AVMediaType::AVMEDIA_TYPE_VIDEO
}
fn filter_frame(
&mut self,
frame: Frame,
_ctx: &mut FrameFilterContext,
) -> Result<Option<Frame>, FrameFilterError> {
unsafe {
if !frame.as_ptr().is_null() && !frame.is_empty() {
self.count.fetch_add(1, Ordering::SeqCst);
}
}
Ok(Some(frame))
}
}
#[test]
fn cross_graph_fps_retimes_from_the_preserved_eof_tail() {
let count = Arc::new(AtomicUsize::new(0));
let pipeline = FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_VIDEO).filter(
"frame_counter",
Box::new(FrameCounter {
count: Arc::clone(&count),
}),
);
let out = tmp_path("cross_graph_fps.mp4");
let ctx = FfmpegContext::builder()
.input(Input::from("color=c=red:s=64x64:r=15:d=0.2").set_format("lavfi"))
.filter_desc("[0:v]hue=s=0[mid]")
.filter_desc("[mid]fps=30[vout]")
.output(
Output::from(out.as_str())
.add_stream_map("vout")
.set_video_codec("mpeg4")
.set_frame_pipelines(vec![pipeline]),
)
.build()
.expect("cross-graph build");
let scheduler = ctx.start().expect("start");
wait_with_watchdog(scheduler, 30, "cross-graph fps retime").expect("run to completion");
assert_eq!(
count.load(Ordering::SeqCst),
6,
"cross-graph EOF must preserve the tail time (3/15) so fps=30 emits the full 6-frame grid"
);
}