Skip to main content

zvidlib_core/
media.rs

1use crate::{Error, ErrorKind, Limits, Result, SampleRange};
2
3/// Container formats understood by capability discovery.
4#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5#[non_exhaustive]
6pub enum Container {
7    Mp4,
8    /// The WebM subset of Matroska, read by `crate::WebmDemuxer` and
9    /// written by `crate::webm::WebmMuxer`.
10    WebM,
11}
12
13impl Container {
14    /// Every container, in the order capability discovery reports them.
15    pub const ALL: [Self; 2] = [Self::Mp4, Self::WebM];
16
17    /// The lowercase name the browser API uses: `"mp4"` or `"webm"`.
18    pub const fn name(self) -> &'static str {
19        match self {
20            Self::Mp4 => "mp4",
21            Self::WebM => "webm",
22        }
23    }
24
25    /// The MIME type of a file in this container.
26    pub const fn mime_type(self) -> &'static str {
27        match self {
28            Self::Mp4 => "video/mp4",
29            Self::WebM => "video/webm",
30        }
31    }
32
33    /// Looks a container up by [`Self::name`], ignoring ASCII case.
34    pub fn from_name(name: &str) -> Option<Self> {
35        Self::ALL
36            .into_iter()
37            .find(|container| container.name().eq_ignore_ascii_case(name))
38    }
39}
40
41/// Normalized codec identifiers used across containers and backends.
42#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
43#[non_exhaustive]
44pub enum Codec {
45    UncompressedVideo,
46    /// H.264/AVC video. MP4 passthrough only: `crate::Mp4Demuxer` reads
47    /// `avc1`/`avc3` sample entries and `crate::mp4::Mp4Muxer` writes `avc1`,
48    /// with an `avcC` box as the decoder configuration. No encoder or decoder
49    /// factory accepts it.
50    H264,
51    Hevc,
52    Av1,
53    /// VP8 video, as carried in WebM (`V_VP8`). It has no decoder
54    /// configuration record. `crate::native_vp8_video_decoder_factory`
55    /// decodes it; no encoder factory accepts it.
56    Vp8,
57    /// VP9 video, carried in MP4 as a `vp09` sample entry whose decoder
58    /// configuration is the complete `vpcC` box (see
59    /// [`crate::Vp9CodecConfig`]).
60    Vp9,
61    Aac,
62    /// Opus audio (RFC 6716). MP4 carries it in an `Opus` sample entry whose
63    /// decoder configuration is a `dOps` box; Matroska and WebM carry the
64    /// RFC 7845 `OpusHead` identification header as `CodecPrivate`. See
65    /// `crate::OpusHead`.
66    Opus,
67    /// Vorbis I audio. There is no widely supported MP4 mapping for Vorbis, so
68    /// it has no MP4 sample entry; Matroska and WebM carry its three setup
69    /// headers Xiph-laced as `CodecPrivate`. See `crate::VorbisConfig`.
70    Vorbis,
71}
72
73/// CPU video pixel formats supported by the portable media layer.
74#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
75#[non_exhaustive]
76pub enum PixelFormat {
77    Rgba8,
78    Bgra8,
79    Rgb8,
80    Gray8,
81    Yuv420p8,
82}
83
84/// Whether component values use the full numeric range or video range.
85#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
86#[non_exhaustive]
87pub enum ColorRange {
88    Full,
89    Limited,
90}
91
92/// Validated coded dimensions for a video frame.
93#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
94pub struct VideoDimensions {
95    pub width: u32,
96    pub height: u32,
97}
98
99impl VideoDimensions {
100    pub fn new(width: u32, height: u32, limits: &Limits) -> Result<Self> {
101        if width == 0 || height == 0 {
102            return Err(invalid_media("video dimensions must be nonzero"));
103        }
104        if width > limits.max_width || height > limits.max_height {
105            return Err(resource_limit("video dimensions exceed configured limits"));
106        }
107        Ok(Self { width, height })
108    }
109}
110
111/// One owned CPU image plane with an explicit row stride.
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct Plane {
114    pub data: Vec<u8>,
115    pub stride: usize,
116}
117
118/// A validated, owned CPU video frame.
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct VideoFrame {
121    pub dimensions: VideoDimensions,
122    pub pixel_format: PixelFormat,
123    pub color_range: ColorRange,
124    pub planes: Vec<Plane>,
125}
126
127impl VideoFrame {
128    pub fn new(
129        dimensions: VideoDimensions,
130        pixel_format: PixelFormat,
131        color_range: ColorRange,
132        planes: Vec<Plane>,
133        limits: &Limits,
134    ) -> Result<Self> {
135        let layouts = required_plane_layouts(dimensions, pixel_format)?;
136        if planes.len() != layouts.len() {
137            return Err(invalid_media("plane count does not match the pixel format"));
138        }
139
140        let mut allocation = 0_u64;
141        for (plane, (row_bytes, rows)) in planes.iter().zip(layouts) {
142            if plane.stride < row_bytes {
143                return Err(invalid_media("a video plane stride is too small"));
144            }
145            let required = plane
146                .stride
147                .checked_mul(rows)
148                .ok_or_else(|| resource_limit("video plane size overflow"))?;
149            if plane.data.len() < required {
150                return Err(invalid_media(
151                    "a video plane is shorter than its stride and height",
152                ));
153            }
154            allocation =
155                allocation
156                    .checked_add(u64::try_from(plane.data.len()).map_err(|_| {
157                        resource_limit("video frame allocation cannot be represented")
158                    })?)
159                    .ok_or_else(|| resource_limit("video frame allocation overflow"))?;
160        }
161        if allocation > limits.max_allocation_bytes {
162            return Err(resource_limit("video frame exceeds the allocation limit"));
163        }
164
165        Ok(Self {
166            dimensions,
167            pixel_format,
168            color_range,
169            planes,
170        })
171    }
172}
173
174/// Interleaved, normalized `f32` audio samples for an exact sample interval.
175#[derive(Clone, Debug, PartialEq)]
176pub struct AudioBuffer {
177    pub range: SampleRange,
178    pub sample_rate: u32,
179    pub channels: u16,
180    pub samples: Vec<f32>,
181}
182
183impl AudioBuffer {
184    pub fn new(
185        range: SampleRange,
186        sample_rate: u32,
187        channels: u16,
188        samples: Vec<f32>,
189        limits: &Limits,
190    ) -> Result<Self> {
191        if sample_rate == 0 || sample_rate > limits.max_sample_rate {
192            return Err(resource_limit(
193                "audio sample rate is outside configured limits",
194            ));
195        }
196        if channels == 0 || channels > limits.max_audio_channels {
197            return Err(resource_limit(
198                "audio channel count is outside configured limits",
199            ));
200        }
201        let expected = range
202            .len()
203            .checked_mul(u64::from(channels))
204            .ok_or_else(|| resource_limit("audio sample count overflow"))?;
205        if u64::try_from(samples.len()).ok() != Some(expected) {
206            return Err(invalid_media(
207                "audio data length does not match its range and channels",
208            ));
209        }
210        let bytes = expected
211            .checked_mul(std::mem::size_of::<f32>() as u64)
212            .ok_or_else(|| resource_limit("audio allocation size overflow"))?;
213        if bytes > limits.max_allocation_bytes {
214            return Err(resource_limit("audio buffer exceeds the allocation limit"));
215        }
216        Ok(Self {
217            range,
218            sample_rate,
219            channels,
220            samples,
221        })
222    }
223}
224
225#[doc(hidden)]
226pub fn required_plane_layouts(
227    dimensions: VideoDimensions,
228    pixel_format: PixelFormat,
229) -> Result<Vec<(usize, usize)>> {
230    let width = usize::try_from(dimensions.width)
231        .map_err(|_| resource_limit("video width cannot be represented"))?;
232    let height = usize::try_from(dimensions.height)
233        .map_err(|_| resource_limit("video height cannot be represented"))?;
234    let packed = |bytes_per_pixel: usize| -> Result<Vec<(usize, usize)>> {
235        Ok(vec![(
236            width
237                .checked_mul(bytes_per_pixel)
238                .ok_or_else(|| resource_limit("video row size overflow"))?,
239            height,
240        )])
241    };
242    match pixel_format {
243        PixelFormat::Rgba8 | PixelFormat::Bgra8 => packed(4),
244        PixelFormat::Rgb8 => packed(3),
245        PixelFormat::Gray8 => packed(1),
246        PixelFormat::Yuv420p8 => Ok(vec![
247            (width, height),
248            (width.div_ceil(2), height.div_ceil(2)),
249            (width.div_ceil(2), height.div_ceil(2)),
250        ]),
251    }
252}
253
254fn invalid_media(message: &str) -> Error {
255    Error::new(ErrorKind::InvalidInput, message)
256}
257
258fn resource_limit(message: &str) -> Error {
259    Error::new(ErrorKind::ResourceLimit, message)
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn dimensions_apply_resource_limits() {
268        let limits = Limits {
269            max_width: 1_920,
270            max_height: 1_080,
271            ..Limits::default()
272        };
273        assert!(VideoDimensions::new(1_920, 1_080, &limits).is_ok());
274        assert_eq!(
275            VideoDimensions::new(3_840, 2_160, &limits)
276                .unwrap_err()
277                .kind(),
278            ErrorKind::ResourceLimit
279        );
280    }
281
282    #[test]
283    fn packed_frame_validates_stride_and_length() {
284        let limits = Limits::default();
285        let dimensions = VideoDimensions::new(2, 2, &limits).unwrap();
286        let frame = VideoFrame::new(
287            dimensions,
288            PixelFormat::Rgba8,
289            ColorRange::Full,
290            vec![Plane {
291                data: vec![0; 16],
292                stride: 8,
293            }],
294            &limits,
295        );
296        assert!(frame.is_ok());
297
298        let error = VideoFrame::new(
299            dimensions,
300            PixelFormat::Rgba8,
301            ColorRange::Full,
302            vec![Plane {
303                data: vec![0; 14],
304                stride: 8,
305            }],
306            &limits,
307        )
308        .unwrap_err();
309        assert_eq!(error.kind(), ErrorKind::InvalidInput);
310    }
311
312    #[test]
313    fn planar_frame_supports_odd_chroma_dimensions() {
314        let limits = Limits::default();
315        let dimensions = VideoDimensions::new(3, 3, &limits).unwrap();
316        let frame = VideoFrame::new(
317            dimensions,
318            PixelFormat::Yuv420p8,
319            ColorRange::Limited,
320            vec![
321                Plane {
322                    data: vec![0; 9],
323                    stride: 3,
324                },
325                Plane {
326                    data: vec![0; 4],
327                    stride: 2,
328                },
329                Plane {
330                    data: vec![0; 4],
331                    stride: 2,
332                },
333            ],
334            &limits,
335        );
336        assert!(frame.is_ok());
337    }
338
339    #[test]
340    fn audio_length_must_match_range_and_channels() {
341        let limits = Limits::default();
342        let range = SampleRange::new(100, 103).unwrap();
343        assert!(AudioBuffer::new(range, 48_000, 2, vec![0.0; 6], &limits).is_ok());
344        let error = AudioBuffer::new(range, 48_000, 2, vec![0.0; 5], &limits).unwrap_err();
345        assert_eq!(error.kind(), ErrorKind::InvalidInput);
346    }
347}