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