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 black YUV420P video frame.
204    ///
205    /// The Y plane is filled with `0x00`; U and V planes are filled with `0x80`
206    /// (neutral chroma). `pts_ms` is the presentation timestamp in milliseconds.
207    ///
208    /// The `format` parameter is accepted for call-site clarity; always pass
209    /// [`PixelFormat::Yuv420p`].
210    #[doc(hidden)]
211    #[must_use]
212    pub fn new_black(width: u32, height: u32, format: PixelFormat, pts_ms: i64) -> Self {
213        let y_w = width as usize;
214        let y_h = height as usize;
215        let uv_w = (width as usize).div_ceil(2);
216        let uv_h = (height as usize).div_ceil(2);
217        let timestamp = Timestamp::from_millis(pts_ms, crate::Rational::new(1, 1000));
218        Self {
219            planes: vec![
220                PooledBuffer::standalone(vec![0u8; y_w * y_h]),
221                PooledBuffer::standalone(vec![0x80u8; uv_w * uv_h]),
222                PooledBuffer::standalone(vec![0x80u8; uv_w * uv_h]),
223            ],
224            strides: vec![y_w, uv_w, uv_w],
225            width,
226            height,
227            format,
228            timestamp,
229            key_frame: true,
230        }
231    }
232
233    // ==========================================================================
234    // Metadata Accessors
235    // ==========================================================================
236
237    /// Returns the frame width in pixels.
238    ///
239    /// # Examples
240    ///
241    /// ```
242    /// use ff_format::{PixelFormat, VideoFrame};
243    ///
244    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
245    /// assert_eq!(frame.width(), 1920);
246    /// ```
247    #[must_use]
248    #[inline]
249    pub const fn width(&self) -> u32 {
250        self.width
251    }
252
253    /// Returns the frame height in pixels.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use ff_format::{PixelFormat, VideoFrame};
259    ///
260    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
261    /// assert_eq!(frame.height(), 1080);
262    /// ```
263    #[must_use]
264    #[inline]
265    pub const fn height(&self) -> u32 {
266        self.height
267    }
268
269    /// Returns the pixel format of this frame.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use ff_format::{PixelFormat, VideoFrame};
275    ///
276    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
277    /// assert_eq!(frame.format(), PixelFormat::Yuv420p);
278    /// ```
279    #[must_use]
280    #[inline]
281    pub const fn format(&self) -> PixelFormat {
282        self.format
283    }
284
285    /// Returns the presentation timestamp of this frame.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
291    ///
292    /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
293    /// let frame = VideoFrame::new(
294    ///     vec![PooledBuffer::standalone(vec![0u8; 1920 * 1080 * 4])],
295    ///     vec![1920 * 4],
296    ///     1920,
297    ///     1080,
298    ///     PixelFormat::Rgba,
299    ///     ts,
300    ///     true,
301    /// ).unwrap();
302    /// assert_eq!(frame.timestamp(), ts);
303    /// ```
304    #[must_use]
305    #[inline]
306    pub const fn timestamp(&self) -> Timestamp {
307        self.timestamp
308    }
309
310    /// Returns whether this frame is a key frame (I-frame).
311    ///
312    /// Key frames are complete frames that don't depend on any other frames
313    /// for decoding. They are used as reference points for seeking.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use ff_format::{PixelFormat, VideoFrame};
319    ///
320    /// let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
321    /// assert!(!frame.is_key_frame());
322    ///
323    /// frame.set_key_frame(true);
324    /// assert!(frame.is_key_frame());
325    /// ```
326    #[must_use]
327    #[inline]
328    pub const fn is_key_frame(&self) -> bool {
329        self.key_frame
330    }
331
332    /// Sets whether this frame is a key frame.
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use ff_format::{PixelFormat, VideoFrame};
338    ///
339    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
340    /// frame.set_key_frame(true);
341    /// assert!(frame.is_key_frame());
342    /// ```
343    #[inline]
344    pub fn set_key_frame(&mut self, key_frame: bool) {
345        self.key_frame = key_frame;
346    }
347
348    /// Sets the timestamp of this frame.
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// use ff_format::{PixelFormat, Rational, Timestamp, VideoFrame};
354    ///
355    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
356    /// let ts = Timestamp::new(3000, Rational::new(1, 90000));
357    /// frame.set_timestamp(ts);
358    /// assert_eq!(frame.timestamp(), ts);
359    /// ```
360    #[inline]
361    pub fn set_timestamp(&mut self, timestamp: Timestamp) {
362        self.timestamp = timestamp;
363    }
364
365    // ==========================================================================
366    // Plane Data Access
367    // ==========================================================================
368
369    /// Returns the number of planes in this frame.
370    ///
371    /// - Packed formats (RGBA, RGB24, etc.): 1 plane
372    /// - Planar YUV (YUV420P, YUV422P, YUV444P): 3 planes
373    /// - Semi-planar (NV12, NV21): 2 planes
374    ///
375    /// # Examples
376    ///
377    /// ```
378    /// use ff_format::{PixelFormat, VideoFrame};
379    ///
380    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
381    /// assert_eq!(rgba.num_planes(), 1);
382    ///
383    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
384    /// assert_eq!(yuv.num_planes(), 3);
385    ///
386    /// let nv12 = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
387    /// assert_eq!(nv12.num_planes(), 2);
388    /// ```
389    #[must_use]
390    #[inline]
391    pub fn num_planes(&self) -> usize {
392        self.planes.len()
393    }
394
395    /// Returns a slice of all plane buffers.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use ff_format::{PixelFormat, VideoFrame};
401    ///
402    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
403    /// let planes = frame.planes();
404    /// assert_eq!(planes.len(), 3);
405    /// ```
406    #[must_use]
407    #[inline]
408    pub fn planes(&self) -> &[PooledBuffer] {
409        &self.planes
410    }
411
412    /// Returns the data for a specific plane, or `None` if the index is out of bounds.
413    ///
414    /// # Arguments
415    ///
416    /// * `index` - The plane index (0 for Y/RGB, 1 for U/UV, 2 for V)
417    ///
418    /// # Examples
419    ///
420    /// ```
421    /// use ff_format::{PixelFormat, VideoFrame};
422    ///
423    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
424    ///
425    /// // Y plane exists
426    /// assert!(frame.plane(0).is_some());
427    ///
428    /// // U and V planes exist
429    /// assert!(frame.plane(1).is_some());
430    /// assert!(frame.plane(2).is_some());
431    ///
432    /// // No fourth plane
433    /// assert!(frame.plane(3).is_none());
434    /// ```
435    #[must_use]
436    #[inline]
437    pub fn plane(&self, index: usize) -> Option<&[u8]> {
438        self.planes.get(index).map(std::convert::AsRef::as_ref)
439    }
440
441    /// Returns mutable access to a specific plane's data.
442    ///
443    /// # Arguments
444    ///
445    /// * `index` - The plane index
446    ///
447    /// # Examples
448    ///
449    /// ```
450    /// use ff_format::{PixelFormat, VideoFrame};
451    ///
452    /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
453    /// if let Some(data) = frame.plane_mut(0) {
454    ///     // Fill with red (RGBA)
455    ///     for chunk in data.chunks_exact_mut(4) {
456    ///         chunk[0] = 255; // R
457    ///         chunk[1] = 0;   // G
458    ///         chunk[2] = 0;   // B
459    ///         chunk[3] = 255; // A
460    ///     }
461    /// }
462    /// ```
463    #[must_use]
464    #[inline]
465    pub fn plane_mut(&mut self, index: usize) -> Option<&mut [u8]> {
466        self.planes.get_mut(index).map(std::convert::AsMut::as_mut)
467    }
468
469    /// Returns a slice of all stride values.
470    ///
471    /// Strides indicate the number of bytes between the start of consecutive
472    /// rows in each plane.
473    ///
474    /// # Examples
475    ///
476    /// ```
477    /// use ff_format::{PixelFormat, VideoFrame};
478    ///
479    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
480    /// let strides = frame.strides();
481    /// assert_eq!(strides[0], 1920 * 4); // RGBA = 4 bytes per pixel
482    /// ```
483    #[must_use]
484    #[inline]
485    pub fn strides(&self) -> &[usize] {
486        &self.strides
487    }
488
489    /// Returns the stride for a specific plane, or `None` if the index is out of bounds.
490    ///
491    /// # Arguments
492    ///
493    /// * `plane` - The plane index
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use ff_format::{PixelFormat, VideoFrame};
499    ///
500    /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
501    ///
502    /// // Y plane stride = width
503    /// assert_eq!(frame.stride(0), Some(640));
504    ///
505    /// // U/V plane stride = width / 2
506    /// assert_eq!(frame.stride(1), Some(320));
507    /// assert_eq!(frame.stride(2), Some(320));
508    /// ```
509    #[must_use]
510    #[inline]
511    pub fn stride(&self, plane: usize) -> Option<usize> {
512        self.strides.get(plane).copied()
513    }
514
515    // ==========================================================================
516    // Contiguous Data Access
517    // ==========================================================================
518
519    /// Returns the frame data as a contiguous byte vector.
520    ///
521    /// For packed formats with a single plane, this returns a copy of the plane data.
522    /// For planar formats, this concatenates all planes into a single buffer.
523    ///
524    /// # Note
525    ///
526    /// This method allocates a new vector and copies the data. For zero-copy
527    /// access, use [`plane()`](Self::plane) or [`planes()`](Self::planes) instead.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use ff_format::{PixelFormat, VideoFrame};
533    ///
534    /// let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
535    /// let data = frame.data();
536    /// assert_eq!(data.len(), 4 * 4 * 4); // 4x4 pixels, 4 bytes each
537    /// ```
538    #[must_use]
539    pub fn data(&self) -> Vec<u8> {
540        let total_size: usize = self.planes.iter().map(PooledBuffer::len).sum();
541        let mut result = Vec::with_capacity(total_size);
542        for plane in &self.planes {
543            result.extend_from_slice(plane.as_ref());
544        }
545        result
546    }
547
548    /// Returns a reference to the first plane's data as a contiguous slice.
549    ///
550    /// This is only meaningful for packed formats (RGBA, RGB24, etc.) where
551    /// all data is in a single plane. Returns `None` if the format is planar
552    /// or if no planes exist.
553    ///
554    /// # Examples
555    ///
556    /// ```
557    /// use ff_format::{PixelFormat, VideoFrame};
558    ///
559    /// // Packed format - returns data
560    /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
561    /// assert!(rgba.data_ref().is_some());
562    ///
563    /// // Planar format - returns None
564    /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
565    /// assert!(yuv.data_ref().is_none());
566    /// ```
567    #[must_use]
568    #[inline]
569    pub fn data_ref(&self) -> Option<&[u8]> {
570        if self.format.is_packed() && self.planes.len() == 1 {
571            Some(self.planes[0].as_ref())
572        } else {
573            None
574        }
575    }
576
577    /// Returns a mutable reference to the first plane's data.
578    ///
579    /// This is only meaningful for packed formats where all data is in a
580    /// single plane. Returns `None` if the format is planar.
581    ///
582    /// # Examples
583    ///
584    /// ```
585    /// use ff_format::{PixelFormat, VideoFrame};
586    ///
587    /// let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
588    /// if let Some(data) = frame.data_mut() {
589    ///     data[0] = 255; // Modify first byte
590    /// }
591    /// ```
592    #[must_use]
593    #[inline]
594    pub fn data_mut(&mut self) -> Option<&mut [u8]> {
595        if self.format.is_packed() && self.planes.len() == 1 {
596            Some(self.planes[0].as_mut())
597        } else {
598            None
599        }
600    }
601
602    // ==========================================================================
603    // Utility Methods
604    // ==========================================================================
605
606    /// Returns the total size in bytes of all plane data.
607    ///
608    /// # Examples
609    ///
610    /// ```
611    /// use ff_format::{PixelFormat, VideoFrame};
612    ///
613    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
614    /// assert_eq!(frame.total_size(), 1920 * 1080 * 4);
615    /// ```
616    #[must_use]
617    pub fn total_size(&self) -> usize {
618        self.planes.iter().map(PooledBuffer::len).sum()
619    }
620
621    /// Returns the resolution as a (width, height) tuple.
622    ///
623    /// # Examples
624    ///
625    /// ```
626    /// use ff_format::{PixelFormat, VideoFrame};
627    ///
628    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
629    /// assert_eq!(frame.resolution(), (1920, 1080));
630    /// ```
631    #[must_use]
632    #[inline]
633    pub const fn resolution(&self) -> (u32, u32) {
634        (self.width, self.height)
635    }
636
637    /// Returns the aspect ratio as a floating-point value.
638    ///
639    /// # Examples
640    ///
641    /// ```
642    /// use ff_format::{PixelFormat, VideoFrame};
643    ///
644    /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
645    /// let aspect = frame.aspect_ratio();
646    /// assert!((aspect - 16.0 / 9.0).abs() < 0.01);
647    /// ```
648    #[must_use]
649    #[inline]
650    pub fn aspect_ratio(&self) -> f64 {
651        if self.height == 0 {
652            log::warn!(
653                "aspect_ratio unavailable, height is 0, returning 0.0 \
654                 width={} height=0 fallback=0.0",
655                self.width
656            );
657            0.0
658        } else {
659            f64::from(self.width) / f64::from(self.height)
660        }
661    }
662}
663
664impl fmt::Debug for VideoFrame {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        f.debug_struct("VideoFrame")
667            .field("width", &self.width)
668            .field("height", &self.height)
669            .field("format", &self.format)
670            .field("timestamp", &self.timestamp)
671            .field("key_frame", &self.key_frame)
672            .field("num_planes", &self.planes.len())
673            .field(
674                "plane_sizes",
675                &self
676                    .planes
677                    .iter()
678                    .map(PooledBuffer::len)
679                    .collect::<Vec<_>>(),
680            )
681            .field("strides", &self.strides)
682            .finish()
683    }
684}
685
686impl fmt::Display for VideoFrame {
687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688        write!(
689            f,
690            "VideoFrame({}x{} {} @ {}{})",
691            self.width,
692            self.height,
693            self.format,
694            self.timestamp,
695            if self.key_frame { " [KEY]" } else { "" }
696        )
697    }
698}
699
700impl Default for VideoFrame {
701    /// Returns a default empty 1x1 YUV420P frame.
702    ///
703    /// This constructs a minimal valid frame directly.
704    fn default() -> Self {
705        // Construct a minimal 1x1 YUV420P frame directly
706        // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
707        Self {
708            planes: vec![
709                PooledBuffer::standalone(vec![0u8; 1]),
710                PooledBuffer::standalone(vec![0u8; 1]),
711                PooledBuffer::standalone(vec![0u8; 1]),
712            ],
713            strides: vec![1, 1, 1],
714            width: 1,
715            height: 1,
716            format: PixelFormat::Yuv420p,
717            timestamp: Timestamp::default(),
718            key_frame: false,
719        }
720    }
721}
722
723#[cfg(test)]
724#[allow(
725    clippy::unwrap_used,
726    clippy::redundant_closure_for_method_calls,
727    clippy::float_cmp
728)]
729mod tests {
730    use super::*;
731    use crate::Rational;
732
733    // ==========================================================================
734    // Construction Tests
735    // ==========================================================================
736
737    #[test]
738    fn test_new_rgba_frame() {
739        let width = 640u32;
740        let height = 480u32;
741        let stride = width as usize * 4;
742        let data = vec![0u8; stride * height as usize];
743        let ts = Timestamp::new(1000, Rational::new(1, 1000));
744
745        let frame = VideoFrame::new(
746            vec![PooledBuffer::standalone(data)],
747            vec![stride],
748            width,
749            height,
750            PixelFormat::Rgba,
751            ts,
752            true,
753        )
754        .unwrap();
755
756        assert_eq!(frame.width(), 640);
757        assert_eq!(frame.height(), 480);
758        assert_eq!(frame.format(), PixelFormat::Rgba);
759        assert_eq!(frame.timestamp(), ts);
760        assert!(frame.is_key_frame());
761        assert_eq!(frame.num_planes(), 1);
762        assert_eq!(frame.stride(0), Some(640 * 4));
763    }
764
765    #[test]
766    fn test_new_yuv420p_frame() {
767        let width = 640u32;
768        let height = 480u32;
769
770        let y_stride = width as usize;
771        let uv_stride = (width / 2) as usize;
772        let uv_height = (height / 2) as usize;
773
774        let y_data = vec![128u8; y_stride * height as usize];
775        let u_data = vec![128u8; uv_stride * uv_height];
776        let v_data = vec![128u8; uv_stride * uv_height];
777
778        let frame = VideoFrame::new(
779            vec![
780                PooledBuffer::standalone(y_data),
781                PooledBuffer::standalone(u_data),
782                PooledBuffer::standalone(v_data),
783            ],
784            vec![y_stride, uv_stride, uv_stride],
785            width,
786            height,
787            PixelFormat::Yuv420p,
788            Timestamp::default(),
789            false,
790        )
791        .unwrap();
792
793        assert_eq!(frame.width(), 640);
794        assert_eq!(frame.height(), 480);
795        assert_eq!(frame.format(), PixelFormat::Yuv420p);
796        assert!(!frame.is_key_frame());
797        assert_eq!(frame.num_planes(), 3);
798        assert_eq!(frame.stride(0), Some(640));
799        assert_eq!(frame.stride(1), Some(320));
800        assert_eq!(frame.stride(2), Some(320));
801    }
802
803    #[test]
804    fn test_new_mismatched_planes_strides() {
805        let result = VideoFrame::new(
806            vec![PooledBuffer::standalone(vec![0u8; 100])],
807            vec![10, 10], // Mismatched length
808            10,
809            10,
810            PixelFormat::Rgba,
811            Timestamp::default(),
812            false,
813        );
814
815        assert!(result.is_err());
816        assert_eq!(
817            result.unwrap_err(),
818            FrameError::MismatchedPlaneStride {
819                planes: 1,
820                strides: 2
821            }
822        );
823    }
824
825    // ==========================================================================
826    // Metadata Tests
827    // ==========================================================================
828
829    #[test]
830    fn test_resolution() {
831        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
832        assert_eq!(frame.resolution(), (1920, 1080));
833    }
834
835    #[test]
836    fn test_aspect_ratio() {
837        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
838        let aspect = frame.aspect_ratio();
839        assert!((aspect - 16.0 / 9.0).abs() < 0.001);
840
841        let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
842        let aspect_4_3 = frame_4_3.aspect_ratio();
843        assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
844    }
845
846    #[test]
847    fn test_aspect_ratio_zero_height() {
848        let frame = VideoFrame::new(
849            vec![PooledBuffer::standalone(vec![])],
850            vec![0],
851            100,
852            0,
853            PixelFormat::Rgba,
854            Timestamp::default(),
855            false,
856        )
857        .unwrap();
858        assert_eq!(frame.aspect_ratio(), 0.0);
859    }
860
861    #[test]
862    fn test_total_size_rgba() {
863        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
864        assert_eq!(frame.total_size(), 1920 * 1080 * 4);
865    }
866
867    #[test]
868    fn test_total_size_yuv420p() {
869        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
870        // Y: 640*480, U: 320*240, V: 320*240
871        let expected = 640 * 480 + 320 * 240 * 2;
872        assert_eq!(frame.total_size(), expected);
873    }
874
875    #[test]
876    fn test_set_key_frame() {
877        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
878        assert!(!frame.is_key_frame());
879
880        frame.set_key_frame(true);
881        assert!(frame.is_key_frame());
882
883        frame.set_key_frame(false);
884        assert!(!frame.is_key_frame());
885    }
886
887    #[test]
888    fn test_set_timestamp() {
889        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
890        let ts = Timestamp::new(90000, Rational::new(1, 90000));
891
892        frame.set_timestamp(ts);
893        assert_eq!(frame.timestamp(), ts);
894    }
895
896    // ==========================================================================
897    // Plane Access Tests
898    // ==========================================================================
899
900    #[test]
901    fn test_plane_access() {
902        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
903
904        assert!(frame.plane(0).is_some());
905        assert!(frame.plane(1).is_some());
906        assert!(frame.plane(2).is_some());
907        assert!(frame.plane(3).is_none());
908    }
909
910    #[test]
911    fn test_plane_mut_access() {
912        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
913
914        if let Some(data) = frame.plane_mut(0) {
915            // Fill with red
916            for chunk in data.chunks_exact_mut(4) {
917                chunk[0] = 255;
918                chunk[1] = 0;
919                chunk[2] = 0;
920                chunk[3] = 255;
921            }
922        }
923
924        let plane = frame.plane(0).unwrap();
925        assert_eq!(plane[0], 255); // R
926        assert_eq!(plane[1], 0); // G
927        assert_eq!(plane[2], 0); // B
928        assert_eq!(plane[3], 255); // A
929    }
930
931    #[test]
932    fn test_planes_slice() {
933        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
934        let planes = frame.planes();
935        assert_eq!(planes.len(), 3);
936    }
937
938    #[test]
939    fn test_strides_slice() {
940        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
941        let strides = frame.strides();
942        assert_eq!(strides.len(), 3);
943        assert_eq!(strides[0], 640);
944        assert_eq!(strides[1], 320);
945        assert_eq!(strides[2], 320);
946    }
947
948    #[test]
949    fn test_stride_out_of_bounds() {
950        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
951        assert!(frame.stride(0).is_some());
952        assert!(frame.stride(1).is_none());
953    }
954
955    // ==========================================================================
956    // Data Access Tests
957    // ==========================================================================
958
959    #[test]
960    fn test_data_contiguous() {
961        let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
962        let data = frame.data();
963        assert_eq!(data.len(), 4 * 4 * 4);
964    }
965
966    #[test]
967    fn test_data_yuv420p_concatenation() {
968        let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
969        let data = frame.data();
970        // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
971        assert_eq!(data.len(), 24);
972    }
973
974    #[test]
975    fn test_data_ref_packed() {
976        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
977        assert!(frame.data_ref().is_some());
978        assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
979    }
980
981    #[test]
982    fn test_data_ref_planar() {
983        let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
984        assert!(frame.data_ref().is_none());
985    }
986
987    #[test]
988    fn test_data_mut_packed() {
989        let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
990        assert!(frame.data_mut().is_some());
991
992        if let Some(data) = frame.data_mut() {
993            data[0] = 123;
994        }
995
996        assert_eq!(frame.plane(0).unwrap()[0], 123);
997    }
998
999    #[test]
1000    fn test_data_mut_planar() {
1001        let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1002        assert!(frame.data_mut().is_none());
1003    }
1004
1005    // ==========================================================================
1006    // Clone Tests
1007    // ==========================================================================
1008
1009    #[test]
1010    fn test_clone() {
1011        let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1012        original.set_key_frame(true);
1013        original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1014
1015        // Modify some data
1016        if let Some(data) = original.plane_mut(0) {
1017            data[0] = 42;
1018        }
1019
1020        let cloned = original.clone();
1021
1022        // Verify metadata matches
1023        assert_eq!(cloned.width(), original.width());
1024        assert_eq!(cloned.height(), original.height());
1025        assert_eq!(cloned.format(), original.format());
1026        assert_eq!(cloned.timestamp(), original.timestamp());
1027        assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1028
1029        // Verify data was cloned
1030        assert_eq!(cloned.plane(0).unwrap()[0], 42);
1031
1032        // Verify it's a deep clone (modifying clone doesn't affect original)
1033        let mut cloned = cloned;
1034        if let Some(data) = cloned.plane_mut(0) {
1035            data[0] = 99;
1036        }
1037        assert_eq!(original.plane(0).unwrap()[0], 42);
1038        assert_eq!(cloned.plane(0).unwrap()[0], 99);
1039    }
1040
1041    #[test]
1042    fn video_frame_clone_should_have_identical_data() {
1043        let width = 320u32;
1044        let height = 240u32;
1045        let stride = width as usize;
1046        let y_data = vec![42u8; stride * height as usize];
1047        let uv_stride = (width / 2) as usize;
1048        let uv_data = vec![128u8; uv_stride * (height / 2) as usize];
1049        let ts = Timestamp::new(1000, Rational::new(1, 1000));
1050
1051        let original = VideoFrame::new(
1052            vec![
1053                PooledBuffer::standalone(y_data.clone()),
1054                PooledBuffer::standalone(uv_data.clone()),
1055                PooledBuffer::standalone(uv_data.clone()),
1056            ],
1057            vec![stride, uv_stride, uv_stride],
1058            width,
1059            height,
1060            PixelFormat::Yuv420p,
1061            ts,
1062            false,
1063        )
1064        .unwrap();
1065
1066        let clone = original.clone();
1067
1068        assert_eq!(clone.width(), original.width());
1069        assert_eq!(clone.height(), original.height());
1070        assert_eq!(clone.format(), original.format());
1071        assert_eq!(clone.timestamp(), original.timestamp());
1072        assert_eq!(clone.is_key_frame(), original.is_key_frame());
1073        assert_eq!(clone.num_planes(), original.num_planes());
1074        assert_eq!(clone.plane(0), original.plane(0));
1075    }
1076
1077    // ==========================================================================
1078    // Display/Debug Tests
1079    // ==========================================================================
1080
1081    #[test]
1082    fn test_debug() {
1083        let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1084        let debug = format!("{frame:?}");
1085        assert!(debug.contains("VideoFrame"));
1086        assert!(debug.contains("640"));
1087        assert!(debug.contains("480"));
1088        assert!(debug.contains("Rgba"));
1089    }
1090
1091    #[test]
1092    fn test_display() {
1093        let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1094        frame.set_key_frame(true);
1095
1096        let display = format!("{frame}");
1097        assert!(display.contains("1920x1080"));
1098        assert!(display.contains("yuv420p"));
1099        assert!(display.contains("[KEY]"));
1100    }
1101
1102    #[test]
1103    fn test_display_non_keyframe() {
1104        let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1105        let display = format!("{frame}");
1106        assert!(!display.contains("[KEY]"));
1107    }
1108}