#![allow(unsafe_code)]
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::too_many_lines)]
mod build;
mod convert;
mod normalize;
mod push_pull;
pub(crate) use build::add_and_link_step;
pub(crate) use build::add_asetrate_resample_chain;
use std::ptr::NonNull;
use ff_format::AudioFrame;
use crate::error::FilterError;
use crate::graph::{FilterStep, HwAccel};
pub(super) const VIDEO_TIME_BASE_NUM: i32 = 1;
pub(super) const VIDEO_TIME_BASE_DEN: i32 = 90_000;
pub(super) const AUDIO_TIME_BASE_NUM: i32 = 1;
type FilterCtxVec = Vec<Option<NonNull<ff_sys::AVFilterContext>>>;
type BuildResult = Result<(FilterCtxVec, NonNull<ff_sys::AVFilterContext>), FilterError>;
type VideoGraphResult = Result<
(
FilterCtxVec,
NonNull<ff_sys::AVFilterContext>,
Option<*mut ff_sys::AVBufferRef>,
),
FilterError,
>;
pub(super) fn ffmpeg_err(code: i32) -> FilterError {
FilterError::Ffmpeg {
code,
message: ff_sys::av_error_string(code),
}
}
pub(crate) fn validate_filter_steps(steps: &[FilterStep]) -> Result<(), FilterError> {
for step in steps {
let name =
std::ffi::CString::new(step.filter_name()).map_err(|_| FilterError::BuildFailed)?;
let filter = unsafe { ff_sys::avfilter_get_by_name(name.as_ptr()) };
if filter.is_null() {
log::warn!(
"filter lookup returned null at build time name={}, \
registry may not be initialised; will be rechecked at push time",
step.filter_name()
);
return Ok(());
}
}
Ok(())
}
pub(crate) struct FilterGraphInner {
graph: Option<NonNull<ff_sys::AVFilterGraph>>,
src_ctxs: Vec<Option<NonNull<ff_sys::AVFilterContext>>>,
vsink_ctx: Option<NonNull<ff_sys::AVFilterContext>>,
asink_ctx: Option<NonNull<ff_sys::AVFilterContext>>,
steps: Vec<FilterStep>,
hw: Option<HwAccel>,
hw_device_ctx: Option<*mut ff_sys::AVBufferRef>,
loudness_buf: Vec<AudioFrame>,
loudness_output: Vec<AudioFrame>,
loudness_output_idx: usize,
loudness_pass2_done: bool,
peak_buf: Vec<AudioFrame>,
peak_output: Vec<AudioFrame>,
peak_output_idx: usize,
peak_pass2_done: bool,
}
unsafe impl Send for FilterGraphInner {}
impl FilterGraphInner {
pub(crate) fn new(steps: Vec<FilterStep>, hw: Option<HwAccel>) -> Self {
Self {
graph: None,
src_ctxs: Vec::new(),
vsink_ctx: None,
asink_ctx: None,
steps,
hw,
hw_device_ctx: None,
loudness_buf: Vec::new(),
loudness_output: Vec::new(),
loudness_output_idx: 0,
loudness_pass2_done: false,
peak_buf: Vec::new(),
peak_output: Vec::new(),
peak_output_idx: 0,
peak_pass2_done: false,
}
}
pub(crate) fn push_step(&mut self, step: FilterStep) {
self.steps.push(step);
}
pub(crate) fn with_prebuilt_video_graph(
graph: NonNull<ff_sys::AVFilterGraph>,
vsink_ctx: NonNull<ff_sys::AVFilterContext>,
) -> Self {
Self {
graph: Some(graph),
src_ctxs: Vec::new(),
vsink_ctx: Some(vsink_ctx),
asink_ctx: None,
steps: Vec::new(),
hw: None,
hw_device_ctx: None,
loudness_buf: Vec::new(),
loudness_output: Vec::new(),
loudness_output_idx: 0,
loudness_pass2_done: false,
peak_buf: Vec::new(),
peak_output: Vec::new(),
peak_output_idx: 0,
peak_pass2_done: false,
}
}
pub(crate) fn with_prebuilt_audio_graph(
graph: NonNull<ff_sys::AVFilterGraph>,
asink_ctx: NonNull<ff_sys::AVFilterContext>,
) -> Self {
Self {
graph: Some(graph),
src_ctxs: Vec::new(),
vsink_ctx: None,
asink_ctx: Some(asink_ctx),
steps: Vec::new(),
hw: None,
hw_device_ctx: None,
loudness_buf: Vec::new(),
loudness_output: Vec::new(),
loudness_output_idx: 0,
loudness_pass2_done: false,
peak_buf: Vec::new(),
peak_output: Vec::new(),
peak_output_idx: 0,
peak_pass2_done: false,
}
}
}
#[cfg(test)]
mod tests {
use super::build::{audio_buffersrc_args, video_buffersrc_args};
use super::*;
use crate::graph::FilterStep;
#[test]
fn new_should_start_with_no_graph_allocated() {
let inner = FilterGraphInner::new(
vec![FilterStep::Scale {
width: 1280,
height: 720,
algorithm: crate::graph::ScaleAlgorithm::Fast,
}],
None,
);
assert!(
inner.graph.is_none(),
"avfilter_graph_alloc must not be called at construction time"
);
assert!(
inner.src_ctxs.is_empty(),
"src_ctxs should be empty before first push"
);
assert!(
inner.vsink_ctx.is_none(),
"vsink_ctx should be None before first push"
);
assert!(
inner.asink_ctx.is_none(),
"asink_ctx should be None before first push"
);
}
#[test]
fn drop_uninitialised_should_be_a_no_op() {
let inner = FilterGraphInner::new(
vec![FilterStep::Scale {
width: 640,
height: 360,
algorithm: crate::graph::ScaleAlgorithm::Fast,
}],
None,
);
drop(inner); }
#[test]
fn filter_graph_inner_should_impl_send() {
fn assert_send<T: Send>() {}
assert_send::<FilterGraphInner>();
}
#[test]
fn video_buffersrc_args_should_contain_size_pix_fmt_and_time_base() {
let args = video_buffersrc_args(1920, 1080, 0);
assert!(
args.contains("video_size=1920x1080"),
"missing video_size: {args}"
);
assert!(args.contains("pix_fmt=0"), "missing pix_fmt: {args}");
assert!(
args.contains("time_base=1/90000"),
"missing time_base: {args}"
);
assert!(
args.contains("pixel_aspect=1/1"),
"missing pixel_aspect: {args}"
);
}
#[test]
fn audio_buffersrc_args_should_contain_sample_rate_format_and_channels() {
let args = audio_buffersrc_args(44100, "fltp", 2);
assert!(
args.contains("sample_rate=44100"),
"missing sample_rate: {args}"
);
assert!(
args.contains("sample_fmt=fltp"),
"missing sample_fmt: {args}"
);
assert!(args.contains("channels=2"), "missing channels: {args}");
assert!(
args.contains("time_base=1/44100"),
"missing time_base: {args}"
);
}
#[test]
fn audio_buffersrc_args_time_base_should_match_sample_rate() {
let args = audio_buffersrc_args(48000, "s16", 1);
assert!(
args.contains("time_base=1/48000"),
"time_base denominator must equal sample_rate: {args}"
);
}
#[test]
fn video_input_count_should_return_1_for_single_input_steps() {
let inner = FilterGraphInner::new(
vec![FilterStep::Scale {
width: 1280,
height: 720,
algorithm: crate::graph::ScaleAlgorithm::Fast,
}],
None,
);
assert_eq!(inner.video_input_count(), 1);
}
#[test]
fn video_input_count_should_return_2_for_overlay() {
let inner = FilterGraphInner::new(vec![FilterStep::Overlay { x: 10, y: 10 }], None);
assert_eq!(inner.video_input_count(), 2);
}
#[test]
fn video_input_count_should_return_1_with_no_overlay_in_chain() {
let inner = FilterGraphInner::new(
vec![
FilterStep::Trim {
start: 0.0,
end: 5.0,
},
FilterStep::Scale {
width: 640,
height: 360,
algorithm: crate::graph::ScaleAlgorithm::Fast,
},
],
None,
);
assert_eq!(inner.video_input_count(), 1);
}
#[test]
fn ffmpeg_err_should_return_ffmpeg_variant_with_code() {
let err = ffmpeg_err(-22);
assert!(
matches!(err, FilterError::Ffmpeg { code: -22, .. }),
"expected Ffmpeg variant with code -22, got {err:?}"
);
}
#[test]
fn ffmpeg_err_should_populate_non_empty_message() {
let err = ffmpeg_err(-22);
if let FilterError::Ffmpeg { message, .. } = err {
assert!(
!message.is_empty(),
"message must not be empty for a known error code"
);
} else {
panic!("expected Ffmpeg variant");
}
}
#[test]
fn apply_animations_with_no_graph_should_be_a_no_op() {
use crate::animation::{AnimationEntry, AnimationTrack, Easing, Keyframe};
use std::time::Duration;
let inner = FilterGraphInner::new(
vec![FilterStep::Scale {
width: 1280,
height: 720,
algorithm: crate::graph::ScaleAlgorithm::Fast,
}],
None,
);
assert!(
inner.graph.is_none(),
"graph must be None before first push"
);
let track = AnimationTrack::new()
.push(Keyframe::new(Duration::ZERO, 0.0_f64, Easing::Linear))
.push(Keyframe::new(
Duration::from_secs(1),
1.0_f64,
Easing::Linear,
));
let animations = vec![AnimationEntry {
node_name: "gblur_0".to_owned(),
param: "sigma",
track,
suffix: "",
}];
inner.apply_animations(&animations, Duration::from_millis(500));
}
#[test]
fn apply_animations_with_empty_slice_should_be_a_no_op() {
use std::time::Duration;
let inner = FilterGraphInner::new(vec![], None);
inner.apply_animations(&[], Duration::ZERO);
}
#[test]
fn validate_filter_steps_should_succeed_for_known_filters() {
let steps = vec![FilterStep::Scale {
width: 640,
height: 360,
algorithm: crate::graph::ScaleAlgorithm::Fast,
}];
assert!(
validate_filter_steps(&steps).is_ok(),
"validate_filter_steps must not return Err for a standard filter: \
either the filter was found, or the registry is not yet initialised \
and validation is deferred"
);
}
}