#![allow(clippy::unwrap_used)]
use ff_filter::{
BlendMode, DrawTextOptions, FilterError, FilterGraph, FilterGraphBuilder, HwAccel, Rgb,
ScaleAlgorithm, ToneMap, XfadeTransition, YadifMode,
};
use ff_format::{AudioFrame, PixelFormat, PooledBuffer, SampleFormat, Timestamp, VideoFrame};
fn make_yuv420p_frame(width: u32, height: u32) -> VideoFrame {
let y = vec![128u8; (width * height) as usize];
let u = vec![128u8; ((width / 2) * (height / 2)) as usize];
let v = vec![128u8; ((width / 2) * (height / 2)) as usize];
VideoFrame::new(
vec![
PooledBuffer::standalone(y),
PooledBuffer::standalone(u),
PooledBuffer::standalone(v),
],
vec![width as usize, (width / 2) as usize, (width / 2) as usize],
width,
height,
PixelFormat::Yuv420p,
Timestamp::default(),
true,
)
.unwrap()
}
fn make_yuv_frame(width: u32, height: u32, y: u8, u: u8, v: u8) -> VideoFrame {
let y_plane = vec![y; (width * height) as usize];
let u_plane = vec![u; ((width / 2) * (height / 2)) as usize];
let v_plane = vec![v; ((width / 2) * (height / 2)) as usize];
VideoFrame::new(
vec![
PooledBuffer::standalone(y_plane),
PooledBuffer::standalone(u_plane),
PooledBuffer::standalone(v_plane),
],
vec![width as usize, (width / 2) as usize, (width / 2) as usize],
width,
height,
PixelFormat::Yuv420p,
Timestamp::default(),
true,
)
.unwrap()
}
fn make_audio_frame() -> AudioFrame {
AudioFrame::empty(1024, 2, 48000, SampleFormat::F32).unwrap()
}
#[test]
fn pull_video_before_push_should_return_none() {
let mut graph = match FilterGraph::builder()
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let result = graph
.pull_video()
.expect("pull_video must not fail before any push");
assert!(
result.is_none(),
"expected None before any push, got Some(frame)"
);
}
#[test]
fn push_video_and_pull_through_scale_should_return_resized_frame() {
let mut graph = match FilterGraph::builder()
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after scale push");
assert_eq!(out.width(), 32, "width should be scaled to 32");
assert_eq!(out.height(), 32, "height should be scaled to 32");
}
#[test]
fn push_video_to_invalid_slot_should_return_error() {
let mut graph = match FilterGraph::builder()
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.push_video(1, &frame);
assert!(
matches!(result, Err(FilterError::InvalidInput { slot: 1, .. })),
"expected InvalidInput for slot 1, got {result:?}"
);
}
#[test]
fn pull_audio_before_push_should_return_none() {
let mut graph = match FilterGraph::builder().volume(0.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let result = graph
.pull_audio()
.expect("pull_audio must not fail before any push");
assert!(
result.is_none(),
"expected None before any push, got Some(frame)"
);
}
#[test]
fn push_audio_and_pull_through_volume_should_return_frame() {
let mut graph = match FilterGraph::builder().volume(0.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after volume push");
assert_eq!(out.sample_rate(), 48000);
assert_eq!(out.channels(), 2);
assert_eq!(out.samples(), 1024);
}
#[test]
fn push_video_through_trim_should_return_frame_within_range() {
let mut graph = match FilterGraph::builder().trim(0.0, 5.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after trim push within range");
assert_eq!(out.width(), 64, "width should be unchanged after trim");
assert_eq!(out.height(), 64, "height should be unchanged after trim");
}
#[test]
fn push_video_through_trim_frame_timestamp_should_be_within_trim_range() {
let mut graph = match FilterGraph::builder().trim(0.0, 5.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after trim push within range");
let secs = out.timestamp().as_secs_f64();
assert!(
secs >= 0.0 && secs < 5.0,
"frame timestamp {secs:.3}s must fall within the [0, 5) trim window"
);
}
#[test]
fn push_video_through_crop_should_return_cropped_frame() {
let mut graph = match FilterGraph::builder().crop(0, 0, 32, 32).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after crop push");
assert_eq!(out.width(), 32, "width should be cropped to 32");
assert_eq!(out.height(), 32, "height should be cropped to 32");
}
#[test]
fn push_1080p_frame_through_crop_100_100_1720_880_should_return_cropped_frame() {
let mut graph = match FilterGraph::builder().crop(100, 100, 1720, 880).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(1920, 1080);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after crop push");
assert_eq!(out.width(), 1720, "width should be 1720 after crop");
assert_eq!(out.height(), 880, "height should be 880 after crop");
}
#[test]
fn push_video_through_overlay_should_return_composited_frame() {
let mut graph = match FilterGraph::builder().overlay(0, 0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let base = make_yuv420p_frame(64, 64);
let overlay = make_yuv420p_frame(64, 64);
match graph.push_video(0, &base) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &overlay) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after overlay push");
assert_eq!(out.width(), 64, "output width should match base video");
assert_eq!(out.height(), 64, "output height should match base video");
}
#[test]
fn push_video_through_fade_in_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().fade_in(0.0, 1.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fade_in push");
assert_eq!(out.width(), 64, "width should be unchanged after fade_in");
assert_eq!(out.height(), 64, "height should be unchanged after fade_in");
}
#[test]
fn push_video_through_fade_out_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().fade_out(0.0, 1.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fade_out push");
assert_eq!(out.width(), 64, "width should be unchanged after fade_out");
assert_eq!(
out.height(),
64,
"height should be unchanged after fade_out"
);
}
#[test]
fn push_video_through_rotate_should_return_frame() {
let mut graph = match FilterGraph::builder().rotate(90.0, "black").build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
assert!(result.is_some(), "expected Some(frame) after rotate push");
}
#[test]
fn push_video_through_tone_map_should_return_frame() {
let mut graph = match FilterGraph::builder().tone_map(ToneMap::Hable).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
assert!(result.is_some(), "expected Some(frame) after tone_map push");
}
#[test]
fn push_audio_through_amix_should_return_mixed_frame() {
let mut graph = match FilterGraph::builder().amix(2).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_audio(1, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after amix push to both slots");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
}
#[test]
fn push_audio_through_equalizer_should_return_frame_with_same_properties() {
let mut graph = match FilterGraph::builder()
.equalizer(vec![ff_filter::EqBand::Peak {
freq_hz: 1000.0,
gain_db: 3.0,
q: 1.0,
}])
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after equalizer push");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
assert_eq!(out.samples(), 1024, "sample count should be unchanged");
}
#[test]
fn push_video_through_cuda_scale_should_return_resized_frame_or_skip() {
let mut graph = match FilterGraph::builder()
.hardware(HwAccel::Cuda)
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping (build): {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping (push): {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after cuda scale push");
assert_eq!(out.width(), 32, "width should be scaled to 32");
assert_eq!(out.height(), 32, "height should be scaled to 32");
}
#[test]
fn push_video_through_videotoolbox_scale_should_return_resized_frame_or_skip() {
let mut graph = match FilterGraph::builder()
.hardware(HwAccel::VideoToolbox)
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping (build): {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping (push): {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after videotoolbox scale push");
assert_eq!(out.width(), 32, "width should be scaled to 32");
assert_eq!(out.height(), 32, "height should be scaled to 32");
}
#[test]
fn push_video_through_vaapi_scale_should_return_resized_frame_or_skip() {
let mut graph = match FilterGraph::builder()
.hardware(HwAccel::Vaapi)
.scale(32, 32, ScaleAlgorithm::Fast)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping (build): {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping (push): {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after vaapi scale push");
assert_eq!(out.width(), 32, "width should be scaled to 32");
assert_eq!(out.height(), 32, "height should be scaled to 32");
}
#[test]
fn push_video_through_eq_saturation_zero_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().eq(0.0, 1.0, 0.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after eq push");
assert_eq!(out.width(), 64, "width should be unchanged after eq");
assert_eq!(out.height(), 64, "height should be unchanged after eq");
}
#[test]
fn push_video_through_curves_s_curve_should_return_frame_with_same_dimensions() {
let s_curve = vec![
(0.0, 0.0),
(0.25, 0.15),
(0.5, 0.5),
(0.75, 0.85),
(1.0, 1.0),
];
let mut graph = match FilterGraph::builder()
.curves(s_curve, vec![], vec![], vec![])
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after curves push");
assert_eq!(out.width(), 64, "width should be unchanged after curves");
assert_eq!(out.height(), 64, "height should be unchanged after curves");
}
#[test]
fn push_video_through_white_balance_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().white_balance(3200, 0.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after white_balance push");
assert_eq!(
out.width(),
64,
"width should be unchanged after white_balance"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after white_balance"
);
}
#[test]
fn push_video_through_hue_180_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().hue(180.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after hue push");
assert_eq!(
out.width(),
64,
"width should be unchanged after hue rotation"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after hue rotation"
);
}
#[test]
fn push_video_through_gamma_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().gamma(2.2, 2.2, 2.2).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after gamma push");
assert_eq!(out.width(), 64, "width should be unchanged after gamma");
assert_eq!(out.height(), 64, "height should be unchanged after gamma");
}
#[test]
fn push_video_through_three_way_cc_neutral_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder()
.three_way_cc(Rgb::NEUTRAL, Rgb::NEUTRAL, Rgb::NEUTRAL)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after three_way_cc push");
assert_eq!(
out.width(),
64,
"width should be unchanged after three_way_cc"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after three_way_cc"
);
}
#[test]
fn push_video_through_vignette_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder()
.vignette(std::f32::consts::PI / 5.0, 0.0, 0.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after vignette push");
assert_eq!(out.width(), 64, "width should be unchanged after vignette");
assert_eq!(
out.height(),
64,
"height should be unchanged after vignette"
);
}
#[test]
fn push_video_through_hflip_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().hflip().build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after hflip push");
assert_eq!(out.width(), 64, "width should be unchanged after hflip");
assert_eq!(out.height(), 64, "height should be unchanged after hflip");
}
#[test]
fn push_video_through_vflip_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().vflip().build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after vflip push");
assert_eq!(out.width(), 64, "width should be unchanged after vflip");
assert_eq!(out.height(), 64, "height should be unchanged after vflip");
}
#[test]
fn push_720p_frame_through_pad_1920_1080_centred_should_return_1080p_frame() {
let mut graph = match FilterGraph::builder()
.pad(1920, 1080, -1, -1, "black")
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(1280, 720);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after pad push");
assert_eq!(out.width(), 1920, "width should be padded to 1920");
assert_eq!(out.height(), 1080, "height should be padded to 1080");
}
#[test]
fn push_4x3_frame_through_fit_to_aspect_16x9_should_return_target_dimensions() {
let mut graph = match FilterGraph::builder()
.fit_to_aspect(128, 72, "black")
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 48);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fit_to_aspect push");
assert_eq!(
out.width(),
128,
"width should match target after fit_to_aspect"
);
assert_eq!(
out.height(),
72,
"height should match target after fit_to_aspect"
);
}
#[test]
fn push_wide_frame_through_fit_to_aspect_should_produce_letterbox() {
let mut graph = match FilterGraph::builder()
.fit_to_aspect(128, 72, "black")
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(128, 54);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fit_to_aspect letterbox push");
assert_eq!(
out.width(),
128,
"width should match target after letterbox fit"
);
assert_eq!(
out.height(),
72,
"height should match target after letterbox fit"
);
}
#[test]
fn push_video_through_gblur_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().gblur(5.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after gblur push");
assert_eq!(out.width(), 64, "width should be unchanged after gblur");
assert_eq!(out.height(), 64, "height should be unchanged after gblur");
}
#[test]
fn push_video_through_unsharp_sharpen_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().unsharp(1.0, 0.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after unsharp push");
assert_eq!(out.width(), 64, "width should be unchanged after unsharp");
assert_eq!(out.height(), 64, "height should be unchanged after unsharp");
}
#[test]
fn push_video_through_hqdn3d_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().hqdn3d(4.0, 3.0, 6.0, 4.5).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after hqdn3d push");
assert_eq!(out.width(), 64, "width should be unchanged after hqdn3d");
assert_eq!(out.height(), 64, "height should be unchanged after hqdn3d");
}
#[test]
fn push_video_through_nlmeans_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().nlmeans(8.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after nlmeans push");
assert_eq!(out.width(), 64, "width should be unchanged after nlmeans");
assert_eq!(out.height(), 64, "height should be unchanged after nlmeans");
}
#[test]
fn push_video_through_yadif_frame_mode_should_accept_frames_without_error() {
let mut graph = match FilterGraph::builder().yadif(YadifMode::Frame).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
if let Some(out) = result {
assert_eq!(out.width(), 64, "width should be unchanged after yadif");
assert_eq!(out.height(), 64, "height should be unchanged after yadif");
}
}
#[test]
fn push_video_through_fade_in_white_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().fade_in_white(0.0, 1.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fade_in_white push");
assert_eq!(
out.width(),
64,
"width should be unchanged after fade_in_white"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after fade_in_white"
);
}
#[test]
fn push_video_through_fade_out_white_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder().fade_out_white(0.0, 1.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after fade_out_white push");
assert_eq!(
out.width(),
64,
"width should be unchanged after fade_out_white"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after fade_out_white"
);
}
#[test]
fn push_two_clips_through_xfade_dissolve_should_return_frame_with_same_dimensions() {
let mut graph = match FilterGraph::builder()
.xfade(XfadeTransition::Dissolve, 1.0, 4.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let clip_a = make_yuv420p_frame(64, 64);
let clip_b = make_yuv420p_frame(64, 64);
match graph.push_video(0, &clip_a) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &clip_b) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after xfade push");
assert_eq!(
out.width(),
64,
"output width should match input after xfade"
);
assert_eq!(
out.height(),
64,
"output height should match input after xfade"
);
}
#[test]
fn push_video_through_drawtext_should_return_frame_with_same_dimensions() {
let opts = DrawTextOptions {
text: "Hello".to_string(),
x: "10".to_string(),
y: "10".to_string(),
font_size: 24,
font_color: "white".to_string(),
font_file: None,
opacity: 1.0,
box_color: None,
box_border_width: 0,
};
let mut graph = match FilterGraph::builder().drawtext(opts).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after drawtext push");
assert_eq!(out.width(), 64, "width should be unchanged after drawtext");
assert_eq!(
out.height(),
64,
"height should be unchanged after drawtext"
);
}
#[test]
fn push_video_through_drawtext_with_box_should_return_frame_with_same_dimensions() {
let opts = DrawTextOptions {
text: "Hello".to_string(),
x: "10".to_string(),
y: "10".to_string(),
font_size: 24,
font_color: "white".to_string(),
font_file: None,
opacity: 1.0,
box_color: Some("black@0.5".to_string()),
box_border_width: 5,
};
let mut graph = match FilterGraph::builder().drawtext(opts).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after drawtext with box push");
assert_eq!(
out.width(),
64,
"width should be unchanged after drawtext with box"
);
assert_eq!(
out.height(),
64,
"height should be unchanged after drawtext with box"
);
}
#[test]
fn push_audio_through_agate_should_return_frame_with_same_properties() {
let mut graph = match FilterGraph::builder().agate(-40.0, 10.0, 100.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after agate push");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
assert_eq!(out.samples(), 1024, "sample count should be unchanged");
}
#[test]
fn push_audio_through_compressor_should_return_frame_with_same_properties() {
let mut graph = match FilterGraph::builder()
.compressor(-20.0, 4.0, 10.0, 100.0, 6.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after compressor push");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
assert_eq!(out.samples(), 1024, "sample count should be unchanged");
}
#[test]
fn push_stereo_audio_through_stereo_to_mono_should_return_mono_frame() {
let mut graph = match FilterGraph::builder().stereo_to_mono().build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after stereo_to_mono push");
assert_eq!(out.channels(), 1, "output should be mono (1 channel)");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
}
#[test]
fn push_audio_through_channel_map_should_return_frame_with_same_properties() {
let mut graph = match FilterGraph::builder().channel_map("FL|FR").build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after channel_map push");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(
out.channels(),
2,
"channel count should be unchanged for identity remap"
);
assert_eq!(out.samples(), 1024, "sample count should be unchanged");
}
#[test]
fn push_audio_through_audio_delay_positive_should_return_frame_with_same_properties() {
let mut graph = match FilterGraph::builder().audio_delay(100.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
if let Some(out) = result {
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
}
}
#[test]
fn push_audio_through_audio_delay_negative_should_not_error() {
let mut graph = match FilterGraph::builder().audio_delay(-5.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
if let Some(out) = result {
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
}
}
#[test]
fn push_video_through_concat_video_should_produce_output() {
let mut graph = match FilterGraph::builder().concat_video(2).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after concat push to both slots");
assert_eq!(out.width(), 64, "width should be unchanged");
assert_eq!(out.height(), 64, "height should be unchanged");
}
#[test]
fn push_audio_through_concat_audio_should_produce_output() {
let mut graph = match FilterGraph::builder().concat_audio(2).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_audio_frame();
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_audio(1, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_audio().expect("pull_audio must not fail");
let out = result.expect("expected Some(frame) after concat push to both slots");
assert_eq!(out.sample_rate(), 48000, "sample rate should be unchanged");
assert_eq!(out.channels(), 2, "channel count should be unchanged");
}
#[test]
fn push_two_clips_through_join_with_dissolve_should_produce_output() {
let mut graph = match FilterGraph::builder()
.join_with_dissolve(4.0, 1.0, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let clip_a = make_yuv420p_frame(64, 64);
let clip_b = make_yuv420p_frame(64, 64);
match graph.push_video(0, &clip_a) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &clip_b) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let result = graph.pull_video().expect("pull_video must not fail");
let out = result.expect("expected Some(frame) after join_with_dissolve push");
assert_eq!(
out.width(),
64,
"output width should match input after join_with_dissolve"
);
assert_eq!(
out.height(),
64,
"output height should match input after join_with_dissolve"
);
}
fn make_solid_yuv_frame(width: u32, height: u32, y_val: u8) -> VideoFrame {
let y = vec![y_val; (width * height) as usize];
let u = vec![128u8; ((width / 2) * (height / 2)) as usize];
let v = vec![128u8; ((width / 2) * (height / 2)) as usize];
VideoFrame::new(
vec![
PooledBuffer::standalone(y),
PooledBuffer::standalone(u),
PooledBuffer::standalone(v),
],
vec![width as usize, (width / 2) as usize, (width / 2) as usize],
width,
height,
PixelFormat::Yuv420p,
Timestamp::default(),
true,
)
.unwrap()
}
#[test]
fn blend_multiply_black_top_should_produce_black_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Multiply, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 0);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"Multiply with black top should produce near-black output (avg={avg})"
);
}
#[test]
fn blend_multiply_white_top_should_be_identity() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Multiply, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 255);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
(avg - 128.0).abs() < 15.0,
"Multiply with white top should preserve bottom luma (avg={avg})"
);
}
#[test]
fn blend_screen_white_top_should_produce_white_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Screen, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 255);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 245.0,
"Screen with white top should produce near-white output (avg={avg})"
);
}
#[test]
fn blend_overlay_midgray_top_should_be_identity() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Overlay, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 128);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
(avg - 128.0).abs() < 20.0,
"Overlay with mid-gray top should leave bottom luma approximately unchanged (avg={avg})"
);
}
#[test]
fn blend_colordodge_should_produce_brighter_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::ColorDodge, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 128);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 140.0,
"ColorDodge should produce brighter output than bottom luma=128 (avg={avg})"
);
}
#[test]
fn blend_colorburn_should_produce_darker_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::ColorBurn, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 128);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 115.0,
"ColorBurn should produce darker output than bottom luma=128 (avg={avg})"
);
}
#[test]
fn blend_darken_black_top_should_produce_black_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Darken, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 0);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"Darken with black top should produce near-black output (avg={avg})"
);
}
#[test]
fn blend_lighten_white_top_should_produce_white_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Lighten, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 255);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 245.0,
"Lighten with white top should produce near-white output (avg={avg})"
);
}
#[test]
fn blend_difference_with_self_should_produce_black() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Difference, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 128);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"Difference of identical layers should produce near-black output (avg={avg})"
);
}
#[test]
fn blend_add_black_top_should_be_identity() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Add, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 0);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
(avg - 128.0).abs() < 15.0,
"Add with black top should preserve bottom luma (avg={avg})"
);
}
#[test]
fn blend_subtract_white_top_should_produce_black() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Subtract, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 255);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"Subtract with white top should produce near-black output (avg={avg})"
);
}
#[test]
fn blend_luminosity_should_preserve_base_hue_and_saturation() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::Luminosity, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 128);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 160.0,
"Luminosity with brighter top should increase output luma toward top's value (avg={avg})"
);
}
#[test]
fn porter_duff_over_opaque_top_should_cover_bottom() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffOver, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 100);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 160.0,
"PorterDuffOver with opaque top should cover the bottom (avg={avg})"
);
}
#[test]
fn porter_duff_over_semitransparent_should_blend_correctly() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffOver, 0.5)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 100);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
}
#[test]
fn porter_duff_under_should_place_bottom_over_top() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffUnder, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 200);
let top_frame = make_solid_yuv_frame(64, 64, 100);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
}
#[test]
fn porter_duff_in_should_produce_black_where_bottom_is_black() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffIn, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 0);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"PorterDuffIn with black bottom should produce black output (avg={avg})"
);
}
#[test]
fn porter_duff_atop_should_use_bottom_alpha_for_output() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffAtop, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 100);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg > 85.0 && avg < 115.0,
"PorterDuffAtop output luma should equal bottom luma (~100), got avg={avg}"
);
}
#[test]
fn porter_duff_xor_identical_shapes_should_produce_zero_alpha() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffXor, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 255);
let top_frame = make_solid_yuv_frame(64, 64, 255);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"PorterDuffXor of identical full-luma shapes should produce black (avg={avg})"
);
}
#[test]
fn porter_duff_out_should_produce_black_where_bottom_is_white() {
let top = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.blend(top, BlendMode::PorterDuffOut, 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let bottom = make_solid_yuv_frame(64, 64, 255);
let top_frame = make_solid_yuv_frame(64, 64, 200);
match graph.push_video(0, &bottom) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &top_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
let luma = out.plane(0).expect("Y plane must exist");
let avg = luma.iter().map(|&b| b as f32).sum::<f32>() / luma.len() as f32;
assert!(
avg < 10.0,
"PorterDuffOut with white bottom should produce black output (avg={avg})"
);
}
#[test]
fn colorkey_solid_color_background_should_become_transparent() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.colorkey("0x808080", 0.3, 0.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv_frame(64, 64, 128, 128, 128);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(data) = out.plane(0) {
if data.len() == 64 * 64 * 4 {
let avg_a0 = data.chunks(4).map(|p| p[0] as f32).sum::<f32>() / (64.0 * 64.0);
let avg_a3 = data.chunks(4).map(|p| p[3] as f32).sum::<f32>() / (64.0 * 64.0);
let avg_alpha = avg_a0.min(avg_a3);
assert!(
avg_alpha < 10.0,
"gray pixels should be keyed out (avg_a0={avg_a0} avg_a3={avg_a3})"
);
}
}
}
#[test]
fn alpha_matte_white_region_should_be_opaque() {
let matte = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.alpha_matte(matte)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let main_frame = make_yuv420p_frame(64, 64);
let white_matte = make_solid_yuv_frame(64, 64, 235);
match graph.push_video(0, &main_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &white_matte) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(alpha) = out.plane(3) {
let avg = alpha.iter().map(|&b| b as f32).sum::<f32>() / alpha.len() as f32;
assert!(
avg > 200.0,
"white matte should produce high (opaque) alpha (avg={avg})"
);
}
}
#[test]
fn alpha_matte_black_region_should_be_transparent() {
let matte = FilterGraphBuilder::new().trim(0.0, 5.0);
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.alpha_matte(matte)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let main_frame = make_yuv420p_frame(64, 64);
let black_matte = make_solid_yuv_frame(64, 64, 16);
match graph.push_video(0, &main_frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
match graph.push_video(1, &black_matte) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(alpha) = out.plane(3) {
let avg = alpha.iter().map(|&b| b as f32).sum::<f32>() / alpha.len() as f32;
assert!(
avg < 30.0,
"black matte should produce low (transparent) alpha (avg={avg})"
);
}
}
#[test]
fn spill_suppress_should_reduce_green_cast_on_subject_edges() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.spill_suppress("green", 1.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv_frame(64, 64, 128, 44, 21);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(u_plane) = out.plane(1) {
let avg_u = u_plane.iter().map(|&b| b as f32).sum::<f32>() / u_plane.len() as f32;
assert!(
avg_u > 55.0,
"U plane should move toward neutral after full desaturation (avg_u={avg_u})"
);
}
}
#[test]
fn chromakey_green_screen_should_produce_transparent_green_area() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.chromakey("0x00FF00", 0.3, 0.0)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv_frame(64, 64, 150, 44, 21);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(alpha) = out.plane(3) {
let avg = alpha.iter().map(|&b| b as f32).sum::<f32>() / alpha.len() as f32;
assert!(
avg < 10.0,
"green pixels should be keyed out (avg alpha={avg})"
);
}
}
#[test]
fn lumakey_white_background_should_be_transparent_at_threshold_1() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.lumakey(0.9, 0.1, 0.0, false)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv_frame(64, 64, 235, 128, 128);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(alpha) = out.plane(3) {
let avg = alpha.iter().map(|&b| b as f32).sum::<f32>() / alpha.len() as f32;
assert!(
avg < 10.0,
"white pixels should be keyed out (avg alpha={avg})"
);
}
}
#[test]
fn lumakey_invert_should_key_out_dark_regions() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.lumakey(1.0, 0.1, 0.0, true)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv_frame(64, 64, 16, 128, 128);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(alpha) = out.plane(3) {
let avg = alpha.iter().map(|&b| b as f32).sum::<f32>() / alpha.len() as f32;
assert!(
avg < 10.0,
"dark pixels should be keyed out after alpha invert (avg alpha={avg})"
);
}
}
#[test]
fn feather_mask_should_produce_smooth_alpha_edges() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.rect_mask(0, 0, 32, 64, false)
.feather_mask(4)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(g_plane) = out.plane(0) {
let avg = g_plane.iter().map(|&b| b as f32).sum::<f32>() / g_plane.len() as f32;
assert!(
avg > 80.0 && avg < 180.0,
"feather_mask should preserve colour data (avg G={avg})"
);
}
}
#[test]
fn polygon_matte_triangle_should_isolate_triangular_region() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.polygon_matte(vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], false)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(data) = out.plane(0) {
if data.len() == 64 * 64 * 4 {
let avg_a0 = data.chunks(4).map(|p| p[0] as f32).sum::<f32>() / (64.0 * 64.0);
let avg_a3 = data.chunks(4).map(|p| p[3] as f32).sum::<f32>() / (64.0 * 64.0);
let alpha = avg_a0.max(avg_a3);
assert!(
alpha > 100.0 && alpha < 160.0,
"triangle mask should cover ~50% of pixels (avg≈129), got avg_a0={avg_a0} avg_a3={avg_a3}"
);
}
}
}
#[test]
fn rect_mask_100x100_makes_outside_transparent() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.rect_mask(0, 0, 100, 100, false)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(128, 128);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 128, "output width must match input");
assert_eq!(out.height(), 128, "output height must match input");
if let Some(data) = out.plane(0) {
if data.len() == 128 * 128 * 4 {
let avg_a0 = data.chunks(4).map(|p| p[0] as f32).sum::<f32>() / (128.0 * 128.0);
let avg_a3 = data.chunks(4).map(|p| p[3] as f32).sum::<f32>() / (128.0 * 128.0);
let alpha = avg_a0.max(avg_a3);
assert!(
alpha > 130.0 && alpha < 180.0,
"rect_mask inside should be opaque (avg≈155), got avg_a0={avg_a0} avg_a3={avg_a3}"
);
}
}
}
#[test]
fn rect_mask_inverted_makes_inside_transparent() {
let mut graph = match FilterGraph::builder()
.trim(0.0, 5.0)
.rect_mask(0, 0, 32, 32, true)
.build()
{
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_yuv420p_frame(64, 64);
match graph.push_video(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping: {e}");
return;
}
}
let out = graph
.pull_video()
.expect("pull_video must not fail")
.expect("expected Some(frame)");
assert_eq!(out.width(), 64, "output width must match input");
assert_eq!(out.height(), 64, "output height must match input");
if let Some(data) = out.plane(0) {
if data.len() == 64 * 64 * 4 {
let avg_a0 = data.chunks(4).map(|p| p[0] as f32).sum::<f32>() / (64.0 * 64.0);
let avg_a3 = data.chunks(4).map(|p| p[3] as f32).sum::<f32>() / (64.0 * 64.0);
let alpha = avg_a0.max(avg_a3);
assert!(
alpha > 165.0 && alpha < 215.0,
"inverted rect_mask outside should be opaque (avg≈191), got avg_a0={avg_a0} avg_a3={avg_a3}"
);
}
}
}