use std::time::Duration;
use ffmpeg_next::{Error as FfmpegError, Packet, Rational};
use crate::error::UnbundleError;
use crate::unbundle::MediaFile;
#[derive(Debug, Clone)]
pub struct VariableFrameRateAnalysis {
pub is_variable_frame_rate: bool,
pub mean_frame_duration: f64,
pub frame_duration_stddev: f64,
pub min_frames_per_second: f64,
pub max_frames_per_second: f64,
pub mean_frames_per_second: f64,
pub frames_analyzed: u64,
pub pts_list: Vec<Duration>,
}
pub(crate) fn analyze_variable_framerate_impl(
unbundler: &mut MediaFile,
video_stream_index: usize,
) -> Result<VariableFrameRateAnalysis, UnbundleError> {
log::debug!("Analyzing VFR (stream={})", video_stream_index);
let time_base: Rational = unbundler
.input_context
.stream(video_stream_index)
.ok_or(UnbundleError::NoVideoStream)?
.time_base();
let time_base_numerator = time_base.numerator() as f64;
let time_base_denominator = time_base.denominator().max(1) as f64;
let mut pts_values: Vec<i64> = Vec::new();
let mut packet = Packet::empty();
loop {
match packet.read(&mut unbundler.input_context) {
Ok(()) => {
if packet.stream() as usize != video_stream_index {
continue;
}
if let Some(pts) = packet.pts() {
pts_values.push(pts);
}
}
Err(FfmpegError::Eof) => break,
Err(e) => return Err(UnbundleError::from(e)),
}
}
pts_values.sort_unstable();
let pts_list: Vec<Duration> = pts_values
.iter()
.map(|&p| {
let secs = p as f64 * time_base_numerator / time_base_denominator;
Duration::from_secs_f64(secs.max(0.0))
})
.collect();
if pts_values.len() < 2 {
return Ok(VariableFrameRateAnalysis {
is_variable_frame_rate: false,
mean_frame_duration: 0.0,
frame_duration_stddev: 0.0,
min_frames_per_second: 0.0,
max_frames_per_second: 0.0,
mean_frames_per_second: 0.0,
frames_analyzed: pts_values.len() as u64,
pts_list,
});
}
let durations: Vec<f64> = pts_values
.windows(2)
.map(|w| ((w[1] - w[0]) as f64) * time_base_numerator / time_base_denominator)
.filter(|&d| d > 0.0)
.collect();
if durations.is_empty() {
return Ok(VariableFrameRateAnalysis {
is_variable_frame_rate: false,
mean_frame_duration: 0.0,
frame_duration_stddev: 0.0,
min_frames_per_second: 0.0,
max_frames_per_second: 0.0,
mean_frames_per_second: 0.0,
frames_analyzed: pts_values.len() as u64,
pts_list,
});
}
let mean = durations.iter().sum::<f64>() / durations.len() as f64;
let variance =
durations.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / durations.len() as f64;
let stddev = variance.sqrt();
let min_duration = durations.iter().copied().fold(f64::INFINITY, f64::min);
let max_duration = durations.iter().copied().fold(0.0_f64, f64::max);
let max_frames_per_second = if min_duration > 0.0 {
1.0 / min_duration
} else {
0.0
};
let min_frames_per_second = if max_duration > 0.0 {
1.0 / max_duration
} else {
0.0
};
let mean_frames_per_second = if mean > 0.0 { 1.0 / mean } else { 0.0 };
let mean_frames_per_second =
mean_frames_per_second.clamp(min_frames_per_second, max_frames_per_second);
let is_variable_frame_rate = mean > 0.0 && (stddev / mean) > 0.10;
Ok(VariableFrameRateAnalysis {
is_variable_frame_rate,
mean_frame_duration: mean,
frame_duration_stddev: stddev,
min_frames_per_second,
max_frames_per_second,
mean_frames_per_second,
frames_analyzed: pts_values.len() as u64,
pts_list,
})
}