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