use crate::core::context::frame_source::{FrameSource, FrameSourceParams};
use crate::core::context::obj_pool::ObjPool;
use crate::core::context::{null_frame, FrameBox, FrameData};
use crate::core::scheduler::ffmpeg_scheduler::{
is_stopping, set_scheduler_error, wait_until_not_paused,
};
use crate::error::{AllocFrameError, Error};
use crate::util::ffmpeg_utils::av_err2str;
use crate::util::thread_synchronizer::{ThreadDoneGuard, ThreadSynchronizer};
use crossbeam_channel::{RecvTimeoutError, SendTimeoutError, Sender};
use ffmpeg_next::Frame;
use ffmpeg_sys_next::{
av_buffer_pool_get, av_buffer_pool_init, av_buffer_pool_uninit, av_frame_get_buffer,
av_image_copy, av_image_fill_arrays, AVBufferPool, AVFrame, AVRational,
};
use log::{debug, error};
use std::ptr::{null_mut, NonNull};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
struct MarkExitedOnDrop(Arc<AtomicBool>);
impl Drop for MarkExitedOnDrop {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
pub(crate) fn frame_source_init(
index: usize,
frame_source: FrameSource,
frame_pool: ObjPool<Frame>,
scheduler_status: Arc<AtomicUsize>,
thread_sync: ThreadSynchronizer,
scheduler_result: Arc<Mutex<Option<crate::error::Result<()>>>>,
producer_exited: Arc<AtomicBool>,
) -> crate::error::Result<()> {
thread_sync.thread_start();
let thread_done_guard = ThreadDoneGuard::adopt(
thread_sync.clone(),
scheduler_status.clone(),
scheduler_result.clone(),
);
let result = std::thread::Builder::new()
.name(format!("framesource{index}"))
.spawn(move || {
let _thread_done = thread_done_guard.activate();
let _producer_exited = MarkExitedOnDrop(producer_exited);
let ingress = frame_source.ingress;
let fg_sender = frame_source.fg_sender;
let params = frame_source.params;
let frame_pool = frame_pool;
let mut plane_pool = PlanePool::empty();
let mut nb_frames: i64 = 0;
loop {
let result = ingress.recv_timeout(Duration::from_millis(100));
if is_stopping(wait_until_not_paused(&scheduler_status)) {
debug!("Frame source received end command, finishing.");
return;
}
let data = match result {
Ok(data) => data,
Err(RecvTimeoutError::Timeout) => continue,
Err(RecvTimeoutError::Disconnected) => break,
};
let frame = match build_video_frame(
&frame_pool,
&mut plane_pool,
¶ms,
&data,
nb_frames,
) {
Ok(frame) => frame,
Err(e) => {
error!("Frame source failed to build a frame: {e}");
set_scheduler_error(&scheduler_status, &scheduler_result, e);
return;
}
};
nb_frames += 1;
let frame_box = FrameBox {
frame,
frame_data: frame_data_for(¶ms),
};
if !send_with_status_poll(&fg_sender, frame_box, &scheduler_status, &frame_pool) {
return;
}
}
let eof_marker = FrameBox {
frame: null_frame(),
frame_data: frame_data_for(¶ms),
};
send_with_status_poll(&fg_sender, eof_marker, &scheduler_status, &frame_pool);
debug!("Frame source finished after {nb_frames} frame(s).");
});
if let Err(e) = result {
error!("Frame source thread exited with error: {e}");
return Err(Error::FrameSourceThreadExited);
}
Ok(())
}
fn send_with_status_poll(
sender: &Sender<FrameBox>,
mut frame_box: FrameBox,
scheduler_status: &Arc<AtomicUsize>,
frame_pool: &ObjPool<Frame>,
) -> bool {
loop {
match sender.send_timeout(frame_box, Duration::from_millis(100)) {
Ok(()) => return true,
Err(SendTimeoutError::Timeout(returned)) => {
if is_stopping(wait_until_not_paused(scheduler_status)) {
debug!("Frame source received end command while sending.");
frame_pool.release(returned.frame);
return false;
}
frame_box = returned;
}
Err(SendTimeoutError::Disconnected(returned)) => {
debug!("Frame source: filtergraph receiver is gone.");
frame_pool.release(returned.frame);
return false;
}
}
}
}
fn frame_data_for(params: &FrameSourceParams) -> FrameData {
FrameData {
framerate: Some(AVRational {
num: params.fps_num,
den: params.fps_den,
}),
bits_per_raw_sample: 0,
input_stream_width: params.width,
input_stream_height: params.height,
subtitle_header: None,
fg_input_index: 0,
side_data: None,
}
}
struct PlanePool {
pool: Option<NonNull<AVBufferPool>>,
linesize: [i32; 4],
offset: [Option<usize>; 4],
armed: bool,
#[cfg(test)]
pooled_builds: usize,
}
impl PlanePool {
fn empty() -> Self {
PlanePool {
pool: None,
linesize: [0; 4],
offset: [None; 4],
armed: false,
#[cfg(test)]
pooled_builds: 0,
}
}
#[cfg(test)]
fn disarmed() -> Self {
PlanePool {
armed: true,
..Self::empty()
}
}
unsafe fn attach(&mut self, f: *mut AVFrame) -> Result<bool, AllocFrameError> {
let Some(pool) = self.pool else {
return Ok(false);
};
let buf = av_buffer_pool_get(pool.as_ptr());
if buf.is_null() {
return Err(AllocFrameError::OutOfMemory);
}
(*f).buf[0] = buf;
let base = (*buf).data;
for i in 0..4 {
(*f).linesize[i] = self.linesize[i];
if let Some(off) = self.offset[i] {
(*f).data[i] = base.add(off);
}
}
(*f).extended_data = (*f).data.as_mut_ptr();
#[cfg(test)]
{
self.pooled_builds += 1;
}
Ok(true)
}
unsafe fn try_arm_from(&mut self, f: *const AVFrame) {
if self.armed {
return;
}
self.armed = true;
let buf0 = (*f).buf[0];
if buf0.is_null() {
return;
}
let base = (*buf0).data as usize;
let buf_size = (*buf0).size;
let size_wide = buf_size as i128;
if size_wide <= 0 {
return;
}
let buf_extent = size_wide as usize;
let mut offset = [None; 4];
for (i, slot) in offset.iter_mut().enumerate() {
let d = (*f).data[i];
if d.is_null() {
continue;
}
match (d as usize).checked_sub(base) {
Some(off) if off < buf_extent => *slot = Some(off),
_ => return,
}
}
let pool = av_buffer_pool_init(buf_size, None);
let Some(pool) = NonNull::new(pool) else {
debug!("av_buffer_pool_init failed; frame source keeps per-frame plane buffers");
return;
};
self.linesize = [
(*f).linesize[0],
(*f).linesize[1],
(*f).linesize[2],
(*f).linesize[3],
];
self.offset = offset;
self.pool = Some(pool);
}
}
impl Drop for PlanePool {
fn drop(&mut self) {
if let Some(pool) = self.pool {
unsafe {
let mut p = pool.as_ptr();
av_buffer_pool_uninit(&mut p);
}
}
}
}
fn build_video_frame(
frame_pool: &ObjPool<Frame>,
plane_pool: &mut PlanePool,
params: &FrameSourceParams,
data: &[u8],
pts: i64,
) -> crate::error::Result<Frame> {
let mut frame = frame_pool.get()?;
unsafe {
let f = frame.as_mut_ptr();
(*f).format = params.pix_fmt as i32;
(*f).width = params.width;
(*f).height = params.height;
match plane_pool.attach(f) {
Ok(true) => {}
Ok(false) => {
let ret = av_frame_get_buffer(f, 0);
if ret < 0 {
error!("av_frame_get_buffer failed: {}", av_err2str(ret));
frame_pool.release(frame);
return Err(AllocFrameError::OutOfMemory.into());
}
plane_pool.try_arm_from(f);
}
Err(e) => {
error!("av_buffer_pool_get returned null");
frame_pool.release(frame);
return Err(e.into());
}
}
let mut src_data: [*mut u8; 4] = [null_mut(); 4];
let mut src_linesize: [libc::c_int; 4] = [0; 4];
let ret = av_image_fill_arrays(
src_data.as_mut_ptr(),
src_linesize.as_mut_ptr(),
data.as_ptr(),
params.pix_fmt,
params.width,
params.height,
1,
);
if ret < 0 {
error!("av_image_fill_arrays failed: {}", av_err2str(ret));
frame_pool.release(frame);
return Err(Error::Bug);
}
av_image_copy(
(*f).data.as_ptr(),
(*f).linesize.as_ptr(),
src_data.as_ptr() as *const *const u8,
src_linesize.as_ptr(),
params.pix_fmt,
params.width,
params.height,
);
(*f).pts = pts;
(*f).duration = 1;
(*f).time_base = AVRational {
num: params.fps_den,
den: params.fps_num,
};
}
Ok(frame)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::scheduler::ffmpeg_scheduler::{frame_is_null, unref_frame};
use ffmpeg_sys_next::AVPixelFormat::{
AV_PIX_FMT_GRAY8, AV_PIX_FMT_NV12, AV_PIX_FMT_PAL8, AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUVA420P,
};
use ffmpeg_sys_next::{av_frame_alloc, av_image_get_buffer_size};
fn test_new_frame() -> crate::error::Result<Frame> {
let f = unsafe { av_frame_alloc() };
assert!(!f.is_null(), "av_frame_alloc failed in test");
Ok(unsafe { Frame::wrap(f) })
}
fn test_pool() -> ObjPool<Frame> {
ObjPool::new(1, test_new_frame, unref_frame, frame_is_null).expect("frame pool")
}
fn params(pix_fmt: ffmpeg_sys_next::AVPixelFormat, w: i32, h: i32) -> FrameSourceParams {
FrameSourceParams {
width: w,
height: h,
pix_fmt,
fps_num: 30,
fps_den: 1,
}
}
fn tight_size(pix_fmt: ffmpeg_sys_next::AVPixelFormat, w: i32, h: i32) -> usize {
unsafe { av_image_get_buffer_size(pix_fmt, w, h, 1) as usize }
}
fn assert_frame_planes(frame: &Frame, planes: &[(usize, usize)], data: &[u8], ctx: &str) {
unsafe {
let f = frame.as_ptr();
let mut base = 0usize;
for (idx, &(bpr, rows)) in planes.iter().enumerate() {
let ls = (*f).linesize[idx] as usize;
for y in 0..rows {
let row = std::slice::from_raw_parts((*f).data[idx].add(y * ls), bpr);
assert_eq!(
row,
&data[base + y * bpr..base + (y + 1) * bpr],
"{ctx}: plane {idx} row {y}"
);
}
base += bpr * rows;
}
assert_eq!(base, data.len(), "{ctx}: plane table must cover the source");
}
}
fn assert_plane_spans_disjoint(frame: &Frame, planes: &[(usize, usize)], ctx: &str) {
let (offsets, linesize, extent, _, _) = frame_layout(frame);
let mut prev_end = 0usize;
for (idx, &(bpr, rows)) in planes.iter().enumerate() {
let off = offsets[idx].unwrap_or_else(|| panic!("{ctx}: plane {idx} missing"));
let ls = linesize[idx] as usize;
assert!(bpr <= ls, "{ctx}: plane {idx} row wider than its linesize");
assert!(
off >= prev_end,
"{ctx}: plane {idx} at {off} overlaps its predecessor ending at {prev_end}"
);
let end = off + rows * ls;
assert!(
(end as i128) <= extent,
"{ctx}: plane {idx} span ends at {end}, past the buffer extent {extent}"
);
prev_end = end;
}
}
fn frame_layout(frame: &Frame) -> ([Option<usize>; 8], [i32; 8], i128, bool, bool) {
unsafe {
let f = frame.as_ptr();
let buf0 = (*f).buf[0];
assert!(!buf0.is_null(), "frame must own a packed buf[0]");
let base = (*buf0).data as usize;
let mut offsets = [None; 8];
for (i, slot) in offsets.iter_mut().enumerate() {
let d = (*f).data[i];
if !d.is_null() {
*slot = Some(
(d as usize)
.checked_sub(base)
.expect("plane pointer below buffer base"),
);
}
}
(
offsets,
(*f).linesize,
(*buf0).size as i128,
std::ptr::eq((*f).extended_data, (*f).data.as_ptr()),
ffmpeg_sys_next::av_buffer_is_writable(buf0) == 1,
)
}
}
#[test]
fn fill_respects_linesize_for_odd_width() {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let p = params(AV_PIX_FMT_GRAY8, 65, 3);
let data: Vec<u8> = (0..tight_size(p.pix_fmt, 65, 3))
.map(|i| (i % 251) as u8)
.collect();
let frame = build_video_frame(&pool, &mut plane_pool, &p, &data, 7).expect("build");
unsafe {
let f = frame.as_ptr();
assert!((*f).linesize[0] >= 65, "padded linesize expected");
for y in 0..3usize {
let row =
std::slice::from_raw_parts((*f).data[0].add(y * (*f).linesize[0] as usize), 65);
assert_eq!(row, &data[y * 65..y * 65 + 65], "row {y} content");
}
assert_eq!((*f).pts, 7);
assert_eq!((*f).duration, 1);
assert_eq!((*f).time_base.num, 1);
assert_eq!((*f).time_base.den, 30);
}
}
#[test]
fn fill_copies_all_planes_for_odd_yuv420p() {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let (w, h) = (65i32, 49i32);
let p = params(AV_PIX_FMT_YUV420P, w, h);
let (cw, ch) = (33usize, 25usize);
let y_size = (w * h) as usize;
let c_size = cw * ch;
let mut data = vec![0u8; tight_size(p.pix_fmt, w, h)];
assert_eq!(data.len(), y_size + 2 * c_size);
for (i, b) in data.iter_mut().enumerate() {
*b = (i * 7 % 253) as u8;
}
let frame = build_video_frame(&pool, &mut plane_pool, &p, &data, 0).expect("build");
unsafe {
let f = frame.as_ptr();
let planes = [
(0usize, w as usize, h as usize, 0usize),
(1, cw, ch, y_size),
(2, cw, ch, y_size + c_size),
];
for (idx, pw, ph, base) in planes {
let ls = (*f).linesize[idx] as usize;
assert!(ls >= pw, "plane {idx} linesize");
for y in 0..ph {
let row = std::slice::from_raw_parts((*f).data[idx].add(y * ls), pw);
assert_eq!(
row,
&data[base + y * pw..base + y * pw + pw],
"plane {idx} row {y}"
);
}
}
}
}
#[test]
fn blocked_send_observes_terminal_status() {
use crate::core::scheduler::ffmpeg_scheduler::STATUS_END;
use std::sync::atomic::AtomicUsize;
use std::time::Instant;
let pool = test_pool();
let p = params(AV_PIX_FMT_GRAY8, 8, 2);
let boxed = |pool: &ObjPool<Frame>| FrameBox {
frame: pool.get().unwrap(),
frame_data: frame_data_for(&p),
};
let (tx, rx) = crossbeam_channel::bounded::<FrameBox>(1);
tx.send(boxed(&pool)).unwrap(); let status = Arc::new(AtomicUsize::new(STATUS_END));
let start = Instant::now();
let delivered = send_with_status_poll(&tx, boxed(&pool), &status, &pool);
assert!(!delivered, "terminal status must abort a blocked send");
assert!(
start.elapsed() < Duration::from_secs(10),
"the abort must land within a few poll intervals, took {:?}",
start.elapsed()
);
drop(rx);
}
#[test]
fn disconnected_filter_channel_fails_send() {
use crate::core::scheduler::ffmpeg_scheduler::STATUS_RUN;
use std::sync::atomic::AtomicUsize;
let pool = test_pool();
let p = params(AV_PIX_FMT_GRAY8, 8, 2);
let (tx, rx) = crossbeam_channel::bounded::<FrameBox>(1);
drop(rx);
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let frame_box = FrameBox {
frame: pool.get().unwrap(),
frame_data: frame_data_for(&p),
};
assert!(!send_with_status_poll(&tx, frame_box, &status, &pool));
}
#[test]
fn worker_parked_in_full_send_exits_on_terminal_status() {
use crate::core::scheduler::ffmpeg_scheduler::{STATUS_END, STATUS_RUN};
use crate::util::thread_synchronizer::ThreadSynchronizer;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use std::time::Instant;
let pool = ObjPool::new(4, test_new_frame, unref_frame, frame_is_null).expect("pool");
let p = params(AV_PIX_FMT_GRAY8, 8, 2);
let size = tight_size(p.pix_fmt, 8, 2);
let (ingress_tx, ingress_rx) = crossbeam_channel::bounded::<Vec<u8>>(4);
let (fg_tx, fg_rx) = crossbeam_channel::bounded::<FrameBox>(1);
let status = Arc::new(AtomicUsize::new(STATUS_RUN));
let thread_sync = ThreadSynchronizer::new();
let result = Arc::new(Mutex::new(None));
frame_source_init(
0,
FrameSource {
ingress: ingress_rx,
fg_sender: fg_tx,
params: p,
},
pool,
status.clone(),
thread_sync.clone(),
result.clone(),
Arc::new(AtomicBool::new(false)),
)
.expect("spawn");
ingress_tx.send(vec![1u8; size]).unwrap();
ingress_tx.send(vec![2u8; size]).unwrap();
let deadline = Instant::now() + Duration::from_secs(20);
while !(fg_rx.is_full() && ingress_tx.is_empty()) {
assert!(
Instant::now() < deadline,
"worker never reached the parked send"
);
std::thread::sleep(Duration::from_millis(1));
}
std::thread::sleep(Duration::from_millis(300));
status.store(STATUS_END, Ordering::Release);
let (tx, rx) = std::sync::mpsc::channel();
let sync2 = thread_sync.clone();
std::thread::spawn(move || {
sync2.wait_for_all_threads();
let _ = tx.send(());
});
rx.recv_timeout(Duration::from_secs(30))
.expect("worker parked in a full-channel send must exit on terminal status");
assert!(
result.lock().unwrap().is_none(),
"a status-driven exit is not an error"
);
drop(fg_rx);
drop(ingress_tx);
}
#[test]
fn recycled_shell_refills_cleanly() {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let p = params(AV_PIX_FMT_GRAY8, 8, 2);
let data_a = vec![0xAA; tight_size(p.pix_fmt, 8, 2)];
let frame = build_video_frame(&pool, &mut plane_pool, &p, &data_a, 0).expect("first build");
pool.release(frame); let data_b = vec![0x55; tight_size(p.pix_fmt, 8, 2)];
let frame =
build_video_frame(&pool, &mut plane_pool, &p, &data_b, 1).expect("recycled build");
unsafe {
let f = frame.as_ptr();
let row = std::slice::from_raw_parts((*f).data[0], 8);
assert_eq!(row, &data_b[..8]);
assert_eq!((*f).pts, 1);
}
}
#[test]
fn pooled_rebuild_reuses_plane_buffer() {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let p = params(AV_PIX_FMT_GRAY8, 65, 3);
let planes: &[(usize, usize)] = &[(65, 3)];
let tight = tight_size(p.pix_fmt, 65, 3);
let pat = |seed: u8| -> Vec<u8> {
(0..tight)
.map(|i| (i as u8).wrapping_mul(31).wrapping_add(seed))
.collect()
};
let data_a = pat(1);
let f1 = build_video_frame(&pool, &mut plane_pool, &p, &data_a, 0).expect("frame 1");
assert_eq!(plane_pool.pooled_builds, 0, "frame 1 is the template build");
assert!(plane_pool.pool.is_some(), "first build must arm the pool");
assert_frame_planes(&f1, planes, &data_a, "frame 1");
let data_b = pat(2);
let f2 = build_video_frame(&pool, &mut plane_pool, &p, &data_b, 1).expect("frame 2");
assert_eq!(plane_pool.pooled_builds, 1, "frame 2 must be pooled");
assert_frame_planes(&f2, planes, &data_b, "frame 2");
let f2_plane0 = unsafe { (*f2.as_ptr()).data[0] as usize };
assert_eq!(unsafe { (*f2.as_ptr()).pts }, 1);
pool.release(f2);
let data_c = pat(3);
let f3 = build_video_frame(&pool, &mut plane_pool, &p, &data_c, 2).expect("frame 3");
assert_eq!(plane_pool.pooled_builds, 2, "frame 3 must be pooled");
let f3_plane0 = unsafe { (*f3.as_ptr()).data[0] as usize };
assert_eq!(
f3_plane0, f2_plane0,
"frame 3 must reuse frame 2's pooled plane buffer"
);
assert_frame_planes(&f3, planes, &data_c, "frame 3");
assert_frame_planes(&f1, planes, &data_a, "frame 1 after reuse");
}
#[test]
fn pooled_layout_matches_template_across_formats() {
let cases: &[(ffmpeg_sys_next::AVPixelFormat, &[(usize, usize)])] = &[
(AV_PIX_FMT_GRAY8, &[(65, 49)]),
(AV_PIX_FMT_NV12, &[(65, 49), (66, 25)]),
(AV_PIX_FMT_YUV420P, &[(65, 49), (33, 25), (33, 25)]),
(
AV_PIX_FMT_YUVA420P,
&[(65, 49), (33, 25), (33, 25), (65, 49)],
),
(AV_PIX_FMT_PAL8, &[(65, 49), (1024, 1)]),
];
for &(fmt, planes) in cases {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let p = params(fmt, 65, 49);
let tight = tight_size(fmt, 65, 49);
assert_eq!(
tight,
planes.iter().map(|&(bpr, rows)| bpr * rows).sum::<usize>(),
"{fmt:?}: plane table out of sync with FFmpeg's tight layout"
);
let mut template_layout = None;
for n in 0..3i64 {
let data: Vec<u8> = (0..tight)
.map(|i| ((i * 7 + n as usize * 31) % 251) as u8)
.collect();
let frame = build_video_frame(&pool, &mut plane_pool, &p, &data, n).expect("build");
unsafe {
let f = frame.as_ptr();
assert_eq!((*f).pts, n, "{fmt:?} frame {n}: pts");
assert_eq!((*f).duration, 1, "{fmt:?} frame {n}: duration");
}
let layout = frame_layout(&frame);
match &template_layout {
None => template_layout = Some(layout),
Some(template) => assert_eq!(
&layout, template,
"{fmt:?} frame {n}: pooled layout must match the template verbatim"
),
}
assert_frame_planes(&frame, planes, &data, &format!("{fmt:?} frame {n}"));
pool.release(frame);
}
assert_eq!(
plane_pool.pooled_builds, 2,
"{fmt:?}: frames 2-3 must build through the pool"
);
}
}
#[test]
fn pooled_buffer_outlives_the_pool() {
let pool = test_pool();
let mut plane_pool = PlanePool::empty();
let p = params(AV_PIX_FMT_YUV420P, 65, 49);
let planes: &[(usize, usize)] = &[(65, 49), (33, 25), (33, 25)];
let tight = tight_size(p.pix_fmt, 65, 49);
let data_a: Vec<u8> = (0..tight).map(|i| (i % 249) as u8).collect();
let data_b: Vec<u8> = (0..tight).map(|i| (i % 247) as u8).collect();
let f1 = build_video_frame(&pool, &mut plane_pool, &p, &data_a, 0).expect("template");
let f2 = build_video_frame(&pool, &mut plane_pool, &p, &data_b, 1).expect("pooled");
assert_eq!(plane_pool.pooled_builds, 1, "frame 2 must be pooled");
drop(plane_pool);
assert_frame_planes(&f2, planes, &data_b, "pooled frame after pool drop");
pool.release(f2);
pool.release(f1);
}
#[test]
fn pool_fallback_builds_identical_frames() {
let pool = test_pool();
let mut plane_pool = PlanePool::disarmed();
let p = params(AV_PIX_FMT_YUV420P, 65, 49);
let planes: &[(usize, usize)] = &[(65, 49), (33, 25), (33, 25)];
let tight = tight_size(p.pix_fmt, 65, 49);
for n in 0..3i64 {
let data: Vec<u8> = (0..tight)
.map(|i| ((i * 3 + n as usize * 17) % 250) as u8)
.collect();
let frame = build_video_frame(&pool, &mut plane_pool, &p, &data, n).expect("build");
if n == 0 {
let mut fresh = PlanePool::empty();
let twin =
build_video_frame(&pool, &mut fresh, &p, &data, n).expect("unpooled twin");
let (offsets, linesize, extent, aliased, writable) = frame_layout(&frame);
let (t_offsets, t_linesize, t_extent, t_aliased, t_writable) = frame_layout(&twin);
assert_eq!(
(linesize, extent, aliased, writable),
(t_linesize, t_extent, t_aliased, t_writable),
"fallback must match a normal unpooled frame's deterministic layout"
);
assert_eq!(
offsets.map(|o| o.is_some()),
t_offsets.map(|o| o.is_some()),
"fallback must populate the same plane set"
);
assert_plane_spans_disjoint(&twin, planes, "unpooled twin");
pool.release(twin);
}
assert_plane_spans_disjoint(&frame, planes, &format!("fallback frame {n}"));
assert_frame_planes(&frame, planes, &data, &format!("fallback frame {n}"));
assert_eq!(unsafe { (*frame.as_ptr()).pts }, n);
pool.release(frame);
}
assert_eq!(
plane_pool.pooled_builds, 0,
"every fallback build must take the av_frame_get_buffer path"
);
assert!(plane_pool.pool.is_none(), "a failed arming is permanent");
}
}