Skip to main content

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}