#![allow(unsafe_code)]
#![allow(unsafe_op_in_unsafe_fn)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::too_many_lines)]
use std::ffi::CString;
use std::path::Path;
use std::time::Duration;
use super::silence_detector::SilenceRange;
use crate::DecodeError;
pub(super) unsafe fn detect_scenes_unsafe(
path: &Path,
threshold: f64,
) -> Result<Vec<Duration>, DecodeError> {
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(DecodeError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = path
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_args =
CString::new(format!("filename={path_str}")).map_err(|_| DecodeError::AnalysisFailed {
reason: "path contains null byte".to_string(),
})?;
let select_args = CString::new(format!("gt(scene\\,{threshold:.6})")).map_err(|_| {
DecodeError::AnalysisFailed {
reason: "select args contained null byte".to_string(),
}
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(DecodeError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = ff_sys::avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut src_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut src_ctx,
movie_filt,
c"scene_src".as_ptr(),
movie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie create_filter failed code={ret}"));
}
let select_filt = ff_sys::avfilter_get_by_name(c"select".as_ptr());
if select_filt.is_null() {
bail!(graph, "filter not found: select");
}
let mut sel_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sel_ctx,
select_filt,
c"scene_select".as_ptr(),
select_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("select create_filter failed code={ret}"));
}
let buffersink_filt = ff_sys::avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
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_filt,
c"scene_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = ff_sys::avfilter_link(src_ctx, 0, sel_ctx, 0);
if ret < 0 {
bail!(graph, format!("avfilter_link src→select failed code={ret}"));
}
let ret = ff_sys::avfilter_link(sel_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link select→sink failed code={ret}")
);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let sink_time_base = ff_sys::av_buffersink_get_time_base(sink_ctx);
let tb_num = f64::from(sink_time_base.num);
let tb_den = f64::from(sink_time_base.den);
let mut timestamps: Vec<Duration> = Vec::new();
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
let pts = (*raw_frame).pts;
if pts != ff_sys::AV_NOPTS_VALUE && tb_den > 0.0 {
let secs = pts as f64 * tb_num / tb_den;
if secs >= 0.0 {
timestamps.push(Duration::from_secs_f64(secs));
}
}
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
log::debug!(
"scene detection complete scenes={} threshold={threshold:.4}",
timestamps.len()
);
Ok(timestamps)
}
pub(super) unsafe fn detect_silence_unsafe(
path: &Path,
threshold_db: f32,
min_duration: Duration,
) -> Result<Vec<SilenceRange>, DecodeError> {
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(DecodeError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = path
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let amovie_args =
CString::new(format!("filename={path_str}")).map_err(|_| DecodeError::AnalysisFailed {
reason: "path contains null byte".to_string(),
})?;
let min_sec = min_duration.as_secs_f64();
let silence_args =
CString::new(format!("n={threshold_db}dB:d={min_sec:.6}")).map_err(|_| {
DecodeError::AnalysisFailed {
reason: "silencedetect args contained null byte".to_string(),
}
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(DecodeError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let amovie_filt = ff_sys::avfilter_get_by_name(c"amovie".as_ptr());
if amovie_filt.is_null() {
bail!(graph, "filter not found: amovie");
}
let mut src_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut src_ctx,
amovie_filt,
c"silence_src".as_ptr(),
amovie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("amovie create_filter failed code={ret}"));
}
let silence_filt = ff_sys::avfilter_get_by_name(c"silencedetect".as_ptr());
if silence_filt.is_null() {
bail!(graph, "filter not found: silencedetect");
}
let mut sd_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut sd_ctx,
silence_filt,
c"silence_detect".as_ptr(),
silence_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("silencedetect create_filter failed code={ret}")
);
}
let abuffersink_filt = ff_sys::avfilter_get_by_name(c"abuffersink".as_ptr());
if abuffersink_filt.is_null() {
bail!(graph, "filter not found: abuffersink");
}
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_filt,
c"silence_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("abuffersink create_filter failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(src_ctx, 0, sd_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link src→silencedetect failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(sd_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link silencedetect→sink failed code={ret}")
);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut pending_start: Option<Duration> = None;
let mut ranges: Vec<SilenceRange> = Vec::new();
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
if let Some(secs) = read_f64_meta(raw_frame, c"lavfi.silence_start".as_ptr())
&& secs >= 0.0
{
pending_start = Some(Duration::from_secs_f64(secs));
}
if let Some(secs) = read_f64_meta(raw_frame, c"lavfi.silence_end".as_ptr())
&& let Some(start) = pending_start.take()
&& secs >= 0.0
{
ranges.push(SilenceRange {
start,
end: Duration::from_secs_f64(secs),
});
}
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
log::debug!(
"silence detection complete ranges={} threshold_db={threshold_db:.1} \
min_duration={min_sec:.3}s",
ranges.len()
);
Ok(ranges)
}
unsafe fn read_f64_meta(
frame: *mut ff_sys::AVFrame,
key: *const std::os::raw::c_char,
) -> Option<f64> {
let entry = ff_sys::av_dict_get((*frame).metadata, key, std::ptr::null(), 0);
if entry.is_null() {
return None;
}
std::ffi::CStr::from_ptr((*entry).value)
.to_str()
.ok()
.and_then(|s| s.parse::<f64>().ok())
}
pub(super) unsafe fn detect_black_frames_unsafe(
path: &Path,
threshold: f64,
) -> Result<Vec<Duration>, DecodeError> {
macro_rules! bail {
($graph:ident, $reason:expr) => {{
let mut g = $graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
return Err(DecodeError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let path_str = path
.to_string_lossy()
.replace('\\', "/")
.replace(':', "\\:");
let movie_args =
CString::new(format!("filename={path_str}")).map_err(|_| DecodeError::AnalysisFailed {
reason: "path contains null byte".to_string(),
})?;
let blackdetect_args = CString::new(format!("d=0.1:pic_th={threshold:.6}")).map_err(|_| {
DecodeError::AnalysisFailed {
reason: "blackdetect args contained null byte".to_string(),
}
})?;
let graph = ff_sys::avfilter_graph_alloc();
if graph.is_null() {
return Err(DecodeError::AnalysisFailed {
reason: "avfilter_graph_alloc failed".to_string(),
});
}
let movie_filt = ff_sys::avfilter_get_by_name(c"movie".as_ptr());
if movie_filt.is_null() {
bail!(graph, "filter not found: movie");
}
let mut src_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut src_ctx,
movie_filt,
c"black_src".as_ptr(),
movie_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("movie create_filter failed code={ret}"));
}
let blackdetect_filt = ff_sys::avfilter_get_by_name(c"blackdetect".as_ptr());
if blackdetect_filt.is_null() {
bail!(graph, "filter not found: blackdetect");
}
let mut bd_ctx: *mut ff_sys::AVFilterContext = std::ptr::null_mut();
let ret = ff_sys::avfilter_graph_create_filter(
&raw mut bd_ctx,
blackdetect_filt,
c"black_detect".as_ptr(),
blackdetect_args.as_ptr(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(
graph,
format!("blackdetect create_filter failed code={ret}")
);
}
let buffersink_filt = ff_sys::avfilter_get_by_name(c"buffersink".as_ptr());
if buffersink_filt.is_null() {
bail!(graph, "filter not found: buffersink");
}
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_filt,
c"black_sink".as_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
graph,
);
if ret < 0 {
bail!(graph, format!("buffersink create_filter failed code={ret}"));
}
let ret = ff_sys::avfilter_link(src_ctx, 0, bd_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link src→blackdetect failed code={ret}")
);
}
let ret = ff_sys::avfilter_link(bd_ctx, 0, sink_ctx, 0);
if ret < 0 {
bail!(
graph,
format!("avfilter_link blackdetect→sink failed code={ret}")
);
}
let ret = ff_sys::avfilter_graph_config(graph, std::ptr::null_mut());
if ret < 0 {
bail!(graph, format!("avfilter_graph_config failed code={ret}"));
}
let mut timestamps: Vec<Duration> = Vec::new();
loop {
let raw_frame = ff_sys::av_frame_alloc();
if raw_frame.is_null() {
break;
}
let ret = ff_sys::av_buffersink_get_frame(sink_ctx, raw_frame);
if ret < 0 {
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
break;
}
if let Some(secs) = read_f64_meta(raw_frame, c"lavfi.black_start".as_ptr())
&& secs >= 0.0
{
timestamps.push(Duration::from_secs_f64(secs));
}
let mut ptr = raw_frame;
ff_sys::av_frame_free(std::ptr::addr_of_mut!(ptr));
}
let mut g = graph;
ff_sys::avfilter_graph_free(std::ptr::addr_of_mut!(g));
log::debug!(
"black frame detection complete intervals={} threshold={threshold:.4}",
timestamps.len()
);
Ok(timestamps)
}
pub(super) unsafe fn enumerate_keyframes_unsafe(
path: &Path,
stream_index: Option<usize>,
) -> Result<Vec<Duration>, DecodeError> {
const AV_PKT_FLAG_KEY: i32 = 1;
macro_rules! bail_ctx {
($ctx:ident, $reason:expr) => {{
ff_sys::avformat::close_input(std::ptr::addr_of_mut!($ctx));
return Err(DecodeError::AnalysisFailed {
reason: format!("{}", $reason),
});
}};
}
let mut format_ctx =
ff_sys::avformat::open_input(path).map_err(|code| DecodeError::AnalysisFailed {
reason: format!("avformat_open_input failed code={code}"),
})?;
if let Err(code) = ff_sys::avformat::find_stream_info(format_ctx) {
bail_ctx!(
format_ctx,
format!("avformat_find_stream_info failed code={code}")
);
}
let nb_streams = (*format_ctx).nb_streams as usize;
let target_stream: usize = if let Some(idx) = stream_index {
if idx >= nb_streams {
bail_ctx!(
format_ctx,
format!("stream_index {idx} out of range (file has {nb_streams} streams)")
);
}
idx
} else {
let mut found: Option<usize> = None;
for i in 0..nb_streams {
let stream = (*format_ctx).streams.add(i);
let codecpar = (*(*stream)).codecpar;
if (*codecpar).codec_type == ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO {
found = Some(i);
break;
}
}
if let Some(i) = found {
i
} else {
bail_ctx!(format_ctx, "no video stream found in file")
}
};
let stream = (*format_ctx).streams.add(target_stream);
let time_base = (*(*stream)).time_base;
let tb_num = f64::from(time_base.num);
let tb_den = f64::from(time_base.den);
let pkt = ff_sys::av_packet_alloc();
if pkt.is_null() {
bail_ctx!(format_ctx, "av_packet_alloc failed");
}
#[allow(clippy::cast_possible_wrap)]
let target_i32 = target_stream as i32;
let mut timestamps: Vec<Duration> = Vec::new();
loop {
let ret = ff_sys::av_read_frame(format_ctx, pkt);
if ret < 0 {
break;
}
if (*pkt).stream_index == target_i32 && (*pkt).flags & AV_PKT_FLAG_KEY != 0 {
let pts = if (*pkt).pts == ff_sys::AV_NOPTS_VALUE {
(*pkt).dts
} else {
(*pkt).pts
};
if pts != ff_sys::AV_NOPTS_VALUE && tb_den > 0.0 {
let secs = pts as f64 * tb_num / tb_den;
if secs >= 0.0 {
timestamps.push(Duration::from_secs_f64(secs));
}
}
}
ff_sys::av_packet_unref(pkt);
}
let mut pkt_ptr = pkt;
ff_sys::av_packet_free(std::ptr::addr_of_mut!(pkt_ptr));
ff_sys::avformat::close_input(std::ptr::addr_of_mut!(format_ctx));
log::debug!(
"keyframe enumeration complete keyframes={} stream_index={target_stream}",
timestamps.len()
);
Ok(timestamps)
}