alpha2_media/io.rs
1//! Platform-neutral media I/O contracts.
2//!
3//! Adapters translate `FFmpeg`, `WebCodecs`, or other host objects at this boundary;
4//! none of those platform types are part of these contracts.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::{error::Error, fmt};
9
10use alpha2_time::{FrameRate, MediaTime};
11
12use crate::{ColorMetadata, SourceId};
13
14/// Rectangle inside a coded video frame.
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
16pub struct Rect {
17 /// Horizontal origin in coded pixels.
18 pub x: u32,
19 /// Vertical origin in coded pixels.
20 pub y: u32,
21 /// Visible width.
22 pub width: u32,
23 /// Visible height.
24 pub height: u32,
25}
26
27/// Clockwise rotation to apply when displaying a frame.
28///
29/// This is the convention of the `WebCodecs` `VideoFrame.rotation`. A binding
30/// whose source states rotation another way converts it: `FFmpeg`'s display
31/// matrix angle is counter-clockwise, so a binding reports `(-angle) mod 360`.
32#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
33pub enum Rotation {
34 /// No rotation.
35 #[default]
36 Deg0,
37 /// Clockwise quarter turn.
38 Deg90,
39 /// Half turn.
40 Deg180,
41 /// Clockwise three-quarter turn, i.e. a counter-clockwise quarter turn.
42 Deg270,
43}
44
45impl Rotation {
46 /// Clockwise quarter turns, `0..4`.
47 #[must_use]
48 pub const fn quarter_turns(self) -> u32 {
49 match self {
50 Self::Deg0 => 0,
51 Self::Deg90 => 1,
52 Self::Deg180 => 2,
53 Self::Deg270 => 3,
54 }
55 }
56
57 /// The rotation of `quarter_turns` clockwise quarter turns, of any sign.
58 #[must_use]
59 pub const fn from_quarter_turns(quarter_turns: i32) -> Self {
60 match quarter_turns.rem_euclid(4) {
61 0 => Self::Deg0,
62 1 => Self::Deg90,
63 2 => Self::Deg180,
64 _ => Self::Deg270,
65 }
66 }
67
68 /// Whether the rotation exchanges width and height.
69 #[must_use]
70 pub const fn swaps_axes(self) -> bool {
71 matches!(self, Self::Deg90 | Self::Deg270)
72 }
73}
74
75/// How to show a decoded picture upright: mirror it left to right when
76/// `mirror` is set, then turn it clockwise by `rotation`.
77///
78/// The two together reach every orientation a container or an image can
79/// declare: the eight of the Exif `Orientation` tag, every display matrix
80/// whose angle is a multiple of 90 degrees, a Matroska projection's pose. A binding
81/// converts its source's own statement to this one; `FFmpeg`'s display matrix
82/// mirrors after it rotates, and counter-clockwise.
83#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
84pub struct Orientation {
85 /// Clockwise rotation, applied after the mirror.
86 pub rotation: Rotation,
87 /// Whether the stored picture is mirrored left to right before it turns.
88 pub mirror: bool,
89}
90
91impl Orientation {
92 /// Shown as stored.
93 pub const UPRIGHT: Self = Self {
94 rotation: Rotation::Deg0,
95 mirror: false,
96 };
97
98 /// A rotation without a mirror.
99 #[must_use]
100 pub const fn rotated(rotation: Rotation) -> Self {
101 Self {
102 rotation,
103 mirror: false,
104 }
105 }
106
107 /// The orientation an Exif `Orientation` value (1 to 8) declares; `None`
108 /// for any other value. 2, 4, 5, and 7 mirror.
109 #[must_use]
110 pub const fn from_exif(value: u16) -> Option<Self> {
111 let (rotation, mirror) = match value {
112 1 => (Rotation::Deg0, false),
113 2 => (Rotation::Deg0, true),
114 3 => (Rotation::Deg180, false),
115 4 => (Rotation::Deg180, true),
116 5 => (Rotation::Deg270, true),
117 6 => (Rotation::Deg90, false),
118 7 => (Rotation::Deg90, true),
119 8 => (Rotation::Deg270, false),
120 _ => return None,
121 };
122 Some(Self { rotation, mirror })
123 }
124
125 /// Whether the picture is shown as stored.
126 #[must_use]
127 pub const fn is_upright(self) -> bool {
128 matches!(self.rotation, Rotation::Deg0) && !self.mirror
129 }
130
131 /// The displayed size of a stored `width` x `height` picture.
132 #[must_use]
133 pub const fn displayed_size(self, width: u32, height: u32) -> (u32, u32) {
134 if self.rotation.swaps_axes() {
135 (height, width)
136 } else {
137 (width, height)
138 }
139 }
140}
141
142/// Reduced pixel-width to pixel-height ratio.
143#[derive(Copy, Clone, Debug, Eq, PartialEq)]
144pub struct SampleAspectRatio {
145 /// Ratio numerator.
146 pub num: u32,
147 /// Ratio denominator, never zero.
148 pub den: u32,
149}
150
151impl Default for SampleAspectRatio {
152 fn default() -> Self {
153 Self { num: 1, den: 1 }
154 }
155}
156
157impl SampleAspectRatio {
158 /// The ratio `num`:`den` in lowest terms, or `None` for a ratio with a
159 /// zero term, which declares nothing (`FFmpeg` writes an unknown ratio
160 /// as 0/1) or does not fit.
161 #[must_use]
162 pub fn new(num: u64, den: u64) -> Option<Self> {
163 if num == 0 || den == 0 {
164 return None;
165 }
166 let divisor = greatest_common_divisor(num, den);
167 Some(Self {
168 num: u32::try_from(num / divisor).ok()?,
169 den: u32::try_from(den / divisor).ok()?,
170 })
171 }
172
173 /// The ratio of the pixels of a `visible` picture shown at `displayed`
174 /// size (width, height), as a `WebCodecs` frame's display size says it.
175 ///
176 /// A display size is the stored size with one side stretched by the
177 /// ratio and rounded to whole pixels, so it cannot say the ratio exactly
178 /// (128x96 of 4:3 pixels shows 171x96, not 170 2/3). The ratio is the
179 /// simplest fraction that rounds to that size, which is the declared one
180 /// wherever a declared ratio's terms are small (4:3, 64:45, 10:11), and
181 /// otherwise within half a displayed pixel of the size.
182 #[must_use]
183 pub fn of_display(visible: (u32, u32), displayed: (u32, u32)) -> Option<Self> {
184 let [
185 visible_width,
186 visible_height,
187 displayed_width,
188 displayed_height,
189 ] = [visible.0, visible.1, displayed.0, displayed.1].map(u128::from);
190 if visible_width == 0
191 || visible_height == 0
192 || displayed_width == 0
193 || displayed_height == 0
194 {
195 return None;
196 }
197 // A side the display stretched is known to half a pixel; one it kept is exact.
198 let slack = |shown: u128, stored: u128| u128::from(shown != stored);
199 let width_slack = slack(displayed_width, visible_width);
200 let height_slack = slack(displayed_height, visible_height);
201 // ratio = (displayed_width / visible_width) / (displayed_height / visible_height), in
202 // halves of a pixel.
203 let low = (
204 (2 * displayed_width - width_slack) * visible_height,
205 (2 * displayed_height + height_slack) * visible_width,
206 );
207 let high = (
208 (2 * displayed_width + width_slack) * visible_height,
209 (2 * displayed_height - height_slack) * visible_width,
210 );
211 let (num, den) = simplest_fraction_between(low, high)?;
212 Self::new(u64::try_from(num).ok()?, u64::try_from(den).ok()?)
213 }
214
215 /// The size a stored `width` x `height` picture of these pixels is shown
216 /// at in square pixels, as a browser demuxer reports it: the side the
217 /// pixels are longer along stretched, rounded half up.
218 #[must_use]
219 pub fn square_pixel_size(self, width: u32, height: u32) -> (u32, u32) {
220 let stretch = |side: u32, by: u32, of: u32| {
221 let scaled =
222 (u64::from(side) * u64::from(by) * 2 + u64::from(of)) / (2 * u64::from(of));
223 u32::try_from(scaled).unwrap_or(u32::MAX)
224 };
225 if self.num == 0 || self.den == 0 {
226 return (width, height);
227 }
228 match self.num.cmp(&self.den) {
229 std::cmp::Ordering::Greater => (stretch(width, self.num, self.den), height),
230 std::cmp::Ordering::Less => (width, stretch(height, self.den, self.num)),
231 std::cmp::Ordering::Equal => (width, height),
232 }
233 }
234}
235
236/// The fraction with the smallest denominator in the closed interval
237/// `low`..=`high` of positive fractions (numerator, denominator), by the
238/// continued fractions of its ends.
239fn simplest_fraction_between(low: (u128, u128), high: (u128, u128)) -> Option<(u128, u128)> {
240 let ((low_num, low_den), (high_num, high_den)) = (low, high);
241 if low_den == 0 || high_den == 0 || low_num * high_den > high_num * low_den {
242 return None;
243 }
244 let whole = low_num / low_den;
245 if whole * low_den == low_num {
246 return Some((whole, 1));
247 }
248 if (whole + 1) * high_den <= high_num {
249 return Some((whole + 1, 1));
250 }
251 // Both ends lie strictly between `whole` and `whole + 1`: x = whole + 1/y, with y between the
252 // reciprocals of the fractional parts.
253 let (num, den) = simplest_fraction_between(
254 (high_den, high_num - whole * high_den),
255 (low_den, low_num - whole * low_den),
256 )?;
257 Some((whole * num + den, num))
258}
259
260fn greatest_common_divisor(mut a: u64, mut b: u64) -> u64 {
261 while b != 0 {
262 (a, b) = (b, a % b);
263 }
264 a
265}
266
267/// Plane organization delivered by a decoder.
268///
269/// A [`Decoder`] delivers only the layouts the render stage consumes directly:
270/// planar or semi-planar YCbCr of any chroma subsampling and bit depth that
271/// [`YuvFormat`] describes, 8-bit RGBA, and linear-light half-float RGBA for
272/// sources with more range than 8 bits hold. Every other decoded layout, such as
273/// packed RGB orders, palettes, or YCbCr with alpha, must be converted by the
274/// decoder before it is handed over, and is then reported as
275/// [`PixelFormat::Rgba8`]. The bundled native decoder does this with
276/// `libswscale`; a custom decoder is responsible for the same conversion.
277#[derive(Copy, Clone, Debug, Eq, PartialEq)]
278#[non_exhaustive]
279pub enum PixelFormat {
280 /// Planar or semi-planar YCbCr, one [`Plane`] per stored plane.
281 Yuv(YuvFormat),
282 /// Straight-alpha 8-bit red, green, blue, alpha in one plane.
283 Rgba8,
284 /// Premultiplied linear-light red, green, blue, alpha in one plane, each
285 /// a little-endian IEEE 754 half float, in BT.709 primaries with `1.0` at
286 /// SDR white. Values above `1.0` and below `0.0` are allowed; the frame's
287 /// color metadata does not apply.
288 LinearRgbaF16,
289}
290
291/// How a planar or semi-planar YCbCr frame stores its samples.
292///
293/// Luma is always the first plane. Chroma follows as separate U and V planes,
294/// or as one plane of interleaved U, V pairs. Each chroma plane covers
295/// `ceil(width / 2^chroma_shift_x)` by `ceil(height / 2^chroma_shift_y)`
296/// samples. A sample of more than eight bits is a little-endian `u16`. A
297/// format with [`alpha`](Self::alpha) stores one more plane after the
298/// chroma: straight, full-range alpha at the luma's size and depth.
299#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
300pub struct YuvFormat {
301 /// Log2 of the horizontal chroma subsampling: 0 for 4:4:4, 1 for 4:2:x.
302 pub chroma_shift_x: u8,
303 /// Log2 of the vertical chroma subsampling: 1 for 4:2:0, 0 otherwise.
304 pub chroma_shift_y: u8,
305 /// Significant bits per sample, 8 through 16.
306 pub bit_depth: u8,
307 /// Whether U and V interleave in one plane, as NV12 and P010 do.
308 pub interleaved_chroma: bool,
309 /// Whether a sample of fewer than 16 bits sits in the high bits of its
310 /// `u16`, as P010's do, rather than the low bits, as `yuv420p10le`'s do.
311 pub msb_aligned: bool,
312 /// Whether an alpha plane follows the chroma, as `yuva444p16le`'s does.
313 pub alpha: bool,
314}
315
316impl YuvFormat {
317 /// 8-bit planar 4:2:0 (`yuv420p`).
318 pub const I420: Self = Self::planar(1, 1, 8);
319 /// 8-bit 4:2:0 with interleaved chroma (`nv12`).
320 pub const NV12: Self = Self {
321 interleaved_chroma: true,
322 ..Self::I420
323 };
324 /// 10-bit 4:2:0 with interleaved, high-aligned chroma (`p010le`).
325 pub const P010: Self = Self {
326 bit_depth: 10,
327 msb_aligned: true,
328 ..Self::NV12
329 };
330
331 /// Planar YCbCr with low-aligned samples.
332 #[must_use]
333 pub const fn planar(chroma_shift_x: u8, chroma_shift_y: u8, bit_depth: u8) -> Self {
334 Self {
335 chroma_shift_x,
336 chroma_shift_y,
337 bit_depth,
338 interleaved_chroma: false,
339 msb_aligned: false,
340 alpha: false,
341 }
342 }
343
344 /// The same format with an alpha plane after the chroma.
345 #[must_use]
346 pub const fn with_alpha(self) -> Self {
347 Self {
348 alpha: true,
349 ..self
350 }
351 }
352
353 /// Whether the renderer can sample this format: 8 to 16 bits, chroma
354 /// subsampled by at most four on either axis, and high alignment only
355 /// where there are spare bits to align in.
356 #[must_use]
357 pub const fn is_supported(self) -> bool {
358 self.bit_depth >= 8
359 && self.bit_depth <= 16
360 && self.chroma_shift_x <= 2
361 && self.chroma_shift_y <= 2
362 && (!self.msb_aligned || self.bit_depth > 8)
363 }
364
365 /// Bytes each stored sample takes: one up to 8 bits, two above.
366 #[must_use]
367 pub const fn bytes_per_sample(self) -> u32 {
368 if self.bit_depth > 8 { 2 } else { 1 }
369 }
370
371 /// Number of stored planes: luma and chroma (three, or two with
372 /// interleaved chroma), then alpha when the format has it.
373 #[must_use]
374 pub const fn plane_count(self) -> usize {
375 self.color_plane_count() + if self.alpha { 1 } else { 0 }
376 }
377
378 /// Number of planes that hold luma and chroma: three, or two with
379 /// interleaved chroma. The alpha plane, if any, is the one after them.
380 #[must_use]
381 pub const fn color_plane_count(self) -> usize {
382 if self.interleaved_chroma { 2 } else { 3 }
383 }
384
385 /// Chroma plane dimensions, in sample positions, for a coded frame size.
386 #[must_use]
387 pub const fn chroma_size(self, width: u32, height: u32) -> (u32, u32) {
388 (
389 width.div_ceil(1 << self.chroma_shift_x),
390 height.div_ceil(1 << self.chroma_shift_y),
391 )
392 }
393}
394
395/// Chroma sample location for subsampled YUV.
396#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
397pub enum ChromaLocation {
398 /// Adapter could not determine a location.
399 #[default]
400 Unspecified,
401 /// MPEG-2/most video: horizontally left, vertically centered.
402 Left,
403 /// JPEG-style centered chroma.
404 Center,
405 /// Top-left co-sited chroma.
406 TopLeft,
407}
408
409/// One decoded image plane.
410///
411/// The bytes are shared rather than owned outright, so a decoder can hand
412/// over its own reference-counted frame memory without copying it, and a
413/// cloned frame shares its planes.
414#[derive(Clone, Debug, Eq, PartialEq)]
415pub struct Plane {
416 /// Plane bytes, including row padding: `stride * height` of them.
417 pub bytes: bytes::Bytes,
418 /// Bytes between adjacent rows.
419 pub stride: u32,
420 /// Plane width in sample positions; an interleaved chroma plane holds a
421 /// U, V pair at each.
422 pub width: u32,
423 /// Plane height in samples.
424 pub height: u32,
425}
426
427/// Largest frame edge, in pixels, anything in alpha2 decodes, renders, or
428/// composes a project at.
429pub const MAX_FRAME_DIMENSION: u32 = 16_384;
430/// Largest frame area, in pixels, anything in alpha2 decodes, renders, or
431/// composes a project at: a decoder refuses a larger frame before it
432/// allocates one.
433pub const MAX_FRAME_PIXELS: u64 = 67_108_864;
434
435/// Whether a `width` x `height` frame is within [`MAX_FRAME_DIMENSION`] and
436/// [`MAX_FRAME_PIXELS`].
437#[must_use]
438pub const fn frame_size_supported(width: u32, height: u32) -> bool {
439 width <= MAX_FRAME_DIMENSION
440 && height <= MAX_FRAME_DIMENSION
441 && (width as u64) * (height as u64) <= MAX_FRAME_PIXELS
442}
443
444/// Host-independent decoded video frame.
445#[derive(Clone, Debug, PartialEq)]
446pub struct DecodedVideoFrame {
447 /// Presentation timestamp.
448 pub pts: MediaTime,
449 /// Optional decode timestamp.
450 pub dts: Option<MediaTime>,
451 /// Nominal frame duration when known.
452 pub duration: Option<MediaTime>,
453 /// The visible crop within the coded picture, in coded pixels.
454 pub visible_rect: Rect,
455 /// Pixel aspect ratio.
456 pub sample_aspect_ratio: SampleAspectRatio,
457 /// How the picture is shown upright.
458 pub orientation: Orientation,
459 /// Plane layout.
460 pub pixel_format: PixelFormat,
461 /// Chroma siting.
462 pub chroma_location: ChromaLocation,
463 /// Declared color metadata: the project's declaration, else the frame's
464 /// or stream's tags. `None` when nothing declares one: the renderer then
465 /// applies [`crate::ColorMetadata::UNDECLARED`] and reports it.
466 pub color: Option<ColorMetadata>,
467 /// Image planes in the order [`Self::pixel_format`] stores them.
468 pub planes: Vec<Plane>,
469}
470
471/// Most planes an [`AudioBlock`] carries: a 22.2 layout's 24, the most of
472/// any layout `FFmpeg` names.
473///
474/// Neither binding downmixes a source with more: both refuse it, so no
475/// source is mixed one way natively and another in the browser.
476pub const MAX_AUDIO_CHANNELS: usize = 24;
477
478/// The loudspeaker one plane of an [`AudioBlock`] feeds.
479///
480/// The positions are `FFmpeg`'s named channels (`AVChannel`), which take in
481/// the WAVE / `WAVEFORMATEXTENSIBLE` mask positions, Apple's channel labels,
482/// and ISO/IEC 23001-8's. [`Speaker::name`] is `FFmpeg`'s abbreviation.
483#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
484pub enum Speaker {
485 /// Front left (`FL`).
486 FrontLeft,
487 /// Front right (`FR`).
488 FrontRight,
489 /// Front center (`FC`): a mono source's one channel, and most films' dialogue.
490 FrontCenter,
491 /// Low-frequency effects (`LFE`).
492 LowFrequency,
493 /// Back left (`BL`).
494 BackLeft,
495 /// Back right (`BR`).
496 BackRight,
497 /// Front left of center (`FLC`).
498 FrontLeftOfCenter,
499 /// Front right of center (`FRC`).
500 FrontRightOfCenter,
501 /// Back center (`BC`).
502 BackCenter,
503 /// Side left (`SL`).
504 SideLeft,
505 /// Side right (`SR`).
506 SideRight,
507 /// Top center (`TC`).
508 TopCenter,
509 /// Top front left (`TFL`).
510 TopFrontLeft,
511 /// Top front center (`TFC`).
512 TopFrontCenter,
513 /// Top front right (`TFR`).
514 TopFrontRight,
515 /// Top back left (`TBL`).
516 TopBackLeft,
517 /// Top back center (`TBC`).
518 TopBackCenter,
519 /// Top back right (`TBR`).
520 TopBackRight,
521 /// Left of a matrix-encoded stereo downmix (`DL`, Lt).
522 DownmixLeft,
523 /// Right of a matrix-encoded stereo downmix (`DR`, Rt).
524 DownmixRight,
525 /// Wide left (`WL`).
526 WideLeft,
527 /// Wide right (`WR`).
528 WideRight,
529 /// Surround direct left (`SDL`).
530 SurroundDirectLeft,
531 /// Surround direct right (`SDR`).
532 SurroundDirectRight,
533 /// Second low-frequency effects (`LFE2`).
534 LowFrequency2,
535 /// Top side left (`TSL`).
536 TopSideLeft,
537 /// Top side right (`TSR`).
538 TopSideRight,
539 /// Bottom front center (`BFC`).
540 BottomFrontCenter,
541 /// Bottom front left (`BFL`).
542 BottomFrontLeft,
543 /// Bottom front right (`BFR`).
544 BottomFrontRight,
545 /// Side surround left (`SSL`).
546 SideSurroundLeft,
547 /// Side surround right (`SSR`).
548 SideSurroundRight,
549 /// Top surround left (`TTL`).
550 TopSurroundLeft,
551 /// Top surround right (`TTR`).
552 TopSurroundRight,
553 /// Binaural left (`BIL`).
554 BinauralLeft,
555 /// Binaural right (`BIR`).
556 BinauralRight,
557 /// A channel no loudspeaker position names (`UNK`): unknown, unused, or
558 /// one ambisonic component. The fold leaves it out.
559 Unnamed,
560}
561
562impl Speaker {
563 /// Every speaker with its `FFmpeg` name and channel number (`AVChannel`);
564 /// [`Speaker::Unnamed`] is `FFmpeg`'s `AV_CHAN_UNKNOWN`.
565 const TABLE: [(Self, &'static str, u32); 37] = [
566 (Self::FrontLeft, "FL", 0),
567 (Self::FrontRight, "FR", 1),
568 (Self::FrontCenter, "FC", 2),
569 (Self::LowFrequency, "LFE", 3),
570 (Self::BackLeft, "BL", 4),
571 (Self::BackRight, "BR", 5),
572 (Self::FrontLeftOfCenter, "FLC", 6),
573 (Self::FrontRightOfCenter, "FRC", 7),
574 (Self::BackCenter, "BC", 8),
575 (Self::SideLeft, "SL", 9),
576 (Self::SideRight, "SR", 10),
577 (Self::TopCenter, "TC", 11),
578 (Self::TopFrontLeft, "TFL", 12),
579 (Self::TopFrontCenter, "TFC", 13),
580 (Self::TopFrontRight, "TFR", 14),
581 (Self::TopBackLeft, "TBL", 15),
582 (Self::TopBackCenter, "TBC", 16),
583 (Self::TopBackRight, "TBR", 17),
584 (Self::DownmixLeft, "DL", 29),
585 (Self::DownmixRight, "DR", 30),
586 (Self::WideLeft, "WL", 31),
587 (Self::WideRight, "WR", 32),
588 (Self::SurroundDirectLeft, "SDL", 33),
589 (Self::SurroundDirectRight, "SDR", 34),
590 (Self::LowFrequency2, "LFE2", 35),
591 (Self::TopSideLeft, "TSL", 36),
592 (Self::TopSideRight, "TSR", 37),
593 (Self::BottomFrontCenter, "BFC", 38),
594 (Self::BottomFrontLeft, "BFL", 39),
595 (Self::BottomFrontRight, "BFR", 40),
596 (Self::SideSurroundLeft, "SSL", 41),
597 (Self::SideSurroundRight, "SSR", 42),
598 (Self::TopSurroundLeft, "TTL", 43),
599 (Self::TopSurroundRight, "TTR", 44),
600 (Self::BinauralLeft, "BIL", 61),
601 (Self::BinauralRight, "BIR", 62),
602 (Self::Unnamed, "UNK", 0x300),
603 ];
604
605 /// `FFmpeg`'s abbreviation (`FL`, `FR`, `FC`, `LFE`, ...); `UNK` for
606 /// [`Speaker::Unnamed`].
607 #[must_use]
608 pub fn name(self) -> &'static str {
609 Self::TABLE
610 .iter()
611 .find(|(speaker, _, _)| *speaker == self)
612 .map_or("UNK", |(_, name, _)| name)
613 }
614
615 /// The speaker `FFmpeg` names `name`, or `None` for a name it does not
616 /// give a loudspeaker position (`UNK` is [`Speaker::Unnamed`]).
617 #[must_use]
618 pub fn from_name(name: &str) -> Option<Self> {
619 Self::TABLE
620 .iter()
621 .find(|(_, known, _)| *known == name)
622 .map(|(speaker, _, _)| *speaker)
623 }
624
625 /// `FFmpeg`'s channel number (`enum AVChannel`) for this speaker.
626 #[must_use]
627 pub fn ffmpeg_channel(self) -> u32 {
628 Self::TABLE
629 .iter()
630 .find(|(speaker, _, _)| *speaker == self)
631 .map_or(0x300, |(_, _, channel)| *channel)
632 }
633
634 /// The speaker of `FFmpeg` channel number `channel`; any channel without
635 /// a loudspeaker position (unknown, unused, ambisonic, user) is
636 /// [`Speaker::Unnamed`].
637 #[must_use]
638 pub fn from_ffmpeg_channel(channel: i64) -> Self {
639 Self::TABLE
640 .iter()
641 .find(|(_, _, number)| i64::from(*number) == channel)
642 .map_or(Self::Unnamed, |(speaker, _, _)| *speaker)
643 }
644}
645
646/// The speakers an [`AudioBlock`]'s planes feed, in plane order: 1 to
647/// [`MAX_AUDIO_CHANNELS`] of them.
648///
649/// A provider reports the layout its source declares (a WAVE channel mask, a
650/// `QuickTime` `chan` box, a codec's channel configuration), in the order its
651/// planes arrive; neither binding rematrixes a source into another layout.
652/// A source that declares none, or only channels no speaker names, has
653/// [`SpeakerLayout::default_for`] its channel count.
654#[derive(Copy, Clone, Eq, Hash, PartialEq)]
655pub struct SpeakerLayout {
656 speakers: [Speaker; MAX_AUDIO_CHANNELS],
657 count: u8,
658}
659
660impl std::fmt::Debug for SpeakerLayout {
661 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662 formatter
663 .debug_list()
664 .entries(self.speakers().iter().map(|speaker| speaker.name()))
665 .finish()
666 }
667}
668
669impl SpeakerLayout {
670 /// One front-center plane: a mono source, and a mono project's mix.
671 pub const MONO: Self = Self::fixed(&[Speaker::FrontCenter]);
672 /// Front left and right: a stereo source, and a stereo project's mix.
673 pub const STEREO: Self = Self::fixed(&[Speaker::FrontLeft, Speaker::FrontRight]);
674
675 const fn fixed(speakers: &[Speaker]) -> Self {
676 let mut layout = [Speaker::Unnamed; MAX_AUDIO_CHANNELS];
677 let mut index = 0;
678 while index < speakers.len() {
679 layout[index] = speakers[index];
680 index += 1;
681 }
682 #[allow(clippy::cast_possible_truncation)]
683 Self {
684 speakers: layout,
685 count: speakers.len() as u8,
686 }
687 }
688
689 /// The layout of these speakers in this plane order, or `None` for none
690 /// or more than [`MAX_AUDIO_CHANNELS`].
691 #[must_use]
692 pub fn new(speakers: &[Speaker]) -> Option<Self> {
693 if speakers.is_empty() || speakers.len() > MAX_AUDIO_CHANNELS {
694 return None;
695 }
696 let mut layout = Self {
697 speakers: [Speaker::Unnamed; MAX_AUDIO_CHANNELS],
698 count: u8::try_from(speakers.len()).ok()?,
699 };
700 layout.speakers[..speakers.len()].copy_from_slice(speakers);
701 Some(layout)
702 }
703
704 /// The layout a source of `channels` planes that declares none has:
705 /// `FFmpeg`'s default for the count (`av_channel_layout_default`), in the
706 /// WAVE order. Mono, stereo, 2.1, 4.0, 5.0, 5.1, 6.1 and 7.1 up to 8;
707 /// 5.1.4, 7.1.4, 9.1.4, 9.1.6 and 22.2 for 10, 12, 14, 16 and 24. A count
708 /// `FFmpeg` names no layout for is a 7.1 with its further channels
709 /// unnamed. `None` for no channels or more than [`MAX_AUDIO_CHANNELS`].
710 #[must_use]
711 pub fn default_for(channels: usize) -> Option<Self> {
712 /// `FFmpeg`'s native-order channel masks (`AV_CH_LAYOUT_*`).
713 const SEVEN_ONE: u64 = 0x63f;
714 let mask: u64 = match channels {
715 1 => 0x4,
716 2 => 0x3,
717 3 => 0xb,
718 4 => 0x107,
719 5 => 0x37,
720 6 => 0x3f,
721 7 => 0x70f,
722 10 => 0x2_d60f,
723 12 => 0x2_d63f,
724 14 => 0x2_d6ff,
725 16 => 0x30_0002_d6ff,
726 24 => 0x1f8_0003_ffff,
727 8..=MAX_AUDIO_CHANNELS => SEVEN_ONE,
728 _ => return None,
729 };
730 let mut layout = Self::of_mask(mask)?;
731 // A count without a layout of its own: the 7.1's further channels name no speaker.
732 layout.count = u8::try_from(channels).ok()?;
733 Some(layout)
734 }
735
736 /// The speakers of a native-order channel mask, in bit order.
737 fn of_mask(mask: u64) -> Option<Self> {
738 let speakers = (0..64_i64)
739 .filter(|bit| mask & (1 << bit) != 0)
740 .map(Speaker::from_ffmpeg_channel)
741 .collect::<Vec<_>>();
742 Self::new(&speakers)
743 }
744
745 /// The layout a source declares, or [`SpeakerLayout::default_for`] its
746 /// channel count when it names no speaker at all. The flag says the
747 /// default was assumed.
748 #[must_use]
749 pub fn declared_or_default(speakers: &[Speaker]) -> Option<(Self, bool)> {
750 if speakers.iter().all(|speaker| *speaker == Speaker::Unnamed) {
751 return Self::default_for(speakers.len()).map(|layout| (layout, true));
752 }
753 Self::new(speakers).map(|layout| (layout, false))
754 }
755
756 /// The speakers, in plane order.
757 #[must_use]
758 pub fn speakers(&self) -> &[Speaker] {
759 &self.speakers[..usize::from(self.count)]
760 }
761
762 /// How many planes the layout has.
763 #[must_use]
764 pub fn channels(&self) -> usize {
765 usize::from(self.count)
766 }
767
768 /// The speakers' `FFmpeg` names, joined by `+` (`FL+FR+FC`), as
769 /// `av_channel_layout_from_string` reads them.
770 #[must_use]
771 pub fn describe(&self) -> String {
772 self.speakers()
773 .iter()
774 .map(|speaker| speaker.name())
775 .collect::<Vec<_>>()
776 .join("+")
777 }
778}
779
780/// Planar floating-point audio at the project sample rate.
781///
782/// Positions are sample indices at `sample_rate`, never ticks: a tick is not a
783/// whole number of samples at 44.1 kHz, so a start in ticks cannot say which
784/// sample a block begins with, and every conversion between the two would need
785/// a rounding rule both sides agree on. Sample `n` is at `n / sample_rate`
786/// seconds on the block's timeline, the source's for a decoder and the
787/// project's for a mixer.
788///
789/// A block from a decoder or an [`AudioBlockProvider`] keeps its source's
790/// channels, 1 to [`MAX_AUDIO_CHANNELS`] planes, and says which speaker each
791/// feeds; the mixer folds them into the project's layout by those speakers.
792/// A mixer's own blocks have the project's channels.
793#[derive(Clone, Debug, PartialEq)]
794pub struct AudioBlock {
795 /// Index of the first sample, at `sample_rate`.
796 pub start_sample: i64,
797 /// Project sample rate used by every returned block.
798 pub sample_rate: u32,
799 /// The speaker each of `channels` feeds, one per plane.
800 pub speakers: SpeakerLayout,
801 /// One vector per channel, all with equal lengths, in `speakers`' order.
802 pub channels: Vec<Vec<f32>>,
803}
804
805impl AudioBlock {
806 /// Samples per channel.
807 #[must_use]
808 pub fn frames(&self) -> usize {
809 self.channels.first().map_or(0, Vec::len)
810 }
811
812 /// Index one past the last sample.
813 #[must_use]
814 pub fn end_sample(&self) -> i64 {
815 self.start_sample
816 .saturating_add(i64::try_from(self.frames()).unwrap_or(i64::MAX))
817 }
818
819 /// The first sample's time, at or before it on the tick grid.
820 ///
821 /// For display and for APIs that take a [`MediaTime`]; positions within
822 /// audio stay in samples.
823 #[must_use]
824 pub fn start_time(&self) -> MediaTime {
825 MediaTime::from_ticks(sample_ticks_floor(self.start_sample, self.sample_rate))
826 }
827}
828
829/// The tick at or before sample `sample` at `sample_rate`.
830#[must_use]
831pub fn sample_ticks_floor(sample: i64, sample_rate: u32) -> i64 {
832 let ticks = (i128::from(sample) * i128::from(alpha2_time::TICKS_PER_SECOND))
833 .div_euclid(i128::from(sample_rate.max(1)));
834 i64::try_from(ticks).unwrap_or(if ticks < 0 { i64::MIN } else { i64::MAX })
835}
836
837/// Generic color declaration for a host-rasterized texture.
838#[derive(Copy, Clone, Debug, Eq, PartialEq)]
839pub enum RasterColorSpace {
840 /// IEC sRGB encoded RGB.
841 Srgb,
842 /// Linear-light RGB with BT.709/sRGB primaries.
843 LinearSrgb,
844}
845
846/// Alpha representation of host pixels.
847#[derive(Copy, Clone, Debug, Eq, PartialEq)]
848pub enum Premultiplication {
849 /// RGB has already been multiplied by alpha.
850 Premultiplied,
851 /// RGB is straight and must be premultiplied at upload.
852 Straight,
853 /// Pixels are opaque.
854 Opaque,
855}
856
857/// Generic image/video/text texture descriptor owned by a host.
858#[derive(Clone, Debug, Eq, PartialEq)]
859pub struct HostTexture {
860 /// Host-defined cache identity.
861 pub content_version: u64,
862 /// Width in pixels.
863 pub width: u32,
864 /// Height in pixels.
865 pub height: u32,
866 /// Alpha representation.
867 pub premultiplication: Premultiplication,
868 /// RGB encoding.
869 pub color_space: RasterColorSpace,
870}
871
872/// Horizontal alignment applied by a host text layout engine.
873#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
874pub enum TextAlign {
875 /// Align each line to its paragraph's leading edge: the left of a
876 /// left-to-right paragraph, the right of a right-to-left one.
877 #[default]
878 Start,
879 /// Center each line in the requested layout width.
880 Center,
881 /// Align each line to its paragraph's trailing edge.
882 End,
883}
884
885/// How a text host draws emoji and other color glyphs.
886#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
887pub enum EmojiPolicy {
888 /// Draw a glyph a font has in color (a COLR painting, or a CBDT or sbix
889 /// bitmap) in its own colors; a grapheme shown as an emoji (as for
890 /// [`Self::Replace`]) tries the stack's color fonts first, and one
891 /// asking for text (U+FE0E) the others.
892 #[default]
893 Color,
894 /// Rasterize a monochrome outline when the selected host stack provides
895 /// one: every glyph is drawn as an outline in the text's color, and an
896 /// emoji tries the fonts without color glyphs first.
897 Monochrome,
898 /// Draw each grapheme shown as an emoji (UTS #51: one with an
899 /// `Emoji_Presentation` character, or U+FE0F with a pictograph or a
900 /// keycap, and no U+FE0E), sequence and modifiers included, as one
901 /// U+FFFD REPLACEMENT CHARACTER. A symbol shown as text by default,
902 /// such as (c) or a sun, stays.
903 Replace,
904}
905
906/// Layout facts attached to a host-rasterized text texture.
907///
908/// The compositor consumes [`Self::texture`] exactly like an image or decoded
909/// video texture. These values are retained for placement, cache validation,
910/// and cross-host contract tests; they never select a text-specific GPU path.
911#[derive(Clone, Debug, PartialEq)]
912pub struct TextTexture {
913 /// Generic texture identity and pixel declaration.
914 pub texture: HostTexture,
915 /// Where the layout box's left edge lies in the texture, in pixels. The
916 /// texture adds the same margin on both sides of the box for ink and
917 /// decorations that reach past it.
918 pub layout_x: f32,
919 /// Where the layout box's top edge lies in the texture, in pixels, with
920 /// the same margin below the box.
921 pub layout_y: f32,
922 /// Logical layout width before integer pixel rounding.
923 pub layout_width: f32,
924 /// Logical layout height before integer pixel rounding.
925 pub layout_height: f32,
926 /// Distance from the layout box's top edge to the first alphabetic
927 /// baseline.
928 pub first_baseline: f32,
929 /// Highest any line's ink reaches above its baseline, and at least the
930 /// nominal ascent the lines are spaced by.
931 pub ascent: f32,
932 /// Lowest any line's ink reaches below its baseline, and at least the
933 /// nominal descent.
934 pub descent: f32,
935 /// Number of laid-out lines.
936 pub line_count: u32,
937}
938
939impl TextTexture {
940 /// Checks dimensions and finite layout metrics at the public boundary.
941 #[must_use]
942 pub fn is_valid(&self) -> bool {
943 self.texture.width > 0
944 && self.texture.height > 0
945 && self.line_count > 0
946 && [
947 self.layout_x,
948 self.layout_y,
949 self.layout_width,
950 self.layout_height,
951 self.first_baseline,
952 self.ascent,
953 self.descent,
954 ]
955 .into_iter()
956 .all(|value| value.is_finite() && value >= 0.0)
957 && self.first_baseline <= self.layout_height
958 && f64::from(self.layout_x) + f64::from(self.layout_width)
959 <= f64::from(self.texture.width)
960 && f64::from(self.layout_y) + f64::from(self.layout_height)
961 <= f64::from(self.texture.height)
962 }
963}
964
965/// RGBA8 pixels returned by an image, video, or text rasterization host.
966#[derive(Clone, Debug, PartialEq)]
967pub struct HostTexturePixels {
968 /// Shared texture descriptor. Text hosts additionally return [`TextTexture`].
969 pub texture: HostTexture,
970 /// Tightly packed top-to-bottom RGBA8 rows.
971 pub rgba8: Vec<u8>,
972}
973
974impl HostTexturePixels {
975 /// Checks that the owned bytes exactly cover the declared texture.
976 #[must_use]
977 pub fn is_valid(&self) -> bool {
978 usize::try_from(self.texture.width)
979 .ok()
980 .and_then(|width| {
981 usize::try_from(self.texture.height)
982 .ok()
983 .and_then(|height| width.checked_mul(height))
984 })
985 .and_then(|pixels| pixels.checked_mul(4))
986 == Some(self.rgba8.len())
987 }
988}
989
990/// Media stream domain.
991#[derive(Copy, Clone, Debug, Eq, PartialEq)]
992pub enum StreamKind {
993 /// Video stream.
994 Video,
995 /// Audio stream.
996 Audio,
997}
998
999/// Platform-neutral probe result for one stream.
1000#[derive(Clone, Debug, PartialEq)]
1001pub struct StreamInfo {
1002 /// Demuxer stream index.
1003 pub index: usize,
1004 /// Stream domain.
1005 pub kind: StreamKind,
1006 /// Codec short name when available.
1007 pub codec: String,
1008 /// Stream duration when known.
1009 pub duration: Option<MediaTime>,
1010 /// Video dimensions.
1011 pub dimensions: Option<(u32, u32)>,
1012 /// Video frame rate.
1013 pub frame_rate: Option<FrameRate>,
1014 /// Audio sample rate.
1015 pub sample_rate: Option<u32>,
1016 /// Audio channel count.
1017 pub channels: Option<u32>,
1018 /// How the stream's pictures are shown upright.
1019 pub orientation: Orientation,
1020 /// Sample aspect ratio.
1021 pub sample_aspect_ratio: SampleAspectRatio,
1022 /// Declared video color.
1023 pub color: Option<ColorMetadata>,
1024}
1025
1026/// Kind of decoder cache/refill operation.
1027#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1028pub enum DecodeRequestKind {
1029 /// Normal look-ahead fill.
1030 Prefetch,
1031 /// Exact seek: flush decoder state and begin at/before the requested time.
1032 Seek,
1033 /// Refill after cache eviction.
1034 Evicted,
1035 /// Refill every referenced input after GPU device recovery.
1036 DeviceRecovery,
1037}
1038
1039/// One idempotent decoder request. Hosts discard results with an old generation.
1040#[derive(Clone, Debug, Eq, PartialEq)]
1041pub struct DecodeRequest {
1042 /// Source identity.
1043 pub source: SourceId,
1044 /// Requested source timestamp.
1045 pub time: MediaTime,
1046 /// Why the engine needs the input.
1047 pub kind: DecodeRequestKind,
1048 /// Monotonically increasing seek/recovery generation.
1049 pub generation: u64,
1050}
1051
1052/// Result correlated to a decoder request, safe for out-of-order completion.
1053#[derive(Clone, Debug, PartialEq)]
1054pub struct DecodeResult {
1055 /// Original request.
1056 pub request: DecodeRequest,
1057 /// Decoder outcome.
1058 pub event: DecodeEvent,
1059}
1060
1061/// Decoder output and terminal states.
1062#[derive(Clone, Debug, PartialEq)]
1063pub enum DecodeEvent {
1064 /// Decoded video became available.
1065 Video(DecodedVideoFrame),
1066 /// Decoded audio became available.
1067 Audio(AudioBlock),
1068 /// No exact frame is available yet; retry after feeding more input.
1069 Retry,
1070 /// The selected stream has ended.
1071 EndOfStream,
1072}
1073
1074/// Cancellation flag shared across adapter operations.
1075#[derive(Clone, Debug, Default)]
1076pub struct CancelToken(Arc<AtomicBool>);
1077
1078impl CancelToken {
1079 /// Requests cancellation. The operation must stop at its next adapter boundary.
1080 pub fn cancel(&self) {
1081 self.0.store(true, Ordering::Release);
1082 }
1083
1084 /// Returns whether cancellation was requested.
1085 #[must_use]
1086 pub fn is_cancelled(&self) -> bool {
1087 self.0.load(Ordering::Acquire)
1088 }
1089}
1090
1091/// The sentence every wait on GPU work that timed out carries, wherever it
1092/// was waited on (the renderer, a native interop helper, an encoder input),
1093/// so a host that sees only error text tells a hung GPU from a generic
1094/// failure. Hosts classify it as `GPU_TIMEOUT`.
1095pub const GPU_TIMEOUT_MESSAGE: &str = "GPU work did not complete within the wait timeout";
1096
1097/// What a failed GPU call says about the device it ran on, read from the
1098/// platform's own status for that call (a `VkResult`, `HRESULT`, or
1099/// `CVReturn`) where it failed, not from any message.
1100#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1101#[non_exhaustive]
1102pub enum GpuFault {
1103 /// The device was lost or removed; nothing more runs on it.
1104 DeviceLost,
1105 /// The device could not allocate memory of its own.
1106 DeviceMemory,
1107 /// The driver could not allocate host (CPU) memory for the call.
1108 HostMemory,
1109 /// GPU work the call waited on did not complete within the wait timeout.
1110 Timeout,
1111}
1112
1113impl GpuFault {
1114 /// A stable name for logs and reports.
1115 #[must_use]
1116 pub const fn as_str(self) -> &'static str {
1117 match self {
1118 Self::DeviceLost => "device-lost",
1119 Self::DeviceMemory => "device-memory",
1120 Self::HostMemory => "host-memory",
1121 Self::Timeout => "timeout",
1122 }
1123 }
1124}
1125
1126/// Typed decoder failure.
1127///
1128/// Damaged input a decoder's policy lets it skip is not one: the decoder
1129/// goes on to the next frame and reports the damage through its adapter's
1130/// diagnostics, so a caller never retries around it. Damage it does not skip
1131/// is an adapter failure.
1132#[derive(Clone, Debug, Eq, PartialEq)]
1133#[non_exhaustive]
1134pub enum DecodeError {
1135 /// The operation was cancelled.
1136 Cancelled,
1137 /// The requested stream or timestamp is outside the resource.
1138 OutOfRange,
1139 /// The adapter does not support this media representation.
1140 Unsupported(String),
1141 /// Adapter-specific failure, represented as stable text rather than a platform type.
1142 Adapter(String),
1143 /// A GPU call the adapter made failed, and its platform status says what
1144 /// that was evidence of. `message` names the call and the status.
1145 Gpu {
1146 /// What the status says about the device.
1147 fault: GpuFault,
1148 /// The failed call and its platform status.
1149 message: String,
1150 },
1151}
1152
1153impl DecodeError {
1154 /// Returns whether the decoder must be discarded instead of retried.
1155 #[must_use]
1156 pub const fn is_fatal(&self) -> bool {
1157 matches!(
1158 self,
1159 Self::Unsupported(_) | Self::Adapter(_) | Self::Gpu { .. }
1160 )
1161 }
1162}
1163
1164impl fmt::Display for DecodeError {
1165 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1166 match self {
1167 Self::Cancelled => formatter.write_str("decode was cancelled"),
1168 Self::OutOfRange => formatter.write_str("decode request is out of range"),
1169 Self::Unsupported(message) => write!(formatter, "unsupported media: {message}"),
1170 Self::Adapter(message) | Self::Gpu { message, .. } => {
1171 write!(formatter, "decoder adapter failed: {message}")
1172 }
1173 }
1174 }
1175}
1176
1177impl Error for DecodeError {}
1178
1179/// Stateful media decoder implemented by a platform binding.
1180pub trait Decoder {
1181 /// Returns deterministic stream metadata and stream selection.
1182 fn streams(&self) -> &[StreamInfo];
1183 /// Flushes queued packets and frames without changing stream selection.
1184 ///
1185 /// # Errors
1186 /// Returns a platform-neutral adapter failure when flushing fails.
1187 fn flush(&mut self) -> Result<(), DecodeError>;
1188 /// Seeks and returns what is presented at the request's time.
1189 ///
1190 /// For video that is the frame whose presentation interval contains the
1191 /// time: the last frame whose timestamp is at or before the time's
1192 /// presentation deadline, the time plus the stream's presentation slack
1193 /// ([`alpha2_time::presentation_slack`]), which absorbs the rounding a
1194 /// stored timestamp went through. A seek starts from the keyframe at or
1195 /// before that deadline. Deciding so takes the frame after, which the
1196 /// next [`Self::decode_next`] returns. A stream whose first frame is later
1197 /// answers with that frame, and past the last frame the last one answers,
1198 /// however far past: a stream shows its last picture after its end as it
1199 /// shows its first before its start. A stream with no frame at all is
1200 /// [`DecodeError::OutOfRange`]. For audio it is the block holding the
1201 /// time's sample.
1202 ///
1203 /// # Errors
1204 /// Returns cancellation, range, unsupported-media, or adapter failures.
1205 fn seek(
1206 &mut self,
1207 request: DecodeRequest,
1208 cancel: &CancelToken,
1209 ) -> Result<DecodeResult, DecodeError>;
1210 /// Advances packet draining by one observable event.
1211 ///
1212 /// # Errors
1213 /// Returns cancellation, unsupported-media, or adapter failures.
1214 fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError>;
1215}
1216
1217/// A provider's answer to one request: source samples, and what the source
1218/// holds on either side of them.
1219#[derive(Clone, Copy, Debug, PartialEq)]
1220pub struct AudioSpan<'a> {
1221 /// Source samples at the project rate, read by their own `start_sample`.
1222 pub block: &'a AudioBlock,
1223 /// Whether the source has no samples before `block.start_sample`: the
1224 /// block begins where the source's audio does, such as a stream that
1225 /// starts 50 ms into its container.
1226 pub starts_source: bool,
1227 /// Whether the source has no samples from `block.end_sample()` on: the
1228 /// block ends where the source's audio does.
1229 pub ends_source: bool,
1230}
1231
1232impl<'a> AudioSpan<'a> {
1233 /// A block that holds its source's entire audio, as a source decoded
1234 /// once from end to end is.
1235 #[must_use]
1236 pub const fn whole(block: &'a AudioBlock) -> Self {
1237 Self {
1238 block,
1239 starts_source: true,
1240 ends_source: true,
1241 }
1242 }
1243
1244 /// Whether the span accounts for every sample of `[start, end)`: each is
1245 /// in the block, or before it with [`Self::starts_source`], or after it
1246 /// with [`Self::ends_source`], where the source is silent.
1247 #[must_use]
1248 pub fn covers(&self, start: i64, end: i64) -> bool {
1249 (self.starts_source || self.block.start_sample <= start)
1250 && (self.ends_source || self.block.end_sample() >= end)
1251 }
1252}
1253
1254/// Retrieves project-rate planar audio without exposing its backing cache.
1255pub trait AudioBlockProvider {
1256 /// Returns source samples `[start_sample, start_sample + frames)` at the
1257 /// project rate, as a span that [covers](AudioSpan::covers) all of them.
1258 ///
1259 /// The block may start before `start_sample` and end after the request.
1260 /// Where the source has no audio, before its first sample or after its
1261 /// last, the provider either holds zeros there or says so with
1262 /// [`AudioSpan::starts_source`] and [`AudioSpan::ends_source`], which
1263 /// spares allocating silence. A provider that cannot yet account for the
1264 /// whole request returns `None`, or a span that does not cover it: the
1265 /// mixer then reports the source missing instead of mixing the gap as
1266 /// silence and calling the block complete.
1267 fn block(&self, source: &SourceId, start_sample: i64, frames: usize) -> Option<AudioSpan<'_>>;
1268}
1269
1270/// Receives project-rate planar audio from offline or realtime graph hosts.
1271pub trait AudioBlockSink {
1272 /// Writes one monotonic audio block.
1273 ///
1274 /// # Errors
1275 /// Returns a platform-neutral encode error on cancellation, invalid timing,
1276 /// or host failure.
1277 fn write_audio(&mut self, block: &AudioBlock, cancel: &CancelToken) -> Result<(), EncodeError>;
1278
1279 /// Drains and commits the sink.
1280 ///
1281 /// # Errors
1282 /// Returns cancellation or host failures; success means all delayed
1283 /// samples were committed.
1284 fn finish_audio(&mut self, cancel: &CancelToken) -> Result<(), EncodeError>;
1285
1286 /// Discards partial output.
1287 ///
1288 /// # Errors
1289 /// Returns an error when cleanup fails.
1290 fn abort_audio(&mut self) -> Result<(), EncodeError>;
1291}
1292
1293/// One input accepted by an encoding/mux adapter.
1294#[derive(Clone, Debug, PartialEq)]
1295pub enum EncodeEvent {
1296 /// Premultiplied rendered video.
1297 Video(DecodedVideoFrame),
1298 /// Project-rate planar audio.
1299 Audio(AudioBlock),
1300}
1301
1302/// Typed encoding failure.
1303#[derive(Clone, Debug, Eq, PartialEq)]
1304#[non_exhaustive]
1305pub enum EncodeError {
1306 /// The operation was cancelled; partial output must be removed by policy.
1307 Cancelled,
1308 /// Input timestamps violated monotonic/interleave requirements.
1309 InvalidTimestamp,
1310 /// Adapter failure without leaking a platform error type.
1311 Adapter(String),
1312 /// The output file could not be created or claimed: it already exists,
1313 /// its path is unusable, or the file system refused. Nothing was encoded.
1314 Output(String),
1315 /// Another export, live in this process or another, is writing the same
1316 /// output: it holds the output's partial file. Nothing was encoded, and
1317 /// the other export's file is untouched.
1318 OutputInUse(String),
1319 /// A GPU call the adapter made failed, and its platform status says what
1320 /// that was evidence of. `message` names the call and the status.
1321 Gpu {
1322 /// What the status says about the device.
1323 fault: GpuFault,
1324 /// The failed call and its platform status.
1325 message: String,
1326 },
1327}
1328
1329impl EncodeError {
1330 /// A wait on GPU work an encode needs, for `what`, that timed out: a
1331 /// [`GpuFault::Timeout`] whose message carries [`GPU_TIMEOUT_MESSAGE`].
1332 #[must_use]
1333 pub fn gpu_timeout(what: impl fmt::Display) -> Self {
1334 Self::Gpu {
1335 fault: GpuFault::Timeout,
1336 message: format!("{what}: {GPU_TIMEOUT_MESSAGE}"),
1337 }
1338 }
1339}
1340
1341impl fmt::Display for EncodeError {
1342 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1343 match self {
1344 Self::Cancelled => formatter.write_str("encode was cancelled"),
1345 Self::InvalidTimestamp => formatter.write_str("encode timestamp is invalid"),
1346 Self::Adapter(message) | Self::Gpu { message, .. } => {
1347 write!(formatter, "encoder adapter failed: {message}")
1348 }
1349 Self::Output(message) | Self::OutputInUse(message) => {
1350 write!(formatter, "cannot create the output: {message}")
1351 }
1352 }
1353 }
1354}
1355
1356impl Error for EncodeError {}
1357
1358/// Host encoding and muxing boundary.
1359pub trait EncodeSink {
1360 /// Queues one timestamped media item.
1361 ///
1362 /// # Errors
1363 /// Returns cancellation, invalid timestamp, or adapter failures.
1364 fn write(&mut self, event: EncodeEvent, cancel: &CancelToken) -> Result<(), EncodeError>;
1365 /// Drains encoder delay, interleaves remaining packets, and writes the trailer.
1366 ///
1367 /// # Errors
1368 /// Returns cancellation or adapter failures; success means the trailer exists.
1369 fn finish(&mut self, cancel: &CancelToken) -> Result<(), EncodeError>;
1370 /// Aborts and applies the adapter's partial-output cleanup policy.
1371 ///
1372 /// # Errors
1373 /// Returns an adapter failure if partial output could not be cleaned up.
1374 fn abort(&mut self) -> Result<(), EncodeError>;
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379 use super::SampleAspectRatio;
1380
1381 #[test]
1382 fn a_pixel_aspect_ratio_is_kept_in_lowest_terms_and_shows_as_a_browser_shows_it() {
1383 let ratio = |num, den| SampleAspectRatio { num, den };
1384 assert_eq!(SampleAspectRatio::new(8, 6), Some(ratio(4, 3)));
1385 assert_eq!(SampleAspectRatio::new(0, 1), None);
1386 assert_eq!(SampleAspectRatio::new(1, 0), None);
1387 assert_eq!(SampleAspectRatio::new(u64::from(u32::MAX) + 1, 1), None);
1388 // A display size rounds the stretched side: the declared ratio is the simplest that rounds
1389 // to it.
1390 let shown = |visible, displayed| SampleAspectRatio::of_display(visible, displayed);
1391 assert_eq!(shown((128, 96), (171, 96)), Some(ratio(4, 3)));
1392 assert_eq!(shown((96, 128), (96, 171)), Some(ratio(3, 4)));
1393 assert_eq!(shown((720, 576), (1024, 576)), Some(ratio(64, 45)));
1394 assert_eq!(shown((720, 480), (655, 480)), Some(ratio(10, 11)));
1395 assert_eq!(shown((720, 480), (720, 524)), Some(ratio(11, 12)));
1396 assert_eq!(shown((1440, 1080), (1920, 1080)), Some(ratio(4, 3)));
1397 assert_eq!(shown((128, 96), (128, 96)), Some(ratio(1, 1)));
1398 // Whatever it finds rounds to the size shown.
1399 for (visible, displayed) in [
1400 ((128, 96), (170, 96)),
1401 ((1000, 999), (1777, 999)),
1402 ((3, 2), (5, 2)),
1403 ] {
1404 let found = shown(visible, displayed).expect("a ratio");
1405 assert_eq!(
1406 found.square_pixel_size(visible.0, visible.1),
1407 displayed,
1408 "{visible:?} {displayed:?}"
1409 );
1410 }
1411 assert_eq!(shown((0, 96), (171, 96)), None);
1412 // mediabunny: Math.round(128 * 4 / 3) = 171, and a narrow pixel stretches the height.
1413 assert_eq!(ratio(4, 3).square_pixel_size(128, 96), (171, 96));
1414 assert_eq!(ratio(64, 45).square_pixel_size(720, 576), (1024, 576));
1415 assert_eq!(ratio(3, 4).square_pixel_size(96, 128), (96, 171));
1416 assert_eq!(ratio(1, 1).square_pixel_size(65, 49), (65, 49));
1417 // Half a pixel rounds up: 3 * 3 / 2 = 4.5.
1418 assert_eq!(ratio(3, 2).square_pixel_size(3, 7), (5, 7));
1419 }
1420
1421 use super::*;
1422
1423 #[test]
1424 fn a_frame_size_is_bounded_on_each_side_and_in_area() {
1425 assert!(frame_size_supported(16_384, 4_096));
1426 assert!(frame_size_supported(8_192, 8_192));
1427 assert!(!frame_size_supported(8_193, 8_192));
1428 assert!(!frame_size_supported(16_385, 1));
1429 assert!(!frame_size_supported(1, 16_385));
1430 assert!(!frame_size_supported(u32::MAX, u32::MAX));
1431 }
1432
1433 #[test]
1434 fn cancellation_is_shared_and_monotonic() {
1435 let token = CancelToken::default();
1436 let other = token.clone();
1437 assert!(!other.is_cancelled());
1438 token.cancel();
1439 assert!(other.is_cancelled());
1440 }
1441
1442 #[test]
1443 fn a_layout_names_each_plane_by_ffmpegs_channel_names() {
1444 let lcr = SpeakerLayout::new(&[
1445 Speaker::FrontCenter,
1446 Speaker::FrontLeft,
1447 Speaker::FrontRight,
1448 ])
1449 .expect("layout");
1450 assert_eq!(lcr.channels(), 3);
1451 assert_eq!(lcr.describe(), "FC+FL+FR");
1452 assert_eq!(format!("{lcr:?}"), r#"["FC", "FL", "FR"]"#);
1453 for (speaker, name, channel) in Speaker::TABLE {
1454 assert_eq!(speaker.name(), name);
1455 assert_eq!(Speaker::from_name(name), Some(speaker));
1456 assert_eq!(speaker.ffmpeg_channel(), channel);
1457 assert_eq!(Speaker::from_ffmpeg_channel(i64::from(channel)), speaker);
1458 }
1459 // Unused (0x200), ambisonic (0x400 and up) and user channels name no speaker.
1460 for channel in [-1, 18, 0x200, 0x400, 0x401, 0x7ff, 1_000] {
1461 assert_eq!(Speaker::from_ffmpeg_channel(channel), Speaker::Unnamed);
1462 }
1463 assert_eq!(Speaker::from_name("front left"), None);
1464 assert!(SpeakerLayout::new(&[]).is_none());
1465 assert!(SpeakerLayout::new(&[Speaker::FrontLeft; MAX_AUDIO_CHANNELS + 1]).is_none());
1466 }
1467
1468 #[test]
1469 fn a_source_that_declares_no_speaker_has_ffmpegs_default_for_its_count() {
1470 let names = |channels| {
1471 SpeakerLayout::default_for(channels)
1472 .expect("a default")
1473 .describe()
1474 };
1475 assert_eq!(names(1), "FC");
1476 assert_eq!(names(3), "FL+FR+LFE");
1477 assert_eq!(names(4), "FL+FR+FC+BC");
1478 assert_eq!(names(8), "FL+FR+FC+LFE+BL+BR+SL+SR");
1479 assert_eq!(names(9), "FL+FR+FC+LFE+BL+BR+SL+SR+UNK");
1480 assert_eq!(names(10), "FL+FR+FC+LFE+SL+SR+TFL+TFR+TBL+TBR");
1481 assert_eq!(SpeakerLayout::default_for(24).expect("22.2").channels(), 24);
1482 assert!(SpeakerLayout::default_for(0).is_none());
1483 assert!(SpeakerLayout::default_for(MAX_AUDIO_CHANNELS + 1).is_none());
1484 let unnamed = [Speaker::Unnamed; 2];
1485 assert_eq!(
1486 SpeakerLayout::declared_or_default(&unnamed),
1487 Some((SpeakerLayout::default_for(2).expect("stereo"), true))
1488 );
1489 let partly = [Speaker::FrontCenter, Speaker::Unnamed];
1490 assert_eq!(
1491 SpeakerLayout::declared_or_default(&partly),
1492 Some((SpeakerLayout::new(&partly).expect("layout"), false))
1493 );
1494 }
1495
1496 #[test]
1497 fn a_span_covers_a_request_only_where_it_holds_samples_or_knows_silence() {
1498 let block = AudioBlock {
1499 start_sample: 100,
1500 sample_rate: 48_000,
1501 speakers: SpeakerLayout::default_for(1).expect("mono"),
1502 channels: vec![vec![0.5; 50]],
1503 };
1504 let partial = AudioSpan {
1505 block: &block,
1506 starts_source: false,
1507 ends_source: false,
1508 };
1509 assert!(partial.covers(100, 150));
1510 assert!(partial.covers(120, 130));
1511 assert!(!partial.covers(99, 150), "a sample before the block");
1512 assert!(!partial.covers(100, 151), "a sample after the block");
1513 let starts = AudioSpan {
1514 starts_source: true,
1515 ..partial
1516 };
1517 assert!(starts.covers(0, 150));
1518 assert!(!starts.covers(0, 151));
1519 assert!(AudioSpan::whole(&block).covers(-5, 1_000));
1520 }
1521
1522 #[test]
1523 fn text_descriptor_and_generic_pixels_validate_without_compositor_types() {
1524 let texture = HostTexture {
1525 content_version: 2,
1526 width: 4,
1527 height: 3,
1528 premultiplication: Premultiplication::Premultiplied,
1529 color_space: RasterColorSpace::Srgb,
1530 };
1531 let text = TextTexture {
1532 texture: texture.clone(),
1533 layout_x: 0.0,
1534 layout_y: 0.0,
1535 layout_width: 4.0,
1536 layout_height: 3.0,
1537 first_baseline: 2.0,
1538 ascent: 2.0,
1539 descent: 1.0,
1540 line_count: 1,
1541 };
1542 assert!(text.is_valid());
1543 assert!(
1544 HostTexturePixels {
1545 texture,
1546 rgba8: vec![0; 4 * 3 * 4],
1547 }
1548 .is_valid()
1549 );
1550 }
1551}