use super::error::FrameExportError;
use crate::core::filter::frame_filter::{FrameFilter, FrameFilterError, RequestFrameMode};
use crate::core::filter::frame_filter_context::FrameFilterContext;
use crate::util::ffmpeg_utils::frame_is_eof_marker;
use ffmpeg_next::Frame;
use ffmpeg_sys_next::{
av_dict_get, av_dict_set, av_rescale_q, AVFrame, AVMediaType, AVMediaType::AVMEDIA_TYPE_VIDEO,
AVRational, AV_NOPTS_VALUE,
};
use std::ffi::CString;
use std::sync::{Arc, OnceLock};
pub(crate) const EMIT_COUNT_KEY: &str = "EZ_FFMPEG_EMIT_COUNT";
pub(crate) type UniformSpan = Arc<OnceLock<i64>>;
const US_PER_SEC: AVRational = AVRational {
num: 1,
den: 1_000_000,
};
pub(crate) struct SamplingClock {
prev_pts_us: i64,
prev_delta_us: i64,
}
impl SamplingClock {
pub(crate) fn new() -> Self {
Self {
prev_pts_us: 0,
prev_delta_us: 0,
}
}
pub(crate) unsafe fn pts_us(&mut self, p: *const AVFrame, track_delta: bool) -> i64 {
let tb = (*p).time_base;
let raw = (*p).pts;
let raw = if raw == AV_NOPTS_VALUE {
(*p).best_effort_timestamp
} else {
raw
};
if tb.den != 0 && raw != AV_NOPTS_VALUE {
let pts = av_rescale_q(raw, tb, US_PER_SEC);
if track_delta {
self.prev_delta_us = pts.saturating_sub(self.prev_pts_us).max(0);
}
self.prev_pts_us = pts;
pts
} else {
let est = self.prev_pts_us.saturating_add(self.prev_delta_us.max(1));
self.prev_pts_us = est;
est
}
}
}
pub(crate) struct ExportSampler {
n: u32,
span_cell: UniformSpan,
anchor_us: Option<i64>,
span_us: i64,
cursor: i128,
held: Option<Frame>,
trim_boundary_us: Option<i64>,
clock: SamplingClock,
emitted_targets: i128,
}
impl ExportSampler {
pub(crate) fn new(n: u32, span_cell: UniformSpan, trim_boundary_us: Option<i64>) -> Self {
Self {
n,
span_cell,
anchor_us: None,
span_us: 0,
cursor: 0,
held: None,
trim_boundary_us,
clock: SamplingClock::new(),
emitted_targets: 0,
}
}
unsafe fn sampling_pts_us(&mut self, p: *const AVFrame) -> i64 {
self.clock.pts_us(p, self.anchor_us.is_some())
}
fn targets_below(&self, pts_us: i64) -> i128 {
let p0 = self.anchor_us.unwrap_or(0) as i128;
let span = self.span_us.max(1) as i128;
let n = self.n as i128;
let num = 2 * n * (pts_us as i128 - p0) - span;
let den = 2 * span;
let ceil_div = (num + den - 1).div_euclid(den);
ceil_div.clamp(0, n)
}
fn emit_held(&mut self, count: i128) -> Result<Option<Frame>, FrameFilterError> {
match self.held.take() {
Some(mut held) => {
stamp_emit_count(&mut held, count as u64)?;
self.emitted_targets += count;
Ok(Some(held))
}
None => Ok(None),
}
}
}
impl FrameFilter for ExportSampler {
fn media_type(&self) -> AVMediaType {
AVMEDIA_TYPE_VIDEO
}
fn request_frame_mode(&self) -> RequestFrameMode {
RequestFrameMode::Never
}
fn filter_frame(
&mut self,
frame: Frame,
_ctx: &mut FrameFilterContext,
) -> Result<Option<Frame>, FrameFilterError> {
if frame_is_eof_marker(&frame) {
let remaining = self.n as i128 - self.cursor;
if remaining <= 0 {
return Ok(Some(frame)); }
self.cursor = self.n as i128;
if self.held.is_some() {
return self.emit_held(remaining);
}
return Err(Box::new(FrameExportError::EmptyVideoStream { n: self.n }));
}
let p = unsafe { frame.as_ptr() };
if p.is_null() {
return Ok(Some(frame));
}
if self.cursor >= self.n as i128 {
return Ok(Some(frame));
}
let f_pts = unsafe { self.sampling_pts_us(p) };
if self.trim_boundary_us.is_some_and(|b| f_pts < b) {
return Ok(None);
}
if self.anchor_us.is_none() {
let span = *self.span_cell.get().ok_or_else(|| -> FrameFilterError {
"frame export: UniformN span was not resolved before the run started".into()
})?;
self.anchor_us = Some(f_pts);
self.span_us = span.max(1);
self.held = Some(frame);
return Ok(None);
}
let new_count = self.targets_below(f_pts);
let k = new_count - self.cursor;
if k > 0 {
self.cursor = new_count;
let out = self.emit_held(k)?;
self.held = Some(frame);
Ok(out)
} else {
self.held = Some(frame);
Ok(None)
}
}
}
fn stamp_emit_count(frame: &mut Frame, count: u64) -> Result<(), FrameFilterError> {
let key = CString::new(EMIT_COUNT_KEY).expect("literal has no NUL");
let val = CString::new(count.to_string()).expect("digits have no NUL");
let ret = unsafe {
let p = frame.as_mut_ptr();
while !av_dict_get((*p).metadata, key.as_ptr(), std::ptr::null(), 0).is_null() {
if av_dict_set(&mut (*p).metadata, key.as_ptr(), std::ptr::null(), 0) < 0 {
break;
}
}
av_dict_set(&mut (*p).metadata, key.as_ptr(), val.as_ptr(), 0)
};
if ret < 0 {
return Err("frame export: failed to stamp EMIT_COUNT metadata".into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sampler(n: u32, p0: i64, span: i64) -> ExportSampler {
let mut s = ExportSampler::new(n, Arc::new(OnceLock::new()), None);
s.anchor_us = Some(p0);
s.span_us = span;
s
}
#[test]
fn targets_below_boundaries() {
let s = sampler(16, 0, 1_600_000);
assert_eq!(s.targets_below(0), 0);
assert_eq!(s.targets_below(50_000), 0, "t_0 is not strictly below");
assert_eq!(s.targets_below(50_001), 1);
assert_eq!(s.targets_below(150_001), 2);
assert_eq!(s.targets_below(i64::MAX), 16, "clamped to n");
}
#[test]
fn targets_below_offset_grid() {
let s = sampler(4, 1000, 4000);
assert_eq!(s.targets_below(1500), 0);
assert_eq!(s.targets_below(1501), 1);
assert_eq!(s.targets_below(4500), 3);
assert_eq!(s.targets_below(4501), 4);
}
#[test]
fn targets_below_is_monotonic() {
let s = sampler(64, 500, 2_000_000);
let mut prev = 0;
for pts in (0..2_600_000).step_by(9_137) {
let c = s.targets_below(pts);
assert!(c >= prev && c <= 64, "non-decreasing, bounded by n");
prev = c;
}
assert_eq!(s.targets_below(i64::MAX), 64);
}
}