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 planar YUV 4:2:2 - 2 bytes per sample
320 PixelFormat::Yuv422p10le => {
321 let y_stride = width as usize * 2;
322 let uv_stride = (width as usize).div_ceil(2) * 2;
323
324 let y_data = vec![0u8; y_stride * height as usize];
325 let u_data = vec![0u8; uv_stride * height as usize];
326 let v_data = vec![0u8; uv_stride * height as usize];
327
328 Ok((
329 vec![
330 PooledBuffer::standalone(y_data),
331 PooledBuffer::standalone(u_data),
332 PooledBuffer::standalone(v_data),
333 ],
334 vec![y_stride, uv_stride, uv_stride],
335 ))
336 }
337
338 // 10-bit planar YUV 4:4:4 - 2 bytes per sample
339 PixelFormat::Yuv444p10le => {
340 let stride = width as usize * 2;
341
342 let y_data = vec![0u8; stride * height as usize];
343 let u_data = vec![0u8; stride * height as usize];
344 let v_data = vec![0u8; stride * height as usize];
345
346 Ok((
347 vec![
348 PooledBuffer::standalone(y_data),
349 PooledBuffer::standalone(u_data),
350 PooledBuffer::standalone(v_data),
351 ],
352 vec![stride, stride, stride],
353 ))
354 }
355
356 // 10-bit planar YUVA 4:4:4 with alpha - 2 bytes per sample
357 PixelFormat::Yuva444p10le => {
358 let stride = width as usize * 2;
359
360 let y_data = vec![0u8; stride * height as usize];
361 let u_data = vec![0u8; stride * height as usize];
362 let v_data = vec![0u8; stride * height as usize];
363 let a_data = vec![0u8; stride * height as usize];
364
365 Ok((
366 vec![
367 PooledBuffer::standalone(y_data),
368 PooledBuffer::standalone(u_data),
369 PooledBuffer::standalone(v_data),
370 PooledBuffer::standalone(a_data),
371 ],
372 vec![stride, stride, stride, stride],
373 ))
374 }
375
376 // 10-bit semi-planar P010
377 PixelFormat::P010le => {
378 let y_stride = width as usize * 2;
379 let uv_stride = width as usize * 2;
380 let uv_height = (height as usize).div_ceil(2);
381
382 let y_data = vec![0u8; y_stride * height as usize];
383 let uv_data = vec![0u8; uv_stride * uv_height];
384
385 Ok((
386 vec![
387 PooledBuffer::standalone(y_data),
388 PooledBuffer::standalone(uv_data),
389 ],
390 vec![y_stride, uv_stride],
391 ))
392 }
393
394 // Planar GBR float (gbrpf32le) - three planes, 4 bytes per sample (f32)
395 PixelFormat::Gbrpf32le => {
396 let stride = width as usize * 4; // 4 bytes per f32 sample
397 let size = stride * height as usize;
398 Ok((
399 vec![
400 PooledBuffer::standalone(vec![0u8; size]),
401 PooledBuffer::standalone(vec![0u8; size]),
402 PooledBuffer::standalone(vec![0u8; size]),
403 ],
404 vec![stride, stride, stride],
405 ))
406 }
407
408 // Unknown format - cannot determine memory layout
409 PixelFormat::Other(_) => Err(FrameError::UnsupportedPixelFormat(format)),
410 }
411 }
412
413 // ==========================================================================
414 // Metadata Accessors
415 // ==========================================================================
416
417 /// Returns the frame width in pixels.
418 ///
419 /// # Examples
420 ///
421 /// ```
422 /// use ff_format::{PixelFormat, VideoFrame};
423 ///
424 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
425 /// assert_eq!(frame.width(), 1920);
426 /// ```
427 #[must_use]
428 #[inline]
429 pub const fn width(&self) -> u32 {
430 self.width
431 }
432
433 /// Returns the frame height in pixels.
434 ///
435 /// # Examples
436 ///
437 /// ```
438 /// use ff_format::{PixelFormat, VideoFrame};
439 ///
440 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
441 /// assert_eq!(frame.height(), 1080);
442 /// ```
443 #[must_use]
444 #[inline]
445 pub const fn height(&self) -> u32 {
446 self.height
447 }
448
449 /// Returns the pixel format of this frame.
450 ///
451 /// # Examples
452 ///
453 /// ```
454 /// use ff_format::{PixelFormat, VideoFrame};
455 ///
456 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
457 /// assert_eq!(frame.format(), PixelFormat::Yuv420p);
458 /// ```
459 #[must_use]
460 #[inline]
461 pub const fn format(&self) -> PixelFormat {
462 self.format
463 }
464
465 /// Returns the presentation timestamp of this frame.
466 ///
467 /// # Examples
468 ///
469 /// ```
470 /// use ff_format::{PixelFormat, PooledBuffer, Rational, Timestamp, VideoFrame};
471 ///
472 /// let ts = Timestamp::new(90000, Rational::new(1, 90000));
473 /// let frame = VideoFrame::new(
474 /// vec![PooledBuffer::standalone(vec![0u8; 1920 * 1080 * 4])],
475 /// vec![1920 * 4],
476 /// 1920,
477 /// 1080,
478 /// PixelFormat::Rgba,
479 /// ts,
480 /// true,
481 /// ).unwrap();
482 /// assert_eq!(frame.timestamp(), ts);
483 /// ```
484 #[must_use]
485 #[inline]
486 pub const fn timestamp(&self) -> Timestamp {
487 self.timestamp
488 }
489
490 /// Returns whether this frame is a key frame (I-frame).
491 ///
492 /// Key frames are complete frames that don't depend on any other frames
493 /// for decoding. They are used as reference points for seeking.
494 ///
495 /// # Examples
496 ///
497 /// ```
498 /// use ff_format::{PixelFormat, VideoFrame};
499 ///
500 /// let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
501 /// assert!(!frame.is_key_frame());
502 ///
503 /// frame.set_key_frame(true);
504 /// assert!(frame.is_key_frame());
505 /// ```
506 #[must_use]
507 #[inline]
508 pub const fn is_key_frame(&self) -> bool {
509 self.key_frame
510 }
511
512 /// Sets whether this frame is a key frame.
513 ///
514 /// # Examples
515 ///
516 /// ```
517 /// use ff_format::{PixelFormat, VideoFrame};
518 ///
519 /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
520 /// frame.set_key_frame(true);
521 /// assert!(frame.is_key_frame());
522 /// ```
523 #[inline]
524 pub fn set_key_frame(&mut self, key_frame: bool) {
525 self.key_frame = key_frame;
526 }
527
528 /// Sets the timestamp of this frame.
529 ///
530 /// # Examples
531 ///
532 /// ```
533 /// use ff_format::{PixelFormat, Rational, Timestamp, VideoFrame};
534 ///
535 /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
536 /// let ts = Timestamp::new(3000, Rational::new(1, 90000));
537 /// frame.set_timestamp(ts);
538 /// assert_eq!(frame.timestamp(), ts);
539 /// ```
540 #[inline]
541 pub fn set_timestamp(&mut self, timestamp: Timestamp) {
542 self.timestamp = timestamp;
543 }
544
545 // ==========================================================================
546 // Plane Data Access
547 // ==========================================================================
548
549 /// Returns the number of planes in this frame.
550 ///
551 /// - Packed formats (RGBA, RGB24, etc.): 1 plane
552 /// - Planar YUV (YUV420P, YUV422P, YUV444P): 3 planes
553 /// - Semi-planar (NV12, NV21): 2 planes
554 ///
555 /// # Examples
556 ///
557 /// ```
558 /// use ff_format::{PixelFormat, VideoFrame};
559 ///
560 /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
561 /// assert_eq!(rgba.num_planes(), 1);
562 ///
563 /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
564 /// assert_eq!(yuv.num_planes(), 3);
565 ///
566 /// let nv12 = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
567 /// assert_eq!(nv12.num_planes(), 2);
568 /// ```
569 #[must_use]
570 #[inline]
571 pub fn num_planes(&self) -> usize {
572 self.planes.len()
573 }
574
575 /// Returns a slice of all plane buffers.
576 ///
577 /// # Examples
578 ///
579 /// ```
580 /// use ff_format::{PixelFormat, VideoFrame};
581 ///
582 /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
583 /// let planes = frame.planes();
584 /// assert_eq!(planes.len(), 3);
585 /// ```
586 #[must_use]
587 #[inline]
588 pub fn planes(&self) -> &[PooledBuffer] {
589 &self.planes
590 }
591
592 /// Returns the data for a specific plane, or `None` if the index is out of bounds.
593 ///
594 /// # Arguments
595 ///
596 /// * `index` - The plane index (0 for Y/RGB, 1 for U/UV, 2 for V)
597 ///
598 /// # Examples
599 ///
600 /// ```
601 /// use ff_format::{PixelFormat, VideoFrame};
602 ///
603 /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
604 ///
605 /// // Y plane exists
606 /// assert!(frame.plane(0).is_some());
607 ///
608 /// // U and V planes exist
609 /// assert!(frame.plane(1).is_some());
610 /// assert!(frame.plane(2).is_some());
611 ///
612 /// // No fourth plane
613 /// assert!(frame.plane(3).is_none());
614 /// ```
615 #[must_use]
616 #[inline]
617 pub fn plane(&self, index: usize) -> Option<&[u8]> {
618 self.planes.get(index).map(std::convert::AsRef::as_ref)
619 }
620
621 /// Returns mutable access to a specific plane's data.
622 ///
623 /// # Arguments
624 ///
625 /// * `index` - The plane index
626 ///
627 /// # Examples
628 ///
629 /// ```
630 /// use ff_format::{PixelFormat, VideoFrame};
631 ///
632 /// let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
633 /// if let Some(data) = frame.plane_mut(0) {
634 /// // Fill with red (RGBA)
635 /// for chunk in data.chunks_exact_mut(4) {
636 /// chunk[0] = 255; // R
637 /// chunk[1] = 0; // G
638 /// chunk[2] = 0; // B
639 /// chunk[3] = 255; // A
640 /// }
641 /// }
642 /// ```
643 #[must_use]
644 #[inline]
645 pub fn plane_mut(&mut self, index: usize) -> Option<&mut [u8]> {
646 self.planes.get_mut(index).map(std::convert::AsMut::as_mut)
647 }
648
649 /// Returns a slice of all stride values.
650 ///
651 /// Strides indicate the number of bytes between the start of consecutive
652 /// rows in each plane.
653 ///
654 /// # Examples
655 ///
656 /// ```
657 /// use ff_format::{PixelFormat, VideoFrame};
658 ///
659 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
660 /// let strides = frame.strides();
661 /// assert_eq!(strides[0], 1920 * 4); // RGBA = 4 bytes per pixel
662 /// ```
663 #[must_use]
664 #[inline]
665 pub fn strides(&self) -> &[usize] {
666 &self.strides
667 }
668
669 /// Returns the stride for a specific plane, or `None` if the index is out of bounds.
670 ///
671 /// # Arguments
672 ///
673 /// * `plane` - The plane index
674 ///
675 /// # Examples
676 ///
677 /// ```
678 /// use ff_format::{PixelFormat, VideoFrame};
679 ///
680 /// let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
681 ///
682 /// // Y plane stride = width
683 /// assert_eq!(frame.stride(0), Some(640));
684 ///
685 /// // U/V plane stride = width / 2
686 /// assert_eq!(frame.stride(1), Some(320));
687 /// assert_eq!(frame.stride(2), Some(320));
688 /// ```
689 #[must_use]
690 #[inline]
691 pub fn stride(&self, plane: usize) -> Option<usize> {
692 self.strides.get(plane).copied()
693 }
694
695 // ==========================================================================
696 // Contiguous Data Access
697 // ==========================================================================
698
699 /// Returns the frame data as a contiguous byte vector.
700 ///
701 /// For packed formats with a single plane, this returns a copy of the plane data.
702 /// For planar formats, this concatenates all planes into a single buffer.
703 ///
704 /// # Note
705 ///
706 /// This method allocates a new vector and copies the data. For zero-copy
707 /// access, use [`plane()`](Self::plane) or [`planes()`](Self::planes) instead.
708 ///
709 /// # Examples
710 ///
711 /// ```
712 /// use ff_format::{PixelFormat, VideoFrame};
713 ///
714 /// let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
715 /// let data = frame.data();
716 /// assert_eq!(data.len(), 4 * 4 * 4); // 4x4 pixels, 4 bytes each
717 /// ```
718 #[must_use]
719 pub fn data(&self) -> Vec<u8> {
720 let total_size: usize = self.planes.iter().map(PooledBuffer::len).sum();
721 let mut result = Vec::with_capacity(total_size);
722 for plane in &self.planes {
723 result.extend_from_slice(plane.as_ref());
724 }
725 result
726 }
727
728 /// Returns a reference to the first plane's data as a contiguous slice.
729 ///
730 /// This is only meaningful for packed formats (RGBA, RGB24, etc.) where
731 /// all data is in a single plane. Returns `None` if the format is planar
732 /// or if no planes exist.
733 ///
734 /// # Examples
735 ///
736 /// ```
737 /// use ff_format::{PixelFormat, VideoFrame};
738 ///
739 /// // Packed format - returns data
740 /// let rgba = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
741 /// assert!(rgba.data_ref().is_some());
742 ///
743 /// // Planar format - returns None
744 /// let yuv = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
745 /// assert!(yuv.data_ref().is_none());
746 /// ```
747 #[must_use]
748 #[inline]
749 pub fn data_ref(&self) -> Option<&[u8]> {
750 if self.format.is_packed() && self.planes.len() == 1 {
751 Some(self.planes[0].as_ref())
752 } else {
753 None
754 }
755 }
756
757 /// Returns a mutable reference to the first plane's data.
758 ///
759 /// This is only meaningful for packed formats where all data is in a
760 /// single plane. Returns `None` if the format is planar.
761 ///
762 /// # Examples
763 ///
764 /// ```
765 /// use ff_format::{PixelFormat, VideoFrame};
766 ///
767 /// let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
768 /// if let Some(data) = frame.data_mut() {
769 /// data[0] = 255; // Modify first byte
770 /// }
771 /// ```
772 #[must_use]
773 #[inline]
774 pub fn data_mut(&mut self) -> Option<&mut [u8]> {
775 if self.format.is_packed() && self.planes.len() == 1 {
776 Some(self.planes[0].as_mut())
777 } else {
778 None
779 }
780 }
781
782 // ==========================================================================
783 // Utility Methods
784 // ==========================================================================
785
786 /// Returns the total size in bytes of all plane data.
787 ///
788 /// # Examples
789 ///
790 /// ```
791 /// use ff_format::{PixelFormat, VideoFrame};
792 ///
793 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
794 /// assert_eq!(frame.total_size(), 1920 * 1080 * 4);
795 /// ```
796 #[must_use]
797 pub fn total_size(&self) -> usize {
798 self.planes.iter().map(PooledBuffer::len).sum()
799 }
800
801 /// Returns the resolution as a (width, height) tuple.
802 ///
803 /// # Examples
804 ///
805 /// ```
806 /// use ff_format::{PixelFormat, VideoFrame};
807 ///
808 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
809 /// assert_eq!(frame.resolution(), (1920, 1080));
810 /// ```
811 #[must_use]
812 #[inline]
813 pub const fn resolution(&self) -> (u32, u32) {
814 (self.width, self.height)
815 }
816
817 /// Returns the aspect ratio as a floating-point value.
818 ///
819 /// # Examples
820 ///
821 /// ```
822 /// use ff_format::{PixelFormat, VideoFrame};
823 ///
824 /// let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
825 /// let aspect = frame.aspect_ratio();
826 /// assert!((aspect - 16.0 / 9.0).abs() < 0.01);
827 /// ```
828 #[must_use]
829 #[inline]
830 pub fn aspect_ratio(&self) -> f64 {
831 if self.height == 0 {
832 log::warn!(
833 "aspect_ratio unavailable, height is 0, returning 0.0 \
834 width={} height=0 fallback=0.0",
835 self.width
836 );
837 0.0
838 } else {
839 f64::from(self.width) / f64::from(self.height)
840 }
841 }
842}
843
844impl fmt::Debug for VideoFrame {
845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
846 f.debug_struct("VideoFrame")
847 .field("width", &self.width)
848 .field("height", &self.height)
849 .field("format", &self.format)
850 .field("timestamp", &self.timestamp)
851 .field("key_frame", &self.key_frame)
852 .field("num_planes", &self.planes.len())
853 .field(
854 "plane_sizes",
855 &self
856 .planes
857 .iter()
858 .map(PooledBuffer::len)
859 .collect::<Vec<_>>(),
860 )
861 .field("strides", &self.strides)
862 .finish()
863 }
864}
865
866impl fmt::Display for VideoFrame {
867 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868 write!(
869 f,
870 "VideoFrame({}x{} {} @ {}{})",
871 self.width,
872 self.height,
873 self.format,
874 self.timestamp,
875 if self.key_frame { " [KEY]" } else { "" }
876 )
877 }
878}
879
880impl Default for VideoFrame {
881 /// Returns a default empty 1x1 YUV420P frame.
882 ///
883 /// This constructs a minimal valid frame directly.
884 fn default() -> Self {
885 // Construct a minimal 1x1 YUV420P frame directly
886 // Y plane: 1 byte, U plane: 1 byte, V plane: 1 byte
887 Self {
888 planes: vec![
889 PooledBuffer::standalone(vec![0u8; 1]),
890 PooledBuffer::standalone(vec![0u8; 1]),
891 PooledBuffer::standalone(vec![0u8; 1]),
892 ],
893 strides: vec![1, 1, 1],
894 width: 1,
895 height: 1,
896 format: PixelFormat::Yuv420p,
897 timestamp: Timestamp::default(),
898 key_frame: false,
899 }
900 }
901}
902
903#[cfg(test)]
904#[allow(
905 clippy::unwrap_used,
906 clippy::redundant_closure_for_method_calls,
907 clippy::float_cmp
908)]
909mod tests {
910 use super::*;
911 use crate::Rational;
912
913 // ==========================================================================
914 // Construction Tests
915 // ==========================================================================
916
917 #[test]
918 fn test_new_rgba_frame() {
919 let width = 640u32;
920 let height = 480u32;
921 let stride = width as usize * 4;
922 let data = vec![0u8; stride * height as usize];
923 let ts = Timestamp::new(1000, Rational::new(1, 1000));
924
925 let frame = VideoFrame::new(
926 vec![PooledBuffer::standalone(data)],
927 vec![stride],
928 width,
929 height,
930 PixelFormat::Rgba,
931 ts,
932 true,
933 )
934 .unwrap();
935
936 assert_eq!(frame.width(), 640);
937 assert_eq!(frame.height(), 480);
938 assert_eq!(frame.format(), PixelFormat::Rgba);
939 assert_eq!(frame.timestamp(), ts);
940 assert!(frame.is_key_frame());
941 assert_eq!(frame.num_planes(), 1);
942 assert_eq!(frame.stride(0), Some(640 * 4));
943 }
944
945 #[test]
946 fn test_new_yuv420p_frame() {
947 let width = 640u32;
948 let height = 480u32;
949
950 let y_stride = width as usize;
951 let uv_stride = (width / 2) as usize;
952 let uv_height = (height / 2) as usize;
953
954 let y_data = vec![128u8; y_stride * height as usize];
955 let u_data = vec![128u8; uv_stride * uv_height];
956 let v_data = vec![128u8; uv_stride * uv_height];
957
958 let frame = VideoFrame::new(
959 vec![
960 PooledBuffer::standalone(y_data),
961 PooledBuffer::standalone(u_data),
962 PooledBuffer::standalone(v_data),
963 ],
964 vec![y_stride, uv_stride, uv_stride],
965 width,
966 height,
967 PixelFormat::Yuv420p,
968 Timestamp::default(),
969 false,
970 )
971 .unwrap();
972
973 assert_eq!(frame.width(), 640);
974 assert_eq!(frame.height(), 480);
975 assert_eq!(frame.format(), PixelFormat::Yuv420p);
976 assert!(!frame.is_key_frame());
977 assert_eq!(frame.num_planes(), 3);
978 assert_eq!(frame.stride(0), Some(640));
979 assert_eq!(frame.stride(1), Some(320));
980 assert_eq!(frame.stride(2), Some(320));
981 }
982
983 #[test]
984 fn test_new_mismatched_planes_strides() {
985 let result = VideoFrame::new(
986 vec![PooledBuffer::standalone(vec![0u8; 100])],
987 vec![10, 10], // Mismatched length
988 10,
989 10,
990 PixelFormat::Rgba,
991 Timestamp::default(),
992 false,
993 );
994
995 assert!(result.is_err());
996 assert_eq!(
997 result.unwrap_err(),
998 FrameError::MismatchedPlaneStride {
999 planes: 1,
1000 strides: 2
1001 }
1002 );
1003 }
1004
1005 #[test]
1006 fn test_empty_rgba() {
1007 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1008 assert_eq!(frame.width(), 1920);
1009 assert_eq!(frame.height(), 1080);
1010 assert_eq!(frame.format(), PixelFormat::Rgba);
1011 assert_eq!(frame.num_planes(), 1);
1012 assert_eq!(frame.stride(0), Some(1920 * 4));
1013 assert_eq!(frame.plane(0).map(|p| p.len()), Some(1920 * 1080 * 4));
1014 }
1015
1016 #[test]
1017 fn test_empty_yuv420p() {
1018 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1019 assert_eq!(frame.num_planes(), 3);
1020 assert_eq!(frame.stride(0), Some(640));
1021 assert_eq!(frame.stride(1), Some(320));
1022 assert_eq!(frame.stride(2), Some(320));
1023
1024 // Y plane: 640 * 480
1025 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1026 // U/V planes: 320 * 240
1027 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240));
1028 assert_eq!(frame.plane(2).map(|p| p.len()), Some(320 * 240));
1029 }
1030
1031 #[test]
1032 fn test_empty_yuv422p() {
1033 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv422p).unwrap();
1034 assert_eq!(frame.num_planes(), 3);
1035 assert_eq!(frame.stride(0), Some(640));
1036 assert_eq!(frame.stride(1), Some(320));
1037
1038 // Y: full resolution, U/V: half width, full height
1039 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1040 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 480));
1041 }
1042
1043 #[test]
1044 fn test_empty_yuv444p() {
1045 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv444p).unwrap();
1046 assert_eq!(frame.num_planes(), 3);
1047
1048 // All planes: full resolution
1049 let expected_size = 640 * 480;
1050 assert_eq!(frame.plane(0).map(|p| p.len()), Some(expected_size));
1051 assert_eq!(frame.plane(1).map(|p| p.len()), Some(expected_size));
1052 assert_eq!(frame.plane(2).map(|p| p.len()), Some(expected_size));
1053 }
1054
1055 #[test]
1056 fn test_empty_nv12() {
1057 let frame = VideoFrame::empty(640, 480, PixelFormat::Nv12).unwrap();
1058 assert_eq!(frame.num_planes(), 2);
1059 assert_eq!(frame.stride(0), Some(640));
1060 assert_eq!(frame.stride(1), Some(640)); // UV interleaved
1061
1062 // Y plane: full resolution
1063 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1064 // UV plane: full width, half height
1065 assert_eq!(frame.plane(1).map(|p| p.len()), Some(640 * 240));
1066 }
1067
1068 #[test]
1069 fn test_empty_gray8() {
1070 let frame = VideoFrame::empty(640, 480, PixelFormat::Gray8).unwrap();
1071 assert_eq!(frame.num_planes(), 1);
1072 assert_eq!(frame.stride(0), Some(640));
1073 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480));
1074 }
1075
1076 #[test]
1077 fn test_default() {
1078 let frame = VideoFrame::default();
1079 assert_eq!(frame.width(), 1);
1080 assert_eq!(frame.height(), 1);
1081 assert_eq!(frame.format(), PixelFormat::default());
1082 assert!(!frame.is_key_frame());
1083 }
1084
1085 // ==========================================================================
1086 // Metadata Tests
1087 // ==========================================================================
1088
1089 #[test]
1090 fn test_resolution() {
1091 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1092 assert_eq!(frame.resolution(), (1920, 1080));
1093 }
1094
1095 #[test]
1096 fn test_aspect_ratio() {
1097 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1098 let aspect = frame.aspect_ratio();
1099 assert!((aspect - 16.0 / 9.0).abs() < 0.001);
1100
1101 let frame_4_3 = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1102 let aspect_4_3 = frame_4_3.aspect_ratio();
1103 assert!((aspect_4_3 - 4.0 / 3.0).abs() < 0.001);
1104 }
1105
1106 #[test]
1107 fn test_aspect_ratio_zero_height() {
1108 let frame = VideoFrame::new(
1109 vec![PooledBuffer::standalone(vec![])],
1110 vec![0],
1111 100,
1112 0,
1113 PixelFormat::Rgba,
1114 Timestamp::default(),
1115 false,
1116 )
1117 .unwrap();
1118 assert_eq!(frame.aspect_ratio(), 0.0);
1119 }
1120
1121 #[test]
1122 fn test_total_size_rgba() {
1123 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1124 assert_eq!(frame.total_size(), 1920 * 1080 * 4);
1125 }
1126
1127 #[test]
1128 fn test_total_size_yuv420p() {
1129 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1130 // Y: 640*480, U: 320*240, V: 320*240
1131 let expected = 640 * 480 + 320 * 240 * 2;
1132 assert_eq!(frame.total_size(), expected);
1133 }
1134
1135 #[test]
1136 fn test_set_key_frame() {
1137 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1138 assert!(!frame.is_key_frame());
1139
1140 frame.set_key_frame(true);
1141 assert!(frame.is_key_frame());
1142
1143 frame.set_key_frame(false);
1144 assert!(!frame.is_key_frame());
1145 }
1146
1147 #[test]
1148 fn test_set_timestamp() {
1149 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1150 let ts = Timestamp::new(90000, Rational::new(1, 90000));
1151
1152 frame.set_timestamp(ts);
1153 assert_eq!(frame.timestamp(), ts);
1154 }
1155
1156 // ==========================================================================
1157 // Plane Access Tests
1158 // ==========================================================================
1159
1160 #[test]
1161 fn test_plane_access() {
1162 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1163
1164 assert!(frame.plane(0).is_some());
1165 assert!(frame.plane(1).is_some());
1166 assert!(frame.plane(2).is_some());
1167 assert!(frame.plane(3).is_none());
1168 }
1169
1170 #[test]
1171 fn test_plane_mut_access() {
1172 let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1173
1174 if let Some(data) = frame.plane_mut(0) {
1175 // Fill with red
1176 for chunk in data.chunks_exact_mut(4) {
1177 chunk[0] = 255;
1178 chunk[1] = 0;
1179 chunk[2] = 0;
1180 chunk[3] = 255;
1181 }
1182 }
1183
1184 let plane = frame.plane(0).unwrap();
1185 assert_eq!(plane[0], 255); // R
1186 assert_eq!(plane[1], 0); // G
1187 assert_eq!(plane[2], 0); // B
1188 assert_eq!(plane[3], 255); // A
1189 }
1190
1191 #[test]
1192 fn test_planes_slice() {
1193 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1194 let planes = frame.planes();
1195 assert_eq!(planes.len(), 3);
1196 }
1197
1198 #[test]
1199 fn test_strides_slice() {
1200 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1201 let strides = frame.strides();
1202 assert_eq!(strides.len(), 3);
1203 assert_eq!(strides[0], 640);
1204 assert_eq!(strides[1], 320);
1205 assert_eq!(strides[2], 320);
1206 }
1207
1208 #[test]
1209 fn test_stride_out_of_bounds() {
1210 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1211 assert!(frame.stride(0).is_some());
1212 assert!(frame.stride(1).is_none());
1213 }
1214
1215 // ==========================================================================
1216 // Data Access Tests
1217 // ==========================================================================
1218
1219 #[test]
1220 fn test_data_contiguous() {
1221 let frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1222 let data = frame.data();
1223 assert_eq!(data.len(), 4 * 4 * 4);
1224 }
1225
1226 #[test]
1227 fn test_data_yuv420p_concatenation() {
1228 let frame = VideoFrame::empty(4, 4, PixelFormat::Yuv420p).unwrap();
1229 let data = frame.data();
1230 // Y: 4*4 + U: 2*2 + V: 2*2 = 16 + 4 + 4 = 24
1231 assert_eq!(data.len(), 24);
1232 }
1233
1234 #[test]
1235 fn test_data_ref_packed() {
1236 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1237 assert!(frame.data_ref().is_some());
1238 assert_eq!(frame.data_ref().map(|d| d.len()), Some(640 * 480 * 4));
1239 }
1240
1241 #[test]
1242 fn test_data_ref_planar() {
1243 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1244 assert!(frame.data_ref().is_none());
1245 }
1246
1247 #[test]
1248 fn test_data_mut_packed() {
1249 let mut frame = VideoFrame::empty(4, 4, PixelFormat::Rgba).unwrap();
1250 assert!(frame.data_mut().is_some());
1251
1252 if let Some(data) = frame.data_mut() {
1253 data[0] = 123;
1254 }
1255
1256 assert_eq!(frame.plane(0).unwrap()[0], 123);
1257 }
1258
1259 #[test]
1260 fn test_data_mut_planar() {
1261 let mut frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p).unwrap();
1262 assert!(frame.data_mut().is_none());
1263 }
1264
1265 // ==========================================================================
1266 // Clone Tests
1267 // ==========================================================================
1268
1269 #[test]
1270 fn test_clone() {
1271 let mut original = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1272 original.set_key_frame(true);
1273 original.set_timestamp(Timestamp::new(1000, Rational::new(1, 1000)));
1274
1275 // Modify some data
1276 if let Some(data) = original.plane_mut(0) {
1277 data[0] = 42;
1278 }
1279
1280 let cloned = original.clone();
1281
1282 // Verify metadata matches
1283 assert_eq!(cloned.width(), original.width());
1284 assert_eq!(cloned.height(), original.height());
1285 assert_eq!(cloned.format(), original.format());
1286 assert_eq!(cloned.timestamp(), original.timestamp());
1287 assert_eq!(cloned.is_key_frame(), original.is_key_frame());
1288
1289 // Verify data was cloned
1290 assert_eq!(cloned.plane(0).unwrap()[0], 42);
1291
1292 // Verify it's a deep clone (modifying clone doesn't affect original)
1293 let mut cloned = cloned;
1294 if let Some(data) = cloned.plane_mut(0) {
1295 data[0] = 99;
1296 }
1297 assert_eq!(original.plane(0).unwrap()[0], 42);
1298 assert_eq!(cloned.plane(0).unwrap()[0], 99);
1299 }
1300
1301 // ==========================================================================
1302 // Display/Debug Tests
1303 // ==========================================================================
1304
1305 #[test]
1306 fn test_debug() {
1307 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgba).unwrap();
1308 let debug = format!("{frame:?}");
1309 assert!(debug.contains("VideoFrame"));
1310 assert!(debug.contains("640"));
1311 assert!(debug.contains("480"));
1312 assert!(debug.contains("Rgba"));
1313 }
1314
1315 #[test]
1316 fn test_display() {
1317 let mut frame = VideoFrame::empty(1920, 1080, PixelFormat::Yuv420p).unwrap();
1318 frame.set_key_frame(true);
1319
1320 let display = format!("{frame}");
1321 assert!(display.contains("1920x1080"));
1322 assert!(display.contains("yuv420p"));
1323 assert!(display.contains("[KEY]"));
1324 }
1325
1326 #[test]
1327 fn test_display_non_keyframe() {
1328 let frame = VideoFrame::empty(1920, 1080, PixelFormat::Rgba).unwrap();
1329 let display = format!("{frame}");
1330 assert!(!display.contains("[KEY]"));
1331 }
1332
1333 // ==========================================================================
1334 // Edge Case Tests
1335 // ==========================================================================
1336
1337 #[test]
1338 fn test_odd_dimensions_yuv420p() {
1339 // Odd dimensions require proper rounding for chroma planes
1340 let frame = VideoFrame::empty(641, 481, PixelFormat::Yuv420p).unwrap();
1341
1342 // Y plane should be full size
1343 assert_eq!(frame.plane(0).map(|p| p.len()), Some(641 * 481));
1344
1345 // U/V planes should be (641+1)/2 * (481+1)/2 = 321 * 241
1346 assert_eq!(frame.plane(1).map(|p| p.len()), Some(321 * 241));
1347 assert_eq!(frame.plane(2).map(|p| p.len()), Some(321 * 241));
1348 }
1349
1350 #[test]
1351 fn test_small_frame() {
1352 let frame = VideoFrame::empty(1, 1, PixelFormat::Rgba).unwrap();
1353 assert_eq!(frame.total_size(), 4);
1354 assert_eq!(frame.plane(0).map(|p| p.len()), Some(4));
1355 }
1356
1357 #[test]
1358 fn test_10bit_yuv420p() {
1359 let frame = VideoFrame::empty(640, 480, PixelFormat::Yuv420p10le).unwrap();
1360 assert_eq!(frame.num_planes(), 3);
1361
1362 // 10-bit uses 2 bytes per sample
1363 assert_eq!(frame.stride(0), Some(640 * 2));
1364 assert_eq!(frame.stride(1), Some(320 * 2));
1365 assert_eq!(frame.plane(0).map(|p| p.len()), Some(640 * 480 * 2));
1366 assert_eq!(frame.plane(1).map(|p| p.len()), Some(320 * 240 * 2));
1367 }
1368
1369 #[test]
1370 fn test_p010le() {
1371 let frame = VideoFrame::empty(640, 480, PixelFormat::P010le).unwrap();
1372 assert_eq!(frame.num_planes(), 2);
1373
1374 // 10-bit semi-planar: Y and UV interleaved, both 2 bytes per sample
1375 assert_eq!(frame.stride(0), Some(640 * 2));
1376 assert_eq!(frame.stride(1), Some(640 * 2));
1377 }
1378
1379 #[test]
1380 fn test_rgb24() {
1381 let frame = VideoFrame::empty(640, 480, PixelFormat::Rgb24).unwrap();
1382 assert_eq!(frame.num_planes(), 1);
1383 assert_eq!(frame.stride(0), Some(640 * 3));
1384 assert_eq!(frame.total_size(), 640 * 480 * 3);
1385 }
1386
1387 #[test]
1388 fn test_bgr24() {
1389 let frame = VideoFrame::empty(640, 480, PixelFormat::Bgr24).unwrap();
1390 assert_eq!(frame.num_planes(), 1);
1391 assert_eq!(frame.stride(0), Some(640 * 3));
1392 }
1393
1394 #[test]
1395 fn test_bgra() {
1396 let frame = VideoFrame::empty(640, 480, PixelFormat::Bgra).unwrap();
1397 assert_eq!(frame.num_planes(), 1);
1398 assert_eq!(frame.stride(0), Some(640 * 4));
1399 }
1400
1401 #[test]
1402 fn test_other_format_returns_error() {
1403 // Unknown formats cannot be allocated - memory layout is unknown
1404 let result = VideoFrame::empty(640, 480, PixelFormat::Other(999));
1405 assert!(result.is_err());
1406 assert_eq!(
1407 result.unwrap_err(),
1408 FrameError::UnsupportedPixelFormat(PixelFormat::Other(999))
1409 );
1410 }
1411}