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