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}