use super::{
AUDIO_TIME_BASE_NUM, BuildResult, FilterCtxVec, VIDEO_TIME_BASE_DEN, VIDEO_TIME_BASE_NUM,
VideoGraphResult, ffmpeg_err,
};
use std::ptr::NonNull;
use std::time::Duration;
use crate::blend::BlendMode;
use crate::error::FilterError;
use crate::graph::filter_step::FilterStep;
use crate::graph::types::{EqBand, HwAccel};
pub(super) fn hw_accel_to_device_type(hw: HwAccel) -> ff_sys::AVHWDeviceType {
match hw {
HwAccel::Cuda => ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_CUDA,
HwAccel::VideoToolbox => ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
HwAccel::Vaapi => ff_sys::AVHWDeviceType_AV_HWDEVICE_TYPE_VAAPI,
}
}
pub(super) unsafe fn create_hw_filter(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
filter_name: &std::ffi::CStr,
instance_name: &std::ffi::CStr,
hw_ctx: *mut ff_sys::AVBufferRef,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let filter = ff_sys::avfilter_get_by_name(filter_name.as_ptr());
if filter.is_null() {
log::warn!(
"hw filter not found name={}",
filter_name.to_str().unwrap_or("?")
);
return Err(FilterError::BuildFailed);
}
let mut ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ctx,
filter,
instance_name.as_ptr(),
std::ptr::null(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"hw filter creation failed name={} code={ret}",
filter_name.to_str().unwrap_or("?")
);
return Err(FilterError::BuildFailed);
}
log::debug!(
"hw filter added name={}",
filter_name.to_str().unwrap_or("?")
);
let filter_hw_ref = ff_sys::av_buffer_ref(hw_ctx);
if filter_hw_ref.is_null() {
log::warn!("av_buffer_ref failed for hw device context");
return Err(FilterError::BuildFailed);
}
(*ctx).hw_device_ctx = filter_hw_ref;
let ret = ff_sys::avfilter_link(prev_ctx, 0, ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(ctx)
}
pub(crate) unsafe fn add_and_link_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
step: &FilterStep,
index: usize,
prefix: &str,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let filter_name =
std::ffi::CString::new(step.filter_name()).map_err(|_| FilterError::BuildFailed)?;
let filter = ff_sys::avfilter_get_by_name(filter_name.as_ptr());
if filter.is_null() {
log::warn!("filter not found name={}", step.filter_name());
return Err(FilterError::BuildFailed);
}
let step_name =
std::ffi::CString::new(format!("{prefix}{index}")).map_err(|_| FilterError::BuildFailed)?;
let step_args_str = step.args();
let step_args =
std::ffi::CString::new(step_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut step_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut step_ctx,
filter,
step_name.as_ptr(),
step_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name={} args={}",
step.filter_name(),
step.args()
);
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter added name={} args={}",
step.filter_name(),
step.args()
);
let ret = ff_sys::avfilter_link(prev_ctx, 0, step_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(step_ctx)
}
pub(super) unsafe fn add_raw_filter_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
filter_name: &str,
args: &str,
index: usize,
prefix: &str,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let filter_cname = std::ffi::CString::new(filter_name).map_err(|_| FilterError::BuildFailed)?;
let filter = ff_sys::avfilter_get_by_name(filter_cname.as_ptr());
if filter.is_null() {
log::warn!("filter not found name={filter_name}");
return Err(FilterError::BuildFailed);
}
let step_name =
std::ffi::CString::new(format!("{prefix}{index}")).map_err(|_| FilterError::BuildFailed)?;
let step_args = std::ffi::CString::new(args).map_err(|_| FilterError::BuildFailed)?;
let mut step_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut step_ctx,
filter,
step_name.as_ptr(),
step_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name={filter_name} args={args}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name={filter_name} args={args}");
let ret = ff_sys::avfilter_link(prev_ctx, 0, step_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(step_ctx)
}
pub(super) unsafe fn add_setpts_after_trim(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
trim_index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let setpts = ff_sys::avfilter_get_by_name(c"setpts".as_ptr());
if setpts.is_null() {
log::warn!("filter not found name=setpts");
return Err(FilterError::BuildFailed);
}
let name = std::ffi::CString::new(format!("setpts{trim_index}"))
.map_err(|_| FilterError::BuildFailed)?;
let mut ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ctx,
setpts,
name.as_ptr(),
c"PTS-STARTPTS".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=setpts args=PTS-STARTPTS");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=setpts args=PTS-STARTPTS");
let ret = ff_sys::avfilter_link(prev_ctx, 0, ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(ctx)
}
pub(super) unsafe fn add_fit_to_aspect_pad(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
width: u32,
height: u32,
color: &str,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let pad_filter = ff_sys::avfilter_get_by_name(c"pad".as_ptr());
if pad_filter.is_null() {
log::warn!("filter not found name=pad (fit_to_aspect)");
return Err(FilterError::BuildFailed);
}
let name =
std::ffi::CString::new(format!("fitpad{index}")).map_err(|_| FilterError::BuildFailed)?;
let args_str = format!("width={width}:height={height}:x=(ow-iw)/2:y=(oh-ih)/2:color={color}");
let args = std::ffi::CString::new(args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ctx,
pad_filter,
name.as_ptr(),
args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=pad args={args_str}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=pad args={args_str} index={index}");
let ret = ff_sys::avfilter_link(prev_ctx, 0, ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(ctx)
}
pub(super) fn decompose_atempo(factor: f64) -> Vec<f64> {
let mut remaining = factor;
let mut chain = Vec::new();
while remaining > 2.0 {
chain.push(2.0);
remaining /= 2.0;
}
while remaining < 0.5 {
chain.push(0.5);
remaining /= 0.5;
}
chain.push(remaining);
chain
}
pub(super) unsafe fn add_atempo_chain(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
factor: f64,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let atempo_filter = ff_sys::avfilter_get_by_name(c"atempo".as_ptr());
if atempo_filter.is_null() {
log::warn!("filter not found name=atempo (speed)");
return Err(FilterError::BuildFailed);
}
let chain = decompose_atempo(factor);
let mut ctx = prev_ctx;
for (j, &val) in chain.iter().enumerate() {
let name = std::ffi::CString::new(format!("atempo{index}_{j}"))
.map_err(|_| FilterError::BuildFailed)?;
let args_str = format!("{val}");
let args =
std::ffi::CString::new(args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut atempo_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut atempo_ctx,
atempo_filter,
name.as_ptr(),
args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=atempo args={val}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=atempo args={val} index={index}_{j}");
let ret = ff_sys::avfilter_link(ctx, 0, atempo_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
ctx = atempo_ctx;
}
Ok(ctx)
}
#[cfg(test)]
mod tests {
use super::decompose_atempo;
#[test]
fn decompose_atempo_should_return_single_value_for_factor_within_range() {
assert_eq!(decompose_atempo(1.5), vec![1.5]);
}
#[test]
fn decompose_atempo_should_return_single_value_for_factor_1() {
assert_eq!(decompose_atempo(1.0), vec![1.0]);
}
#[test]
fn decompose_atempo_should_chain_two_instances_for_factor_4() {
assert_eq!(decompose_atempo(4.0), vec![2.0, 2.0]);
}
#[test]
fn decompose_atempo_should_chain_three_instances_for_factor_8() {
assert_eq!(decompose_atempo(8.0), vec![2.0, 2.0, 2.0]);
}
#[test]
fn decompose_atempo_should_chain_two_instances_for_factor_0_25() {
let chain = decompose_atempo(0.25);
let product: f64 = chain.iter().product();
assert!(
(product - 0.25).abs() < 1e-9,
"product should be ~0.25, got {product}, chain={chain:?}"
);
for &v in &chain {
assert!(
(0.5..=2.0).contains(&v),
"each value must be in [0.5, 2.0], got {v}"
);
}
}
#[test]
fn decompose_atempo_should_produce_values_all_within_valid_range() {
for factor in [0.1, 0.25, 0.5, 1.0, 1.5, 2.0, 4.0, 8.0, 16.0, 100.0] {
let chain = decompose_atempo(factor);
assert!(
!chain.is_empty(),
"chain must not be empty for factor={factor}"
);
let product: f64 = chain.iter().product();
assert!(
(product - factor).abs() < 1e-6,
"product {product} must equal factor {factor}, chain={chain:?}"
);
for &v in &chain {
assert!(
(0.5..=2.0).contains(&v),
"each value must be in [0.5, 2.0], got {v} for factor={factor}"
);
}
}
}
}
pub(super) unsafe fn add_overlay_image_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
path: &str,
x: &str,
y: &str,
opacity: f32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let movie_filter = ff_sys::avfilter_get_by_name(c"movie".as_ptr());
if movie_filter.is_null() {
log::warn!("filter not found name=movie (overlay_image)");
return Err(FilterError::BuildFailed);
}
let movie_name = CString::new(format!("movie{index}")).map_err(|_| FilterError::BuildFailed)?;
let movie_args =
CString::new(format!("filename={path}")).map_err(|_| FilterError::BuildFailed)?;
let mut movie_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut movie_ctx,
movie_filter,
movie_name.as_ptr(),
movie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=movie args=filename={path}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=movie args=filename={path} index={index}");
let lut_filter = ff_sys::avfilter_get_by_name(c"lut".as_ptr());
if lut_filter.is_null() {
log::warn!("filter not found name=lut (overlay_image)");
return Err(FilterError::BuildFailed);
}
let lut_name = CString::new(format!("lut{index}")).map_err(|_| FilterError::BuildFailed)?;
let lut_args_str = format!("a=val*{opacity}");
let lut_args = CString::new(lut_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut lut_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut lut_ctx,
lut_filter,
lut_name.as_ptr(),
lut_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=lut args={lut_args_str}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=lut args={lut_args_str} index={index}");
let ret = ff_sys::avfilter_link(movie_ctx, 0, lut_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let overlay_filter = ff_sys::avfilter_get_by_name(c"overlay".as_ptr());
if overlay_filter.is_null() {
log::warn!("filter not found name=overlay (overlay_image)");
return Err(FilterError::BuildFailed);
}
let overlay_name =
CString::new(format!("step{index}")).map_err(|_| FilterError::BuildFailed)?;
let overlay_args_str = format!("{x}:{y}");
let overlay_args =
CString::new(overlay_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut overlay_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut overlay_ctx,
overlay_filter,
overlay_name.as_ptr(),
overlay_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=overlay args={overlay_args_str}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=overlay args={overlay_args_str} index={index}");
let ret = ff_sys::avfilter_link(prev_ctx, 0, overlay_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(lut_ctx, 0, overlay_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
Ok(overlay_ctx)
}
pub(super) unsafe fn add_blend_normal_step(
graph: *mut ff_sys::AVFilterGraph,
bottom_ctx: *mut ff_sys::AVFilterContext,
top_src_ctx: *mut ff_sys::AVFilterContext,
top_steps: &[FilterStep],
opacity: f32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let mut top_ctx = top_src_ctx;
for (j, step) in top_steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
| FilterStep::LoudnessNormalize { .. }
| FilterStep::NormalizePeak { .. }
) {
continue;
}
top_ctx = add_and_link_step(graph, top_ctx, step, index * 1000 + j, "blend_top")?;
}
if opacity < 1.0 {
let ccm_name =
CString::new(format!("blend_ccm{index}")).map_err(|_| FilterError::BuildFailed)?;
let ccm_args_str = format!("aa={opacity}");
let ccm_args = CString::new(ccm_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let ccm_filter = ff_sys::avfilter_get_by_name(c"colorchannelmixer".as_ptr());
if ccm_filter.is_null() {
log::warn!("filter not found name=colorchannelmixer (blend_normal)");
return Err(FilterError::BuildFailed);
}
let mut ccm_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ccm_ctx,
ccm_filter,
ccm_name.as_ptr(),
ccm_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=colorchannelmixer args={ccm_args_str}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=colorchannelmixer args={ccm_args_str} index={index}");
let ret = ff_sys::avfilter_link(top_ctx, 0, ccm_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
top_ctx = ccm_ctx;
}
let overlay_filter = ff_sys::avfilter_get_by_name(c"overlay".as_ptr());
if overlay_filter.is_null() {
log::warn!("filter not found name=overlay (blend_normal)");
return Err(FilterError::BuildFailed);
}
let overlay_name =
CString::new(format!("blend_overlay{index}")).map_err(|_| FilterError::BuildFailed)?;
let overlay_args = c"format=auto:shortest=1";
let mut overlay_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut overlay_ctx,
overlay_filter,
overlay_name.as_ptr(),
overlay_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name=overlay args=format=auto:shortest=1 (blend_normal)"
);
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter added name=overlay args=format=auto:shortest=1 index={index} (blend_normal)"
);
let ret = ff_sys::avfilter_link(bottom_ctx, 0, overlay_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(top_ctx, 0, overlay_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter blend_normal expanded opacity={opacity} index={index}");
Ok(overlay_ctx)
}
pub(super) unsafe fn add_blend_photographic_step(
graph: *mut ff_sys::AVFilterGraph,
bottom_ctx: *mut ff_sys::AVFilterContext,
top_src_ctx: *mut ff_sys::AVFilterContext,
top_steps: &[FilterStep],
mode_name: &str,
opacity: f32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let mut top_ctx = top_src_ctx;
for (j, step) in top_steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
| FilterStep::LoudnessNormalize { .. }
| FilterStep::NormalizePeak { .. }
) {
continue;
}
top_ctx = add_and_link_step(graph, top_ctx, step, index * 1000 + j, "blend_top")?;
}
let blend_filter = ff_sys::avfilter_get_by_name(c"blend".as_ptr());
if blend_filter.is_null() {
log::warn!("filter not found name=blend (blend_photographic)");
return Err(FilterError::BuildFailed);
}
let blend_name =
CString::new(format!("blend_phot{index}")).map_err(|_| FilterError::BuildFailed)?;
let blend_args_str = if (opacity - 1.0).abs() < f32::EPSILON {
format!("all_mode={mode_name}")
} else {
format!("all_mode={mode_name}:all_opacity={opacity}")
};
let blend_args = CString::new(blend_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut blend_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut blend_ctx,
blend_filter,
blend_name.as_ptr(),
blend_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=blend args={blend_args_str} (blend_photographic)");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=blend args={blend_args_str} index={index} (blend_photographic)");
let ret = ff_sys::avfilter_link(bottom_ctx, 0, blend_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(top_ctx, 0, blend_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter blend_photographic expanded mode={mode_name} opacity={opacity} index={index}"
);
Ok(blend_ctx)
}
pub(super) unsafe fn add_blend_under_step(
graph: *mut ff_sys::AVFilterGraph,
bottom_ctx: *mut ff_sys::AVFilterContext,
top_src_ctx: *mut ff_sys::AVFilterContext,
top_steps: &[FilterStep],
opacity: f32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let mut top_ctx = top_src_ctx;
for (j, step) in top_steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
| FilterStep::LoudnessNormalize { .. }
| FilterStep::NormalizePeak { .. }
) {
continue;
}
top_ctx = add_and_link_step(graph, top_ctx, step, index * 1000 + j, "blend_top")?;
}
if opacity < 1.0 {
let ccm_name = CString::new(format!("blend_under_ccm{index}"))
.map_err(|_| FilterError::BuildFailed)?;
let ccm_args_str = format!("aa={opacity}");
let ccm_args = CString::new(ccm_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let ccm_filter = ff_sys::avfilter_get_by_name(c"colorchannelmixer".as_ptr());
if ccm_filter.is_null() {
log::warn!("filter not found name=colorchannelmixer (blend_under)");
return Err(FilterError::BuildFailed);
}
let mut ccm_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut ccm_ctx,
ccm_filter,
ccm_name.as_ptr(),
ccm_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=colorchannelmixer args={ccm_args_str}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=colorchannelmixer args={ccm_args_str} index={index}");
let ret = ff_sys::avfilter_link(top_ctx, 0, ccm_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
top_ctx = ccm_ctx;
}
let overlay_filter = ff_sys::avfilter_get_by_name(c"overlay".as_ptr());
if overlay_filter.is_null() {
log::warn!("filter not found name=overlay (blend_under)");
return Err(FilterError::BuildFailed);
}
let overlay_name = CString::new(format!("blend_under_overlay{index}"))
.map_err(|_| FilterError::BuildFailed)?;
let overlay_args = c"format=auto:shortest=1";
let mut overlay_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut overlay_ctx,
overlay_filter,
overlay_name.as_ptr(),
overlay_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=overlay args=format=auto:shortest=1 (blend_under)");
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter added name=overlay args=format=auto:shortest=1 index={index} (blend_under)"
);
let ret = ff_sys::avfilter_link(top_ctx, 0, overlay_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(bottom_ctx, 0, overlay_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter blend_under expanded opacity={opacity} index={index}");
Ok(overlay_ctx)
}
pub(super) unsafe fn add_blend_expr_step(
graph: *mut ff_sys::AVFilterGraph,
bottom_ctx: *mut ff_sys::AVFilterContext,
top_src_ctx: *mut ff_sys::AVFilterContext,
top_steps: &[FilterStep],
expr: &str,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let mut top_ctx = top_src_ctx;
for (j, step) in top_steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
| FilterStep::LoudnessNormalize { .. }
| FilterStep::NormalizePeak { .. }
) {
continue;
}
top_ctx = add_and_link_step(graph, top_ctx, step, index * 1000 + j, "blend_top")?;
}
let blend_filter = ff_sys::avfilter_get_by_name(c"blend".as_ptr());
if blend_filter.is_null() {
log::warn!("filter not found name=blend (blend_expr)");
return Err(FilterError::BuildFailed);
}
let blend_name =
CString::new(format!("blend_expr{index}")).map_err(|_| FilterError::BuildFailed)?;
let blend_args_str = format!("all_expr={expr}");
let blend_args = CString::new(blend_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut blend_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut blend_ctx,
blend_filter,
blend_name.as_ptr(),
blend_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=blend args={blend_args_str} (blend_expr)");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=blend args={blend_args_str} index={index} (blend_expr)");
let ret = ff_sys::avfilter_link(bottom_ctx, 0, blend_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(top_ctx, 0, blend_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter blend_expr expanded expr={expr} index={index}");
Ok(blend_ctx)
}
pub(super) unsafe fn add_glow_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
threshold: f32,
radius: f32,
intensity: f32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let t = threshold.clamp(0.0, 1.0);
let r = radius.clamp(0.5, 50.0);
let iv = intensity.clamp(0.0, 2.0);
let split_filter = ff_sys::avfilter_get_by_name(c"split".as_ptr());
if split_filter.is_null() {
log::warn!("filter not found name=split (glow)");
return Err(FilterError::BuildFailed);
}
let split_name =
CString::new(format!("glow_split{index}")).map_err(|_| FilterError::BuildFailed)?;
let mut split_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut split_ctx,
split_filter,
split_name.as_ptr(),
c"2".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=split (glow) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=split args=2 index={index} (glow)");
let ret = ff_sys::avfilter_link(prev_ctx, 0, split_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let curves_filter = ff_sys::avfilter_get_by_name(c"curves".as_ptr());
if curves_filter.is_null() {
log::warn!("filter not found name=curves (glow)");
return Err(FilterError::BuildFailed);
}
let curves_name =
CString::new(format!("glow_curves{index}")).map_err(|_| FilterError::BuildFailed)?;
let hi_lo = format!("0/0 {t}/0 1/1");
let curves_args_str = format!("all='{hi_lo}'");
let curves_args =
CString::new(curves_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut curves_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut curves_ctx,
curves_filter,
curves_name.as_ptr(),
curves_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=curves args={curves_args_str} (glow) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=curves args={curves_args_str} index={index} (glow)");
let ret = ff_sys::avfilter_link(split_ctx, 1, curves_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let gblur_filter = ff_sys::avfilter_get_by_name(c"gblur".as_ptr());
if gblur_filter.is_null() {
log::warn!("filter not found name=gblur (glow)");
return Err(FilterError::BuildFailed);
}
let gblur_name =
CString::new(format!("glow_gblur{index}")).map_err(|_| FilterError::BuildFailed)?;
let gblur_args_str = format!("sigma={r}");
let gblur_args = CString::new(gblur_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut gblur_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut gblur_ctx,
gblur_filter,
gblur_name.as_ptr(),
gblur_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=gblur args={gblur_args_str} (glow) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=gblur args={gblur_args_str} index={index} (glow)");
let ret = ff_sys::avfilter_link(curves_ctx, 0, gblur_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let blend_filter = ff_sys::avfilter_get_by_name(c"blend".as_ptr());
if blend_filter.is_null() {
log::warn!("filter not found name=blend (glow)");
return Err(FilterError::BuildFailed);
}
let blend_name =
CString::new(format!("glow_blend{index}")).map_err(|_| FilterError::BuildFailed)?;
let blend_args_str = format!("all_mode=addition:all_opacity={iv}");
let blend_args = CString::new(blend_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut blend_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut blend_ctx,
blend_filter,
blend_name.as_ptr(),
blend_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=blend args={blend_args_str} (glow) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=blend args={blend_args_str} index={index} (glow)");
let ret = ff_sys::avfilter_link(split_ctx, 0, blend_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(gblur_ctx, 0, blend_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter glow expanded threshold={threshold} radius={radius} intensity={intensity} index={index}"
);
Ok(blend_ctx)
}
pub(super) unsafe fn add_feather_mask_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
radius: u32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let split_filter = ff_sys::avfilter_get_by_name(c"split".as_ptr());
if split_filter.is_null() {
log::warn!("filter not found name=split (feather_mask)");
return Err(FilterError::BuildFailed);
}
let split_name =
CString::new(format!("feather_split{index}")).map_err(|_| FilterError::BuildFailed)?;
let mut split_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut split_ctx,
split_filter,
split_name.as_ptr(),
c"2".as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=split (feather_mask) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=split args=2 index={index} (feather_mask)");
let ret = ff_sys::avfilter_link(prev_ctx, 0, split_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let alphaextract_filter = ff_sys::avfilter_get_by_name(c"alphaextract".as_ptr());
if alphaextract_filter.is_null() {
log::warn!("filter not found name=alphaextract (feather_mask)");
return Err(FilterError::BuildFailed);
}
let alphaextract_name = CString::new(format!("feather_alphaextract{index}"))
.map_err(|_| FilterError::BuildFailed)?;
let mut alphaextract_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut alphaextract_ctx,
alphaextract_filter,
alphaextract_name.as_ptr(),
std::ptr::null(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=alphaextract (feather_mask) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=alphaextract index={index} (feather_mask)");
let ret = ff_sys::avfilter_link(split_ctx, 1, alphaextract_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let gblur_filter = ff_sys::avfilter_get_by_name(c"gblur".as_ptr());
if gblur_filter.is_null() {
log::warn!("filter not found name=gblur (feather_mask)");
return Err(FilterError::BuildFailed);
}
let gblur_name =
CString::new(format!("feather_gblur{index}")).map_err(|_| FilterError::BuildFailed)?;
let gblur_args_str = format!("sigma={radius}");
let gblur_args = CString::new(gblur_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut gblur_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut gblur_ctx,
gblur_filter,
gblur_name.as_ptr(),
gblur_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name=gblur args={gblur_args_str} (feather_mask) code={ret}"
);
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=gblur args={gblur_args_str} index={index} (feather_mask)");
let ret = ff_sys::avfilter_link(alphaextract_ctx, 0, gblur_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let alphamerge_filter = ff_sys::avfilter_get_by_name(c"alphamerge".as_ptr());
if alphamerge_filter.is_null() {
log::warn!("filter not found name=alphamerge (feather_mask)");
return Err(FilterError::BuildFailed);
}
let alphamerge_name =
CString::new(format!("feather_alphamerge{index}")).map_err(|_| FilterError::BuildFailed)?;
let mut alphamerge_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut alphamerge_ctx,
alphamerge_filter,
alphamerge_name.as_ptr(),
std::ptr::null(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=alphamerge (feather_mask) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=alphamerge index={index} (feather_mask)");
let ret = ff_sys::avfilter_link(split_ctx, 0, alphamerge_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(gblur_ctx, 0, alphamerge_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter feather_mask expanded radius={radius} index={index}");
Ok(alphamerge_ctx)
}
pub(super) unsafe fn add_alphamerge_step(
graph: *mut ff_sys::AVFilterGraph,
bottom_ctx: *mut ff_sys::AVFilterContext,
matte_src_ctx: *mut ff_sys::AVFilterContext,
matte_steps: &[FilterStep],
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let mut matte_ctx = matte_src_ctx;
for (j, step) in matte_steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
| FilterStep::LoudnessNormalize { .. }
| FilterStep::NormalizePeak { .. }
) {
continue;
}
matte_ctx = add_and_link_step(graph, matte_ctx, step, index * 1000 + j, "matte")?;
}
let alphamerge_filter = ff_sys::avfilter_get_by_name(c"alphamerge".as_ptr());
if alphamerge_filter.is_null() {
log::warn!("filter not found name=alphamerge");
return Err(FilterError::BuildFailed);
}
let alphamerge_name =
CString::new(format!("alphamerge{index}")).map_err(|_| FilterError::BuildFailed)?;
let mut alphamerge_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut alphamerge_ctx,
alphamerge_filter,
alphamerge_name.as_ptr(),
std::ptr::null(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=alphamerge code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=alphamerge index={index}");
let ret = ff_sys::avfilter_link(bottom_ctx, 0, alphamerge_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(matte_ctx, 0, alphamerge_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter alphamerge linked index={index}");
Ok(alphamerge_ctx)
}
pub(super) fn video_buffersrc_args(
width: u32,
height: u32,
pix_fmt: std::os::raw::c_int,
) -> String {
format!(
"video_size={}x{}:pix_fmt={}:time_base={}/{}:pixel_aspect=1/1",
width, height, pix_fmt, VIDEO_TIME_BASE_NUM, VIDEO_TIME_BASE_DEN,
)
}
pub(super) fn audio_buffersrc_args(
sample_rate: u32,
sample_fmt_name: &str,
channels: u32,
) -> String {
format!(
"sample_rate={}:sample_fmt={}:channels={}:time_base={}/{}",
sample_rate, sample_fmt_name, channels, AUDIO_TIME_BASE_NUM, sample_rate,
)
}
pub(super) fn parse_sample_rate_from_buffersrc(buffersrc_args: &str) -> u32 {
buffersrc_args
.split(':')
.find_map(|kv| {
let (k, v) = kv.split_once('=')?;
if k == "sample_rate" {
v.parse().ok()
} else {
None
}
})
.unwrap_or(44100)
}
pub(super) unsafe fn add_parametric_eq_chain(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
bands: &[EqBand],
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let mut ctx = prev_ctx;
for (j, band) in bands.iter().enumerate() {
let filter_name =
std::ffi::CString::new(band.filter_name()).map_err(|_| FilterError::BuildFailed)?;
let filter = ff_sys::avfilter_get_by_name(filter_name.as_ptr());
if filter.is_null() {
log::warn!("filter not found name={}", band.filter_name());
return Err(FilterError::BuildFailed);
}
let name = std::ffi::CString::new(format!("eq{index}_{j}"))
.map_err(|_| FilterError::BuildFailed)?;
let args_str = band.args();
let args =
std::ffi::CString::new(args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut band_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut band_ctx,
filter,
name.as_ptr(),
args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name={} args={}",
band.filter_name(),
args_str
);
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter added name={} args={} index={index} band={j}",
band.filter_name(),
args_str
);
let ret = ff_sys::avfilter_link(ctx, 0, band_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
ctx = band_ctx;
}
Ok(ctx)
}
pub(super) unsafe fn add_join_with_dissolve_step(
graph: *mut ff_sys::AVFilterGraph,
clip_a_src: *mut ff_sys::AVFilterContext,
clip_b_src: *mut ff_sys::AVFilterContext,
clip_a_end: f64,
clip_b_start: f64,
dissolve_dur: f64,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
let a_trim_end = clip_a_end + dissolve_dur;
let trim_a = add_raw_filter_step(
graph,
clip_a_src,
"trim",
&format!("end={a_trim_end}"),
index,
"jwd_trima",
)?;
let setpts_a = add_raw_filter_step(
graph,
trim_a,
"setpts",
"PTS-STARTPTS",
index,
"jwd_setptsa",
)?;
let xfade_filter = ff_sys::avfilter_get_by_name(c"xfade".as_ptr());
if xfade_filter.is_null() {
log::warn!("filter not found name=xfade (join_with_dissolve)");
return Err(FilterError::BuildFailed);
}
let xfade_name = std::ffi::CString::new(format!("jwd_xfade{index}"))
.map_err(|_| FilterError::BuildFailed)?;
let xfade_args_str = format!("transition=dissolve:duration={dissolve_dur}:offset={clip_a_end}");
let xfade_args =
std::ffi::CString::new(xfade_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut xfade_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut xfade_ctx,
xfade_filter,
xfade_name.as_ptr(),
xfade_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=xfade args={xfade_args_str} (join_with_dissolve)");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=xfade args={xfade_args_str} (join_with_dissolve)");
let ret = ff_sys::avfilter_link(setpts_a, 0, xfade_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let b_trim_start = (clip_b_start - dissolve_dur).max(0.0);
let trim_b = add_raw_filter_step(
graph,
clip_b_src,
"trim",
&format!("start={b_trim_start}"),
index,
"jwd_trimb",
)?;
let setpts_b = add_raw_filter_step(
graph,
trim_b,
"setpts",
"PTS-STARTPTS",
index,
"jwd_setptsb",
)?;
let ret = ff_sys::avfilter_link(setpts_b, 0, xfade_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter join_with_dissolve expanded dissolve_dur={dissolve_dur} offset={clip_a_end}"
);
Ok(xfade_ctx)
}
use super::FilterGraphInner;
impl FilterGraphInner {
pub(super) unsafe fn build_video_graph(
graph_nn: NonNull<ff_sys::AVFilterGraph>,
buffersrc_args: &str,
num_inputs: usize,
steps: &[FilterStep],
hw: Option<&HwAccel>,
) -> VideoGraphResult {
let graph = graph_nn.as_ptr();
let hw_device_ctx: Option<*mut ff_sys::AVBufferRef> = if let Some(hw) = hw {
let device_type = hw_accel_to_device_type(*hw);
let mut raw_hw_ctx: *mut ff_sys::AVBufferRef = std::ptr::null_mut();
let ret = ff_sys::av_hwdevice_ctx_create(
&raw mut raw_hw_ctx,
device_type,
std::ptr::null(), std::ptr::null_mut(), 0,
);
if ret < 0 {
log::warn!("av_hwdevice_ctx_create failed hw={hw:?} code={ret}");
return Err(FilterError::BuildFailed);
}
ff_sys::avfilter_graph_set_auto_convert(graph, 0u32);
log::debug!("hw device context created hw={hw:?}");
Some(raw_hw_ctx)
} else {
None
};
macro_rules! bail {
($err:expr) => {{
if let Some(mut hw_ctx) = hw_device_ctx {
ff_sys::av_buffer_unref(std::ptr::addr_of_mut!(hw_ctx));
}
return Err($err);
}};
}
let buffersrc = ff_sys::avfilter_get_by_name(c"buffer".as_ptr());
if buffersrc.is_null() {
bail!(FilterError::BuildFailed);
}
let Ok(src_args) = std::ffi::CString::new(buffersrc_args) else {
bail!(FilterError::BuildFailed)
};
let mut src_ctxs: FilterCtxVec = Vec::with_capacity(num_inputs);
let mut raw_ctx0: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut raw_ctx0,
buffersrc,
c"in0".as_ptr(),
src_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
log::debug!("filter added name=buffersrc slot=0");
src_ctxs.push(Some(NonNull::new_unchecked(raw_ctx0)));
for slot in 1..num_inputs {
let Ok(ctx_name) = std::ffi::CString::new(format!("in{slot}")) else {
bail!(FilterError::BuildFailed)
};
let mut raw_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut raw_ctx,
buffersrc,
ctx_name.as_ptr(),
src_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
log::debug!("filter added name=buffersrc slot={slot}");
src_ctxs.push(Some(NonNull::new_unchecked(raw_ctx)));
}
let buffersink = ff_sys::avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink.is_null() {
bail!(FilterError::BuildFailed);
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sink_ctx,
buffersink,
c"out".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
let mut prev_ctx: *mut ff_sys::AVFilterContext = raw_ctx0;
if let (Some(hw_ctx), Some(hw_backend)) = (hw_device_ctx, hw) {
let upload_name = match hw_backend {
HwAccel::Cuda => c"hwupload_cuda",
HwAccel::VideoToolbox | HwAccel::Vaapi => c"hwupload",
};
prev_ctx = match create_hw_filter(graph, prev_ctx, upload_name, c"hwupload0", hw_ctx) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
}
for (i, step) in steps.iter().enumerate() {
if matches!(
step,
FilterStep::AReverse
| FilterStep::AFadeIn { .. }
| FilterStep::AFadeOut { .. }
| FilterStep::ParametricEq { .. }
| FilterStep::ANoiseGate { .. }
| FilterStep::ACompressor { .. }
| FilterStep::StereoToMono
| FilterStep::ChannelMap { .. }
| FilterStep::AudioDelay { .. }
| FilterStep::ConcatAudio { .. }
) {
continue;
}
if matches!(step, FilterStep::LoudnessNormalize { .. }) {
continue;
}
if matches!(step, FilterStep::NormalizePeak { .. }) {
continue;
}
if let FilterStep::OverlayImage {
path,
x,
y,
opacity,
} = step
{
prev_ctx = match add_overlay_image_step(graph, prev_ctx, path, x, y, *opacity, i) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::JoinWithDissolve {
clip_a_end,
clip_b_start,
dissolve_dur,
} = step
{
let Some(b_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
prev_ctx = match add_join_with_dissolve_step(
graph,
prev_ctx,
b_src.as_ptr(),
*clip_a_end,
*clip_b_start,
*dissolve_dur,
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::Glow {
threshold,
radius,
intensity,
} = step
{
prev_ctx = match add_glow_step(graph, prev_ctx, *threshold, *radius, *intensity, i)
{
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::FeatherMask { radius } = step {
prev_ctx = match add_feather_mask_step(graph, prev_ctx, *radius, i) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::AlphaMatte { matte } = step {
let Some(matte_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
prev_ctx = match add_alphamerge_step(
graph,
prev_ctx,
matte_src.as_ptr(),
matte.steps(),
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::Blend {
top,
mode: BlendMode::Normal | BlendMode::PorterDuffOver,
opacity,
} = step
{
let Some(top_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
prev_ctx = match add_blend_normal_step(
graph,
prev_ctx,
top_src.as_ptr(),
top.steps(),
*opacity,
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::Blend {
top,
mode:
mode @ (BlendMode::Multiply
| BlendMode::Screen
| BlendMode::Overlay
| BlendMode::SoftLight
| BlendMode::HardLight
| BlendMode::ColorDodge
| BlendMode::ColorBurn
| BlendMode::Darken
| BlendMode::Lighten
| BlendMode::Difference
| BlendMode::Exclusion
| BlendMode::Add
| BlendMode::Subtract
| BlendMode::Hue
| BlendMode::Saturation
| BlendMode::Color
| BlendMode::Luminosity),
opacity,
} = step
{
let Some(top_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
let mode_name = match mode {
BlendMode::Multiply => "multiply",
BlendMode::Screen => "screen",
BlendMode::Overlay => "overlay",
BlendMode::SoftLight => "softlight",
BlendMode::HardLight => "hardlight",
BlendMode::ColorDodge => "dodge",
BlendMode::ColorBurn => "burn",
BlendMode::Darken => "darken",
BlendMode::Lighten => "lighten",
BlendMode::Difference => "difference",
BlendMode::Exclusion => "exclusion",
BlendMode::Add => "addition",
BlendMode::Subtract => "subtract",
BlendMode::Hue => "hue",
BlendMode::Saturation => "saturation",
BlendMode::Color => "color",
BlendMode::Luminosity => "luminosity",
_ => unreachable!(),
};
prev_ctx = match add_blend_photographic_step(
graph,
prev_ctx,
top_src.as_ptr(),
top.steps(),
mode_name,
*opacity,
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::Blend {
top,
mode: BlendMode::PorterDuffUnder,
opacity,
} = step
{
let Some(top_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
prev_ctx = match add_blend_under_step(
graph,
prev_ctx,
top_src.as_ptr(),
top.steps(),
*opacity,
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
if let FilterStep::Blend {
top,
mode:
mode @ (BlendMode::PorterDuffIn
| BlendMode::PorterDuffOut
| BlendMode::PorterDuffAtop
| BlendMode::PorterDuffXor),
..
} = step
{
let Some(top_src) = src_ctxs.get(1).and_then(|o| *o) else {
bail!(FilterError::BuildFailed)
};
let expr = match mode {
BlendMode::PorterDuffIn => "B*A/255",
BlendMode::PorterDuffOut => "B*(255-A)/255",
BlendMode::PorterDuffAtop => "B*A/255 + A*(255-B)/255",
BlendMode::PorterDuffXor => "B*(255-A)/255 + A*(255-B)/255",
_ => unreachable!(),
};
prev_ctx = match add_blend_expr_step(
graph,
prev_ctx,
top_src.as_ptr(),
top.steps(),
expr,
i,
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
continue;
}
let (step_prefix, step_index): (&str, usize) = match step {
FilterStep::CropAnimated { .. } => {
let n = steps[..i]
.iter()
.filter(|s| matches!(s, FilterStep::CropAnimated { .. }))
.count();
("crop_", n)
}
FilterStep::GBlurAnimated { .. } => {
let n = steps[..i]
.iter()
.filter(|s| matches!(s, FilterStep::GBlurAnimated { .. }))
.count();
("gblur_", n)
}
FilterStep::EqAnimated { .. } => {
let n = steps[..i]
.iter()
.filter(|s| matches!(s, FilterStep::EqAnimated { .. }))
.count();
("eq_", n)
}
FilterStep::ColorBalanceAnimated { .. } => {
let n = steps[..i]
.iter()
.filter(|s| matches!(s, FilterStep::ColorBalanceAnimated { .. }))
.count();
("colorbalance_", n)
}
_ => ("step", i),
};
prev_ctx = match add_and_link_step(graph, prev_ctx, step, step_index, step_prefix) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
if matches!(step, FilterStep::Trim { .. }) {
prev_ctx = match add_setpts_after_trim(graph, prev_ctx, i) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
}
if let FilterStep::FitToAspect {
width,
height,
color,
} = step
{
prev_ctx = match add_fit_to_aspect_pad(graph, prev_ctx, *width, *height, color, i) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
}
if let FilterStep::LumaKey { invert: true, .. } = step {
prev_ctx = match add_raw_filter_step(
graph,
prev_ctx,
"geq",
"r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255-alpha(X,Y)'",
i,
"geqluma",
) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
}
if matches!(step, FilterStep::Overlay { .. } | FilterStep::XFade { .. })
&& let Some(Some(extra_src)) = src_ctxs.get(1)
{
let ret = ff_sys::avfilter_link(extra_src.as_ptr(), 0, prev_ctx, 1);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
log::debug!("filter linked extra_input=in1 to two-input filter pad=1");
}
if let FilterStep::ConcatVideo { n } = step {
for slot in 1..*n as usize {
if let Some(Some(extra_src)) = src_ctxs.get(slot) {
let ret =
ff_sys::avfilter_link(extra_src.as_ptr(), 0, prev_ctx, slot as u32);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
log::debug!("filter linked extra_input=in{slot} to concat pad={slot}");
}
}
}
}
if let Some(hw_ctx) = hw_device_ctx {
prev_ctx =
match create_hw_filter(graph, prev_ctx, c"hwdownload", c"hwdownload0", hw_ctx) {
Ok(ctx) => ctx,
Err(e) => bail!(e),
};
}
let ret = ff_sys::avfilter_link(prev_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
log::warn!("avfilter_graph_config failed code={ret}");
let crop_info = steps.iter().find_map(|s| match s {
FilterStep::Crop {
x,
y,
width,
height,
} => Some((
f64::from(*x),
f64::from(*y),
f64::from(*width),
f64::from(*height),
)),
FilterStep::CropAnimated {
x,
y,
width,
height,
} => Some((
x.value_at(Duration::ZERO),
y.value_at(Duration::ZERO),
width.value_at(Duration::ZERO),
height.value_at(Duration::ZERO),
)),
_ => None,
});
if let Some((x, y, w, h)) = crop_info {
bail!(FilterError::InvalidConfig {
reason: format!("crop rect {x},{y}+{w}x{h} exceeds source frame dimensions"),
});
}
bail!(ffmpeg_err(ret));
}
let sink_nn = NonNull::new_unchecked(sink_ctx);
Ok((src_ctxs, sink_nn, hw_device_ctx))
}
pub(super) unsafe fn build_audio_graph(
graph_nn: NonNull<ff_sys::AVFilterGraph>,
buffersrc_args: &str,
num_inputs: usize,
steps: &[FilterStep],
_hw: Option<&HwAccel>,
) -> BuildResult {
let graph = graph_nn.as_ptr();
let mut src_ctxs = Vec::with_capacity(num_inputs);
let abuffer = ff_sys::avfilter_get_by_name(c"abuffer".as_ptr());
if abuffer.is_null() {
return Err(FilterError::BuildFailed);
}
let src_args =
std::ffi::CString::new(buffersrc_args).map_err(|_| FilterError::BuildFailed)?;
let first_src_ctx = {
let mut raw_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut raw_ctx,
abuffer,
c"in0".as_ptr(),
src_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=abuffersrc slot=0");
let nn = NonNull::new_unchecked(raw_ctx);
src_ctxs.push(Some(nn));
nn
};
for slot in 1..num_inputs {
let ctx_name = std::ffi::CString::new(format!("in{slot}"))
.map_err(|_| FilterError::BuildFailed)?;
let mut raw_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut raw_ctx,
abuffer,
ctx_name.as_ptr(),
src_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=abuffersrc slot={slot}");
src_ctxs.push(Some(NonNull::new_unchecked(raw_ctx)));
}
let abuffersink = ff_sys::avfilter_get_by_name(c"abuffersink".as_ptr());
if abuffersink.is_null() {
return Err(FilterError::BuildFailed);
}
let mut sink_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sink_ctx,
abuffersink,
c"aout".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let mut prev_ctx = first_src_ctx.as_ptr();
for (i, step) in steps.iter().enumerate() {
if matches!(
step,
FilterStep::Reverse
| FilterStep::ConcatVideo { .. }
| FilterStep::JoinWithDissolve { .. }
| FilterStep::Blend { .. }
) {
continue;
}
if matches!(step, FilterStep::LoudnessNormalize { .. }) {
continue;
}
if matches!(step, FilterStep::NormalizePeak { .. }) {
continue;
}
if let FilterStep::Speed { factor } = step {
prev_ctx = add_atempo_chain(graph, prev_ctx, *factor, i)?;
continue;
}
if let FilterStep::ParametricEq { bands } = step {
prev_ctx = add_parametric_eq_chain(graph, prev_ctx, bands, i)?;
continue;
}
if let FilterStep::ReverbIr {
ir_path,
wet,
dry,
pre_delay_ms,
} = step
{
prev_ctx =
add_reverb_ir_step(graph, prev_ctx, ir_path, *wet, *dry, *pre_delay_ms, i)?;
continue;
}
if let FilterStep::TimeStretch { factor } = step {
prev_ctx = add_atempo_chain(graph, prev_ctx, f64::from(*factor), i)?;
continue;
}
if let FilterStep::SpeedChange { factor } = step {
let sr = parse_sample_rate_from_buffersrc(buffersrc_args);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let new_sr = (f64::from(sr) * factor).round() as u64;
prev_ctx = add_raw_filter_step(
graph,
prev_ctx,
"asetrate",
&format!("r={new_sr}"),
i,
"speed_asetrate",
)?;
continue;
}
if let FilterStep::PitchShift { semitones } = step {
let rate = 2f64.powf(f64::from(*semitones) / 12.0);
let sr = parse_sample_rate_from_buffersrc(buffersrc_args);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let new_sr = (f64::from(sr) * rate).round() as u64;
let atempo = 1.0 / rate;
prev_ctx = add_raw_filter_step(
graph,
prev_ctx,
"asetrate",
&format!("r={new_sr}"),
i,
"pitch_asetrate",
)?;
prev_ctx = add_raw_filter_step(
graph,
prev_ctx,
"atempo",
&format!("{atempo:.6}"),
i,
"pitch_atempo",
)?;
continue;
}
if let FilterStep::Duck {
threshold_linear,
ratio,
attack_ms,
release_ms,
} = step
{
let side_ctx = src_ctxs
.get(1)
.and_then(|s| *s)
.ok_or(FilterError::BuildFailed)?
.as_ptr();
prev_ctx = add_sidechain_compress_step(
graph,
prev_ctx,
side_ctx,
&DuckArgs {
threshold_linear: *threshold_linear,
ratio: *ratio,
attack_ms: *attack_ms,
release_ms: *release_ms,
},
i,
)?;
continue;
}
if let FilterStep::AudioDelay { ms } = step {
let (filter_name, args) = if *ms >= 0.0 {
("adelay".to_string(), format!("delays={ms}:all=1"))
} else {
("atrim".to_string(), format!("start={}", -ms / 1000.0))
};
prev_ctx = add_raw_filter_step(graph, prev_ctx, &filter_name, &args, i, "adelay")?;
continue;
}
prev_ctx = add_and_link_step(graph, prev_ctx, step, i, "astep")?;
if let FilterStep::ConcatAudio { n } = step {
for slot in 1..*n as usize {
if let Some(Some(extra_src)) = src_ctxs.get(slot) {
let ret =
ff_sys::avfilter_link(extra_src.as_ptr(), 0, prev_ctx, slot as u32);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!("filter linked extra_input=in{slot} to concat pad={slot}");
}
}
}
}
let ret = ff_sys::avfilter_link(prev_ctx, 0, sink_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
log::warn!("avfilter_graph_config failed code={ret}");
return Err(ffmpeg_err(ret));
}
let sink_nn = NonNull::new_unchecked(sink_ctx);
Ok((src_ctxs, sink_nn))
}
}
pub(super) unsafe fn add_reverb_ir_step(
graph: *mut ff_sys::AVFilterGraph,
prev_ctx: *mut ff_sys::AVFilterContext,
ir_path: &str,
wet: f32,
dry: f32,
pre_delay_ms: u32,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let wet = wet.clamp(0.0, 1.0);
let dry = dry.clamp(0.0, 1.0);
let delay = pre_delay_ms.min(500);
let amovie_filter = ff_sys::avfilter_get_by_name(c"amovie".as_ptr());
if amovie_filter.is_null() {
log::warn!("filter not found name=amovie (reverb_ir)");
return Err(FilterError::BuildFailed);
}
let amovie_name =
CString::new(format!("reverb_amovie{index}")).map_err(|_| FilterError::BuildFailed)?;
let amovie_args_str = format!("filename={ir_path}");
let amovie_args =
CString::new(amovie_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut amovie_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut amovie_ctx,
amovie_filter,
amovie_name.as_ptr(),
amovie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name=amovie args={amovie_args_str} (reverb_ir) code={ret}"
);
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=amovie args={amovie_args_str} index={index} (reverb_ir)");
let ir_out_ctx = if delay > 0 {
let adelay_filter = ff_sys::avfilter_get_by_name(c"adelay".as_ptr());
if adelay_filter.is_null() {
log::warn!("filter not found name=adelay (reverb_ir)");
return Err(FilterError::BuildFailed);
}
let adelay_name =
CString::new(format!("reverb_adelay{index}")).map_err(|_| FilterError::BuildFailed)?;
let adelay_args_str = format!("delays={delay}:all=1");
let adelay_args =
CString::new(adelay_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut adelay_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut adelay_ctx,
adelay_filter,
adelay_name.as_ptr(),
adelay_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name=adelay args={adelay_args_str} (reverb_ir) code={ret}"
);
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=adelay args={adelay_args_str} index={index} (reverb_ir)");
let ret = ff_sys::avfilter_link(amovie_ctx, 0, adelay_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
adelay_ctx
} else {
amovie_ctx
};
let afir_filter = ff_sys::avfilter_get_by_name(c"afir".as_ptr());
if afir_filter.is_null() {
log::warn!("filter not found name=afir (reverb_ir)");
return Err(FilterError::BuildFailed);
}
let afir_name =
CString::new(format!("reverb_afir{index}")).map_err(|_| FilterError::BuildFailed)?;
let afir_args_str = format!("dry={dry}:wet={wet}");
let afir_args = CString::new(afir_args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut afir_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut afir_ctx,
afir_filter,
afir_name.as_ptr(),
afir_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!("filter creation failed name=afir args={afir_args_str} (reverb_ir) code={ret}");
return Err(FilterError::BuildFailed);
}
log::debug!("filter added name=afir args={afir_args_str} index={index} (reverb_ir)");
let ret = ff_sys::avfilter_link(prev_ctx, 0, afir_ctx, 0);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
let ret = ff_sys::avfilter_link(ir_out_ctx, 0, afir_ctx, 1);
if ret < 0 {
return Err(FilterError::BuildFailed);
}
log::debug!(
"filter reverb_ir expanded ir_path={ir_path} wet={wet} dry={dry} pre_delay_ms={pre_delay_ms} index={index}"
);
Ok(afir_ctx)
}
pub(super) struct DuckArgs {
pub threshold_linear: f32,
pub ratio: f32,
pub attack_ms: f32,
pub release_ms: f32,
}
pub(super) unsafe fn add_sidechain_compress_step(
graph: *mut ff_sys::AVFilterGraph,
main_ctx: *mut ff_sys::AVFilterContext,
side_ctx: *mut ff_sys::AVFilterContext,
args: &DuckArgs,
index: usize,
) -> Result<*mut ff_sys::AVFilterContext, FilterError> {
use std::ffi::CString;
let DuckArgs {
threshold_linear,
ratio,
attack_ms,
release_ms,
} = args;
let filter = ff_sys::avfilter_get_by_name(c"sidechaincompress".as_ptr());
if filter.is_null() {
log::warn!("filter not found name=sidechaincompress (duck)");
return Err(FilterError::BuildFailed);
}
let name = CString::new(format!("duck{index}")).map_err(|_| FilterError::BuildFailed)?;
let args_str = format!(
"threshold={threshold_linear}:ratio={ratio}:attack={attack_ms}:release={release_ms}"
);
let cargs = CString::new(args_str.as_str()).map_err(|_| FilterError::BuildFailed)?;
let mut sc_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sc_ctx,
filter,
name.as_ptr(),
cargs.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
log::warn!(
"filter creation failed name=sidechaincompress args={args_str} code={ret} (duck)"
);
return Err(ffmpeg_err(ret));
}
log::debug!("filter added name=sidechaincompress args={args_str} index={index} (duck)");
let ret = ff_sys::avfilter_link(main_ctx, 0, sc_ctx, 0);
if ret < 0 {
log::warn!("avfilter_link failed main→sidechaincompress[0] code={ret}");
return Err(ffmpeg_err(ret));
}
let ret = ff_sys::avfilter_link(side_ctx, 0, sc_ctx, 1);
if ret < 0 {
log::warn!("avfilter_link failed sidechain→sidechaincompress[1] code={ret}");
return Err(ffmpeg_err(ret));
}
log::debug!(
"filter duck expanded threshold_linear={threshold_linear} ratio={ratio} \
attack_ms={attack_ms} release_ms={release_ms} index={index}"
);
Ok(sc_ctx)
}