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