Skip to main content

ff_format/frame/
video.rs

1//! Video frame type.
2//!
3//! This module provides [`VideoFrame`] for working with decoded video frames.
4//!
5//! # Examples
6//!
7//! ```
8//! use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
9//!
10//! // Create a simple 1920x1080 RGBA frame
11//! let width = 1920u32;
12//! let height = 1080u32;
13//! let bytes_per_pixel = 4; // RGBA
14//! let stride = width as usize * bytes_per_pixel;
15//! let data = vec![0u8; stride * height as usize];
16//!
17//! let frame = VideoFrame::new(
18//!     vec![PooledBuffer::standalone(data)],
19//!     vec![stride],
20//!     width,
21//!     height,
22//!     PixelFormat::Rgba,
23//!     Timestamp::default(),
24//!     true,
25//! ).unwrap();
26//!
27//! assert_eq!(frame.width(), 1920);
28//! assert_eq!(frame.height(), 1080);
29//! assert!(frame.is_key_frame());
30//! assert_eq!(frame.num_planes(), 1);
31//! ```
32
33use std::fmt;
34
35use crate::error::FrameError;
36use crate::{PixelFormat, PooledBuffer, Timestamp};
37
38/// A decoded video frame.
39///
40/// This structure holds the pixel data and metadata for a single video frame.
41/// It supports both packed formats (like RGBA) where all data is in a single
42/// plane, and planar formats (like YUV420P) where each color component is
43/// stored in a separate plane.
44///
45/// # Memory Layout
46///
47/// For packed formats (RGB, RGBA, BGR, BGRA):
48/// - Single plane containing all pixel data
49/// - Stride equals width × `bytes_per_pixel` (plus optional padding)
50///
51/// For planar YUV formats (YUV420P, YUV422P, YUV444P):
52/// - Plane 0: Y (luma) - full resolution
53/// - Plane 1: U (Cb) - may be subsampled
54/// - Plane 2: V (Cr) - may be subsampled
55///
56/// For semi-planar formats (NV12, NV21):
57/// - Plane 0: Y (luma) - full resolution
58/// - Plane 1: UV interleaved - half height
59///
60/// # Strides
61///
62/// Each plane has an associated stride (also called line size or pitch),
63/// which is the number of bytes from the start of one row to the start
64/// of the next. This may be larger than the actual data width due to
65/// alignment requirements.
66#[derive(Clone)]
67pub struct VideoFrame {
68    /// Pixel data for each plane
69    planes: Vec<PooledBuffer>,
70    /// Stride (bytes per row) for each plane
71    strides: Vec<usize>,
72    /// Frame width in pixels
73    width: u32,
74    /// Frame height in pixels
75    height: u32,
76    /// Pixel format
77    format: PixelFormat,
78    /// Presentation timestamp
79    timestamp: Timestamp,
80    /// Whether this is a key frame (I-frame)
81    key_frame: bool,
82}
83
84impl VideoFrame {
85    /// Creates a new video frame with the specified parameters.
86    ///
87    /// # Arguments
88    ///
89    /// * `planes` - Pixel data for each plane
90    /// * `strides` - Stride (bytes per row) for each plane
91    /// * `width` - Frame width in pixels
92    /// * `height` - Frame height in pixels
93    /// * `format` - Pixel format
94    /// * `timestamp` - Presentation timestamp
95    /// * `key_frame` - Whether this is a key frame
96    ///
97    /// # Errors
98    ///
99    /// Returns [`FrameError::MismatchedPlaneStride`] if `planes.len() != strides.len()`.
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
105    ///
106    /// // Create a 640x480 YUV420P frame
107    /// let width = 640u32;
108    /// let height = 480u32;
109    ///
110    /// // Y plane: full resolution
111    /// let y_stride = width as usize;
112    /// let y_data = vec![128u8; y_stride * height as usize];
113    ///
114    /// // U/V planes: half resolution in both dimensions
115    /// let uv_stride = (width / 2) as usize;
116    /// let uv_height = (height / 2) as usize;
117    /// let u_data = vec![128u8; uv_stride * uv_height];
118    /// let v_data = vec![128u8; uv_stride * uv_height];
119    ///
120    /// let frame = VideoFrame::new(
121    ///     vec![
122    ///         PooledBuffer::standalone(y_data),
123    ///         PooledBuffer::standalone(u_data),
124    ///         PooledBuffer::standalone(v_data),
125    ///     ],
126    ///     vec![y_stride, uv_stride, uv_stride],
127    ///     width,
128    ///     height,
129    ///     PixelFormat::Yuv420p,
130    ///     Timestamp::default(),
131    ///     true,
132    /// ).unwrap();
133    ///
134    /// assert_eq!(frame.num_planes(), 3);
135    /// ```
136    pub fn new(
137        planes: Vec<PooledBuffer>,
138        strides: Vec<usize>,
139        width: u32,
140        height: u32,
141        format: PixelFormat,
142        timestamp: Timestamp,
143        key_frame: bool,
144    ) -> Result<Self, FrameError> {
145        if planes.len() != strides.len() {
146            return Err(FrameError::MismatchedPlaneStride {
147                planes: planes.len(),
148                strides: strides.len(),
149            });
150        }
151        Ok(Self {
152            planes,
153            strides,
154            width,
155            height,
156            format,
157            timestamp,
158            key_frame,
159        })
160    }
161
162    /// Creates an empty video frame with the specified dimensions and format.
163    ///
164    /// The frame will have properly sized planes filled with zeros based
165    /// on the pixel format.
166    ///
167    /// # Arguments
168    ///
169    /// * `width` - Frame width in pixels
170    /// * `height` - Frame height in pixels
171    /// * `format` - Pixel format
172    ///
173    /// # Errors
174    ///
175    /// Returns [`FrameError::UnsupportedPixelFormat`] if the format is
176    /// [`PixelFormat::Other`], as the memory layout cannot be determined.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use ff_format::{PixelFormat, VideoFrame};
182    ///
183    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
184    /// assert_eq!(frame.width(), 1920);
185    /// assert_eq!(frame.height(), 1080);
186    /// assert_eq!(frame.num_planes(), 1);
187    /// ```
188    pub fn empty(width: u32, height: u32, format: PixelFormat) -> Result<Self, FrameError> {
189        let (planes, strides) = Self::allocate_planes(width, height, format)?;
190        Ok(Self {
191            planes,
192            strides,
193            width,
194            height,
195            format,
196            timestamp: Timestamp::default(),
197            key_frame: false,
198        })
199    }
200
201    /// Allocates planes and strides for the given dimensions and format.
202    #[allow(clippy::too_many_lines)]
203    fn allocate_planes(
204        width: u32,
205        height: u32,
206        format: PixelFormat,
207    ) -> Result<(Vec<PooledBuffer>, Vec<usize>), FrameError> {
208        match format {
209            // Packed RGB formats - single plane
210            PixelFormat::Rgb24 | PixelFormat::Bgr24 => {
211                let stride = width as usize * 3;
212                let data = vec![0u8; stride * height as usize];
213                Ok((vec![PooledBuffer::standalone(data)], vec![stride]))
214            }
215            // RGBA/BGRA - 4 bytes per pixel
216            PixelFormat::Rgba | PixelFormat::Bgra => {
217                let stride = width as usize * 4;
218                let data = vec![0u8; stride * height as usize];
219                Ok((vec![PooledBuffer::standalone(data)], vec![stride]))
220            }
221            PixelFormat::Gray8 => {
222                let stride = width as usize;
223                let data = vec![0u8; stride * height as usize];
224                Ok((vec![PooledBuffer::standalone(data)], vec![stride]))
225            }
226
227            // Planar YUV 4:2:0 - Y full, U/V quarter size
228            PixelFormat::Yuv420p => {
229                let y_stride = width as usize;
230                let uv_stride = (width as usize).div_ceil(2);
231                let uv_height = (height as usize).div_ceil(2);
232
233                let y_data = vec![0u8; y_stride * height as usize];
234                let u_data = vec![0u8; uv_stride * uv_height];
235                let v_data = vec![0u8; uv_stride * uv_height];
236
237                Ok((
238                    vec![
239                        PooledBuffer::standalone(y_data),
240                        PooledBuffer::standalone(u_data),
241                        PooledBuffer::standalone(v_data),
242                    ],
243                    vec![y_stride, uv_stride, uv_stride],
244                ))
245            }
246
247            // Planar YUV 4:2:2 - Y full, U/V half width, full height
248            PixelFormat::Yuv422p => {
249                let y_stride = width as usize;
250                let uv_stride = (width as usize).div_ceil(2);
251
252                let y_data = vec![0u8; y_stride * height as usize];
253                let u_data = vec![0u8; uv_stride * height as usize];
254                let v_data = vec![0u8; uv_stride * height as usize];
255
256                Ok((
257                    vec![
258                        PooledBuffer::standalone(y_data),
259                        PooledBuffer::standalone(u_data),
260                        PooledBuffer::standalone(v_data),
261                    ],
262                    vec![y_stride, uv_stride, uv_stride],
263                ))
264            }
265
266            // Planar YUV 4:4:4 - all planes full size
267            PixelFormat::Yuv444p => {
268                let stride = width as usize;
269                let size = stride * height as usize;
270
271                Ok((
272                    vec![
273                        PooledBuffer::standalone(vec![0u8; size]),
274                        PooledBuffer::standalone(vec![0u8; size]),
275                        PooledBuffer::standalone(vec![0u8; size]),
276                    ],
277                    vec![stride, stride, stride],
278                ))
279            }
280
281            // Semi-planar NV12/NV21 - Y full, UV interleaved half height
282            PixelFormat::Nv12 | PixelFormat::Nv21 => {
283                let y_stride = width as usize;
284                let uv_stride = width as usize; // UV interleaved, so same width
285                let uv_height = (height as usize).div_ceil(2);
286
287                let y_data = vec![0u8; y_stride * height as usize];
288                let uv_data = vec![0u8; uv_stride * uv_height];
289
290                Ok((
291                    vec![
292                        PooledBuffer::standalone(y_data),
293                        PooledBuffer::standalone(uv_data),
294                    ],
295                    vec![y_stride, uv_stride],
296                ))
297            }
298
299            // 10-bit planar YUV 4:2:0 - 2 bytes per sample
300            PixelFormat::Yuv420p10le => {
301                let y_stride = width as usize * 2;
302                let uv_stride = (width as usize).div_ceil(2) * 2;
303                let uv_height = (height as usize).div_ceil(2);
304
305                let y_data = vec![0u8; y_stride * height as usize];
306                let u_data = vec![0u8; uv_stride * uv_height];
307                let v_data = vec![0u8; uv_stride * uv_height];
308
309                Ok((
310                    vec![
311                        PooledBuffer::standalone(y_data),
312                        PooledBuffer::standalone(u_data),
313                        PooledBuffer::standalone(v_data),
314                    ],
315                    vec![y_stride, uv_stride, uv_stride],
316                ))
317            }
318
319            // 10-bit semi-planar P010
320            PixelFormat::P010le => {
321                let y_stride = width as usize * 2;
322                let uv_stride = width as usize * 2;
323                let uv_height = (height as usize).div_ceil(2);
324
325                let y_data = vec![0u8; y_stride * height as usize];
326                let uv_data = vec![0u8; uv_stride * uv_height];
327
328                Ok((
329                    vec![
330                        PooledBuffer::standalone(y_data),
331                        PooledBuffer::standalone(uv_data),
332                    ],
333                    vec![y_stride, uv_stride],
334                ))
335            }
336
337            // Unknown format - cannot determine memory layout
338            PixelFormat::Other(_) => Err(FrameError::UnsupportedPixelFormat(format)),
339        }
340    }
341
342    // ==========================================================================
343    // Metadata Accessors
344    // ==========================================================================
345
346    /// Returns the frame width in pixels.
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use ff_format::{PixelFormat, VideoFrame};
352    ///
353    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
354    /// assert_eq!(frame.width(), 1920);
355    /// ```
356    #[must_use]
357    #[inline]
358    pub const fn width(&self) -> u32 {
359        self.width
360    }
361
362    /// Returns the frame height in pixels.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// use ff_format::{PixelFormat, VideoFrame};
368    ///
369    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
370    /// assert_eq!(frame.height(), 1080);
371    /// ```
372    #[must_use]
373    #[inline]
374    pub const fn height(&self) -> u32 {
375        self.height
376    }
377
378    /// Returns the pixel format of this frame.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use ff_format::{PixelFormat, VideoFrame};
384    ///
385    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
386    /// assert_eq!(frame.format(), PixelFormat::Yuv420p);
387    /// ```
388    #[must_use]
389    #[inline]
390    pub const fn format(&self) -> PixelFormat {
391        self.format
392    }
393
394    /// Returns the presentation timestamp of this frame.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
400    ///
401    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
402    /// let frame = VideoFrame::new(
403    ///     vec![PooledBuffer::standalone(vec![0u8; 1920 * 1080 * 4])],
404    ///     vec![1920 * 4],
405    ///     1920,
406    ///     1080,
407    ///     PixelFormat::Rgba,
408    ///     ts,
409    ///     true,
410    /// ).unwrap();
411    /// assert_eq!(frame.timestamp(), ts);
412    /// ```
413    #[must_use]
414    #[inline]
415    pub const fn timestamp(&self) -> Timestamp {
416        self.timestamp
417    }
418
419    /// Returns whether this frame is a key frame (I-frame).
420    ///
421    /// Key frames are complete frames that don't depend on any other frames
422    /// for decoding. They are used as reference points for seeking.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// use ff_format::{PixelFormat, VideoFrame};
428    ///
429    /// let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
430    /// assert!(!frame.is_key_frame());
431    ///
432    /// frame.set_key_frame(true);
433    /// assert!(frame.is_key_frame());
434    /// ```
435    #[must_use]
436    #[inline]
437    pub const fn is_key_frame(&self) -> bool {
438        self.key_frame
439    }
440
441    /// Sets whether this frame is a key frame.
442    ///
443    /// # Examples
444    ///
445    /// ```
446    /// use ff_format::{PixelFormat, VideoFrame};
447    ///
448    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
449    /// frame.set_key_frame(true);
450    /// assert!(frame.is_key_frame());
451    /// ```
452    #[inline]
453    pub fn set_key_frame(&mut self, key_frame: bool) {
454        self.key_frame = key_frame;
455    }
456
457    /// Sets the timestamp of this frame.
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// use ff_format::{PixelFormat, Rational, Timestamp, VideoFrame};
463    ///
464    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
465    /// let ts = Timestamp::new(3000, Rational::new(1, 90000));
466    /// frame.set_timestamp(ts);
467    /// assert_eq!(frame.timestamp(), ts);
468    /// ```
469    #[inline]
470    pub fn set_timestamp(&mut self, timestamp: Timestamp) {
471        self.timestamp = timestamp;
472    }
473
474    // ==========================================================================
475    // Plane Data Access
476    // ==========================================================================
477
478    /// Returns the number of planes in this frame.
479    ///
480    /// - Packed formats (RGBA, RGB24, etc.): 1 plane
481    /// - Planar YUV (YUV420P, YUV422P, YUV444P): 3 planes
482    /// - Semi-planar (NV12, NV21): 2 planes
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// use ff_format::{PixelFormat, VideoFrame};
488    ///
489    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
490    /// assert_eq!(rgba.num_planes(), 1);
491    ///
492    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
493    /// assert_eq!(yuv.num_planes(), 3);
494    ///
495    /// let nv12 = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
496    /// assert_eq!(nv12.num_planes(), 2);
497    /// ```
498    #[must_use]
499    #[inline]
500    pub fn num_planes(&self) -> usize {
501        self.planes.len()
502    }
503
504    /// Returns a slice of all plane buffers.
505    ///
506    /// # Examples
507    ///
508    /// ```
509    /// use ff_format::{PixelFormat, VideoFrame};
510    ///
511    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
512    /// let planes = frame.planes();
513    /// assert_eq!(planes.len(), 3);
514    /// ```
515    #[must_use]
516    #[inline]
517    pub fn planes(&self) -> &[PooledBuffer] {
518        &self.planes
519    }
520
521    /// Returns the data for a specific plane, or `None` if the index is out of bounds.
522    ///
523    /// # Arguments
524    ///
525    /// * `index` - The plane index (0 for Y/RGB, 1 for U/UV, 2 for V)
526    ///
527    /// # Examples
528    ///
529    /// ```
530    /// use ff_format::{PixelFormat, VideoFrame};
531    ///
532    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
533    ///
534    /// // Y plane exists
535    /// assert!(frame.plane(0).is_some());
536    ///
537    /// // U and V planes exist
538    /// assert!(frame.plane(1).is_some());
539    /// assert!(frame.plane(2).is_some());
540    ///
541    /// // No fourth plane
542    /// assert!(frame.plane(3).is_none());
543    /// ```
544    #[must_use]
545    #[inline]
546    pub fn plane(&self, index: usize) -> Option<&[u8]> {
547        self.planes.get(index).map(std::convert::AsRef::as_ref)
548    }
549
550    /// Returns mutable access to a specific plane's data.
551    ///
552    /// # Arguments
553    ///
554    /// * `index` - The plane index
555    ///
556    /// # Examples
557    ///
558    /// ```
559    /// use ff_format::{PixelFormat, VideoFrame};
560    ///
561    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
562    /// if let Some(data) = frame.plane_mut(0) {
563    ///     // Fill with red (RGBA)
564    ///     for chunk in data.chunks_exact_mut(4) {
565    ///         chunk[0] = 255; // R
566    ///         chunk[1] = 0;   // G
567    ///         chunk[2] = 0;   // B
568    ///         chunk[3] = 255; // A
569    ///     }
570    /// }
571    /// ```
572    #[must_use]
573    #[inline]
574    pub fn plane_mut(&mut self, index: usize) -> Option<&mut [u8]> {
575        self.planes.get_mut(index).map(std::convert::AsMut::as_mut)
576    }
577
578    /// Returns a slice of all stride values.
579    ///
580    /// Strides indicate the number of bytes between the start of consecutive
581    /// rows in each plane.
582    ///
583    /// # Examples
584    ///
585    /// ```
586    /// use ff_format::{PixelFormat, VideoFrame};
587    ///
588    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
589    /// let strides = frame.strides();
590    /// assert_eq!(strides[0], 1920 * 4); // RGBA = 4 bytes per pixel
591    /// ```
592    #[must_use]
593    #[inline]
594    pub fn strides(&self) -> &[usize] {
595        &self.strides
596    }
597
598    /// Returns the stride for a specific plane, or `None` if the index is out of bounds.
599    ///
600    /// # Arguments
601    ///
602    /// * `plane` - The plane index
603    ///
604    /// # Examples
605    ///
606    /// ```
607    /// use ff_format::{PixelFormat, VideoFrame};
608    ///
609    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
610    ///
611    /// // Y plane stride = width
612    /// assert_eq!(frame.stride(0), Some(640));
613    ///
614    /// // U/V plane stride = width / 2
615    /// assert_eq!(frame.stride(1), Some(320));
616    /// assert_eq!(frame.stride(2), Some(320));
617    /// ```
618    #[must_use]
619    #[inline]
620    pub fn stride(&self, plane: usize) -> Option<usize> {
621        self.strides.get(plane).copied()
622    }
623
624    // ==========================================================================
625    // Contiguous Data Access
626    // ==========================================================================
627
628    /// Returns the frame data as a contiguous byte vector.
629    ///
630    /// For packed formats with a single plane, this returns a copy of the plane data.
631    /// For planar formats, this concatenates all planes into a single buffer.
632    ///
633    /// # Note
634    ///
635    /// This method allocates a new vector and copies the data. For zero-copy
636    /// access, use [`plane()`](Self::plane) or [`planes()`](Self::planes) instead.
637    ///
638    /// # Examples
639    ///
640    /// ```
641    /// use ff_format::{PixelFormat, VideoFrame};
642    ///
643    /// let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
644    /// let data = frame.data();
645    /// assert_eq!(data.len(), 4 * 4 * 4); // 4x4 pixels, 4 bytes each
646    /// ```
647    #[must_use]
648    pub fn data(&self) -> Vec<u8> {
649        let total_size: usize = self.planes.iter().map(PooledBuffer::len).sum();
650        let mut result = Vec::with_capacity(total_size);
651        for plane in &self.planes {
652            result.extend_from_slice(plane.as_ref());
653        }
654        result
655    }
656
657    /// Returns a reference to the first plane's data as a contiguous slice.
658    ///
659    /// This is only meaningful for packed formats (RGBA, RGB24, etc.) where
660    /// all data is in a single plane. Returns `None` if the format is planar
661    /// or if no planes exist.
662    ///
663    /// # Examples
664    ///
665    /// ```
666    /// use ff_format::{PixelFormat, VideoFrame};
667    ///
668    /// // Packed format - returns data
669    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
670    /// assert!(rgba.data_ref().is_some());
671    ///
672    /// // Planar format - returns None
673    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
674    /// assert!(yuv.data_ref().is_none());
675    /// ```
676    #[must_use]
677    #[inline]
678    pub fn data_ref(&self) -> Option<&[u8]> {
679        if self.format.is_packed() && self.planes.len() == 1 {
680            Some(self.planes[0].as_ref())
681        } else {
682            None
683        }
684    }
685
686    /// Returns a mutable reference to the first plane's data.
687    ///
688    /// This is only meaningful for packed formats where all data is in a
689    /// single plane. Returns `None` if the format is planar.
690    ///
691    /// # Examples
692    ///
693    /// ```
694    /// use ff_format::{PixelFormat, VideoFrame};
695    ///
696    /// let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
697    /// if let Some(data) = frame.data_mut() {
698    ///     data[0] = 255; // Modify first byte
699    /// }
700    /// ```
701    #[must_use]
702    #[inline]
703    pub fn data_mut(&mut self) -> Option<&mut [u8]> {
704        if self.format.is_packed() && self.planes.len() == 1 {
705            Some(self.planes[0].as_mut())
706        } else {
707            None
708        }
709    }
710
711    // ==========================================================================
712    // Utility Methods
713    // ==========================================================================
714
715    /// Returns the total size in bytes of all plane data.
716    ///
717    /// # Examples
718    ///
719    /// ```
720    /// use ff_format::{PixelFormat, VideoFrame};
721    ///
722    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
723    /// assert_eq!(frame.total_size(), 1920 * 1080 * 4);
724    /// ```
725    #[must_use]
726    pub fn total_size(&self) -> usize {
727        self.planes.iter().map(PooledBuffer::len).sum()
728    }
729
730    /// Returns the resolution as a (width, height) tuple.
731    ///
732    /// # Examples
733    ///
734    /// ```
735    /// use ff_format::{PixelFormat, VideoFrame};
736    ///
737    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
738    /// assert_eq!(frame.resolution(), (1920, 1080));
739    /// ```
740    #[must_use]
741    #[inline]
742    pub const fn resolution(&self) -> (u32, u32) {
743        (self.width, self.height)
744    }
745
746    /// Returns the aspect ratio as a floating-point value.
747    ///
748    /// # Examples
749    ///
750    /// ```
751    /// use ff_format::{PixelFormat, VideoFrame};
752    ///
753    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
754    /// let aspect = frame.aspect_ratio();
755    /// assert!((aspect - 16.0 / 9.0).abs() < 0.01);
756    /// ```
757    #[must_use]
758    #[inline]
759    pub fn aspect_ratio(&self) -> f64 {
760        if self.height == 0 {
761            log::warn!(
762                "aspect_ratio unavailable, height is 0, returning 0.0 \
763                 width={} height=0 fallback=0.0",
764                self.width
765            );
766            0.0
767        } else {
768            f64::from(self.width) / f64::from(self.height)
769        }
770    }
771}
772
773impl fmt::Debug for VideoFrame {
774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775        f.debug_struct("VideoFrame")
776            .field("width", &self.width)
777            .field("height", &self.height)
778            .field("format", &self.format)
779            .field("timestamp", &self.timestamp)
780            .field("key_frame", &self.key_frame)
781            .field("num_planes", &self.planes.len())
782            .field(
783                "plane_sizes",
784                &self
785                    .planes
786                    .iter()
787                    .map(PooledBuffer::len)
788                    .collect::<Vec<_>>(),
789            )
790            .field("strides", &self.strides)
791            .finish()
792    }
793}
794
795impl fmt::Display for VideoFrame {
796    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
797        write!(
798            f,
799            "VideoFrame({}x{} {} @ {}{})",
800            self.width,
801            self.height,
802            self.format,
803            self.timestamp,
804            if self.key_frame { " [KEY]" } else { "" }
805        )
806    }
807}
808
809impl Default for VideoFrame {
810    /// Returns a default empty 1x1 YUV420P frame.
811    ///
812    /// This constructs a minimal valid frame directly.
813    fn default() -> Self {
814        // Construct a minimal 1x1 YUV420P frame directly
815        // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
816        Self {
817            planes: vec![
818                PooledBuffer::standalone(vec![0u8; 1]),
819                PooledBuffer::standalone(vec![0u8; 1]),
820                PooledBuffer::standalone(vec![0u8; 1]),
821            ],
822            strides: vec![1, 1, 1],
823            width: 1,
824            height: 1,
825            format: PixelFormat::Yuv420p,
826            timestamp: Timestamp::default(),
827            key_frame: false,
828        }
829    }
830}
831
832#[cfg(test)]
833#[allow(
834    clippy::unwrap_used,
835    clippy::redundant_closure_for_method_calls,
836    clippy::float_cmp
837)]
838mod tests {
839    use super::*;
840    use crate::Rational;
841
842    // ==========================================================================
843    // Construction Tests
844    // ==========================================================================
845
846    #[test]
847    fn test_new_rgba_frame() {
848        let width = 640u32;
849        let height = 480u32;
850        let stride = width as usize * 4;
851        let data = vec![0u8; stride * height as usize];
852        let ts = Timestamp::new(1000, Rational::new(1, 1000));
853
854        let frame = VideoFrame::new(
855            vec![PooledBuffer::standalone(data)],
856            vec![stride],
857            width,
858            height,
859            PixelFormat::Rgba,
860            ts,
861            true,
862        )
863        .unwrap();
864
865        assert_eq!(frame.width(), 640);
866        assert_eq!(frame.height(), 480);
867        assert_eq!(frame.format(), PixelFormat::Rgba);
868        assert_eq!(frame.timestamp(), ts);
869        assert!(frame.is_key_frame());
870        assert_eq!(frame.num_planes(), 1);
871        assert_eq!(frame.stride(0), Some(640 * 4));
872    }
873
874    #[test]
875    fn test_new_yuv420p_frame() {
876        let width = 640u32;
877        let height = 480u32;
878
879        let y_stride = width as usize;
880        let uv_stride = (width / 2) as usize;
881        let uv_height = (height / 2) as usize;
882
883        let y_data = vec![128u8; y_stride * height as usize];
884        let u_data = vec![128u8; uv_stride * uv_height];
885        let v_data = vec![128u8; uv_stride * uv_height];
886
887        let frame = VideoFrame::new(
888            vec![
889                PooledBuffer::standalone(y_data),
890                PooledBuffer::standalone(u_data),
891                PooledBuffer::standalone(v_data),
892            ],
893            vec![y_stride, uv_stride, uv_stride],
894            width,
895            height,
896            PixelFormat::Yuv420p,
897            Timestamp::default(),
898            false,
899        )
900        .unwrap();
901
902        assert_eq!(frame.width(), 640);
903        assert_eq!(frame.height(), 480);
904        assert_eq!(frame.format(), PixelFormat::Yuv420p);
905        assert!(!frame.is_key_frame());
906        assert_eq!(frame.num_planes(), 3);
907        assert_eq!(frame.stride(0), Some(640));
908        assert_eq!(frame.stride(1), Some(320));
909        assert_eq!(frame.stride(2), Some(320));
910    }
911
912    #[test]
913    fn test_new_mismatched_planes_strides() {
914        let result = VideoFrame::new(
915            vec![PooledBuffer::standalone(vec![0u8; 100])],
916            vec![10, 10], // Mismatched length
917            10,
918            10,
919            PixelFormat::Rgba,
920            Timestamp::default(),
921            false,
922        );
923
924        assert!(result.is_err());
925        assert_eq!(
926            result.unwrap_err(),
927            FrameError::MismatchedPlaneStride {
928                planes: 1,
929                strides: 2
930            }
931        );
932    }
933
934    #[test]
935    fn test_empty_rgba() {
936        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
937        assert_eq!(frame.width(), 1920);
938        assert_eq!(frame.height(), 1080);
939        assert_eq!(frame.format(), PixelFormat::Rgba);
940        assert_eq!(frame.num_planes(), 1);
941        assert_eq!(frame.stride(0), Some(1920 * 4));
942        assert_eq!(frame.plane(0).map(|p| p.len()), Some(1920 * 1080 * 4));
943    }
944
945    #[test]
946    fn test_empty_yuv420p() {
947        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
948        assert_eq!(frame.num_planes(), 3);
949        assert_eq!(frame.stride(0), Some(640));
950        assert_eq!(frame.stride(1), Some(320));
951        assert_eq!(frame.stride(2), Some(320));
952
953        // Y plane: 640 * 480
954        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
955        // U/V planes: 320 * 240
956        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240));
957        assert_eq!(frame.plane(2).map(|p| p.len()), Some(320 * 240));
958    }
959
960    #[test]
961    fn test_empty_yuv422p() {
962        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv422p).unwrap();
963        assert_eq!(frame.num_planes(), 3);
964        assert_eq!(frame.stride(0), Some(640));
965        assert_eq!(frame.stride(1), Some(320));
966
967        // Y: full resolution, U/V: half width, full height
968        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
969        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 480));
970    }
971
972    #[test]
973    fn test_empty_yuv444p() {
974        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv444p).unwrap();
975        assert_eq!(frame.num_planes(), 3);
976
977        // All planes: full resolution
978        let expected_size = 640 * 480;
979        assert_eq!(frame.plane(0).map(|p| p.len()), Some(expected_size));
980        assert_eq!(frame.plane(1).map(|p| p.len()), Some(expected_size));
981        assert_eq!(frame.plane(2).map(|p| p.len()), Some(expected_size));
982    }
983
984    #[test]
985    fn test_empty_nv12() {
986        let frame = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
987        assert_eq!(frame.num_planes(), 2);
988        assert_eq!(frame.stride(0), Some(640));
989        assert_eq!(frame.stride(1), Some(640)); // UV interleaved
990
991        // Y plane: full resolution
992        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
993        // UV plane: full width, half height
994        assert_eq!(frame.plane(1).map(|p| p.len()), Some(640 * 240));
995    }
996
997    #[test]
998    fn test_empty_gray8() {
999        let frame = VideoFrame::empty(640, 480, PixelFormat::Gray8).unwrap();
1000        assert_eq!(frame.num_planes(), 1);
1001        assert_eq!(frame.stride(0), Some(640));
1002        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1003    }
1004
1005    #[test]
1006    fn test_default() {
1007        let frame = VideoFrame::default();
1008        assert_eq!(frame.width(), 1);
1009        assert_eq!(frame.height(), 1);
1010        assert_eq!(frame.format(), PixelFormat::default());
1011        assert!(!frame.is_key_frame());
1012    }
1013
1014    // ==========================================================================
1015    // Metadata Tests
1016    // ==========================================================================
1017
1018    #[test]
1019    fn test_resolution() {
1020        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1021        assert_eq!(frame.resolution(), (1920, 1080));
1022    }
1023
1024    #[test]
1025    fn test_aspect_ratio() {
1026        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1027        let aspect = frame.aspect_ratio();
1028        assert!((aspect - 16.0 / 9.0).abs() < 0.001);
1029
1030        let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1031        let aspect_4_3 = frame_4_3.aspect_ratio();
1032        assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
1033    }
1034
1035    #[test]
1036    fn test_aspect_ratio_zero_height() {
1037        let frame = VideoFrame::new(
1038            vec![PooledBuffer::standalone(vec![])],
1039            vec![0],
1040            100,
1041            0,
1042            PixelFormat::Rgba,
1043            Timestamp::default(),
1044            false,
1045        )
1046        .unwrap();
1047        assert_eq!(frame.aspect_ratio(), 0.0);
1048    }
1049
1050    #[test]
1051    fn test_total_size_rgba() {
1052        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1053        assert_eq!(frame.total_size(), 1920 * 1080 * 4);
1054    }
1055
1056    #[test]
1057    fn test_total_size_yuv420p() {
1058        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1059        // Y: 640*480, U: 320*240, V: 320*240
1060        let expected = 640 * 480 + 320 * 240 * 2;
1061        assert_eq!(frame.total_size(), expected);
1062    }
1063
1064    #[test]
1065    fn test_set_key_frame() {
1066        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1067        assert!(!frame.is_key_frame());
1068
1069        frame.set_key_frame(true);
1070        assert!(frame.is_key_frame());
1071
1072        frame.set_key_frame(false);
1073        assert!(!frame.is_key_frame());
1074    }
1075
1076    #[test]
1077    fn test_set_timestamp() {
1078        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1079        let ts = Timestamp::new(90000, Rational::new(1, 90000));
1080
1081        frame.set_timestamp(ts);
1082        assert_eq!(frame.timestamp(), ts);
1083    }
1084
1085    // ==========================================================================
1086    // Plane Access Tests
1087    // ==========================================================================
1088
1089    #[test]
1090    fn test_plane_access() {
1091        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1092
1093        assert!(frame.plane(0).is_some());
1094        assert!(frame.plane(1).is_some());
1095        assert!(frame.plane(2).is_some());
1096        assert!(frame.plane(3).is_none());
1097    }
1098
1099    #[test]
1100    fn test_plane_mut_access() {
1101        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1102
1103        if let Some(data) = frame.plane_mut(0) {
1104            // Fill with red
1105            for chunk in data.chunks_exact_mut(4) {
1106                chunk[0] = 255;
1107                chunk[1] = 0;
1108                chunk[2] = 0;
1109                chunk[3] = 255;
1110            }
1111        }
1112
1113        let plane = frame.plane(0).unwrap();
1114        assert_eq!(plane[0], 255); // R
1115        assert_eq!(plane[1], 0); // G
1116        assert_eq!(plane[2], 0); // B
1117        assert_eq!(plane[3], 255); // A
1118    }
1119
1120    #[test]
1121    fn test_planes_slice() {
1122        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1123        let planes = frame.planes();
1124        assert_eq!(planes.len(), 3);
1125    }
1126
1127    #[test]
1128    fn test_strides_slice() {
1129        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1130        let strides = frame.strides();
1131        assert_eq!(strides.len(), 3);
1132        assert_eq!(strides[0], 640);
1133        assert_eq!(strides[1], 320);
1134        assert_eq!(strides[2], 320);
1135    }
1136
1137    #[test]
1138    fn test_stride_out_of_bounds() {
1139        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1140        assert!(frame.stride(0).is_some());
1141        assert!(frame.stride(1).is_none());
1142    }
1143
1144    // ==========================================================================
1145    // Data Access Tests
1146    // ==========================================================================
1147
1148    #[test]
1149    fn test_data_contiguous() {
1150        let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1151        let data = frame.data();
1152        assert_eq!(data.len(), 4 * 4 * 4);
1153    }
1154
1155    #[test]
1156    fn test_data_yuv420p_concatenation() {
1157        let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
1158        let data = frame.data();
1159        // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
1160        assert_eq!(data.len(), 24);
1161    }
1162
1163    #[test]
1164    fn test_data_ref_packed() {
1165        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1166        assert!(frame.data_ref().is_some());
1167        assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
1168    }
1169
1170    #[test]
1171    fn test_data_ref_planar() {
1172        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1173        assert!(frame.data_ref().is_none());
1174    }
1175
1176    #[test]
1177    fn test_data_mut_packed() {
1178        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1179        assert!(frame.data_mut().is_some());
1180
1181        if let Some(data) = frame.data_mut() {
1182            data[0] = 123;
1183        }
1184
1185        assert_eq!(frame.plane(0).unwrap()[0], 123);
1186    }
1187
1188    #[test]
1189    fn test_data_mut_planar() {
1190        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1191        assert!(frame.data_mut().is_none());
1192    }
1193
1194    // ==========================================================================
1195    // Clone Tests
1196    // ==========================================================================
1197
1198    #[test]
1199    fn test_clone() {
1200        let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1201        original.set_key_frame(true);
1202        original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1203
1204        // Modify some data
1205        if let Some(data) = original.plane_mut(0) {
1206            data[0] = 42;
1207        }
1208
1209        let cloned = original.clone();
1210
1211        // Verify metadata matches
1212        assert_eq!(cloned.width(), original.width());
1213        assert_eq!(cloned.height(), original.height());
1214        assert_eq!(cloned.format(), original.format());
1215        assert_eq!(cloned.timestamp(), original.timestamp());
1216        assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1217
1218        // Verify data was cloned
1219        assert_eq!(cloned.plane(0).unwrap()[0], 42);
1220
1221        // Verify it's a deep clone (modifying clone doesn't affect original)
1222        let mut cloned = cloned;
1223        if let Some(data) = cloned.plane_mut(0) {
1224            data[0] = 99;
1225        }
1226        assert_eq!(original.plane(0).unwrap()[0], 42);
1227        assert_eq!(cloned.plane(0).unwrap()[0], 99);
1228    }
1229
1230    // ==========================================================================
1231    // Display/Debug Tests
1232    // ==========================================================================
1233
1234    #[test]
1235    fn test_debug() {
1236        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1237        let debug = format!("{frame:?}");
1238        assert!(debug.contains("VideoFrame"));
1239        assert!(debug.contains("640"));
1240        assert!(debug.contains("480"));
1241        assert!(debug.contains("Rgba"));
1242    }
1243
1244    #[test]
1245    fn test_display() {
1246        let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1247        frame.set_key_frame(true);
1248
1249        let display = format!("{frame}");
1250        assert!(display.contains("1920x1080"));
1251        assert!(display.contains("yuv420p"));
1252        assert!(display.contains("[KEY]"));
1253    }
1254
1255    #[test]
1256    fn test_display_non_keyframe() {
1257        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1258        let display = format!("{frame}");
1259        assert!(!display.contains("[KEY]"));
1260    }
1261
1262    // ==========================================================================
1263    // Edge Case Tests
1264    // ==========================================================================
1265
1266    #[test]
1267    fn test_odd_dimensions_yuv420p() {
1268        // Odd dimensions require proper rounding for chroma planes
1269        let frame = VideoFrame::empty(641, 481, PixelFormat::Yuv420p).unwrap();
1270
1271        // Y plane should be full size
1272        assert_eq!(frame.plane(0).map(|p| p.len()), Some(641 * 481));
1273
1274        // U/V planes should be (641+1)/2 * (481+1)/2 = 321 * 241
1275        assert_eq!(frame.plane(1).map(|p| p.len()), Some(321 * 241));
1276        assert_eq!(frame.plane(2).map(|p| p.len()), Some(321 * 241));
1277    }
1278
1279    #[test]
1280    fn test_small_frame() {
1281        let frame = VideoFrame::empty(1, 1, PixelFormat::Rgba).unwrap();
1282        assert_eq!(frame.total_size(), 4);
1283        assert_eq!(frame.plane(0).map(|p| p.len()), Some(4));
1284    }
1285
1286    #[test]
1287    fn test_10bit_yuv420p() {
1288        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p10le).unwrap();
1289        assert_eq!(frame.num_planes(), 3);
1290
1291        // 10-bit uses 2 bytes per sample
1292        assert_eq!(frame.stride(0), Some(640 * 2));
1293        assert_eq!(frame.stride(1), Some(320 * 2));
1294        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480 * 2));
1295        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240 * 2));
1296    }
1297
1298    #[test]
1299    fn test_p010le() {
1300        let frame = VideoFrame::empty(640, 480, PixelFormat::P010le).unwrap();
1301        assert_eq!(frame.num_planes(), 2);
1302
1303        // 10-bit semi-planar: Y and UV interleaved, both 2 bytes per sample
1304        assert_eq!(frame.stride(0), Some(640 * 2));
1305        assert_eq!(frame.stride(1), Some(640 * 2));
1306    }
1307
1308    #[test]
1309    fn test_rgb24() {
1310        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgb24).unwrap();
1311        assert_eq!(frame.num_planes(), 1);
1312        assert_eq!(frame.stride(0), Some(640 * 3));
1313        assert_eq!(frame.total_size(), 640 * 480 * 3);
1314    }
1315
1316    #[test]
1317    fn test_bgr24() {
1318        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgr24).unwrap();
1319        assert_eq!(frame.num_planes(), 1);
1320        assert_eq!(frame.stride(0), Some(640 * 3));
1321    }
1322
1323    #[test]
1324    fn test_bgra() {
1325        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgra).unwrap();
1326        assert_eq!(frame.num_planes(), 1);
1327        assert_eq!(frame.stride(0), Some(640 * 4));
1328    }
1329
1330    #[test]
1331    fn test_other_format_returns_error() {
1332        // Unknown formats cannot be allocated - memory layout is unknown
1333        let result = VideoFrame::empty(640, 480, PixelFormat::Other(999));
1334        assert!(result.is_err());
1335        assert_eq!(
1336            result.unwrap_err(),
1337            FrameError::UnsupportedPixelFormat(PixelFormat::Other(999))
1338        );
1339    }
1340}