Skip to main content

oximedia_graph/
frame.rs

1//! Frame types for passing through the filter graph.
2//!
3//! This module provides wrapper types for video and audio frames that can be
4//! passed between nodes in the filter graph.
5
6use std::sync::Arc;
7
8use oximedia_audio::{AudioFrame, ChannelLayout};
9use oximedia_codec::VideoFrame;
10use oximedia_core::{PixelFormat, SampleFormat, Timestamp};
11
12/// A frame that can be passed through the filter graph.
13#[derive(Clone, Debug)]
14pub enum FilterFrame {
15    /// Video frame.
16    Video(VideoFrame),
17    /// Audio frame.
18    Audio(AudioFrame),
19}
20
21impl FilterFrame {
22    /// Get the timestamp of the frame.
23    #[must_use]
24    pub fn timestamp(&self) -> &Timestamp {
25        match self {
26            Self::Video(f) => &f.timestamp,
27            Self::Audio(f) => &f.timestamp,
28        }
29    }
30
31    /// Check if this is a video frame.
32    #[must_use]
33    pub fn is_video(&self) -> bool {
34        matches!(self, Self::Video(_))
35    }
36
37    /// Check if this is an audio frame.
38    #[must_use]
39    pub fn is_audio(&self) -> bool {
40        matches!(self, Self::Audio(_))
41    }
42
43    /// Get as video frame if applicable.
44    #[must_use]
45    pub fn as_video(&self) -> Option<&VideoFrame> {
46        match self {
47            Self::Video(f) => Some(f),
48            Self::Audio(_) => None,
49        }
50    }
51
52    /// Get as audio frame if applicable.
53    #[must_use]
54    pub fn as_audio(&self) -> Option<&AudioFrame> {
55        match self {
56            Self::Video(_) => None,
57            Self::Audio(f) => Some(f),
58        }
59    }
60
61    /// Get mutable video frame if applicable.
62    pub fn as_video_mut(&mut self) -> Option<&mut VideoFrame> {
63        match self {
64            Self::Video(f) => Some(f),
65            Self::Audio(_) => None,
66        }
67    }
68
69    /// Get mutable audio frame if applicable.
70    pub fn as_audio_mut(&mut self) -> Option<&mut AudioFrame> {
71        match self {
72            Self::Video(_) => None,
73            Self::Audio(f) => Some(f),
74        }
75    }
76}
77
78impl From<VideoFrame> for FilterFrame {
79    fn from(frame: VideoFrame) -> Self {
80        Self::Video(frame)
81    }
82}
83
84impl From<AudioFrame> for FilterFrame {
85    fn from(frame: AudioFrame) -> Self {
86        Self::Audio(frame)
87    }
88}
89
90/// Reference-counted frame for zero-copy passing.
91///
92/// When a frame needs to be shared between multiple consumers without copying,
93/// use `FrameRef` to wrap it in an `Arc`.
94#[derive(Clone, Debug)]
95pub struct FrameRef {
96    inner: Arc<FilterFrame>,
97}
98
99impl FrameRef {
100    /// Create a new frame reference.
101    pub fn new(frame: FilterFrame) -> Self {
102        Self {
103            inner: Arc::new(frame),
104        }
105    }
106
107    /// Get a reference to the inner frame.
108    #[must_use]
109    pub fn frame(&self) -> &FilterFrame {
110        &self.inner
111    }
112
113    /// Try to get exclusive access to the frame.
114    ///
115    /// Returns `Some` if this is the only reference, `None` otherwise.
116    pub fn try_unwrap(self) -> Option<FilterFrame> {
117        Arc::try_unwrap(self.inner).ok()
118    }
119
120    /// Get the reference count.
121    #[must_use]
122    pub fn ref_count(&self) -> usize {
123        Arc::strong_count(&self.inner)
124    }
125
126    /// Make a copy of the frame if needed for mutation.
127    ///
128    /// If this is the only reference, returns the frame directly.
129    /// Otherwise, clones the frame.
130    #[must_use]
131    pub fn make_mut(self) -> FilterFrame {
132        match Arc::try_unwrap(self.inner) {
133            Ok(frame) => frame,
134            Err(arc) => (*arc).clone(),
135        }
136    }
137}
138
139impl From<FilterFrame> for FrameRef {
140    fn from(frame: FilterFrame) -> Self {
141        Self::new(frame)
142    }
143}
144
145impl From<VideoFrame> for FrameRef {
146    fn from(frame: VideoFrame) -> Self {
147        Self::new(FilterFrame::Video(frame))
148    }
149}
150
151impl From<AudioFrame> for FrameRef {
152    fn from(frame: AudioFrame) -> Self {
153        Self::new(FilterFrame::Audio(frame))
154    }
155}
156
157/// Frame pool for reusing frame allocations.
158///
159/// This is a placeholder for integration with `oximedia-core`'s buffer pool.
160#[allow(dead_code)]
161pub struct FramePool {
162    /// Maximum number of frames to keep in the pool.
163    capacity: usize,
164    /// Pooled video frames.
165    video_frames: Vec<VideoFrame>,
166    /// Pooled audio frames.
167    audio_frames: Vec<AudioFrame>,
168}
169
170impl FramePool {
171    /// Create a new frame pool with the given capacity.
172    #[must_use]
173    pub fn new(capacity: usize) -> Self {
174        Self {
175            capacity,
176            video_frames: Vec::with_capacity(capacity),
177            audio_frames: Vec::with_capacity(capacity),
178        }
179    }
180
181    /// Get a video frame from the pool or create a new one.
182    #[must_use]
183    pub fn get_video_frame(&mut self, format: PixelFormat, width: u32, height: u32) -> VideoFrame {
184        // Try to find a matching frame in the pool
185        if let Some(pos) = self
186            .video_frames
187            .iter()
188            .position(|f| f.format == format && f.width == width && f.height == height)
189        {
190            return self.video_frames.swap_remove(pos);
191        }
192
193        // Create a new frame
194        let mut frame = VideoFrame::new(format, width, height);
195        frame.allocate();
196        frame
197    }
198
199    /// Return a video frame to the pool.
200    pub fn return_video_frame(&mut self, frame: VideoFrame) {
201        if self.video_frames.len() < self.capacity {
202            self.video_frames.push(frame);
203        }
204    }
205
206    /// Get an audio frame from the pool or create a new one.
207    #[must_use]
208    pub fn get_audio_frame(
209        &mut self,
210        format: SampleFormat,
211        sample_rate: u32,
212        channels: ChannelLayout,
213    ) -> AudioFrame {
214        // Try to find a matching frame in the pool
215        if let Some(pos) = self.audio_frames.iter().position(|f| {
216            f.format == format && f.sample_rate == sample_rate && f.channels == channels
217        }) {
218            return self.audio_frames.swap_remove(pos);
219        }
220
221        // Create a new frame
222        AudioFrame::new(format, sample_rate, channels)
223    }
224
225    /// Return an audio frame to the pool.
226    pub fn return_audio_frame(&mut self, frame: AudioFrame) {
227        if self.audio_frames.len() < self.capacity {
228            self.audio_frames.push(frame);
229        }
230    }
231
232    /// Clear all pooled frames.
233    pub fn clear(&mut self) {
234        self.video_frames.clear();
235        self.audio_frames.clear();
236    }
237
238    /// Get the number of video frames in the pool.
239    #[must_use]
240    pub fn video_frame_count(&self) -> usize {
241        self.video_frames.len()
242    }
243
244    /// Get the number of audio frames in the pool.
245    #[must_use]
246    pub fn audio_frame_count(&self) -> usize {
247        self.audio_frames.len()
248    }
249}
250
251impl Default for FramePool {
252    fn default() -> Self {
253        Self::new(16)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_filter_frame_video() {
263        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
264        let frame = FilterFrame::Video(video);
265
266        assert!(frame.is_video());
267        assert!(!frame.is_audio());
268        assert!(frame.as_video().is_some());
269        assert!(frame.as_audio().is_none());
270    }
271
272    #[test]
273    fn test_filter_frame_audio() {
274        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
275        let frame = FilterFrame::Audio(audio);
276
277        assert!(!frame.is_video());
278        assert!(frame.is_audio());
279        assert!(frame.as_video().is_none());
280        assert!(frame.as_audio().is_some());
281    }
282
283    #[test]
284    fn test_frame_ref() {
285        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
286        let frame = FilterFrame::Video(video);
287        let frame_ref = FrameRef::new(frame);
288
289        assert_eq!(frame_ref.ref_count(), 1);
290
291        let frame_ref2 = frame_ref.clone();
292        assert_eq!(frame_ref.ref_count(), 2);
293        assert_eq!(frame_ref2.ref_count(), 2);
294    }
295
296    #[test]
297    fn test_frame_ref_try_unwrap() {
298        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
299        let frame = FilterFrame::Video(video);
300        let frame_ref = FrameRef::new(frame);
301
302        // Should succeed with single reference
303        let unwrapped = frame_ref.try_unwrap();
304        assert!(unwrapped.is_some());
305    }
306
307    #[test]
308    fn test_frame_ref_make_mut() {
309        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
310        let frame = FilterFrame::Video(video);
311        let frame_ref = FrameRef::new(frame);
312        let frame_ref2 = frame_ref.clone();
313
314        // Should clone since there are multiple references
315        let owned = frame_ref.make_mut();
316        assert!(owned.is_video());
317
318        // frame_ref2 should still be valid
319        assert!(frame_ref2.frame().is_video());
320    }
321
322    #[test]
323    fn test_frame_pool() {
324        let mut pool = FramePool::new(4);
325
326        // Get a new frame
327        let frame = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
328        assert_eq!(frame.width, 1920);
329        assert_eq!(frame.height, 1080);
330
331        // Return it to the pool
332        pool.return_video_frame(frame);
333        assert_eq!(pool.video_frame_count(), 1);
334
335        // Get it back (should be the same allocation)
336        let frame2 = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
337        assert_eq!(frame2.width, 1920);
338        assert_eq!(pool.video_frame_count(), 0);
339    }
340
341    #[test]
342    fn test_filter_frame_from() {
343        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
344        let frame: FilterFrame = video.into();
345        assert!(frame.is_video());
346
347        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
348        let frame: FilterFrame = audio.into();
349        assert!(frame.is_audio());
350    }
351
352    #[test]
353    fn test_frame_timestamp() {
354        use oximedia_core::Rational;
355
356        let mut video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
357        video.timestamp = Timestamp::new(1000, Rational::new(1, 1000));
358        let frame = FilterFrame::Video(video);
359
360        assert_eq!(frame.timestamp().pts, 1000);
361    }
362}