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 log::warn!(
762 "aspect_ratio unavailable, height is 0, returning 0.0 \
763 width={} height=0 fallback=0.0",
764 self.width
765 );
766 0.0
767 } else {
768 f64::from(self.width) / f64::from(self.height)
769 }
770 }
771}
772
773impl fmt::Debug for VideoFrame {
774 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775 f.debug_struct("VideoFrame")
776 .field("width", &self.width)
777 .field("height", &self.height)
778 .field("format", &self.format)
779 .field("timestamp", &self.timestamp)
780 .field("key_frame", &self.key_frame)
781 .field("num_planes", &self.planes.len())
782 .field(
783 "plane_sizes",
784 &self
785 .planes
786 .iter()
787 .map(PooledBuffer::len)
788 .collect::<Vec<_>>(),
789 )
790 .field("strides", &self.strides)
791 .finish()
792 }
793}
794
795impl fmt::Display for VideoFrame {
796 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
797 write!(
798 f,
799 "VideoFrame({}x{} {} @ {}{})",
800 self.width,
801 self.height,
802 self.format,
803 self.timestamp,
804 if self.key_frame { " [KEY]" } else { "" }
805 )
806 }
807}
808
809impl Default for VideoFrame {
810 /// Returns a default empty 1x1 YUV420P frame.
811 ///
812 /// This constructs a minimal valid frame directly.
813 fn default() -> Self {
814 // Construct a minimal 1x1 YUV420P frame directly
815 // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
816 Self {
817 planes: vec![
818 PooledBuffer::standalone(vec![0u8; 1]),
819 PooledBuffer::standalone(vec![0u8; 1]),
820 PooledBuffer::standalone(vec![0u8; 1]),
821 ],
822 strides: vec![1, 1, 1],
823 width: 1,
824 height: 1,
825 format: PixelFormat::Yuv420p,
826 timestamp: Timestamp::default(),
827 key_frame: false,
828 }
829 }
830}
831
832#[cfg(test)]
833#[allow(
834 clippy::unwrap_used,
835 clippy::redundant_closure_for_method_calls,
836 clippy::float_cmp
837)]
838mod tests {
839 use super::*;
840 use crate::Rational;
841
842 // ==========================================================================
843 // Construction Tests
844 // ==========================================================================
845
846 #[test]
847 fn test_new_rgba_frame() {
848 let width = 640u32;
849 let height = 480u32;
850 let stride = width as usize * 4;
851 let data = vec![0u8; stride * height as usize];
852 let ts = Timestamp::new(1000, Rational::new(1, 1000));
853
854 let frame = VideoFrame::new(
855 vec![PooledBuffer::standalone(data)],
856 vec![stride],
857 width,
858 height,
859 PixelFormat::Rgba,
860 ts,
861 true,
862 )
863 .unwrap();
864
865 assert_eq!(frame.width(), 640);
866 assert_eq!(frame.height(), 480);
867 assert_eq!(frame.format(), PixelFormat::Rgba);
868 assert_eq!(frame.timestamp(), ts);
869 assert!(frame.is_key_frame());
870 assert_eq!(frame.num_planes(), 1);
871 assert_eq!(frame.stride(0), Some(640 * 4));
872 }
873
874 #[test]
875 fn test_new_yuv420p_frame() {
876 let width = 640u32;
877 let height = 480u32;
878
879 let y_stride = width as usize;
880 let uv_stride = (width / 2) as usize;
881 let uv_height = (height / 2) as usize;
882
883 let y_data = vec![128u8; y_stride * height as usize];
884 let u_data = vec![128u8; uv_stride * uv_height];
885 let v_data = vec![128u8; uv_stride * uv_height];
886
887 let frame = VideoFrame::new(
888 vec![
889 PooledBuffer::standalone(y_data),
890 PooledBuffer::standalone(u_data),
891 PooledBuffer::standalone(v_data),
892 ],
893 vec![y_stride, uv_stride, uv_stride],
894 width,
895 height,
896 PixelFormat::Yuv420p,
897 Timestamp::default(),
898 false,
899 )
900 .unwrap();
901
902 assert_eq!(frame.width(), 640);
903 assert_eq!(frame.height(), 480);
904 assert_eq!(frame.format(), PixelFormat::Yuv420p);
905 assert!(!frame.is_key_frame());
906 assert_eq!(frame.num_planes(), 3);
907 assert_eq!(frame.stride(0), Some(640));
908 assert_eq!(frame.stride(1), Some(320));
909 assert_eq!(frame.stride(2), Some(320));
910 }
911
912 #[test]
913 fn test_new_mismatched_planes_strides() {
914 let result = VideoFrame::new(
915 vec![PooledBuffer::standalone(vec![0u8; 100])],
916 vec![10, 10], // Mismatched length
917 10,
918 10,
919 PixelFormat::Rgba,
920 Timestamp::default(),
921 false,
922 );
923
924 assert!(result.is_err());
925 assert_eq!(
926 result.unwrap_err(),
927 FrameError::MismatchedPlaneStride {
928 planes: 1,
929 strides: 2
930 }
931 );
932 }
933
934 #[test]
935 fn test_empty_rgba() {
936 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
937 assert_eq!(frame.width(), 1920);
938 assert_eq!(frame.height(), 1080);
939 assert_eq!(frame.format(), PixelFormat::Rgba);
940 assert_eq!(frame.num_planes(), 1);
941 assert_eq!(frame.stride(0), Some(1920 * 4));
942 assert_eq!(frame.plane(0).map(|p| p.len()), Some(1920 * 1080 * 4));
943 }
944
945 #[test]
946 fn test_empty_yuv420p() {
947 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
948 assert_eq!(frame.num_planes(), 3);
949 assert_eq!(frame.stride(0), Some(640));
950 assert_eq!(frame.stride(1), Some(320));
951 assert_eq!(frame.stride(2), Some(320));
952
953 // Y plane: 640 * 480
954 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
955 // U/V planes: 320 * 240
956 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240));
957 assert_eq!(frame.plane(2).map(|p| p.len()), Some(320 * 240));
958 }
959
960 #[test]
961 fn test_empty_yuv422p() {
962 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv422p).unwrap();
963 assert_eq!(frame.num_planes(), 3);
964 assert_eq!(frame.stride(0), Some(640));
965 assert_eq!(frame.stride(1), Some(320));
966
967 // Y: full resolution, U/V: half width, full height
968 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
969 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 480));
970 }
971
972 #[test]
973 fn test_empty_yuv444p() {
974 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv444p).unwrap();
975 assert_eq!(frame.num_planes(), 3);
976
977 // All planes: full resolution
978 let expected_size = 640 * 480;
979 assert_eq!(frame.plane(0).map(|p| p.len()), Some(expected_size));
980 assert_eq!(frame.plane(1).map(|p| p.len()), Some(expected_size));
981 assert_eq!(frame.plane(2).map(|p| p.len()), Some(expected_size));
982 }
983
984 #[test]
985 fn test_empty_nv12() {
986 let frame = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
987 assert_eq!(frame.num_planes(), 2);
988 assert_eq!(frame.stride(0), Some(640));
989 assert_eq!(frame.stride(1), Some(640)); // UV interleaved
990
991 // Y plane: full resolution
992 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
993 // UV plane: full width, half height
994 assert_eq!(frame.plane(1).map(|p| p.len()), Some(640 * 240));
995 }
996
997 #[test]
998 fn test_empty_gray8() {
999 let frame = VideoFrame::empty(640, 480, PixelFormat::Gray8).unwrap();
1000 assert_eq!(frame.num_planes(), 1);
1001 assert_eq!(frame.stride(0), Some(640));
1002 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1003 }
1004
1005 #[test]
1006 fn test_default() {
1007 let frame = VideoFrame::default();
1008 assert_eq!(frame.width(), 1);
1009 assert_eq!(frame.height(), 1);
1010 assert_eq!(frame.format(), PixelFormat::default());
1011 assert!(!frame.is_key_frame());
1012 }
1013
1014 // ==========================================================================
1015 // Metadata Tests
1016 // ==========================================================================
1017
1018 #[test]
1019 fn test_resolution() {
1020 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1021 assert_eq!(frame.resolution(), (1920, 1080));
1022 }
1023
1024 #[test]
1025 fn test_aspect_ratio() {
1026 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1027 let aspect = frame.aspect_ratio();
1028 assert!((aspect - 16.0 / 9.0).abs() < 0.001);
1029
1030 let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1031 let aspect_4_3 = frame_4_3.aspect_ratio();
1032 assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
1033 }
1034
1035 #[test]
1036 fn test_aspect_ratio_zero_height() {
1037 let frame = VideoFrame::new(
1038 vec![PooledBuffer::standalone(vec![])],
1039 vec![0],
1040 100,
1041 0,
1042 PixelFormat::Rgba,
1043 Timestamp::default(),
1044 false,
1045 )
1046 .unwrap();
1047 assert_eq!(frame.aspect_ratio(), 0.0);
1048 }
1049
1050 #[test]
1051 fn test_total_size_rgba() {
1052 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1053 assert_eq!(frame.total_size(), 1920 * 1080 * 4);
1054 }
1055
1056 #[test]
1057 fn test_total_size_yuv420p() {
1058 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1059 // Y: 640*480, U: 320*240, V: 320*240
1060 let expected = 640 * 480 + 320 * 240 * 2;
1061 assert_eq!(frame.total_size(), expected);
1062 }
1063
1064 #[test]
1065 fn test_set_key_frame() {
1066 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1067 assert!(!frame.is_key_frame());
1068
1069 frame.set_key_frame(true);
1070 assert!(frame.is_key_frame());
1071
1072 frame.set_key_frame(false);
1073 assert!(!frame.is_key_frame());
1074 }
1075
1076 #[test]
1077 fn test_set_timestamp() {
1078 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1079 let ts = Timestamp::new(90000, Rational::new(1, 90000));
1080
1081 frame.set_timestamp(ts);
1082 assert_eq!(frame.timestamp(), ts);
1083 }
1084
1085 // ==========================================================================
1086 // Plane Access Tests
1087 // ==========================================================================
1088
1089 #[test]
1090 fn test_plane_access() {
1091 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1092
1093 assert!(frame.plane(0).is_some());
1094 assert!(frame.plane(1).is_some());
1095 assert!(frame.plane(2).is_some());
1096 assert!(frame.plane(3).is_none());
1097 }
1098
1099 #[test]
1100 fn test_plane_mut_access() {
1101 let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1102
1103 if let Some(data) = frame.plane_mut(0) {
1104 // Fill with red
1105 for chunk in data.chunks_exact_mut(4) {
1106 chunk[0] = 255;
1107 chunk[1] = 0;
1108 chunk[2] = 0;
1109 chunk[3] = 255;
1110 }
1111 }
1112
1113 let plane = frame.plane(0).unwrap();
1114 assert_eq!(plane[0], 255); // R
1115 assert_eq!(plane[1], 0); // G
1116 assert_eq!(plane[2], 0); // B
1117 assert_eq!(plane[3], 255); // A
1118 }
1119
1120 #[test]
1121 fn test_planes_slice() {
1122 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1123 let planes = frame.planes();
1124 assert_eq!(planes.len(), 3);
1125 }
1126
1127 #[test]
1128 fn test_strides_slice() {
1129 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1130 let strides = frame.strides();
1131 assert_eq!(strides.len(), 3);
1132 assert_eq!(strides[0], 640);
1133 assert_eq!(strides[1], 320);
1134 assert_eq!(strides[2], 320);
1135 }
1136
1137 #[test]
1138 fn test_stride_out_of_bounds() {
1139 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1140 assert!(frame.stride(0).is_some());
1141 assert!(frame.stride(1).is_none());
1142 }
1143
1144 // ==========================================================================
1145 // Data Access Tests
1146 // ==========================================================================
1147
1148 #[test]
1149 fn test_data_contiguous() {
1150 let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1151 let data = frame.data();
1152 assert_eq!(data.len(), 4 * 4 * 4);
1153 }
1154
1155 #[test]
1156 fn test_data_yuv420p_concatenation() {
1157 let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
1158 let data = frame.data();
1159 // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
1160 assert_eq!(data.len(), 24);
1161 }
1162
1163 #[test]
1164 fn test_data_ref_packed() {
1165 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1166 assert!(frame.data_ref().is_some());
1167 assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
1168 }
1169
1170 #[test]
1171 fn test_data_ref_planar() {
1172 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1173 assert!(frame.data_ref().is_none());
1174 }
1175
1176 #[test]
1177 fn test_data_mut_packed() {
1178 let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1179 assert!(frame.data_mut().is_some());
1180
1181 if let Some(data) = frame.data_mut() {
1182 data[0] = 123;
1183 }
1184
1185 assert_eq!(frame.plane(0).unwrap()[0], 123);
1186 }
1187
1188 #[test]
1189 fn test_data_mut_planar() {
1190 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1191 assert!(frame.data_mut().is_none());
1192 }
1193
1194 // ==========================================================================
1195 // Clone Tests
1196 // ==========================================================================
1197
1198 #[test]
1199 fn test_clone() {
1200 let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1201 original.set_key_frame(true);
1202 original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1203
1204 // Modify some data
1205 if let Some(data) = original.plane_mut(0) {
1206 data[0] = 42;
1207 }
1208
1209 let cloned = original.clone();
1210
1211 // Verify metadata matches
1212 assert_eq!(cloned.width(), original.width());
1213 assert_eq!(cloned.height(), original.height());
1214 assert_eq!(cloned.format(), original.format());
1215 assert_eq!(cloned.timestamp(), original.timestamp());
1216 assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1217
1218 // Verify data was cloned
1219 assert_eq!(cloned.plane(0).unwrap()[0], 42);
1220
1221 // Verify it's a deep clone (modifying clone doesn't affect original)
1222 let mut cloned = cloned;
1223 if let Some(data) = cloned.plane_mut(0) {
1224 data[0] = 99;
1225 }
1226 assert_eq!(original.plane(0).unwrap()[0], 42);
1227 assert_eq!(cloned.plane(0).unwrap()[0], 99);
1228 }
1229
1230 // ==========================================================================
1231 // Display/Debug Tests
1232 // ==========================================================================
1233
1234 #[test]
1235 fn test_debug() {
1236 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1237 let debug = format!("{frame:?}");
1238 assert!(debug.contains("VideoFrame"));
1239 assert!(debug.contains("640"));
1240 assert!(debug.contains("480"));
1241 assert!(debug.contains("Rgba"));
1242 }
1243
1244 #[test]
1245 fn test_display() {
1246 let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1247 frame.set_key_frame(true);
1248
1249 let display = format!("{frame}");
1250 assert!(display.contains("1920x1080"));
1251 assert!(display.contains("yuv420p"));
1252 assert!(display.contains("[KEY]"));
1253 }
1254
1255 #[test]
1256 fn test_display_non_keyframe() {
1257 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1258 let display = format!("{frame}");
1259 assert!(!display.contains("[KEY]"));
1260 }
1261
1262 // ==========================================================================
1263 // Edge Case Tests
1264 // ==========================================================================
1265
1266 #[test]
1267 fn test_odd_dimensions_yuv420p() {
1268 // Odd dimensions require proper rounding for chroma planes
1269 let frame = VideoFrame::empty(641, 481, PixelFormat::Yuv420p).unwrap();
1270
1271 // Y plane should be full size
1272 assert_eq!(frame.plane(0).map(|p| p.len()), Some(641 * 481));
1273
1274 // U/V planes should be (641+1)/2 * (481+1)/2 = 321 * 241
1275 assert_eq!(frame.plane(1).map(|p| p.len()), Some(321 * 241));
1276 assert_eq!(frame.plane(2).map(|p| p.len()), Some(321 * 241));
1277 }
1278
1279 #[test]
1280 fn test_small_frame() {
1281 let frame = VideoFrame::empty(1, 1, PixelFormat::Rgba).unwrap();
1282 assert_eq!(frame.total_size(), 4);
1283 assert_eq!(frame.plane(0).map(|p| p.len()), Some(4));
1284 }
1285
1286 #[test]
1287 fn test_10bit_yuv420p() {
1288 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p10le).unwrap();
1289 assert_eq!(frame.num_planes(), 3);
1290
1291 // 10-bit uses 2 bytes per sample
1292 assert_eq!(frame.stride(0), Some(640 * 2));
1293 assert_eq!(frame.stride(1), Some(320 * 2));
1294 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480 * 2));
1295 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240 * 2));
1296 }
1297
1298 #[test]
1299 fn test_p010le() {
1300 let frame = VideoFrame::empty(640, 480, PixelFormat::P010le).unwrap();
1301 assert_eq!(frame.num_planes(), 2);
1302
1303 // 10-bit semi-planar: Y and UV interleaved, both 2 bytes per sample
1304 assert_eq!(frame.stride(0), Some(640 * 2));
1305 assert_eq!(frame.stride(1), Some(640 * 2));
1306 }
1307
1308 #[test]
1309 fn test_rgb24() {
1310 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgb24).unwrap();
1311 assert_eq!(frame.num_planes(), 1);
1312 assert_eq!(frame.stride(0), Some(640 * 3));
1313 assert_eq!(frame.total_size(), 640 * 480 * 3);
1314 }
1315
1316 #[test]
1317 fn test_bgr24() {
1318 let frame = VideoFrame::empty(640, 480, PixelFormat::Bgr24).unwrap();
1319 assert_eq!(frame.num_planes(), 1);
1320 assert_eq!(frame.stride(0), Some(640 * 3));
1321 }
1322
1323 #[test]
1324 fn test_bgra() {
1325 let frame = VideoFrame::empty(640, 480, PixelFormat::Bgra).unwrap();
1326 assert_eq!(frame.num_planes(), 1);
1327 assert_eq!(frame.stride(0), Some(640 * 4));
1328 }
1329
1330 #[test]
1331 fn test_other_format_returns_error() {
1332 // Unknown formats cannot be allocated - memory layout is unknown
1333 let result = VideoFrame::empty(640, 480, PixelFormat::Other(999));
1334 assert!(result.is_err());
1335 assert_eq!(
1336 result.unwrap_err(),
1337 FrameError::UnsupportedPixelFormat(PixelFormat::Other(999))
1338 );
1339 }
1340}