1use crate::{Error, ErrorKind, Limits, Result, SampleRange};
2
3#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
5#[non_exhaustive]
6pub enum Container {
7 Mp4,
8 WebM,
11}
12
13impl Container {
14 pub const ALL: [Self; 2] = [Self::Mp4, Self::WebM];
16
17 pub const fn name(self) -> &'static str {
19 match self {
20 Self::Mp4 => "mp4",
21 Self::WebM => "webm",
22 }
23 }
24
25 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 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
43#[non_exhaustive]
44pub enum Codec {
45 UncompressedVideo,
46 H264,
51 Hevc,
52 Av1,
53 Vp8,
57 Vp9,
61 Aac,
62 Opus,
67 Vorbis,
71}
72
73#[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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
86#[non_exhaustive]
87pub enum ColorRange {
88 Full,
89 Limited,
90}
91
92#[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#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct Plane {
114 pub data: Vec<u8>,
115 pub stride: usize,
116}
117
118#[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#[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}