use super::frame::VideoFrame;
use super::options::{PixelLayout, Sampling};
use super::sampler::EMIT_COUNT_KEY;
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 crate::util::frame_utils::ensure_software_format;
use crossbeam_channel::Sender;
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_FRAME_FLAG_KEY, AV_NOPTS_VALUE,
};
use std::ffi::{CStr, CString};
const US_PER_SEC: AVRational = AVRational {
num: 1,
den: 1_000_000,
};
pub(crate) struct ExportSink {
tx: Sender<VideoFrame>,
sampling: Sampling,
layout: PixelLayout,
max_frames: Option<u64>,
uniform: bool,
emitted: u64,
done: bool,
}
impl ExportSink {
pub(crate) fn new(
tx: Sender<VideoFrame>,
sampling: Sampling,
layout: PixelLayout,
max_frames: Option<u64>,
) -> Self {
let uniform = matches!(sampling, Sampling::UniformN(_));
Self {
tx,
sampling,
layout,
max_frames,
uniform,
emitted: 0,
done: false,
}
}
unsafe fn frame_pts_us(p: *const AVFrame) -> Option<i64> {
let tb = (*p).time_base;
if tb.den == 0 {
return None;
}
let pts = (*p).pts;
if pts == AV_NOPTS_VALUE {
return None;
}
Some(av_rescale_q(pts, tb, US_PER_SEC))
}
unsafe fn select(&self, p: *const AVFrame) -> bool {
match self.sampling {
Sampling::KeyframesOnly => (*p).flags & AV_FRAME_FLAG_KEY != 0,
_ => true,
}
}
fn deliver(&mut self, w: u32, h: u32, pts_us: Option<i64>, data: Vec<u8>) -> bool {
if let Some(max) = self.max_frames {
if self.emitted >= max {
self.done = true;
return false;
}
}
let vf = VideoFrame::new(w, h, self.layout, pts_us, self.emitted, data);
match self.tx.send(vf) {
Ok(()) => {
self.emitted += 1;
true
}
Err(_) => {
self.done = true;
false
}
}
}
}
impl FrameFilter for ExportSink {
fn media_type(&self) -> AVMediaType {
AVMEDIA_TYPE_VIDEO
}
fn request_frame_mode(&self) -> RequestFrameMode {
RequestFrameMode::Never
}
fn filter_frame(
&mut self,
mut frame: Frame,
_ctx: &mut FrameFilterContext,
) -> Result<Option<Frame>, FrameFilterError> {
if frame_is_eof_marker(&frame) {
return Ok(Some(frame));
}
let p = unsafe { frame.as_ptr() };
if p.is_null() {
return Ok(Some(frame));
}
if self.done {
if self.uniform {
let _ = unsafe { read_and_strip_emit_count(&mut frame) };
}
return Ok(Some(frame));
}
let pts_us = unsafe { Self::frame_pts_us(p) };
if self.uniform {
let count = unsafe { read_and_strip_emit_count(&mut frame) };
let count = match self.sampling {
Sampling::UniformN(n) => count.min((n as u64).saturating_sub(self.emitted)),
_ => count,
};
if count == 0 {
return Ok(Some(frame));
}
let (w, h, mut bytes) = unsafe { pack_bytes(p, self.layout)? };
let mut remaining = count;
while remaining > 0 {
remaining -= 1;
let data = if remaining == 0 {
std::mem::take(&mut bytes)
} else {
bytes.clone()
};
if !self.deliver(w, h, pts_us, data) {
break;
}
}
return Ok(Some(frame));
}
if !unsafe { self.select(p) } {
return Ok(None);
}
let (w, h, bytes) = unsafe { pack_bytes(p, self.layout)? };
if self.deliver(w, h, pts_us, bytes) {
Ok(Some(frame))
} else {
Ok(Some(frame))
}
}
}
unsafe fn read_and_strip_emit_count(frame: &mut Frame) -> u64 {
let key = CString::new(EMIT_COUNT_KEY).expect("literal has no NUL");
let p = frame.as_mut_ptr();
let entry = av_dict_get((*p).metadata, key.as_ptr(), std::ptr::null(), 0);
let count = if entry.is_null() {
1
} else {
CStr::from_ptr((*entry).value)
.to_str()
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(1)
.max(1)
};
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;
}
}
count
}
unsafe fn pack_bytes(
p: *const AVFrame,
layout: PixelLayout,
) -> Result<(u32, u32, Vec<u8>), FrameFilterError> {
ensure_software_format((*p).format)
.map_err(|e| -> FrameFilterError { format!("frame export: {e}").into() })?;
let expected = layout.av_pixel_format() as i32;
if (*p).format != expected {
return Err(format!(
"frame export: expected pixel format {expected}, got {}",
(*p).format
)
.into());
}
let width = (*p).width;
let height = (*p).height;
if width <= 0 || height <= 0 {
return Err(format!("frame export: non-positive frame dimensions {width}x{height}").into());
}
let bpp = layout.bytes_per_pixel();
let w = width as usize;
let h = height as usize;
let row_bytes = w
.checked_mul(bpp)
.ok_or_else(|| -> FrameFilterError { "frame export: row size overflow".into() })?;
let total = row_bytes
.checked_mul(h)
.ok_or_else(|| -> FrameFilterError { "frame export: frame size overflow".into() })?;
let base = (*p).data[0];
if base.is_null() {
return Err("frame export: packed plane pointer is null".into());
}
let linesize = (*p).linesize[0] as isize;
let linesize_abs = linesize.unsigned_abs();
if linesize_abs < row_bytes {
return Err(format!(
"frame export: linesize {linesize} smaller than {row_bytes} packed row bytes"
)
.into());
}
let src_footprint = h
.saturating_sub(1)
.checked_mul(linesize_abs)
.and_then(|x| x.checked_add(row_bytes))
.ok_or_else(|| -> FrameFilterError { "frame export: source footprint overflow".into() })?;
if src_footprint > isize::MAX as usize {
return Err("frame export: source footprint exceeds isize::MAX".into());
}
let mut out = vec![0u8; total];
for row in 0..h {
let src_row = base.offset(row as isize * linesize);
let dst = out.as_mut_ptr().add(row * row_bytes);
std::ptr::copy_nonoverlapping(src_row, dst, row_bytes);
}
Ok((width as u32, height as u32, out))
}
#[cfg(test)]
mod tests {
use super::*;
use ffmpeg_sys_next::av_frame_alloc;
fn flag_frame(key: bool) -> Frame {
unsafe {
let p = av_frame_alloc();
assert!(!p.is_null());
if key {
(*p).flags |= AV_FRAME_FLAG_KEY;
}
Frame::wrap(p)
}
}
#[test]
fn keyframes_only_selects_by_key_flag() {
let (tx, _rx) = crossbeam_channel::bounded(1);
let sink = ExportSink::new(tx, Sampling::KeyframesOnly, PixelLayout::Rgb24, None);
let key = flag_frame(true);
let delta = flag_frame(false);
unsafe {
assert!(
sink.select(key.as_ptr()),
"a KEY-flagged frame must be selected"
);
assert!(
!sink.select(delta.as_ptr()),
"a delta frame must not be selected"
);
}
}
}