use std::sync::Arc;
use ffmpeg_next::{self as ffmpeg, ffi};
use crate::{
buffer::{picture_id, picture_is_referenced, release_picture},
error::Result,
pool::{UnboundObjectPool, UnboundObjectPoolRef},
};
pub(crate) type InputIdentity = (
(usize, usize),
ffmpeg::format::Pixel,
u32,
u32,
ffmpeg::color::Space,
ffmpeg::color::Range,
);
pub(crate) fn input_identity(frame: &ffmpeg::frame::Video) -> InputIdentity {
(
picture_id(frame),
frame.format(),
frame.width(),
frame.height(),
frame.color_space(),
frame.color_range(),
)
}
pub(crate) trait PerFrameTransform {
fn repeated(&mut self) -> &mut RepeatedOutput;
fn produce(
&mut self,
input: &Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
) -> Result<UnboundObjectPoolRef<ffmpeg::frame::Video>>;
fn frame_ref_failed(&self, code: i32) -> crate::error::Error;
fn transform(
&mut self,
input: &Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
) -> Result<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>> {
if self.repeated().made_from(input) {
return match self.repeated().repeat(input) {
Ok(output) => Ok(Arc::new(output)),
Err(code) => Err(self.frame_ref_failed(code)),
};
}
let produced = Arc::new(self.produce(input)?);
self.repeated().store(input, Arc::clone(&produced));
Ok(produced)
}
}
pub(crate) struct RepeatedOutput {
held: Option<Held>,
retired: Vec<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>>,
wrappers: UnboundObjectPool<ffmpeg::frame::Video>,
}
struct Held {
input: InputIdentity,
_input: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
output: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
}
impl RepeatedOutput {
pub(crate) fn new() -> Self {
Self {
held: None,
retired: Vec::new(),
wrappers: UnboundObjectPool::new(0, ffmpeg::frame::Video::empty, release_picture),
}
}
pub(crate) fn made_from(&self, input: &ffmpeg::frame::Video) -> bool {
self.held
.as_ref()
.is_some_and(|held| held.input == input_identity(input))
}
pub(crate) fn repeat(
&self,
input: &ffmpeg::frame::Video,
) -> std::result::Result<UnboundObjectPoolRef<ffmpeg::frame::Video>, i32> {
let held = self
.held
.as_ref()
.expect("only reached with a previous output");
let mut wrapper = self.wrappers.get();
unsafe {
let ptr = wrapper.as_mut_ptr();
ffi::av_frame_unref(ptr);
let code = ffi::av_frame_ref(ptr, held.output.as_ptr());
if code < 0 {
return Err(code);
}
(*ptr).pts = (*input.as_ptr()).pts;
(*ptr).pkt_dts = (*input.as_ptr()).pkt_dts;
(*ptr).duration = (*input.as_ptr()).duration;
}
Ok(wrapper)
}
pub(crate) fn store(
&mut self,
input: &Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
output: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
) {
if let Some(previous) = self.held.take() {
self.retired.push(previous.output);
}
self.retired.retain(|output| picture_is_referenced(output));
self.held = Some(Held {
input: input_identity(input),
_input: Arc::clone(input),
output,
});
}
pub(crate) fn clear(&mut self) {
self.held = None;
self.retired.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pooled(frame: ffmpeg::frame::Video) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
let pool = UnboundObjectPool::new(0, ffmpeg::frame::Video::empty, |_| {});
let mut slot = pool.get();
*slot = frame;
Arc::new(slot)
}
fn picture(pts: i64) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
let mut frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::NV12, 16, 16);
frame.set_pts(Some(pts));
pooled(frame)
}
fn repeat_of(
frame: &Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
pts: i64,
) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
let mut kept = ffmpeg::frame::Video::empty();
unsafe {
assert!(ffi::av_frame_ref(kept.as_mut_ptr(), frame.as_ptr()) >= 0);
}
kept.set_pts(Some(pts));
pooled(kept)
}
#[test]
fn a_repeated_input_is_answered_with_the_output_it_already_made() {
let mut cache = RepeatedOutput::new();
let input = picture(1);
let output = picture(1);
assert!(!cache.made_from(&input), "nothing produced yet");
cache.store(&input, Arc::clone(&output));
let again = repeat_of(&input, 2);
assert!(cache.made_from(&again));
let repeated = cache.repeat(&again).expect("reference the held output");
assert_eq!(
picture_id(&repeated),
picture_id(&output),
"a repeat points at the output already produced"
);
assert_eq!(
repeated.pts(),
Some(2),
"under this frame's own timestamp, not the held output's"
);
}
#[test]
fn a_repeat_carries_the_output_description_not_the_input_one() {
let mut cache = RepeatedOutput::new();
let input = picture(1);
let mut output = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::NV12, 16, 16);
output.set_color_space(ffmpeg::color::Space::BT709);
output.set_color_range(ffmpeg::color::Range::MPEG);
cache.store(&input, pooled(output));
let repeated = cache.repeat(&repeat_of(&input, 2)).expect("repeat");
assert_eq!(repeated.color_space(), ffmpeg::color::Space::BT709);
assert_eq!(repeated.color_range(), ffmpeg::color::Range::MPEG);
}
#[test]
fn a_re_tagged_picture_is_not_the_input_it_was_made_from() {
let mut cache = RepeatedOutput::new();
let input = picture(1);
cache.store(&input, picture(1));
let mut re_tagged = repeat_of(&input, 2);
Arc::get_mut(&mut re_tagged)
.expect("sole owner")
.set_color_space(ffmpeg::color::Space::BT470BG);
assert!(!cache.made_from(&re_tagged));
}
#[test]
fn an_output_still_under_a_repeat_is_not_released() {
let pool = UnboundObjectPool::new(
1,
|| ffmpeg::frame::Video::new(ffmpeg::format::Pixel::NV12, 16, 16),
|_| {},
);
let mut cache = RepeatedOutput::new();
let first_input = picture(1);
cache.store(&first_input, Arc::new(pool.get()));
let in_flight = cache.repeat(&first_input).expect("repeat");
cache.store(&picture(2), Arc::new(pool.get()));
assert_eq!(
pool.size(),
0,
"the pool handed back a frame a repeat is still showing"
);
drop(in_flight);
cache.store(&picture(3), Arc::new(pool.get()));
assert_eq!(
pool.size(),
2,
"a picture nobody holds must be reusable — both the one the repeat \
was showing and the one that replaced it"
);
}
}