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}