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            0.0
762        } else {
763            f64::from(self.width) / f64::from(self.height)
764        }
765    }
766}
767
768impl fmt::Debug for VideoFrame {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        f.debug_struct("VideoFrame")
771            .field("width", &self.width)
772            .field("height", &self.height)
773            .field("format", &self.format)
774            .field("timestamp", &self.timestamp)
775            .field("key_frame", &self.key_frame)
776            .field("num_planes", &self.planes.len())
777            .field(
778                "plane_sizes",
779                &self
780                    .planes
781                    .iter()
782                    .map(PooledBuffer::len)
783                    .collect::<Vec<_>>(),
784            )
785            .field("strides", &self.strides)
786            .finish()
787    }
788}
789
790impl fmt::Display for VideoFrame {
791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792        write!(
793            f,
794            "VideoFrame({}x{} {} @ {}{})",
795            self.width,
796            self.height,
797            self.format,
798            self.timestamp,
799            if self.key_frame { " [KEY]" } else { "" }
800        )
801    }
802}
803
804impl Default for VideoFrame {
805    /// Returns a default empty 1x1 YUV420P frame.
806    ///
807    /// This constructs a minimal valid frame directly.
808    fn default() -> Self {
809        // Construct a minimal 1x1 YUV420P frame directly
810        // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
811        Self {
812            planes: vec![
813                PooledBuffer::standalone(vec![0u8; 1]),
814                PooledBuffer::standalone(vec![0u8; 1]),
815                PooledBuffer::standalone(vec![0u8; 1]),
816            ],
817            strides: vec![1, 1, 1],
818            width: 1,
819            height: 1,
820            format: PixelFormat::Yuv420p,
821            timestamp: Timestamp::default(),
822            key_frame: false,
823        }
824    }
825}
826
827#[cfg(test)]
828#[allow(
829    clippy::unwrap_used,
830    clippy::redundant_closure_for_method_calls,
831    clippy::float_cmp
832)]
833mod tests {
834    use super::*;
835    use crate::Rational;
836
837    // ==========================================================================
838    // Construction Tests
839    // ==========================================================================
840
841    #[test]
842    fn test_new_rgba_frame() {
843        let width = 640u32;
844        let height = 480u32;
845        let stride = width as usize * 4;
846        let data = vec![0u8; stride * height as usize];
847        let ts = Timestamp::new(1000, Rational::new(1, 1000));
848
849        let frame = VideoFrame::new(
850            vec![PooledBuffer::standalone(data)],
851            vec![stride],
852            width,
853            height,
854            PixelFormat::Rgba,
855            ts,
856            true,
857        )
858        .unwrap();
859
860        assert_eq!(frame.width(), 640);
861        assert_eq!(frame.height(), 480);
862        assert_eq!(frame.format(), PixelFormat::Rgba);
863        assert_eq!(frame.timestamp(), ts);
864        assert!(frame.is_key_frame());
865        assert_eq!(frame.num_planes(), 1);
866        assert_eq!(frame.stride(0), Some(640 * 4));
867    }
868
869    #[test]
870    fn test_new_yuv420p_frame() {
871        let width = 640u32;
872        let height = 480u32;
873
874        let y_stride = width as usize;
875        let uv_stride = (width / 2) as usize;
876        let uv_height = (height / 2) as usize;
877
878        let y_data = vec![128u8; y_stride * height as usize];
879        let u_data = vec![128u8; uv_stride * uv_height];
880        let v_data = vec![128u8; uv_stride * uv_height];
881
882        let frame = VideoFrame::new(
883            vec![
884                PooledBuffer::standalone(y_data),
885                PooledBuffer::standalone(u_data),
886                PooledBuffer::standalone(v_data),
887            ],
888            vec![y_stride, uv_stride, uv_stride],
889            width,
890            height,
891            PixelFormat::Yuv420p,
892            Timestamp::default(),
893            false,
894        )
895        .unwrap();
896
897        assert_eq!(frame.width(), 640);
898        assert_eq!(frame.height(), 480);
899        assert_eq!(frame.format(), PixelFormat::Yuv420p);
900        assert!(!frame.is_key_frame());
901        assert_eq!(frame.num_planes(), 3);
902        assert_eq!(frame.stride(0), Some(640));
903        assert_eq!(frame.stride(1), Some(320));
904        assert_eq!(frame.stride(2), Some(320));
905    }
906
907    #[test]
908    fn test_new_mismatched_planes_strides() {
909        let result = VideoFrame::new(
910            vec![PooledBuffer::standalone(vec![0u8; 100])],
911            vec![10, 10], // Mismatched length
912            10,
913            10,
914            PixelFormat::Rgba,
915            Timestamp::default(),
916            false,
917        );
918
919        assert!(result.is_err());
920        assert_eq!(
921            result.unwrap_err(),
922            FrameError::MismatchedPlaneStride {
923                planes: 1,
924                strides: 2
925            }
926        );
927    }
928
929    #[test]
930    fn test_empty_rgba() {
931        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
932        assert_eq!(frame.width(), 1920);
933        assert_eq!(frame.height(), 1080);
934        assert_eq!(frame.format(), PixelFormat::Rgba);
935        assert_eq!(frame.num_planes(), 1);
936        assert_eq!(frame.stride(0), Some(1920 * 4));
937        assert_eq!(frame.plane(0).map(|p| p.len()), Some(1920 * 1080 * 4));
938    }
939
940    #[test]
941    fn test_empty_yuv420p() {
942        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
943        assert_eq!(frame.num_planes(), 3);
944        assert_eq!(frame.stride(0), Some(640));
945        assert_eq!(frame.stride(1), Some(320));
946        assert_eq!(frame.stride(2), Some(320));
947
948        // Y plane: 640 * 480
949        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
950        // U/V planes: 320 * 240
951        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240));
952        assert_eq!(frame.plane(2).map(|p| p.len()), Some(320 * 240));
953    }
954
955    #[test]
956    fn test_empty_yuv422p() {
957        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv422p).unwrap();
958        assert_eq!(frame.num_planes(), 3);
959        assert_eq!(frame.stride(0), Some(640));
960        assert_eq!(frame.stride(1), Some(320));
961
962        // Y: full resolution, U/V: half width, full height
963        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
964        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 480));
965    }
966
967    #[test]
968    fn test_empty_yuv444p() {
969        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv444p).unwrap();
970        assert_eq!(frame.num_planes(), 3);
971
972        // All planes: full resolution
973        let expected_size = 640 * 480;
974        assert_eq!(frame.plane(0).map(|p| p.len()), Some(expected_size));
975        assert_eq!(frame.plane(1).map(|p| p.len()), Some(expected_size));
976        assert_eq!(frame.plane(2).map(|p| p.len()), Some(expected_size));
977    }
978
979    #[test]
980    fn test_empty_nv12() {
981        let frame = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
982        assert_eq!(frame.num_planes(), 2);
983        assert_eq!(frame.stride(0), Some(640));
984        assert_eq!(frame.stride(1), Some(640)); // UV interleaved
985
986        // Y plane: full resolution
987        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
988        // UV plane: full width, half height
989        assert_eq!(frame.plane(1).map(|p| p.len()), Some(640 * 240));
990    }
991
992    #[test]
993    fn test_empty_gray8() {
994        let frame = VideoFrame::empty(640, 480, PixelFormat::Gray8).unwrap();
995        assert_eq!(frame.num_planes(), 1);
996        assert_eq!(frame.stride(0), Some(640));
997        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
998    }
999
1000    #[test]
1001    fn test_default() {
1002        let frame = VideoFrame::default();
1003        assert_eq!(frame.width(), 1);
1004        assert_eq!(frame.height(), 1);
1005        assert_eq!(frame.format(), PixelFormat::default());
1006        assert!(!frame.is_key_frame());
1007    }
1008
1009    // ==========================================================================
1010    // Metadata Tests
1011    // ==========================================================================
1012
1013    #[test]
1014    fn test_resolution() {
1015        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1016        assert_eq!(frame.resolution(), (1920, 1080));
1017    }
1018
1019    #[test]
1020    fn test_aspect_ratio() {
1021        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1022        let aspect = frame.aspect_ratio();
1023        assert!((aspect - 16.0 / 9.0).abs() < 0.001);
1024
1025        let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1026        let aspect_4_3 = frame_4_3.aspect_ratio();
1027        assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
1028    }
1029
1030    #[test]
1031    fn test_aspect_ratio_zero_height() {
1032        let frame = VideoFrame::new(
1033            vec![PooledBuffer::standalone(vec![])],
1034            vec![0],
1035            100,
1036            0,
1037            PixelFormat::Rgba,
1038            Timestamp::default(),
1039            false,
1040        )
1041        .unwrap();
1042        assert_eq!(frame.aspect_ratio(), 0.0);
1043    }
1044
1045    #[test]
1046    fn test_total_size_rgba() {
1047        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1048        assert_eq!(frame.total_size(), 1920 * 1080 * 4);
1049    }
1050
1051    #[test]
1052    fn test_total_size_yuv420p() {
1053        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1054        // Y: 640*480, U: 320*240, V: 320*240
1055        let expected = 640 * 480 + 320 * 240 * 2;
1056        assert_eq!(frame.total_size(), expected);
1057    }
1058
1059    #[test]
1060    fn test_set_key_frame() {
1061        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1062        assert!(!frame.is_key_frame());
1063
1064        frame.set_key_frame(true);
1065        assert!(frame.is_key_frame());
1066
1067        frame.set_key_frame(false);
1068        assert!(!frame.is_key_frame());
1069    }
1070
1071    #[test]
1072    fn test_set_timestamp() {
1073        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1074        let ts = Timestamp::new(90000, Rational::new(1, 90000));
1075
1076        frame.set_timestamp(ts);
1077        assert_eq!(frame.timestamp(), ts);
1078    }
1079
1080    // ==========================================================================
1081    // Plane Access Tests
1082    // ==========================================================================
1083
1084    #[test]
1085    fn test_plane_access() {
1086        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1087
1088        assert!(frame.plane(0).is_some());
1089        assert!(frame.plane(1).is_some());
1090        assert!(frame.plane(2).is_some());
1091        assert!(frame.plane(3).is_none());
1092    }
1093
1094    #[test]
1095    fn test_plane_mut_access() {
1096        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1097
1098        if let Some(data) = frame.plane_mut(0) {
1099            // Fill with red
1100            for chunk in data.chunks_exact_mut(4) {
1101                chunk[0] = 255;
1102                chunk[1] = 0;
1103                chunk[2] = 0;
1104                chunk[3] = 255;
1105            }
1106        }
1107
1108        let plane = frame.plane(0).unwrap();
1109        assert_eq!(plane[0], 255); // R
1110        assert_eq!(plane[1], 0); // G
1111        assert_eq!(plane[2], 0); // B
1112        assert_eq!(plane[3], 255); // A
1113    }
1114
1115    #[test]
1116    fn test_planes_slice() {
1117        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1118        let planes = frame.planes();
1119        assert_eq!(planes.len(), 3);
1120    }
1121
1122    #[test]
1123    fn test_strides_slice() {
1124        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1125        let strides = frame.strides();
1126        assert_eq!(strides.len(), 3);
1127        assert_eq!(strides[0], 640);
1128        assert_eq!(strides[1], 320);
1129        assert_eq!(strides[2], 320);
1130    }
1131
1132    #[test]
1133    fn test_stride_out_of_bounds() {
1134        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1135        assert!(frame.stride(0).is_some());
1136        assert!(frame.stride(1).is_none());
1137    }
1138
1139    // ==========================================================================
1140    // Data Access Tests
1141    // ==========================================================================
1142
1143    #[test]
1144    fn test_data_contiguous() {
1145        let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1146        let data = frame.data();
1147        assert_eq!(data.len(), 4 * 4 * 4);
1148    }
1149
1150    #[test]
1151    fn test_data_yuv420p_concatenation() {
1152        let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
1153        let data = frame.data();
1154        // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
1155        assert_eq!(data.len(), 24);
1156    }
1157
1158    #[test]
1159    fn test_data_ref_packed() {
1160        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1161        assert!(frame.data_ref().is_some());
1162        assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
1163    }
1164
1165    #[test]
1166    fn test_data_ref_planar() {
1167        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1168        assert!(frame.data_ref().is_none());
1169    }
1170
1171    #[test]
1172    fn test_data_mut_packed() {
1173        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1174        assert!(frame.data_mut().is_some());
1175
1176        if let Some(data) = frame.data_mut() {
1177            data[0] = 123;
1178        }
1179
1180        assert_eq!(frame.plane(0).unwrap()[0], 123);
1181    }
1182
1183    #[test]
1184    fn test_data_mut_planar() {
1185        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1186        assert!(frame.data_mut().is_none());
1187    }
1188
1189    // ==========================================================================
1190    // Clone Tests
1191    // ==========================================================================
1192
1193    #[test]
1194    fn test_clone() {
1195        let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1196        original.set_key_frame(true);
1197        original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1198
1199        // Modify some data
1200        if let Some(data) = original.plane_mut(0) {
1201            data[0] = 42;
1202        }
1203
1204        let cloned = original.clone();
1205
1206        // Verify metadata matches
1207        assert_eq!(cloned.width(), original.width());
1208        assert_eq!(cloned.height(), original.height());
1209        assert_eq!(cloned.format(), original.format());
1210        assert_eq!(cloned.timestamp(), original.timestamp());
1211        assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1212
1213        // Verify data was cloned
1214        assert_eq!(cloned.plane(0).unwrap()[0], 42);
1215
1216        // Verify it's a deep clone (modifying clone doesn't affect original)
1217        let mut cloned = cloned;
1218        if let Some(data) = cloned.plane_mut(0) {
1219            data[0] = 99;
1220        }
1221        assert_eq!(original.plane(0).unwrap()[0], 42);
1222        assert_eq!(cloned.plane(0).unwrap()[0], 99);
1223    }
1224
1225    // ==========================================================================
1226    // Display/Debug Tests
1227    // ==========================================================================
1228
1229    #[test]
1230    fn test_debug() {
1231        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1232        let debug = format!("{frame:?}");
1233        assert!(debug.contains("VideoFrame"));
1234        assert!(debug.contains("640"));
1235        assert!(debug.contains("480"));
1236        assert!(debug.contains("Rgba"));
1237    }
1238
1239    #[test]
1240    fn test_display() {
1241        let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1242        frame.set_key_frame(true);
1243
1244        let display = format!("{frame}");
1245        assert!(display.contains("1920x1080"));
1246        assert!(display.contains("yuv420p"));
1247        assert!(display.contains("[KEY]"));
1248    }
1249
1250    #[test]
1251    fn test_display_non_keyframe() {
1252        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1253        let display = format!("{frame}");
1254        assert!(!display.contains("[KEY]"));
1255    }
1256
1257    // ==========================================================================
1258    // Edge Case Tests
1259    // ==========================================================================
1260
1261    #[test]
1262    fn test_odd_dimensions_yuv420p() {
1263        // Odd dimensions require proper rounding for chroma planes
1264        let frame = VideoFrame::empty(641, 481, PixelFormat::Yuv420p).unwrap();
1265
1266        // Y plane should be full size
1267        assert_eq!(frame.plane(0).map(|p| p.len()), Some(641 * 481));
1268
1269        // U/V planes should be (641+1)/2 * (481+1)/2 = 321 * 241
1270        assert_eq!(frame.plane(1).map(|p| p.len()), Some(321 * 241));
1271        assert_eq!(frame.plane(2).map(|p| p.len()), Some(321 * 241));
1272    }
1273
1274    #[test]
1275    fn test_small_frame() {
1276        let frame = VideoFrame::empty(1, 1, PixelFormat::Rgba).unwrap();
1277        assert_eq!(frame.total_size(), 4);
1278        assert_eq!(frame.plane(0).map(|p| p.len()), Some(4));
1279    }
1280
1281    #[test]
1282    fn test_10bit_yuv420p() {
1283        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p10le).unwrap();
1284        assert_eq!(frame.num_planes(), 3);
1285
1286        // 10-bit uses 2 bytes per sample
1287        assert_eq!(frame.stride(0), Some(640 * 2));
1288        assert_eq!(frame.stride(1), Some(320 * 2));
1289        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480 * 2));
1290        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240 * 2));
1291    }
1292
1293    #[test]
1294    fn test_p010le() {
1295        let frame = VideoFrame::empty(640, 480, PixelFormat::P010le).unwrap();
1296        assert_eq!(frame.num_planes(), 2);
1297
1298        // 10-bit semi-planar: Y and UV interleaved, both 2 bytes per sample
1299        assert_eq!(frame.stride(0), Some(640 * 2));
1300        assert_eq!(frame.stride(1), Some(640 * 2));
1301    }
1302
1303    #[test]
1304    fn test_rgb24() {
1305        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgb24).unwrap();
1306        assert_eq!(frame.num_planes(), 1);
1307        assert_eq!(frame.stride(0), Some(640 * 3));
1308        assert_eq!(frame.total_size(), 640 * 480 * 3);
1309    }
1310
1311    #[test]
1312    fn test_bgr24() {
1313        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgr24).unwrap();
1314        assert_eq!(frame.num_planes(), 1);
1315        assert_eq!(frame.stride(0), Some(640 * 3));
1316    }
1317
1318    #[test]
1319    fn test_bgra() {
1320        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgra).unwrap();
1321        assert_eq!(frame.num_planes(), 1);
1322        assert_eq!(frame.stride(0), Some(640 * 4));
1323    }
1324
1325    #[test]
1326    fn test_other_format_returns_error() {
1327        // Unknown formats cannot be allocated - memory layout is unknown
1328        let result = VideoFrame::empty(640, 480, PixelFormat::Other(999));
1329        assert!(result.is_err());
1330        assert_eq!(
1331            result.unwrap_err(),
1332            FrameError::UnsupportedPixelFormat(PixelFormat::Other(999))
1333        );
1334    }
1335}