use std::sync::Arc;
use oximedia_audio::{AudioFrame, ChannelLayout};
use oximedia_codec::VideoFrame;
use oximedia_core::{PixelFormat, SampleFormat, Timestamp};
#[derive(Clone, Debug)]
pub enum FilterFrame {
Video(VideoFrame),
Audio(AudioFrame),
}
impl FilterFrame {
#[must_use]
pub fn timestamp(&self) -> &Timestamp {
match self {
Self::Video(f) => &f.timestamp,
Self::Audio(f) => &f.timestamp,
}
}
#[must_use]
pub fn is_video(&self) -> bool {
matches!(self, Self::Video(_))
}
#[must_use]
pub fn is_audio(&self) -> bool {
matches!(self, Self::Audio(_))
}
#[must_use]
pub fn as_video(&self) -> Option<&VideoFrame> {
match self {
Self::Video(f) => Some(f),
Self::Audio(_) => None,
}
}
#[must_use]
pub fn as_audio(&self) -> Option<&AudioFrame> {
match self {
Self::Video(_) => None,
Self::Audio(f) => Some(f),
}
}
pub fn as_video_mut(&mut self) -> Option<&mut VideoFrame> {
match self {
Self::Video(f) => Some(f),
Self::Audio(_) => None,
}
}
pub fn as_audio_mut(&mut self) -> Option<&mut AudioFrame> {
match self {
Self::Video(_) => None,
Self::Audio(f) => Some(f),
}
}
}
impl From<VideoFrame> for FilterFrame {
fn from(frame: VideoFrame) -> Self {
Self::Video(frame)
}
}
impl From<AudioFrame> for FilterFrame {
fn from(frame: AudioFrame) -> Self {
Self::Audio(frame)
}
}
#[derive(Clone, Debug)]
pub struct FrameRef {
inner: Arc<FilterFrame>,
}
impl FrameRef {
pub fn new(frame: FilterFrame) -> Self {
Self {
inner: Arc::new(frame),
}
}
#[must_use]
pub fn frame(&self) -> &FilterFrame {
&self.inner
}
pub fn try_unwrap(self) -> Option<FilterFrame> {
Arc::try_unwrap(self.inner).ok()
}
#[must_use]
pub fn ref_count(&self) -> usize {
Arc::strong_count(&self.inner)
}
#[must_use]
pub fn make_mut(self) -> FilterFrame {
match Arc::try_unwrap(self.inner) {
Ok(frame) => frame,
Err(arc) => (*arc).clone(),
}
}
}
impl From<FilterFrame> for FrameRef {
fn from(frame: FilterFrame) -> Self {
Self::new(frame)
}
}
impl From<VideoFrame> for FrameRef {
fn from(frame: VideoFrame) -> Self {
Self::new(FilterFrame::Video(frame))
}
}
impl From<AudioFrame> for FrameRef {
fn from(frame: AudioFrame) -> Self {
Self::new(FilterFrame::Audio(frame))
}
}
pub fn simd_copy_frame(src: &[u8], dst: &mut [u8]) {
assert_eq!(src.len(), dst.len(), "simd_copy_frame: length mismatch");
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("avx2") {
avx2_copy_safe(src, dst);
return;
}
}
dst.copy_from_slice(src);
}
#[cfg(target_arch = "x86_64")]
#[inline(never)]
fn avx2_copy_safe(src: &[u8], dst: &mut [u8]) {
const CHUNK: usize = 32;
let chunks = src.len() / CHUNK;
for i in 0..chunks {
let offset = i * CHUNK;
dst[offset..offset + CHUNK].copy_from_slice(&src[offset..offset + CHUNK]);
}
let done = chunks * CHUNK;
if done < src.len() {
dst[done..].copy_from_slice(&src[done..]);
}
}
#[derive(Debug, Clone)]
pub struct FramePoolConfig {
pub pre_allocate: usize,
pub max_size: usize,
pub frame_bytes: usize,
}
impl Default for FramePoolConfig {
fn default() -> Self {
Self {
pre_allocate: 0,
max_size: 32,
frame_bytes: 0,
}
}
}
pub struct FramePool {
capacity: usize,
video_frames: Vec<VideoFrame>,
audio_frames: Vec<AudioFrame>,
free_list: Vec<Vec<u8>>,
raw_max: usize,
raw_frame_bytes: usize,
}
impl FramePool {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
capacity,
video_frames: Vec::with_capacity(capacity),
audio_frames: Vec::with_capacity(capacity),
free_list: Vec::new(),
raw_max: capacity,
raw_frame_bytes: 0,
}
}
#[must_use]
pub fn with_config(config: FramePoolConfig) -> Self {
let pre = config.pre_allocate.min(config.max_size);
let mut free_list = Vec::with_capacity(pre.max(config.max_size));
for _ in 0..pre {
free_list.push(vec![0u8; config.frame_bytes]);
}
Self {
capacity: config.max_size,
video_frames: Vec::new(),
audio_frames: Vec::new(),
free_list,
raw_max: config.max_size,
raw_frame_bytes: config.frame_bytes,
}
}
pub fn acquire_raw(&mut self) -> Vec<u8> {
self.free_list.pop().unwrap_or_else(|| {
if self.raw_frame_bytes > 0 {
vec![0u8; self.raw_frame_bytes]
} else {
Vec::new()
}
})
}
pub fn release_raw(&mut self, buf: Vec<u8>) {
if self.free_list.len() < self.raw_max {
self.free_list.push(buf);
}
}
#[must_use]
pub fn pre_allocated_count(&self) -> usize {
self.free_list.len()
}
#[must_use]
pub fn get_video_frame(&mut self, format: PixelFormat, width: u32, height: u32) -> VideoFrame {
if let Some(pos) = self
.video_frames
.iter()
.position(|f| f.format == format && f.width == width && f.height == height)
{
return self.video_frames.swap_remove(pos);
}
let mut frame = VideoFrame::new(format, width, height);
frame.allocate();
frame
}
pub fn return_video_frame(&mut self, frame: VideoFrame) {
if self.video_frames.len() < self.capacity {
self.video_frames.push(frame);
}
}
#[must_use]
pub fn get_audio_frame(
&mut self,
format: SampleFormat,
sample_rate: u32,
channels: ChannelLayout,
) -> AudioFrame {
if let Some(pos) = self.audio_frames.iter().position(|f| {
f.format == format && f.sample_rate == sample_rate && f.channels == channels
}) {
return self.audio_frames.swap_remove(pos);
}
AudioFrame::new(format, sample_rate, channels)
}
pub fn return_audio_frame(&mut self, frame: AudioFrame) {
if self.audio_frames.len() < self.capacity {
self.audio_frames.push(frame);
}
}
pub fn clear(&mut self) {
self.video_frames.clear();
self.audio_frames.clear();
}
#[must_use]
pub fn video_frame_count(&self) -> usize {
self.video_frames.len()
}
#[must_use]
pub fn audio_frame_count(&self) -> usize {
self.audio_frames.len()
}
}
impl Default for FramePool {
fn default() -> Self {
Self::new(16)
}
}
#[derive(Clone, Debug)]
pub enum SharedFrame {
Owned(Vec<u8>),
Shared(Arc<Vec<u8>>),
}
impl SharedFrame {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Owned(v) => v.as_slice(),
Self::Shared(arc) => arc.as_slice(),
}
}
#[must_use]
pub fn into_owned(self) -> Vec<u8> {
match self {
Self::Owned(v) => v,
Self::Shared(arc) => match Arc::try_unwrap(arc) {
Ok(v) => v,
Err(arc) => (*arc).clone(),
},
}
}
#[must_use]
pub fn try_share(&self) -> Arc<Vec<u8>> {
match self {
Self::Owned(v) => Arc::new(v.clone()),
Self::Shared(arc) => Arc::clone(arc),
}
}
#[must_use]
pub fn promote(self) -> Self {
match self {
Self::Owned(v) => Self::Shared(Arc::new(v)),
already_shared => already_shared,
}
}
#[must_use]
pub fn is_shared(&self) -> bool {
matches!(self, Self::Shared(_))
}
#[must_use]
pub fn is_owned(&self) -> bool {
matches!(self, Self::Owned(_))
}
}
impl From<Vec<u8>> for SharedFrame {
fn from(v: Vec<u8>) -> Self {
Self::Owned(v)
}
}
impl From<Arc<Vec<u8>>> for SharedFrame {
fn from(arc: Arc<Vec<u8>>) -> Self {
Self::Shared(arc)
}
}
pub trait ZeroCopyPort {
fn accepts_zero_copy(&self) -> bool;
fn pass_frame(&self, frame: SharedFrame) -> SharedFrame;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_frame_video() {
let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
let frame = FilterFrame::Video(video);
assert!(frame.is_video());
assert!(!frame.is_audio());
assert!(frame.as_video().is_some());
assert!(frame.as_audio().is_none());
}
#[test]
fn test_filter_frame_audio() {
let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
let frame = FilterFrame::Audio(audio);
assert!(!frame.is_video());
assert!(frame.is_audio());
assert!(frame.as_video().is_none());
assert!(frame.as_audio().is_some());
}
#[test]
fn test_frame_ref() {
let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
let frame = FilterFrame::Video(video);
let frame_ref = FrameRef::new(frame);
assert_eq!(frame_ref.ref_count(), 1);
let frame_ref2 = frame_ref.clone();
assert_eq!(frame_ref.ref_count(), 2);
assert_eq!(frame_ref2.ref_count(), 2);
}
#[test]
fn test_frame_ref_try_unwrap() {
let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
let frame = FilterFrame::Video(video);
let frame_ref = FrameRef::new(frame);
let unwrapped = frame_ref.try_unwrap();
assert!(unwrapped.is_some());
}
#[test]
fn test_frame_ref_make_mut() {
let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
let frame = FilterFrame::Video(video);
let frame_ref = FrameRef::new(frame);
let frame_ref2 = frame_ref.clone();
let owned = frame_ref.make_mut();
assert!(owned.is_video());
assert!(frame_ref2.frame().is_video());
}
#[test]
fn test_frame_pool() {
let mut pool = FramePool::new(4);
let frame = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
assert_eq!(frame.width, 1920);
assert_eq!(frame.height, 1080);
pool.return_video_frame(frame);
assert_eq!(pool.video_frame_count(), 1);
let frame2 = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
assert_eq!(frame2.width, 1920);
assert_eq!(pool.video_frame_count(), 0);
}
#[test]
fn test_filter_frame_from() {
let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
let frame: FilterFrame = video.into();
assert!(frame.is_video());
let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
let frame: FilterFrame = audio.into();
assert!(frame.is_audio());
}
#[test]
fn test_frame_timestamp() {
use oximedia_core::Rational;
let mut video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
video.timestamp = Timestamp::new(1000, Rational::new(1, 1000));
let frame = FilterFrame::Video(video);
assert_eq!(frame.timestamp().pts, 1000);
}
#[test]
fn test_simd_copy_correctness() {
let src: Vec<u8> = (0u32..4096).map(|i| (i % 251) as u8).collect();
let mut dst = vec![0u8; 4096];
super::simd_copy_frame(&src, &mut dst);
assert_eq!(src, dst, "4096-byte copy must be byte-perfect");
}
#[test]
fn test_simd_copy_non_aligned() {
let src: Vec<u8> = (0u32..4097).map(|i| (i % 197) as u8).collect();
let mut dst = vec![0u8; 4097];
super::simd_copy_frame(&src, &mut dst);
assert_eq!(src, dst, "4097-byte copy (tail) must be byte-perfect");
}
#[test]
fn test_pool_pre_allocation_count() {
let config = FramePoolConfig {
pre_allocate: 5,
max_size: 10,
frame_bytes: 64,
};
let pool = FramePool::with_config(config);
assert_eq!(
pool.pre_allocated_count(),
5,
"pool must expose 5 pre-allocated buffers before any acquire"
);
}
#[test]
fn test_pool_pre_allocation_acquire() {
let config = FramePoolConfig {
pre_allocate: 5,
max_size: 10,
frame_bytes: 64,
};
let mut pool = FramePool::with_config(config);
for i in 0..5 {
let buf = pool.acquire_raw();
assert_eq!(buf.len(), 64, "pre-allocated buffer {i} must be 64 bytes");
}
assert_eq!(
pool.pre_allocated_count(),
0,
"free-list should be empty now"
);
let buf6 = pool.acquire_raw();
assert_eq!(buf6.len(), 64, "6th (dynamic) buffer must also be 64 bytes");
}
#[test]
fn test_shared_frame_zero_copy_count() {
let data: Vec<u8> = vec![1, 2, 3, 4];
let arc = Arc::new(data);
let frame = SharedFrame::Shared(Arc::clone(&arc));
let shared = frame.try_share();
assert_eq!(
Arc::strong_count(&shared),
3,
"strong_count must be 3 after arc + frame + try_share clone"
);
}
#[test]
fn test_shared_frame_into_owned_clone() {
let data: Vec<u8> = vec![10, 20, 30];
let arc = Arc::new(data.clone());
let _second_ref = Arc::clone(&arc);
let frame = SharedFrame::Shared(arc);
let owned = frame.into_owned();
assert_eq!(owned, data, "cloned bytes must match the original");
}
#[test]
fn test_owned_frame_to_shared() {
let data: Vec<u8> = vec![7, 8, 9];
let frame = SharedFrame::Owned(data.clone());
assert!(frame.is_owned());
let promoted = frame.promote();
assert!(
promoted.is_shared(),
"Owned frame must become Shared after promote()"
);
assert_eq!(promoted.as_bytes(), data.as_slice());
}
}