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 planar YUV 4:2:2 - 2 bytes per sample
320            PixelFormat::Yuv422p10le => {
321                let y_stride = width as usize * 2;
322                let uv_stride = (width as usize).div_ceil(2) * 2;
323
324                let y_data = vec![0u8; y_stride * height as usize];
325                let u_data = vec![0u8; uv_stride * height as usize];
326                let v_data = vec![0u8; uv_stride * height as usize];
327
328                Ok((
329                    vec![
330                        PooledBuffer::standalone(y_data),
331                        PooledBuffer::standalone(u_data),
332                        PooledBuffer::standalone(v_data),
333                    ],
334                    vec![y_stride, uv_stride, uv_stride],
335                ))
336            }
337
338            // 10-bit planar YUV 4:4:4 - 2 bytes per sample
339            PixelFormat::Yuv444p10le => {
340                let stride = width as usize * 2;
341
342                let y_data = vec![0u8; stride * height as usize];
343                let u_data = vec![0u8; stride * height as usize];
344                let v_data = vec![0u8; stride * height as usize];
345
346                Ok((
347                    vec![
348                        PooledBuffer::standalone(y_data),
349                        PooledBuffer::standalone(u_data),
350                        PooledBuffer::standalone(v_data),
351                    ],
352                    vec![stride, stride, stride],
353                ))
354            }
355
356            // 10-bit planar YUVA 4:4:4 with alpha - 2 bytes per sample
357            PixelFormat::Yuva444p10le => {
358                let stride = width as usize * 2;
359
360                let y_data = vec![0u8; stride * height as usize];
361                let u_data = vec![0u8; stride * height as usize];
362                let v_data = vec![0u8; stride * height as usize];
363                let a_data = vec![0u8; stride * height as usize];
364
365                Ok((
366                    vec![
367                        PooledBuffer::standalone(y_data),
368                        PooledBuffer::standalone(u_data),
369                        PooledBuffer::standalone(v_data),
370                        PooledBuffer::standalone(a_data),
371                    ],
372                    vec![stride, stride, stride, stride],
373                ))
374            }
375
376            // 10-bit semi-planar P010
377            PixelFormat::P010le => {
378                let y_stride = width as usize * 2;
379                let uv_stride = width as usize * 2;
380                let uv_height = (height as usize).div_ceil(2);
381
382                let y_data = vec![0u8; y_stride * height as usize];
383                let uv_data = vec![0u8; uv_stride * uv_height];
384
385                Ok((
386                    vec![
387                        PooledBuffer::standalone(y_data),
388                        PooledBuffer::standalone(uv_data),
389                    ],
390                    vec![y_stride, uv_stride],
391                ))
392            }
393
394            // Planar GBR float (gbrpf32le) - three planes, 4 bytes per sample (f32)
395            PixelFormat::Gbrpf32le => {
396                let stride = width as usize * 4; // 4 bytes per f32 sample
397                let size = stride * height as usize;
398                Ok((
399                    vec![
400                        PooledBuffer::standalone(vec![0u8; size]),
401                        PooledBuffer::standalone(vec![0u8; size]),
402                        PooledBuffer::standalone(vec![0u8; size]),
403                    ],
404                    vec![stride, stride, stride],
405                ))
406            }
407
408            // Unknown format - cannot determine memory layout
409            PixelFormat::Other(_) => Err(FrameError::UnsupportedPixelFormat(format)),
410        }
411    }
412
413    // ==========================================================================
414    // Metadata Accessors
415    // ==========================================================================
416
417    /// Returns the frame width in pixels.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use ff_format::{PixelFormat, VideoFrame};
423    ///
424    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
425    /// assert_eq!(frame.width(), 1920);
426    /// ```
427    #[must_use]
428    #[inline]
429    pub const fn width(&self) -> u32 {
430        self.width
431    }
432
433    /// Returns the frame height in pixels.
434    ///
435    /// # Examples
436    ///
437    /// ```
438    /// use ff_format::{PixelFormat, VideoFrame};
439    ///
440    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
441    /// assert_eq!(frame.height(), 1080);
442    /// ```
443    #[must_use]
444    #[inline]
445    pub const fn height(&self) -> u32 {
446        self.height
447    }
448
449    /// Returns the pixel format of this frame.
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use ff_format::{PixelFormat, VideoFrame};
455    ///
456    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
457    /// assert_eq!(frame.format(), PixelFormat::Yuv420p);
458    /// ```
459    #[must_use]
460    #[inline]
461    pub const fn format(&self) -> PixelFormat {
462        self.format
463    }
464
465    /// Returns the presentation timestamp of this frame.
466    ///
467    /// # Examples
468    ///
469    /// ```
470    /// use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
471    ///
472    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
473    /// let frame = VideoFrame::new(
474    ///     vec![PooledBuffer::standalone(vec![0u8; 1920 * 1080 * 4])],
475    ///     vec![1920 * 4],
476    ///     1920,
477    ///     1080,
478    ///     PixelFormat::Rgba,
479    ///     ts,
480    ///     true,
481    /// ).unwrap();
482    /// assert_eq!(frame.timestamp(), ts);
483    /// ```
484    #[must_use]
485    #[inline]
486    pub const fn timestamp(&self) -> Timestamp {
487        self.timestamp
488    }
489
490    /// Returns whether this frame is a key frame (I-frame).
491    ///
492    /// Key frames are complete frames that don't depend on any other frames
493    /// for decoding. They are used as reference points for seeking.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use ff_format::{PixelFormat, VideoFrame};
499    ///
500    /// let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
501    /// assert!(!frame.is_key_frame());
502    ///
503    /// frame.set_key_frame(true);
504    /// assert!(frame.is_key_frame());
505    /// ```
506    #[must_use]
507    #[inline]
508    pub const fn is_key_frame(&self) -> bool {
509        self.key_frame
510    }
511
512    /// Sets whether this frame is a key frame.
513    ///
514    /// # Examples
515    ///
516    /// ```
517    /// use ff_format::{PixelFormat, VideoFrame};
518    ///
519    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
520    /// frame.set_key_frame(true);
521    /// assert!(frame.is_key_frame());
522    /// ```
523    #[inline]
524    pub fn set_key_frame(&mut self, key_frame: bool) {
525        self.key_frame = key_frame;
526    }
527
528    /// Sets the timestamp of this frame.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// use ff_format::{PixelFormat, Rational, Timestamp, VideoFrame};
534    ///
535    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
536    /// let ts = Timestamp::new(3000, Rational::new(1, 90000));
537    /// frame.set_timestamp(ts);
538    /// assert_eq!(frame.timestamp(), ts);
539    /// ```
540    #[inline]
541    pub fn set_timestamp(&mut self, timestamp: Timestamp) {
542        self.timestamp = timestamp;
543    }
544
545    // ==========================================================================
546    // Plane Data Access
547    // ==========================================================================
548
549    /// Returns the number of planes in this frame.
550    ///
551    /// - Packed formats (RGBA, RGB24, etc.): 1 plane
552    /// - Planar YUV (YUV420P, YUV422P, YUV444P): 3 planes
553    /// - Semi-planar (NV12, NV21): 2 planes
554    ///
555    /// # Examples
556    ///
557    /// ```
558    /// use ff_format::{PixelFormat, VideoFrame};
559    ///
560    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
561    /// assert_eq!(rgba.num_planes(), 1);
562    ///
563    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
564    /// assert_eq!(yuv.num_planes(), 3);
565    ///
566    /// let nv12 = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
567    /// assert_eq!(nv12.num_planes(), 2);
568    /// ```
569    #[must_use]
570    #[inline]
571    pub fn num_planes(&self) -> usize {
572        self.planes.len()
573    }
574
575    /// Returns a slice of all plane buffers.
576    ///
577    /// # Examples
578    ///
579    /// ```
580    /// use ff_format::{PixelFormat, VideoFrame};
581    ///
582    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
583    /// let planes = frame.planes();
584    /// assert_eq!(planes.len(), 3);
585    /// ```
586    #[must_use]
587    #[inline]
588    pub fn planes(&self) -> &[PooledBuffer] {
589        &self.planes
590    }
591
592    /// Returns the data for a specific plane, or `None` if the index is out of bounds.
593    ///
594    /// # Arguments
595    ///
596    /// * `index` - The plane index (0 for Y/RGB, 1 for U/UV, 2 for V)
597    ///
598    /// # Examples
599    ///
600    /// ```
601    /// use ff_format::{PixelFormat, VideoFrame};
602    ///
603    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
604    ///
605    /// // Y plane exists
606    /// assert!(frame.plane(0).is_some());
607    ///
608    /// // U and V planes exist
609    /// assert!(frame.plane(1).is_some());
610    /// assert!(frame.plane(2).is_some());
611    ///
612    /// // No fourth plane
613    /// assert!(frame.plane(3).is_none());
614    /// ```
615    #[must_use]
616    #[inline]
617    pub fn plane(&self, index: usize) -> Option<&[u8]> {
618        self.planes.get(index).map(std::convert::AsRef::as_ref)
619    }
620
621    /// Returns mutable access to a specific plane's data.
622    ///
623    /// # Arguments
624    ///
625    /// * `index` - The plane index
626    ///
627    /// # Examples
628    ///
629    /// ```
630    /// use ff_format::{PixelFormat, VideoFrame};
631    ///
632    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
633    /// if let Some(data) = frame.plane_mut(0) {
634    ///     // Fill with red (RGBA)
635    ///     for chunk in data.chunks_exact_mut(4) {
636    ///         chunk[0] = 255; // R
637    ///         chunk[1] = 0;   // G
638    ///         chunk[2] = 0;   // B
639    ///         chunk[3] = 255; // A
640    ///     }
641    /// }
642    /// ```
643    #[must_use]
644    #[inline]
645    pub fn plane_mut(&mut self, index: usize) -> Option<&mut [u8]> {
646        self.planes.get_mut(index).map(std::convert::AsMut::as_mut)
647    }
648
649    /// Returns a slice of all stride values.
650    ///
651    /// Strides indicate the number of bytes between the start of consecutive
652    /// rows in each plane.
653    ///
654    /// # Examples
655    ///
656    /// ```
657    /// use ff_format::{PixelFormat, VideoFrame};
658    ///
659    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
660    /// let strides = frame.strides();
661    /// assert_eq!(strides[0], 1920 * 4); // RGBA = 4 bytes per pixel
662    /// ```
663    #[must_use]
664    #[inline]
665    pub fn strides(&self) -> &[usize] {
666        &self.strides
667    }
668
669    /// Returns the stride for a specific plane, or `None` if the index is out of bounds.
670    ///
671    /// # Arguments
672    ///
673    /// * `plane` - The plane index
674    ///
675    /// # Examples
676    ///
677    /// ```
678    /// use ff_format::{PixelFormat, VideoFrame};
679    ///
680    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
681    ///
682    /// // Y plane stride = width
683    /// assert_eq!(frame.stride(0), Some(640));
684    ///
685    /// // U/V plane stride = width / 2
686    /// assert_eq!(frame.stride(1), Some(320));
687    /// assert_eq!(frame.stride(2), Some(320));
688    /// ```
689    #[must_use]
690    #[inline]
691    pub fn stride(&self, plane: usize) -> Option<usize> {
692        self.strides.get(plane).copied()
693    }
694
695    // ==========================================================================
696    // Contiguous Data Access
697    // ==========================================================================
698
699    /// Returns the frame data as a contiguous byte vector.
700    ///
701    /// For packed formats with a single plane, this returns a copy of the plane data.
702    /// For planar formats, this concatenates all planes into a single buffer.
703    ///
704    /// # Note
705    ///
706    /// This method allocates a new vector and copies the data. For zero-copy
707    /// access, use [`plane()`](Self::plane) or [`planes()`](Self::planes) instead.
708    ///
709    /// # Examples
710    ///
711    /// ```
712    /// use ff_format::{PixelFormat, VideoFrame};
713    ///
714    /// let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
715    /// let data = frame.data();
716    /// assert_eq!(data.len(), 4 * 4 * 4); // 4x4 pixels, 4 bytes each
717    /// ```
718    #[must_use]
719    pub fn data(&self) -> Vec<u8> {
720        let total_size: usize = self.planes.iter().map(PooledBuffer::len).sum();
721        let mut result = Vec::with_capacity(total_size);
722        for plane in &self.planes {
723            result.extend_from_slice(plane.as_ref());
724        }
725        result
726    }
727
728    /// Returns a reference to the first plane's data as a contiguous slice.
729    ///
730    /// This is only meaningful for packed formats (RGBA, RGB24, etc.) where
731    /// all data is in a single plane. Returns `None` if the format is planar
732    /// or if no planes exist.
733    ///
734    /// # Examples
735    ///
736    /// ```
737    /// use ff_format::{PixelFormat, VideoFrame};
738    ///
739    /// // Packed format - returns data
740    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
741    /// assert!(rgba.data_ref().is_some());
742    ///
743    /// // Planar format - returns None
744    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
745    /// assert!(yuv.data_ref().is_none());
746    /// ```
747    #[must_use]
748    #[inline]
749    pub fn data_ref(&self) -> Option<&[u8]> {
750        if self.format.is_packed() && self.planes.len() == 1 {
751            Some(self.planes[0].as_ref())
752        } else {
753            None
754        }
755    }
756
757    /// Returns a mutable reference to the first plane's data.
758    ///
759    /// This is only meaningful for packed formats where all data is in a
760    /// single plane. Returns `None` if the format is planar.
761    ///
762    /// # Examples
763    ///
764    /// ```
765    /// use ff_format::{PixelFormat, VideoFrame};
766    ///
767    /// let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
768    /// if let Some(data) = frame.data_mut() {
769    ///     data[0] = 255; // Modify first byte
770    /// }
771    /// ```
772    #[must_use]
773    #[inline]
774    pub fn data_mut(&mut self) -> Option<&mut [u8]> {
775        if self.format.is_packed() && self.planes.len() == 1 {
776            Some(self.planes[0].as_mut())
777        } else {
778            None
779        }
780    }
781
782    // ==========================================================================
783    // Utility Methods
784    // ==========================================================================
785
786    /// Returns the total size in bytes of all plane data.
787    ///
788    /// # Examples
789    ///
790    /// ```
791    /// use ff_format::{PixelFormat, VideoFrame};
792    ///
793    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
794    /// assert_eq!(frame.total_size(), 1920 * 1080 * 4);
795    /// ```
796    #[must_use]
797    pub fn total_size(&self) -> usize {
798        self.planes.iter().map(PooledBuffer::len).sum()
799    }
800
801    /// Returns the resolution as a (width, height) tuple.
802    ///
803    /// # Examples
804    ///
805    /// ```
806    /// use ff_format::{PixelFormat, VideoFrame};
807    ///
808    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
809    /// assert_eq!(frame.resolution(), (1920, 1080));
810    /// ```
811    #[must_use]
812    #[inline]
813    pub const fn resolution(&self) -> (u32, u32) {
814        (self.width, self.height)
815    }
816
817    /// Returns the aspect ratio as a floating-point value.
818    ///
819    /// # Examples
820    ///
821    /// ```
822    /// use ff_format::{PixelFormat, VideoFrame};
823    ///
824    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
825    /// let aspect = frame.aspect_ratio();
826    /// assert!((aspect - 16.0 / 9.0).abs() < 0.01);
827    /// ```
828    #[must_use]
829    #[inline]
830    pub fn aspect_ratio(&self) -> f64 {
831        if self.height == 0 {
832            log::warn!(
833                "aspect_ratio unavailable, height is 0, returning 0.0 \
834                 width={} height=0 fallback=0.0",
835                self.width
836            );
837            0.0
838        } else {
839            f64::from(self.width) / f64::from(self.height)
840        }
841    }
842}
843
844impl fmt::Debug for VideoFrame {
845    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
846        f.debug_struct("VideoFrame")
847            .field("width", &self.width)
848            .field("height", &self.height)
849            .field("format", &self.format)
850            .field("timestamp", &self.timestamp)
851            .field("key_frame", &self.key_frame)
852            .field("num_planes", &self.planes.len())
853            .field(
854                "plane_sizes",
855                &self
856                    .planes
857                    .iter()
858                    .map(PooledBuffer::len)
859                    .collect::<Vec<_>>(),
860            )
861            .field("strides", &self.strides)
862            .finish()
863    }
864}
865
866impl fmt::Display for VideoFrame {
867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868        write!(
869            f,
870            "VideoFrame({}x{} {} @ {}{})",
871            self.width,
872            self.height,
873            self.format,
874            self.timestamp,
875            if self.key_frame { " [KEY]" } else { "" }
876        )
877    }
878}
879
880impl Default for VideoFrame {
881    /// Returns a default empty 1x1 YUV420P frame.
882    ///
883    /// This constructs a minimal valid frame directly.
884    fn default() -> Self {
885        // Construct a minimal 1x1 YUV420P frame directly
886        // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
887        Self {
888            planes: vec![
889                PooledBuffer::standalone(vec![0u8; 1]),
890                PooledBuffer::standalone(vec![0u8; 1]),
891                PooledBuffer::standalone(vec![0u8; 1]),
892            ],
893            strides: vec![1, 1, 1],
894            width: 1,
895            height: 1,
896            format: PixelFormat::Yuv420p,
897            timestamp: Timestamp::default(),
898            key_frame: false,
899        }
900    }
901}
902
903#[cfg(test)]
904#[allow(
905    clippy::unwrap_used,
906    clippy::redundant_closure_for_method_calls,
907    clippy::float_cmp
908)]
909mod tests {
910    use super::*;
911    use crate::Rational;
912
913    // ==========================================================================
914    // Construction Tests
915    // ==========================================================================
916
917    #[test]
918    fn test_new_rgba_frame() {
919        let width = 640u32;
920        let height = 480u32;
921        let stride = width as usize * 4;
922        let data = vec![0u8; stride * height as usize];
923        let ts = Timestamp::new(1000, Rational::new(1, 1000));
924
925        let frame = VideoFrame::new(
926            vec![PooledBuffer::standalone(data)],
927            vec![stride],
928            width,
929            height,
930            PixelFormat::Rgba,
931            ts,
932            true,
933        )
934        .unwrap();
935
936        assert_eq!(frame.width(), 640);
937        assert_eq!(frame.height(), 480);
938        assert_eq!(frame.format(), PixelFormat::Rgba);
939        assert_eq!(frame.timestamp(), ts);
940        assert!(frame.is_key_frame());
941        assert_eq!(frame.num_planes(), 1);
942        assert_eq!(frame.stride(0), Some(640 * 4));
943    }
944
945    #[test]
946    fn test_new_yuv420p_frame() {
947        let width = 640u32;
948        let height = 480u32;
949
950        let y_stride = width as usize;
951        let uv_stride = (width / 2) as usize;
952        let uv_height = (height / 2) as usize;
953
954        let y_data = vec![128u8; y_stride * height as usize];
955        let u_data = vec![128u8; uv_stride * uv_height];
956        let v_data = vec![128u8; uv_stride * uv_height];
957
958        let frame = VideoFrame::new(
959            vec![
960                PooledBuffer::standalone(y_data),
961                PooledBuffer::standalone(u_data),
962                PooledBuffer::standalone(v_data),
963            ],
964            vec![y_stride, uv_stride, uv_stride],
965            width,
966            height,
967            PixelFormat::Yuv420p,
968            Timestamp::default(),
969            false,
970        )
971        .unwrap();
972
973        assert_eq!(frame.width(), 640);
974        assert_eq!(frame.height(), 480);
975        assert_eq!(frame.format(), PixelFormat::Yuv420p);
976        assert!(!frame.is_key_frame());
977        assert_eq!(frame.num_planes(), 3);
978        assert_eq!(frame.stride(0), Some(640));
979        assert_eq!(frame.stride(1), Some(320));
980        assert_eq!(frame.stride(2), Some(320));
981    }
982
983    #[test]
984    fn test_new_mismatched_planes_strides() {
985        let result = VideoFrame::new(
986            vec![PooledBuffer::standalone(vec![0u8; 100])],
987            vec![10, 10], // Mismatched length
988            10,
989            10,
990            PixelFormat::Rgba,
991            Timestamp::default(),
992            false,
993        );
994
995        assert!(result.is_err());
996        assert_eq!(
997            result.unwrap_err(),
998            FrameError::MismatchedPlaneStride {
999                planes: 1,
1000                strides: 2
1001            }
1002        );
1003    }
1004
1005    #[test]
1006    fn test_empty_rgba() {
1007        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1008        assert_eq!(frame.width(), 1920);
1009        assert_eq!(frame.height(), 1080);
1010        assert_eq!(frame.format(), PixelFormat::Rgba);
1011        assert_eq!(frame.num_planes(), 1);
1012        assert_eq!(frame.stride(0), Some(1920 * 4));
1013        assert_eq!(frame.plane(0).map(|p| p.len()), Some(1920 * 1080 * 4));
1014    }
1015
1016    #[test]
1017    fn test_empty_yuv420p() {
1018        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1019        assert_eq!(frame.num_planes(), 3);
1020        assert_eq!(frame.stride(0), Some(640));
1021        assert_eq!(frame.stride(1), Some(320));
1022        assert_eq!(frame.stride(2), Some(320));
1023
1024        // Y plane: 640 * 480
1025        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1026        // U/V planes: 320 * 240
1027        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240));
1028        assert_eq!(frame.plane(2).map(|p| p.len()), Some(320 * 240));
1029    }
1030
1031    #[test]
1032    fn test_empty_yuv422p() {
1033        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv422p).unwrap();
1034        assert_eq!(frame.num_planes(), 3);
1035        assert_eq!(frame.stride(0), Some(640));
1036        assert_eq!(frame.stride(1), Some(320));
1037
1038        // Y: full resolution, U/V: half width, full height
1039        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1040        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 480));
1041    }
1042
1043    #[test]
1044    fn test_empty_yuv444p() {
1045        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv444p).unwrap();
1046        assert_eq!(frame.num_planes(), 3);
1047
1048        // All planes: full resolution
1049        let expected_size = 640 * 480;
1050        assert_eq!(frame.plane(0).map(|p| p.len()), Some(expected_size));
1051        assert_eq!(frame.plane(1).map(|p| p.len()), Some(expected_size));
1052        assert_eq!(frame.plane(2).map(|p| p.len()), Some(expected_size));
1053    }
1054
1055    #[test]
1056    fn test_empty_nv12() {
1057        let frame = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
1058        assert_eq!(frame.num_planes(), 2);
1059        assert_eq!(frame.stride(0), Some(640));
1060        assert_eq!(frame.stride(1), Some(640)); // UV interleaved
1061
1062        // Y plane: full resolution
1063        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1064        // UV plane: full width, half height
1065        assert_eq!(frame.plane(1).map(|p| p.len()), Some(640 * 240));
1066    }
1067
1068    #[test]
1069    fn test_empty_gray8() {
1070        let frame = VideoFrame::empty(640, 480, PixelFormat::Gray8).unwrap();
1071        assert_eq!(frame.num_planes(), 1);
1072        assert_eq!(frame.stride(0), Some(640));
1073        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1074    }
1075
1076    #[test]
1077    fn test_default() {
1078        let frame = VideoFrame::default();
1079        assert_eq!(frame.width(), 1);
1080        assert_eq!(frame.height(), 1);
1081        assert_eq!(frame.format(), PixelFormat::default());
1082        assert!(!frame.is_key_frame());
1083    }
1084
1085    // ==========================================================================
1086    // Metadata Tests
1087    // ==========================================================================
1088
1089    #[test]
1090    fn test_resolution() {
1091        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1092        assert_eq!(frame.resolution(), (1920, 1080));
1093    }
1094
1095    #[test]
1096    fn test_aspect_ratio() {
1097        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1098        let aspect = frame.aspect_ratio();
1099        assert!((aspect - 16.0 / 9.0).abs() < 0.001);
1100
1101        let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1102        let aspect_4_3 = frame_4_3.aspect_ratio();
1103        assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
1104    }
1105
1106    #[test]
1107    fn test_aspect_ratio_zero_height() {
1108        let frame = VideoFrame::new(
1109            vec![PooledBuffer::standalone(vec![])],
1110            vec![0],
1111            100,
1112            0,
1113            PixelFormat::Rgba,
1114            Timestamp::default(),
1115            false,
1116        )
1117        .unwrap();
1118        assert_eq!(frame.aspect_ratio(), 0.0);
1119    }
1120
1121    #[test]
1122    fn test_total_size_rgba() {
1123        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1124        assert_eq!(frame.total_size(), 1920 * 1080 * 4);
1125    }
1126
1127    #[test]
1128    fn test_total_size_yuv420p() {
1129        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1130        // Y: 640*480, U: 320*240, V: 320*240
1131        let expected = 640 * 480 + 320 * 240 * 2;
1132        assert_eq!(frame.total_size(), expected);
1133    }
1134
1135    #[test]
1136    fn test_set_key_frame() {
1137        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1138        assert!(!frame.is_key_frame());
1139
1140        frame.set_key_frame(true);
1141        assert!(frame.is_key_frame());
1142
1143        frame.set_key_frame(false);
1144        assert!(!frame.is_key_frame());
1145    }
1146
1147    #[test]
1148    fn test_set_timestamp() {
1149        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1150        let ts = Timestamp::new(90000, Rational::new(1, 90000));
1151
1152        frame.set_timestamp(ts);
1153        assert_eq!(frame.timestamp(), ts);
1154    }
1155
1156    // ==========================================================================
1157    // Plane Access Tests
1158    // ==========================================================================
1159
1160    #[test]
1161    fn test_plane_access() {
1162        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1163
1164        assert!(frame.plane(0).is_some());
1165        assert!(frame.plane(1).is_some());
1166        assert!(frame.plane(2).is_some());
1167        assert!(frame.plane(3).is_none());
1168    }
1169
1170    #[test]
1171    fn test_plane_mut_access() {
1172        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1173
1174        if let Some(data) = frame.plane_mut(0) {
1175            // Fill with red
1176            for chunk in data.chunks_exact_mut(4) {
1177                chunk[0] = 255;
1178                chunk[1] = 0;
1179                chunk[2] = 0;
1180                chunk[3] = 255;
1181            }
1182        }
1183
1184        let plane = frame.plane(0).unwrap();
1185        assert_eq!(plane[0], 255); // R
1186        assert_eq!(plane[1], 0); // G
1187        assert_eq!(plane[2], 0); // B
1188        assert_eq!(plane[3], 255); // A
1189    }
1190
1191    #[test]
1192    fn test_planes_slice() {
1193        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1194        let planes = frame.planes();
1195        assert_eq!(planes.len(), 3);
1196    }
1197
1198    #[test]
1199    fn test_strides_slice() {
1200        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1201        let strides = frame.strides();
1202        assert_eq!(strides.len(), 3);
1203        assert_eq!(strides[0], 640);
1204        assert_eq!(strides[1], 320);
1205        assert_eq!(strides[2], 320);
1206    }
1207
1208    #[test]
1209    fn test_stride_out_of_bounds() {
1210        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1211        assert!(frame.stride(0).is_some());
1212        assert!(frame.stride(1).is_none());
1213    }
1214
1215    // ==========================================================================
1216    // Data Access Tests
1217    // ==========================================================================
1218
1219    #[test]
1220    fn test_data_contiguous() {
1221        let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1222        let data = frame.data();
1223        assert_eq!(data.len(), 4 * 4 * 4);
1224    }
1225
1226    #[test]
1227    fn test_data_yuv420p_concatenation() {
1228        let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
1229        let data = frame.data();
1230        // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
1231        assert_eq!(data.len(), 24);
1232    }
1233
1234    #[test]
1235    fn test_data_ref_packed() {
1236        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1237        assert!(frame.data_ref().is_some());
1238        assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
1239    }
1240
1241    #[test]
1242    fn test_data_ref_planar() {
1243        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1244        assert!(frame.data_ref().is_none());
1245    }
1246
1247    #[test]
1248    fn test_data_mut_packed() {
1249        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1250        assert!(frame.data_mut().is_some());
1251
1252        if let Some(data) = frame.data_mut() {
1253            data[0] = 123;
1254        }
1255
1256        assert_eq!(frame.plane(0).unwrap()[0], 123);
1257    }
1258
1259    #[test]
1260    fn test_data_mut_planar() {
1261        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1262        assert!(frame.data_mut().is_none());
1263    }
1264
1265    // ==========================================================================
1266    // Clone Tests
1267    // ==========================================================================
1268
1269    #[test]
1270    fn test_clone() {
1271        let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1272        original.set_key_frame(true);
1273        original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1274
1275        // Modify some data
1276        if let Some(data) = original.plane_mut(0) {
1277            data[0] = 42;
1278        }
1279
1280        let cloned = original.clone();
1281
1282        // Verify metadata matches
1283        assert_eq!(cloned.width(), original.width());
1284        assert_eq!(cloned.height(), original.height());
1285        assert_eq!(cloned.format(), original.format());
1286        assert_eq!(cloned.timestamp(), original.timestamp());
1287        assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1288
1289        // Verify data was cloned
1290        assert_eq!(cloned.plane(0).unwrap()[0], 42);
1291
1292        // Verify it's a deep clone (modifying clone doesn't affect original)
1293        let mut cloned = cloned;
1294        if let Some(data) = cloned.plane_mut(0) {
1295            data[0] = 99;
1296        }
1297        assert_eq!(original.plane(0).unwrap()[0], 42);
1298        assert_eq!(cloned.plane(0).unwrap()[0], 99);
1299    }
1300
1301    // ==========================================================================
1302    // Display/Debug Tests
1303    // ==========================================================================
1304
1305    #[test]
1306    fn test_debug() {
1307        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1308        let debug = format!("{frame:?}");
1309        assert!(debug.contains("VideoFrame"));
1310        assert!(debug.contains("640"));
1311        assert!(debug.contains("480"));
1312        assert!(debug.contains("Rgba"));
1313    }
1314
1315    #[test]
1316    fn test_display() {
1317        let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1318        frame.set_key_frame(true);
1319
1320        let display = format!("{frame}");
1321        assert!(display.contains("1920x1080"));
1322        assert!(display.contains("yuv420p"));
1323        assert!(display.contains("[KEY]"));
1324    }
1325
1326    #[test]
1327    fn test_display_non_keyframe() {
1328        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1329        let display = format!("{frame}");
1330        assert!(!display.contains("[KEY]"));
1331    }
1332
1333    // ==========================================================================
1334    // Edge Case Tests
1335    // ==========================================================================
1336
1337    #[test]
1338    fn test_odd_dimensions_yuv420p() {
1339        // Odd dimensions require proper rounding for chroma planes
1340        let frame = VideoFrame::empty(641, 481, PixelFormat::Yuv420p).unwrap();
1341
1342        // Y plane should be full size
1343        assert_eq!(frame.plane(0).map(|p| p.len()), Some(641 * 481));
1344
1345        // U/V planes should be (641+1)/2 * (481+1)/2 = 321 * 241
1346        assert_eq!(frame.plane(1).map(|p| p.len()), Some(321 * 241));
1347        assert_eq!(frame.plane(2).map(|p| p.len()), Some(321 * 241));
1348    }
1349
1350    #[test]
1351    fn test_small_frame() {
1352        let frame = VideoFrame::empty(1, 1, PixelFormat::Rgba).unwrap();
1353        assert_eq!(frame.total_size(), 4);
1354        assert_eq!(frame.plane(0).map(|p| p.len()), Some(4));
1355    }
1356
1357    #[test]
1358    fn test_10bit_yuv420p() {
1359        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p10le).unwrap();
1360        assert_eq!(frame.num_planes(), 3);
1361
1362        // 10-bit uses 2 bytes per sample
1363        assert_eq!(frame.stride(0), Some(640 * 2));
1364        assert_eq!(frame.stride(1), Some(320 * 2));
1365        assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480 * 2));
1366        assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240 * 2));
1367    }
1368
1369    #[test]
1370    fn test_p010le() {
1371        let frame = VideoFrame::empty(640, 480, PixelFormat::P010le).unwrap();
1372        assert_eq!(frame.num_planes(), 2);
1373
1374        // 10-bit semi-planar: Y and UV interleaved, both 2 bytes per sample
1375        assert_eq!(frame.stride(0), Some(640 * 2));
1376        assert_eq!(frame.stride(1), Some(640 * 2));
1377    }
1378
1379    #[test]
1380    fn test_rgb24() {
1381        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgb24).unwrap();
1382        assert_eq!(frame.num_planes(), 1);
1383        assert_eq!(frame.stride(0), Some(640 * 3));
1384        assert_eq!(frame.total_size(), 640 * 480 * 3);
1385    }
1386
1387    #[test]
1388    fn test_bgr24() {
1389        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgr24).unwrap();
1390        assert_eq!(frame.num_planes(), 1);
1391        assert_eq!(frame.stride(0), Some(640 * 3));
1392    }
1393
1394    #[test]
1395    fn test_bgra() {
1396        let frame = VideoFrame::empty(640, 480, PixelFormat::Bgra).unwrap();
1397        assert_eq!(frame.num_planes(), 1);
1398        assert_eq!(frame.stride(0), Some(640 * 4));
1399    }
1400
1401    #[test]
1402    fn test_other_format_returns_error() {
1403        // Unknown formats cannot be allocated - memory layout is unknown
1404        let result = VideoFrame::empty(640, 480, PixelFormat::Other(999));
1405        assert!(result.is_err());
1406        assert_eq!(
1407            result.unwrap_err(),
1408            FrameError::UnsupportedPixelFormat(PixelFormat::Other(999))
1409        );
1410    }
1411}