Skip to main content

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