Skip to main content

alpha2_native/
decode.rs

1//! Container probing and the native audio/video decoders.
2
3use std::collections::VecDeque;
4use std::ffi::{CStr, CString};
5use std::path::{Path, PathBuf};
6use std::time::{Duration, Instant};
7
8use crate::audio_timeline::SourceTimeline;
9use crate::recovery::{DecodeDiagnostics, DecodePolicy, Recovery, WorkBudget, submit_packet};
10
11use crate::convert::{
12    adapter_error, codec_thread_budget, color_metadata, convert_video_frame, declared_sar,
13    frame_color_metadata, guess_sar, orientation_from_display, reject_unused_decode_options,
14    stream_rotation, tick_base, timestamp, video_decoder_options,
15};
16#[cfg(all(
17    any(target_os = "linux", target_os = "macos", target_os = "windows"),
18    feature = "hwdecode"
19))]
20use crate::video_frame::{HardwareDecoderSpec, HardwareOpenError};
21use alpha2_ffmpeg_bridge::{DecodedFrame, DemuxInput, OpenInputError};
22use alpha2_media::{
23    AudioBlock, CancelToken, ColorMetadata, DecodeError, DecodeEvent, DecodeRequest, DecodeResult,
24    DecodedVideoFrame, Decoder, MAX_AUDIO_CHANNELS, Orientation, SampleAspectRatio, Speaker,
25    SpeakerLayout, StreamInfo, StreamKind,
26};
27use alpha2_time::{FrameRate, MediaTime, TICKS_PER_SECOND};
28use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef, AVPacket};
29#[cfg(all(
30    any(target_os = "linux", target_os = "macos", target_os = "windows"),
31    feature = "hwdecode"
32))]
33use rsmpeg::avutil::AVHWDeviceContext;
34use rsmpeg::avutil::{AVChannelLayout, AVFrame, AVRational, av_rescale_q, av_rescale_q_rnd, ra};
35use rsmpeg::error::RsmpegError;
36use rsmpeg::ffi;
37use rsmpeg::swresample::SwrContext;
38
39/// Container-level probe and deterministic stream selection.
40#[derive(Clone, Debug, PartialEq)]
41pub struct ProbeResult {
42    /// Demuxer name.
43    pub format: String,
44    /// Container duration: from the first presentation to the end of the
45    /// last, so the media ends at `start + duration`.
46    pub duration: Option<MediaTime>,
47    /// When the container's first presentation is, on the media's own
48    /// timeline: zero for most files, later for a transport stream or for
49    /// streams that start after a delay.
50    pub start: Option<MediaTime>,
51    /// Every audio/video stream in demuxer order. Other streams (subtitles,
52    /// data, attachments) are left out, so a stream's position here is not
53    /// its index: look one up by [`StreamInfo::index`].
54    pub streams: Vec<StreamInfo>,
55    /// The `AVStream` index ([`StreamInfo::index`]) of the video stream a
56    /// reader shows: never an attached picture, and `None` when no video
57    /// stream decodes. [`Self::selected_video_stream`] finds its entry.
58    pub selected_video: Option<usize>,
59    /// The `AVStream` index of the audio stream a reader plays, `None` when
60    /// no audio stream decodes. [`Self::selected_audio_stream`] finds its
61    /// entry.
62    pub selected_audio: Option<usize>,
63}
64
65impl ProbeResult {
66    /// The entry of `streams` with `AVStream` index `index`.
67    #[must_use]
68    pub fn stream(&self, index: usize) -> Option<&StreamInfo> {
69        self.streams.iter().find(|stream| stream.index == index)
70    }
71
72    /// The selected video stream's entry.
73    #[must_use]
74    pub fn selected_video_stream(&self) -> Option<&StreamInfo> {
75        self.selected_video.and_then(|index| self.stream(index))
76    }
77
78    /// The selected audio stream's entry.
79    #[must_use]
80    pub fn selected_audio_stream(&self) -> Option<&StreamInfo> {
81        self.selected_audio.and_then(|index| self.stream(index))
82    }
83}
84
85/// Opens a local resource using the system `FFmpeg` 8 shared libraries.
86///
87/// # Errors
88/// Returns a platform-neutral adapter error for invalid paths or demux failures.
89pub fn probe(path: impl AsRef<Path>) -> Result<ProbeResult, DecodeError> {
90    let input = open_input(path.as_ref())?;
91    probe_context(&input, path.as_ref())
92}
93
94/// Visits the presentation time, in seconds, of each packet of the stream
95/// with `AVStream` index `stream`, in the order the file stores them, until
96/// `visit` returns false or the file ends. A packet without a presentation
97/// time is visited at its decode time; one with neither is skipped. Only
98/// packets are read: nothing is decoded.
99///
100/// # Errors
101/// Returns why the file cannot be opened or read.
102pub fn visit_packet_times(
103    path: impl AsRef<Path>,
104    stream: usize,
105    mut visit: impl FnMut(f64) -> bool,
106) -> Result<(), DecodeError> {
107    let mut input = open_input(path.as_ref())?;
108    let time_base = input
109        .streams()
110        .get(stream)
111        .map(|found| found.time_base)
112        .ok_or_else(|| DecodeError::Unsupported(format!("the file has no stream {stream}")))?;
113    input.discard_all_but(stream);
114    while let Some(packet) = input.read_packet().map_err(adapter_error)? {
115        if usize::try_from(packet.stream_index).ok() != Some(stream) {
116            continue;
117        }
118        let time = [packet.pts, packet.dts]
119            .into_iter()
120            .find(|time| *time != ffi::AV_NOPTS_VALUE);
121        let Some(time) = time else { continue };
122        #[allow(clippy::cast_precision_loss)] // A packet time is far below 2^52 time-base units.
123        let seconds = time as f64 * f64::from(time_base.num) / f64::from(time_base.den);
124        if !visit(seconds) {
125            break;
126        }
127    }
128    Ok(())
129}
130
131/// Software video decoder with exact timestamp conversion and drain semantics.
132pub struct NativeDecoder {
133    pub(crate) input: DemuxInput,
134    pub(crate) streams: Vec<StreamInfo>,
135    pub(crate) container_duration: Option<MediaTime>,
136    pub(crate) video_index: usize,
137    pub(crate) time_base: AVRational,
138    pub(crate) decoder: AVCodecContext,
139    pub(crate) orientation: Orientation,
140    /// The pixel aspect ratio the stream's pictures are shown with, as
141    /// probed: the container's declaration, else the codec parameters'.
142    pub(crate) sample_aspect_ratio: SampleAspectRatio,
143    /// The color the stream's tags describe, as probed.
144    pub(crate) color: Option<ColorMetadata>,
145    /// The color a project declares for the source, which wins over every
146    /// tag ([`Self::declare_color`]).
147    pub(crate) declared_color: Option<ColorMetadata>,
148    pub(crate) video_scaler: crate::convert::FrameScaler,
149    pub(crate) pending: VecDeque<DecodedFrame>,
150    pub(crate) demux_eof: bool,
151    pub(crate) decoder_flushed: bool,
152    pub(crate) stages: DecodeStageTimings,
153    /// When the stream's first frame is presented, once a seek from its first
154    /// packet has decoded it.
155    first_video_pts: Option<MediaTime>,
156    recovery: Recovery,
157    /// How late a frame's timestamp may be and still be the frame shown at a
158    /// time ([`Self::presentation_deadline`]), from the time base and the
159    /// rounding the stream's first timestamps show.
160    slack: MediaTime,
161    /// What the read of the stream's first packets for [`Self::slack`] took
162    /// from the demuxer, in order, which the decode reads before the
163    /// demuxer's next until a seek drops it.
164    prefetched: Prefetched,
165}
166
167/// What a read of a stream's first packets took from the demuxer, in order,
168/// which the decode reads before the demuxer's next until a seek drops it.
169type Prefetched = VecDeque<Result<Option<AVPacket>, RsmpegError>>;
170
171/// The first packets of `input`, which reads only stream `index`, up to
172/// [`alpha2_time::ROUNDING_SCAN_FRAMES`] of its own or
173/// [`alpha2_time::ROUNDING_SCAN_BYTES`] of them, the end of the input or a
174/// read failure, in the order the demuxer gave them; and the presentation
175/// timestamps of that stream's among them, which
176/// [`alpha2_time::rounding_quantum`] and [`alpha2_time::audio_frame_grid`]
177/// read.
178///
179/// The decode reads these packets first, so the input never seeks back and a
180/// failure is reported where a decode meets it.
181fn read_first_packets(input: &mut DemuxInput, index: usize) -> (Prefetched, Vec<i64>) {
182    let mut read = VecDeque::new();
183    let mut timestamps = Vec::with_capacity(alpha2_time::ROUNDING_SCAN_FRAMES);
184    let (mut packets, mut bytes) = (0, 0_u64);
185    while packets < alpha2_time::ROUNDING_SCAN_FRAMES && bytes < alpha2_time::ROUNDING_SCAN_BYTES {
186        let packet = input.read_packet();
187        let more = match &packet {
188            Ok(Some(packet)) => {
189                if usize::try_from(packet.stream_index).ok() == Some(index) {
190                    packets += 1;
191                    bytes = bytes.saturating_add(u64::try_from(packet.size).unwrap_or(0));
192                    if packet.pts != ffi::AV_NOPTS_VALUE {
193                        timestamps.push(packet.pts);
194                    }
195                }
196                true
197            }
198            Ok(None) | Err(_) => false,
199        };
200        read.push_back(packet);
201        if !more {
202            break;
203        }
204    }
205    (read, timestamps)
206}
207
208/// The first packets of `input`'s stream `video_index` ([`read_first_packets`])
209/// and the presentation slack their timestamps show
210/// ([`alpha2_time::rounding_quantum`], [`alpha2_time::presentation_slack`]).
211fn read_presentation_slack(
212    input: &mut DemuxInput,
213    video_index: usize,
214    time_base: AVRational,
215) -> (Prefetched, MediaTime) {
216    let (read, timestamps) = read_first_packets(input, video_index);
217    let slack = alpha2_time::presentation_slack(
218        i64::from(time_base.num),
219        i64::from(time_base.den),
220        alpha2_time::rounding_quantum(&timestamps),
221    );
222    (read, slack)
223}
224
225/// Where one video decode session's wall time went.
226///
227/// A single `decodeMs` cannot say whether a hardware session is fast because
228/// the GPU decoded quickly or because the work has only been submitted. These
229/// spans are the calls that take the time, so the answer is readable.
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub struct DecodeStageTimings {
232    /// `av_read_frame`: container demux and file I/O.
233    pub demux: Duration,
234    /// `avcodec_send_packet`: handing a packet to the codec.
235    pub send_packet: Duration,
236    /// `avcodec_receive_frame`: producing one picture. A hardware session
237    /// waits here for whatever `FFmpeg` synchronizes before it hands a surface
238    /// over, so this is the span that separates decode from submission.
239    pub receive_frame: Duration,
240    /// Turning the produced picture into a frame this host can use: pixel
241    /// conversion for a software session, surface metadata for a hardware one.
242    pub convert: Duration,
243    /// `av_seek_frame` and the codec flush that follows it.
244    pub seek: Duration,
245}
246
247impl DecodeStageTimings {
248    /// Adds another session's spans to these, saturating on overflow.
249    #[must_use]
250    pub fn merged(self, other: Self) -> Self {
251        Self {
252            demux: self.demux.saturating_add(other.demux),
253            send_packet: self.send_packet.saturating_add(other.send_packet),
254            receive_frame: self.receive_frame.saturating_add(other.receive_frame),
255            convert: self.convert.saturating_add(other.convert),
256            seek: self.seek.saturating_add(other.seek),
257        }
258    }
259}
260
261/// Where a demuxed input is.
262#[derive(Clone, Copy, PartialEq, Eq)]
263enum InputPosition {
264    /// Where it was opened: nothing read from it, and no seek, since.
265    Opened,
266    /// Anywhere a read or a seek left it.
267    Moved,
268}
269
270#[derive(Clone, Copy, PartialEq, Eq)]
271enum AudioContinuity {
272    Continuous,
273    Damaged,
274}
275
276/// Streaming `FFmpeg` audio decoder normalized to project-rate planar `f32`.
277///
278/// Blocks keep the stream's channels in its own order and name the speaker
279/// each feeds ([`NativeAudioDecoder::speakers`]). `swresample` turns each
280/// decoded frame into `f32` at its own rate, rematrixing only a frame whose
281/// layout differs from the stream's; the source timeline places it by
282/// its timestamp and `alpha2_audio::Resampler`, the converter a browser host
283/// runs as wasm, brings it to the project rate.
284pub struct NativeAudioDecoder {
285    pub(crate) path: PathBuf,
286    pub(crate) input: DemuxInput,
287    /// Whether `input` is where it was opened, at the stream's start.
288    input_position: InputPosition,
289    pub(crate) streams: Vec<StreamInfo>,
290    pub(crate) container_duration: Option<MediaTime>,
291    pub(crate) audio_index: usize,
292    pub(crate) time_base: AVRational,
293    pub(crate) decoder: AVCodecContext,
294    /// Sample format, and the speaker layout of a frame unlike the stream's:
295    /// never the rate.
296    converter: Option<SwrContext>,
297    pub(crate) sample_rate: u32,
298    pub(crate) channel_count: usize,
299    /// The speakers the stream declares, or its count's default.
300    speakers: SpeakerLayout,
301    /// Whether the stream declared no speaker and `speakers` is assumed.
302    speakers_assumed: bool,
303    /// `speakers` as `FFmpeg`'s layout: what every frame is converted to.
304    output_layout: AVChannelLayout,
305    /// The decoded source, placed and not yet resampled.
306    timeline: SourceTimeline,
307    pub(crate) pending: Vec<VecDeque<f32>>,
308    /// Source-timeline index, at the output rate, of the first pending
309    /// sample; `None` until a decoded frame's timestamp says where it is.
310    pub(crate) next_sample: Option<i64>,
311    pub(crate) block_frames: usize,
312    pub(crate) demux_eof: bool,
313    pub(crate) decoder_flushed: bool,
314    recovery: Recovery,
315    repair_audio: AudioContinuity,
316    /// For a stream whose timestamps its demuxer makes up (raw ADTS), how
317    /// they compare with the audio; `None` when the container has them.
318    made_up: Option<MadeUpTimestamps>,
319    /// The source rate and the frame grid the stream's first timestamps show
320    /// at it ([`alpha2_time::audio_frame_grid`]), where a first frame after
321    /// an open or a seek is placed: `None` when they show none.
322    grid: Option<(u32, alpha2_time::AudioFrameGrid)>,
323    /// What the read of the stream's first packets for [`Self::grid`] took
324    /// from the demuxer, which the decode reads before the demuxer's next
325    /// until a seek drops it.
326    prefetched: Prefetched,
327    /// Frames placed since the decoder opened: a fill that places one has
328    /// made progress, even before the frames reach its target.
329    frames_placed: u64,
330    /// How far before a seek target decoding restarts, in ticks: the codec's
331    /// pre-roll.
332    seek_preroll: i64,
333}
334
335/// Timestamps a demuxer made up by counting each packet's samples at one rate.
336struct MadeUpTimestamps {
337    /// The rate every packet is counted at: the stream's first.
338    rate: u32,
339    /// How far, in the stream's time base, the timestamps have run ahead of
340    /// the frames decoded since the last restart.
341    drift: i64,
342    /// Whether a frame came at another rate, past which the timestamps are
343    /// no longer the audio's.
344    rate_changed: bool,
345}
346
347/// How far before a seek target decoding restarts.
348///
349/// A decoder resuming mid-stream has no history: AAC's first frame lacks the
350/// previous frame's overlap. Starting a tenth of a second early and
351/// discarding up to the target gives the samples decoding straight through
352/// would.
353const AUDIO_SEEK_PREROLL_TICKS: i64 = TICKS_PER_SECOND / 10;
354
355/// How far before a seek target decoding restarts in Opus (400 ms).
356///
357/// Opus asks for at least 80 ms of pre-roll (RFC 7845), but its decoder's
358/// state converges on a straight decode's only gradually: from 100 ms early
359/// the samples at the target are still 2e-2 off (about -34 dB), from 250 ms
360/// 1e-4, from 400 ms under 1e-6.
361const OPUS_SEEK_PREROLL_TICKS: i64 = TICKS_PER_SECOND * 2 / 5;
362
363impl NativeAudioDecoder {
364    /// Selects strict failure (default) or bounded, reported recovery.
365    pub fn set_decode_policy(&mut self, policy: DecodePolicy) {
366        self.recovery.policy = policy;
367    }
368
369    /// Receives each diagnostic before recovery continues, including before errors.
370    pub fn set_decode_reporter(
371        &mut self,
372        reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
373    ) {
374        self.recovery.reporter = Some(Box::new(reporter));
375    }
376
377    /// Cumulative corruption and concealment diagnostics, including seeks.
378    #[must_use]
379    pub fn decode_diagnostics(&self) -> &DecodeDiagnostics {
380        &self.recovery.diagnostics
381    }
382
383    /// Decodes the selected audio stream through the installed `FFmpeg` 8 runtime.
384    ///
385    /// Stream selection and decoding use the linked `FFmpeg` libraries through
386    /// rsmpeg; no command-line program is spawned. Sample-rate conversion is
387    /// `alpha2_audio::Resampler`.
388    ///
389    /// # Errors
390    /// Returns a platform-neutral error for invalid rates, missing streams,
391    /// unsupported layouts, codec failures, or malformed decoded audio.
392    pub fn open(path: impl AsRef<Path>, project_sample_rate: u32) -> Result<Self, DecodeError> {
393        if project_sample_rate == 0 {
394            return Err(DecodeError::OutOfRange);
395        }
396        let path = path.as_ref().to_owned();
397        let mut input = open_input(&path)?;
398        let probe = probe_context(&input, &path)?;
399        let selected = probe.selected_audio.ok_or_else(|| {
400            DecodeError::Unsupported("source has no audio stream this runtime decodes".into())
401        })?;
402        let stream = probe
403            .stream(selected)
404            .ok_or_else(|| DecodeError::Adapter("selected audio stream disappeared".into()))?;
405        check_source_sample_rate(i64::from(stream.sample_rate.unwrap_or(0)))?;
406        let source_channels =
407            usize::try_from(stream.channels.unwrap_or(0)).map_err(|_| DecodeError::OutOfRange)?;
408        if source_channels == 0 {
409            return Err(DecodeError::Unsupported(
410                "audio stream declares no channels".into(),
411            ));
412        }
413        check_audio_channels(source_channels)?;
414        let decoder = open_audio_decoder(&mut input, selected)?;
415        let (prefetched, timestamps) = read_first_packets(&mut input, selected);
416        let stream = input
417            .streams()
418            .get(selected)
419            .ok_or(DecodeError::OutOfRange)?;
420        let time_base = stream.time_base;
421        let seek_preroll = if stream.codecpar().codec_id == ffi::AV_CODEC_ID_OPUS {
422            OPUS_SEEK_PREROLL_TICKS
423        } else {
424            AUDIO_SEEK_PREROLL_TICKS
425        };
426        // ADTS has no timestamps: FFmpeg's `aac` demuxer counts each packet as `frame_size` samples
427        // at the starting rate, whatever rate a later header names.
428        let made_up = (input.iformat().name() == c"aac")
429            .then(|| u32::try_from(stream.codecpar().sample_rate).ok())
430            .flatten()
431            .filter(|rate| *rate > 0)
432            .map(|rate| MadeUpTimestamps {
433                rate,
434                drift: 0,
435                rate_changed: false,
436            });
437        // Timestamps a demuxer makes up count samples exactly: there is no rounding to place by.
438        let grid = u32::try_from(stream.codecpar().sample_rate)
439            .ok()
440            .filter(|_| made_up.is_none())
441            .and_then(|rate| {
442                alpha2_time::audio_frame_grid(
443                    &timestamps,
444                    i64::from(time_base.num),
445                    i64::from(time_base.den),
446                    rate,
447                )
448                .map(|grid| (rate, grid))
449            });
450        // Channels stay as the stream lays them out; the engine folds them by speaker. The layout
451        // comes from the decoder (codec header), else the container, else the count's default.
452        let declared = [decoder.ch_layout(), stream.codecpar().ch_layout()]
453            .into_iter()
454            .find(|layout| {
455                layout.order != ffi::AV_CHANNEL_ORDER_UNSPEC
456                    && usize::try_from(layout.nb_channels).ok() == Some(source_channels)
457            })
458            .map_or_else(
459                || vec![Speaker::Unnamed; source_channels],
460                |layout| layout_speakers(&layout),
461            );
462        let (speakers, speakers_assumed) = SpeakerLayout::declared_or_default(&declared)
463            .ok_or_else(|| unsupported_channels(source_channels))?;
464        let channel_count = speakers.channels();
465        let output_layout = ffmpeg_layout(&speakers)?;
466        Ok(Self {
467            path,
468            input,
469            input_position: InputPosition::Opened,
470            container_duration: probe.duration,
471            streams: probe.streams,
472            audio_index: selected,
473            time_base,
474            decoder,
475            converter: None,
476            sample_rate: project_sample_rate,
477            channel_count,
478            speakers,
479            speakers_assumed,
480            output_layout,
481            timeline: SourceTimeline::new(project_sample_rate, channel_count, None),
482            pending: (0..channel_count).map(|_| VecDeque::new()).collect(),
483            next_sample: None,
484            block_frames: 1_024,
485            demux_eof: false,
486            decoder_flushed: false,
487            recovery: Recovery::new(time_base),
488            repair_audio: AudioContinuity::Continuous,
489            made_up,
490            grid,
491            prefetched,
492            frames_placed: 0,
493            seek_preroll,
494        })
495    }
496
497    /// Sets the maximum observable block size returned by `decode_next`.
498    ///
499    /// # Errors
500    /// Returns [`DecodeError::OutOfRange`] for zero frames.
501    pub fn set_block_frames(&mut self, frames: usize) -> Result<(), DecodeError> {
502        if frames == 0 {
503            return Err(DecodeError::OutOfRange);
504        }
505        self.block_frames = frames;
506        Ok(())
507    }
508
509    /// Channels of every block this decoder returns: the source's, 1 to
510    /// [`MAX_AUDIO_CHANNELS`].
511    #[must_use]
512    pub const fn channels(&self) -> usize {
513        self.channel_count
514    }
515
516    /// The speaker each plane of every block feeds: the layout the stream
517    /// declares, in its own order, or the default for its channel count.
518    #[must_use]
519    pub const fn speakers(&self) -> SpeakerLayout {
520        self.speakers
521    }
522
523    /// Whether the stream declares no layout, so [`Self::speakers`] is the
524    /// default for its channel count rather than what it says.
525    #[must_use]
526    pub const fn speakers_assumed(&self) -> bool {
527        self.speakers_assumed
528    }
529
530    /// Effective end of the selected audio source.
531    ///
532    /// Containers commonly declare a slightly longer presentation duration
533    /// than the selected stream because of codec priming and time-base
534    /// rounding. Browser-authored projects use that container duration, so the
535    /// adapter preserves it as the source boundary instead of rejecting the
536    /// final presentation window.
537    #[must_use]
538    pub fn audio_duration(&self) -> Option<MediaTime> {
539        effective_media_duration(
540            self.streams
541                .iter()
542                .find(|stream| stream.index == self.audio_index)
543                .and_then(|stream| stream.duration),
544            self.container_duration,
545        )
546    }
547
548    /// The selected stream's first timestamp in its time base, zero when the
549    /// demuxer does not know it.
550    fn stream_start(&self) -> i64 {
551        self.input
552            .streams()
553            .get(self.audio_index)
554            .map(|stream| stream.start_time)
555            .filter(|start| *start != ffi::AV_NOPTS_VALUE)
556            .unwrap_or(0)
557    }
558
559    /// The most decoded source samples per channel held at once since the
560    /// last open or seek.
561    #[cfg(test)]
562    pub(crate) const fn peak_held_samples(&self) -> usize {
563        self.timeline.peak_held()
564    }
565
566    /// Goes back to the stream's first packet. An input nothing has been
567    /// decoded from is already there, its first packets still to be read
568    /// from [`Self::prefetched`]; any other is opened again, since a raw
569    /// elementary stream cannot seek before its first packet and a seek that
570    /// lands later would drop samples unnoticed.
571    pub(crate) fn reset_to_start(&mut self) -> Result<(), DecodeError> {
572        if self.input_position == InputPosition::Opened {
573            self.decoder.flush_buffers();
574        } else {
575            let mut input = open_input(&self.path)?;
576            self.decoder = open_audio_decoder(&mut input, self.audio_index)?;
577            self.input = input;
578            self.input_position = InputPosition::Opened;
579            self.prefetched.clear();
580        }
581        self.restart(None);
582        Ok(())
583    }
584
585    /// Forgets everything decoded; the next output is no earlier than `first`.
586    fn restart(&mut self, first: Option<i64>) {
587        self.converter = None;
588        self.timeline = SourceTimeline::new(self.sample_rate, self.channel_count, first);
589        self.pending.iter_mut().for_each(VecDeque::clear);
590        self.next_sample = None;
591        self.demux_eof = false;
592        self.decoder_flushed = false;
593        self.repair_audio = AudioContinuity::Continuous;
594        self.recovery.reset_progress();
595        if let Some(made_up) = &mut self.made_up {
596            made_up.drift = 0;
597        }
598    }
599
600    /// Whether a frame came at another rate than the made-up timestamps
601    /// count in.
602    fn made_up_rate_changed(&self) -> bool {
603        self.made_up
604            .as_ref()
605            .is_some_and(|made_up| made_up.rate_changed)
606    }
607
608    /// The time `pts` of a frame of `samples` samples at `rate` really is, in a
609    /// stream whose demuxer made the timestamps up: each packet before it was
610    /// counted as lasting its samples at the stream's first rate, and lasted
611    /// them at its own. Anywhere else `pts` as it is.
612    fn true_timestamp(&mut self, pts: Option<i64>, rate: u32, samples: i32) -> Option<i64> {
613        let time_base = self.time_base;
614        let Some(made_up) = &mut self.made_up else {
615            return pts;
616        };
617        let declared = made_up.rate;
618        made_up.rate_changed |= rate != declared;
619        let corrected = pts.map(|pts| pts.saturating_sub(made_up.drift));
620        let (num, den) = (
621            i128::from(time_base.num.max(1)),
622            i128::from(time_base.den.max(1)),
623        );
624        let samples = i128::from(samples.max(0));
625        // The demuxer's duration rounds down (`compute_pkt_fields`); the frame's own is rounded to
626        // the nearest unit.
627        let counted = samples * den / (i128::from(declared) * num);
628        let lasted = (2 * samples * den + i128::from(rate) * num) / (2 * i128::from(rate) * num);
629        made_up.drift = made_up
630            .drift
631            .saturating_add(i64::try_from(counted - lasted).unwrap_or(0));
632        corrected
633    }
634
635    pub(crate) fn receive_audio_frame(&mut self) -> Result<bool, DecodeError> {
636        let mut frame = match self.decoder.receive_frame() {
637            Ok(frame) => frame,
638            Err(RsmpegError::DecoderDrainError | RsmpegError::DecoderFlushedError) => {
639                return Ok(false);
640            }
641            Err(error) => {
642                self.recovery.error(error, "audio receive", None)?;
643                self.repair_audio = AudioContinuity::Damaged;
644                return Ok(true);
645            }
646        };
647        if frame.flags & i32::try_from(ffi::AV_FRAME_FLAG_CORRUPT).expect("flag fits i32") != 0
648            || frame.decode_error_flags != 0
649        {
650            self.recovery.corrupt("audio frame", Some(frame.pts))?;
651            self.repair_audio = AudioContinuity::Damaged;
652            return Ok(true);
653        }
654        let pts = Some(if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
655            frame.pts
656        } else {
657            frame.best_effort_timestamp
658        })
659        .filter(|pts| *pts != ffi::AV_NOPTS_VALUE);
660        if pts.is_some_and(|pts| !audio_timestamp_in_range(pts, self.time_base)) {
661            self.recovery.corrupt("audio timestamp", pts)?;
662            self.repair_audio = AudioContinuity::Damaged;
663            return Ok(true);
664        }
665        self.recovery.progress(pts.unwrap_or(ffi::AV_NOPTS_VALUE))?;
666        normalize_audio_frame_layout(&mut frame, &self.output_layout)?;
667        // A decoder may change rate mid-stream, so the frame's own rate is checked, not only the
668        // stream's.
669        check_source_sample_rate(i64::from(frame.sample_rate))?;
670        let rate = u32::try_from(frame.sample_rate).map_err(|_| DecodeError::OutOfRange)?;
671        let pts = self.true_timestamp(pts, rate, frame.nb_samples);
672        let planes = self.convert_audio_frame(&frame)?;
673        self.place_audio(rate, pts, planes)?;
674        Ok(true)
675    }
676
677    /// The frame as `f32` planes feeding [`Self::speakers`], at its own rate.
678    fn convert_audio_frame(&mut self, frame: &AVFrame) -> Result<Vec<Vec<f32>>, DecodeError> {
679        let mut packed = audio_output_frame(frame.sample_rate, &self.output_layout);
680        let mut converter = match self.converter.take() {
681            Some(converter) => converter,
682            None => audio_converter(frame, &self.output_layout)?,
683        };
684        if let Err(error) = converter.convert_frame(Some(frame), &mut packed) {
685            if error.raw_error() != Some(ffi::AVERROR_INPUT_CHANGED) {
686                return Err(adapter_error(error));
687            }
688            converter = audio_converter(frame, &self.output_layout)?;
689            packed = audio_output_frame(frame.sample_rate, &self.output_layout);
690            converter
691                .convert_frame(Some(frame), &mut packed)
692                .map_err(adapter_error)?;
693        }
694        self.converter = Some(converter);
695        // At one rate `swresample` holds nothing back: what goes in comes out.
696        if packed.nb_samples != frame.nb_samples {
697            return Err(DecodeError::Adapter(format!(
698                "converting {} decoded audio samples gave {}",
699                frame.nb_samples, packed.nb_samples
700            )));
701        }
702        let mut planes = (0..self.channel_count)
703            .map(|_| VecDeque::new())
704            .collect::<Vec<_>>();
705        append_interleaved_audio(&packed, &mut planes)?;
706        Ok(planes.into_iter().map(Vec::from).collect())
707    }
708
709    /// Places one decoded frame where its timestamp says.
710    ///
711    /// A timestamp within the frame's duration, 2 ms, or two time-base units
712    /// of where the previous frame ended continues it: containers round
713    /// timestamps (Matroska to the millisecond), and mediabunny, the browser
714    /// host's demuxer, ignores a jump shorter than a frame. Further ahead is a
715    /// gap, which stays silent; further back repeats samples already placed,
716    /// which are dropped. After damage the timestamp is trusted exactly.
717    fn place_audio(
718        &mut self,
719        rate: u32,
720        pts: Option<i64>,
721        mut planes: Vec<Vec<f32>>,
722    ) -> Result<(), DecodeError> {
723        let rate_i32 = i32::try_from(rate).map_err(|_| DecodeError::OutOfRange)?;
724        let at = pts.map(|pts| {
725            av_rescale_q_rnd(
726                pts,
727                self.time_base,
728                ra(1, rate_i32),
729                ffi::AV_ROUND_NEAR_INF | ffi::AV_ROUND_PASS_MINMAX,
730            )
731        });
732        let damaged = self.repair_audio == AudioContinuity::Damaged;
733        self.repair_audio = AudioContinuity::Continuous;
734        self.frames_placed = self.frames_placed.wrapping_add(1);
735        let frames = i64::try_from(planes.first().map_or(0, Vec::len)).unwrap_or(i64::MAX);
736        let Some(expected) = self.timeline.expected(rate) else {
737            // The first frame after an open or a seek: on the grid its stream's timestamps show, so
738            // it is where a decode straight through puts it however it is reached.
739            let at = match (at, self.grid) {
740                (Some(at), Some((grid_rate, grid))) if !damaged && grid_rate == rate => {
741                    grid.place(at, frames)
742                }
743                (at, _) => at.unwrap_or(0),
744            };
745            return self.timeline.place(rate, at, planes);
746        };
747        let Some(at) = at else {
748            if damaged {
749                return Err(DecodeError::Adapter(
750                    "cannot locate damaged audio gap without PTS".into(),
751                ));
752            }
753            return self.timeline.place(rate, expected, planes);
754        };
755        let ahead = at
756            .checked_sub(expected)
757            .ok_or_else(|| DecodeError::Adapter("audio timestamps are out of range".into()))?;
758        if damaged {
759            if ahead < 0 || ahead > i64::from(rate) * 10 {
760                return Err(DecodeError::Adapter(
761                    "damaged audio gap is ambiguous or exceeds 10 seconds".into(),
762                ));
763            }
764            if ahead > 0 {
765                self.recovery.audio_gap(
766                    self.timeline.output_at(rate, expected),
767                    self.timeline.output_at(rate, at),
768                    self.sample_rate,
769                    "corrupt input",
770                )?;
771            }
772            return self.timeline.place(rate, at, planes);
773        }
774        // One time-base unit in source samples, rounded up; 2 ms likewise.
775        let den = i64::from(self.time_base.den.max(1));
776        let unit = (i64::from(rate) * i64::from(self.time_base.num) + den - 1) / den;
777        let tolerance = frames
778            .max((i64::from(rate) + 499) / 500)
779            .max(unit.saturating_mul(2));
780        if ahead.abs() < tolerance {
781            return self.timeline.place(rate, expected, planes);
782        }
783        if ahead > 0 {
784            self.recovery.timestamp_gap(
785                self.timeline.output_at(rate, expected),
786                self.timeline.output_at(rate, at),
787                self.sample_rate,
788            );
789            return self.timeline.place(rate, at, planes);
790        }
791        let repeated = usize::try_from(-ahead).unwrap_or(usize::MAX);
792        self.recovery.timestamp_overlap(
793            self.timeline.output_at(rate, at),
794            self.timeline
795                .output_at(rate, at.saturating_add(frames.min(-ahead))),
796            self.sample_rate,
797        );
798        for plane in &mut planes {
799            plane.drain(..repeated.min(plane.len()));
800        }
801        self.timeline.place(rate, expected, planes)
802    }
803
804    pub(crate) fn fill_pending(
805        &mut self,
806        frames: usize,
807        cancel: &CancelToken,
808    ) -> Result<(), DecodeError> {
809        interrupt_on_cancel(&self.input, cancel);
810        let mut budget = WorkBudget::new();
811        let mut foreign = false;
812        let mut placed = self.frames_placed;
813        loop {
814            let held = self.pending.first().map_or(0, VecDeque::len);
815            if held >= frames {
816                return Ok(());
817            }
818            // A placed frame is progress even before it reaches the output, so a seek decoding far
819            // to its target (or from the start, after a rate change) is not cut short.
820            if self.frames_placed != placed {
821                placed = self.frames_placed;
822                budget.progressed();
823            }
824            // Another stream's packet is skipped, not work: a demuxer that ignores the discard
825            // still reads a long stream's packets to reach this one's end.
826            if std::mem::take(&mut foreign) {
827                if cancel.is_cancelled() {
828                    return Err(DecodeError::Cancelled);
829                }
830            } else {
831                budget.step(cancel)?;
832            }
833            if let Some((start, _)) = self.timeline.produce(&mut self.pending, frames - held) {
834                if held == 0 {
835                    self.next_sample = Some(start);
836                }
837                continue;
838            }
839            if self.receive_audio_frame()? {
840                continue;
841            }
842            if self.demux_eof {
843                if !self.decoder_flushed {
844                    match self.decoder.send_packet(None) {
845                        Ok(()) | Err(RsmpegError::DecoderFlushedError) => {}
846                        Err(RsmpegError::DecoderFullError) => continue,
847                        Err(error) => {
848                            self.recovery.error(error, "flush", None)?;
849                            continue;
850                        }
851                    }
852                    self.decoder_flushed = true;
853                    continue;
854                }
855                if self.repair_audio == AudioContinuity::Damaged {
856                    // Damage running to the end (Tolerant only) ends the stream at its last healthy
857                    // frame, as a damaged video tail does; the damage is already reported.
858                    self.recovery.damaged_tail("audio")?;
859                    self.repair_audio = AudioContinuity::Continuous;
860                }
861                if !self.timeline.finished() {
862                    self.timeline.finish();
863                    continue;
864                }
865                return Ok(());
866            }
867            self.input_position = InputPosition::Moved;
868            let packet = match self
869                .prefetched
870                .pop_front()
871                .unwrap_or_else(|| self.input.read_packet())
872            {
873                Ok(Some(packet)) => packet,
874                Ok(None) => {
875                    self.demux_eof = true;
876                    continue;
877                }
878                Err(error) => {
879                    let error = demux_failure(error, cancel)?;
880                    self.recovery.error(error, "audio demux", None)?;
881                    self.repair_audio = AudioContinuity::Damaged;
882                    continue;
883                }
884            };
885            if usize::try_from(packet.stream_index).ok() != Some(self.audio_index) {
886                foreign = true;
887                continue;
888            }
889            if packet.flags & i32::try_from(ffi::AV_PKT_FLAG_CORRUPT).expect("flag fits i32") != 0 {
890                self.recovery.corrupt("audio packet", Some(packet.pts))?;
891                self.repair_audio = AudioContinuity::Damaged;
892                continue;
893            }
894            submit_packet(
895                |drain| {
896                    if drain {
897                        return self.receive_audio_frame();
898                    }
899                    match self.decoder.send_packet(Some(&packet)) {
900                        Ok(()) => Ok(true),
901                        Err(RsmpegError::DecoderFullError) => Ok(false),
902                        Err(error) => {
903                            self.recovery.error(error, "audio send", Some(packet.pts))?;
904                            self.repair_audio = AudioContinuity::Damaged;
905                            Ok(true)
906                        }
907                    }
908                },
909                &mut budget,
910                cancel,
911            )?;
912        }
913    }
914}
915
916impl Decoder for NativeAudioDecoder {
917    fn streams(&self) -> &[StreamInfo] {
918        &self.streams
919    }
920    fn flush(&mut self) -> Result<(), DecodeError> {
921        self.reset_to_start()
922    }
923    fn seek(
924        &mut self,
925        request: DecodeRequest,
926        cancel: &CancelToken,
927    ) -> Result<DecodeResult, DecodeError> {
928        if cancel.is_cancelled() {
929            return Err(DecodeError::Cancelled);
930        }
931        let target = request
932            .time
933            .ticks()
934            .saturating_mul(i64::from(self.sample_rate))
935            .div_euclid(TICKS_PER_SECOND);
936        let timestamp = av_rescale_q(
937            request.time.ticks().saturating_sub(self.seek_preroll),
938            tick_base(),
939            self.time_base,
940        );
941        // Raw elementary streams (MP3, ADTS AAC, AC-3) index from the first packet, so a pre-roll
942        // seek before the start fails with a bare -1 (EPERM); reopen at the start instead. Same for
943        // seeks into made-up timestamps a rate change has put off the audio.
944        let rate_changed = self.made_up_rate_changed();
945        let from_start = timestamp <= self.stream_start() || rate_changed;
946        if from_start {
947            self.reset_to_start()?;
948        } else {
949            interrupt_on_cancel(&self.input, cancel);
950            self.input_position = InputPosition::Moved;
951            self.prefetched.clear();
952            self.input
953                .seek(
954                    i32::try_from(self.audio_index).map_err(|_| DecodeError::OutOfRange)?,
955                    timestamp,
956                    i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).expect("seek flag fits i32"),
957                )
958                .map_err(|error| seek_failure(error, cancel))?;
959        }
960        self.decoder.flush_buffers();
961        self.restart(Some(target));
962        let mut event = self.decode_next(cancel)?;
963        if !from_start && !rate_changed && self.made_up_rate_changed() {
964            // The seek landed past a rate change the made-up timestamps hide.
965            self.reset_to_start()?;
966            self.decoder.flush_buffers();
967            self.restart(Some(target));
968            event = self.decode_next(cancel)?;
969        }
970        if matches!(event, DecodeEvent::EndOfStream) {
971            return Err(DecodeError::OutOfRange);
972        }
973        Ok(DecodeResult { request, event })
974    }
975    fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError> {
976        if cancel.is_cancelled() {
977            return Err(DecodeError::Cancelled);
978        }
979        self.fill_pending(self.block_frames, cancel)?;
980        let frames = self
981            .pending
982            .first()
983            .map_or(0, VecDeque::len)
984            .min(self.block_frames);
985        if frames == 0 {
986            return Ok(DecodeEvent::EndOfStream);
987        }
988        let start_sample = self.next_sample.unwrap_or(0);
989        let block = AudioBlock {
990            start_sample,
991            sample_rate: self.sample_rate,
992            speakers: self.speakers,
993            channels: self
994                .pending
995                .iter_mut()
996                .map(|channel| channel.drain(..frames).collect())
997                .collect(),
998        };
999        self.next_sample =
1000            Some(start_sample.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX)));
1001        Ok(DecodeEvent::Audio(block))
1002    }
1003}
1004
1005/// Opens the decoder of `input`'s stream `stream_index`, and has the demuxer
1006/// skip every other stream.
1007fn open_audio_decoder(
1008    input: &mut DemuxInput,
1009    stream_index: usize,
1010) -> Result<AVCodecContext, DecodeError> {
1011    input.discard_all_but(stream_index);
1012    let stream = input
1013        .streams()
1014        .get(stream_index)
1015        .ok_or(DecodeError::OutOfRange)?;
1016    let parameters = stream.codecpar();
1017    let codec = AVCodec::find_decoder(parameters.codec_id)
1018        .ok_or_else(|| DecodeError::Unsupported("audio decoder is unavailable".into()))?;
1019    let mut decoder = AVCodecContext::new(&codec);
1020    decoder.apply_codecpar(&parameters).map_err(adapter_error)?;
1021    decoder.set_pkt_timebase(stream.time_base);
1022    decoder.open(None).map_err(adapter_error)?;
1023    Ok(decoder)
1024}
1025
1026/// How far from zero, in seconds, an audio frame's timestamp may be: 2^31 s,
1027/// some 68 years. No recording is longer, and within it a timestamp counted
1028/// in samples at the fastest source rate stays below 2^51, so placing samples
1029/// by it cannot overflow. A frame further away is damaged input.
1030const AUDIO_TIMESTAMP_LIMIT_SECONDS: i128 = 1 << 31;
1031
1032/// Whether `pts`, in `time_base`, is within [`AUDIO_TIMESTAMP_LIMIT_SECONDS`]
1033/// of zero.
1034fn audio_timestamp_in_range(pts: i64, time_base: AVRational) -> bool {
1035    let (num, den) = (i128::from(time_base.num), i128::from(time_base.den));
1036    num > 0 && den > 0 && (i128::from(pts) * num).abs() <= AUDIO_TIMESTAMP_LIMIT_SECONDS * den
1037}
1038
1039/// The sample rates a source's audio may have: none is recorded below
1040/// 1 kHz or above 768 kHz, the most `alpha2_audio::Resampler` takes. A file
1041/// that declares 1 Hz would otherwise turn each of its samples into a
1042/// project's 48,000.
1043pub const SOURCE_SAMPLE_RATES: std::ops::RangeInclusive<u32> = 1_000..=768_000;
1044
1045fn check_source_sample_rate(rate: i64) -> Result<(), DecodeError> {
1046    if u32::try_from(rate).is_ok_and(|rate| SOURCE_SAMPLE_RATES.contains(&rate)) {
1047        return Ok(());
1048    }
1049    Err(DecodeError::Unsupported(format!(
1050        "the source's audio is {rate} Hz, and alpha2 reads audio from {} Hz to {} Hz",
1051        SOURCE_SAMPLE_RATES.start(),
1052        SOURCE_SAMPLE_RATES.end()
1053    )))
1054}
1055
1056/// Converts a frame's sample format to packed `f32` in `output`, the
1057/// stream's layout, keeping its rate. A frame laid out as `output` is copied
1058/// channel for channel; one laid out otherwise (a stream whose layout
1059/// changed) is rematrixed into it.
1060pub(crate) fn audio_converter(
1061    frame: &AVFrame,
1062    output: &AVChannelLayout,
1063) -> Result<SwrContext, DecodeError> {
1064    if frame.sample_rate <= 0 || frame.format == ffi::AV_SAMPLE_FMT_NONE {
1065        return Err(DecodeError::Adapter(
1066            "decoded audio frame has an incomplete format".into(),
1067        ));
1068    }
1069    // Borrowed, never cloned: rsmpeg 0.18's `AVChannelLayout::clone` copies into uninitialized
1070    // memory that `av_channel_layout_copy` first frees (valgrind catches it).
1071    let input_layout = frame.ch_layout();
1072    let mut converter = SwrContext::new(
1073        output,
1074        ffi::AV_SAMPLE_FMT_FLT,
1075        frame.sample_rate,
1076        &input_layout,
1077        frame.format,
1078        frame.sample_rate,
1079    )
1080    .map_err(adapter_error)?;
1081    converter.init().map_err(adapter_error)?;
1082    Ok(converter)
1083}
1084
1085/// The speaker each channel of `layout` feeds, in its order; all
1086/// [`Speaker::Unnamed`] for a layout that gives only a count.
1087pub(crate) fn layout_speakers(layout: &AVChannelLayout) -> Vec<Speaker> {
1088    let channels = alpha2_ffmpeg_bridge::channel_layout_channels(layout);
1089    if channels.is_empty() {
1090        return vec![Speaker::Unnamed; usize::try_from(layout.nb_channels).unwrap_or(0)];
1091    }
1092    channels
1093        .into_iter()
1094        .map(|channel| Speaker::from_ffmpeg_channel(i64::from(channel)))
1095        .collect()
1096}
1097
1098/// `speakers` as an `FFmpeg` channel layout: native order when the speakers
1099/// are in it, a custom order otherwise.
1100pub(crate) fn ffmpeg_layout(speakers: &SpeakerLayout) -> Result<AVChannelLayout, DecodeError> {
1101    let channels = speakers
1102        .speakers()
1103        .iter()
1104        .map(|speaker| {
1105            ffi::AVChannel::try_from(speaker.ffmpeg_channel()).unwrap_or(ffi::AV_CHAN_UNKNOWN)
1106        })
1107        .collect::<Vec<_>>();
1108    alpha2_ffmpeg_bridge::channel_layout_of(&channels).map_err(|error| {
1109        DecodeError::Adapter(format!(
1110            "could not lay out audio channels {}: {error:?}",
1111            speakers.describe()
1112        ))
1113    })
1114}
1115
1116fn unsupported_channels(channels: usize) -> DecodeError {
1117    DecodeError::Unsupported(format!(
1118        "audio with {channels} channels is unsupported: the engine takes 1 to {MAX_AUDIO_CHANNELS}"
1119    ))
1120}
1121
1122fn check_audio_channels(channels: usize) -> Result<(), DecodeError> {
1123    if (1..=MAX_AUDIO_CHANNELS).contains(&channels) {
1124        Ok(())
1125    } else {
1126        Err(unsupported_channels(channels))
1127    }
1128}
1129
1130/// Gives a frame whose layout names no speakers the layout it is converted
1131/// to, so `swresample` copies it channel for channel instead of refusing to
1132/// rematrix it.
1133///
1134/// Such a layout gives only a count: `UNSPEC` (classic PCM WAV records no
1135/// channel mask), `AMBISONIC` (ambisonic components are not speakers), or a
1136/// custom order with a channel `FFmpeg` has no speaker for (`QuickTime`'s
1137/// discrete labels). With the stream's count it is the stream's own layout,
1138/// which [`NativeAudioDecoder::speakers`] already reports as assumed; with
1139/// another count it is that count's default, rematrixed into the stream's
1140/// where `swresample` can. A frame whose layout names its speakers is left
1141/// alone and rematrixed by name.
1142///
1143/// Leaving an `UNSPEC` frame as it is would also fail on its own:
1144/// `swresample` resolves the input to a concrete layout, so the original
1145/// frame then looks like a format change and returns `AVERROR_INPUT_CHANGED`
1146/// on every retry.
1147pub(crate) fn normalize_audio_frame_layout(
1148    frame: &mut AVFrame,
1149    stream: &AVChannelLayout,
1150) -> Result<(), DecodeError> {
1151    let (channels, named) = {
1152        let layout = frame.ch_layout();
1153        let named = matches!(
1154            layout.order,
1155            ffi::AV_CHANNEL_ORDER_NATIVE | ffi::AV_CHANNEL_ORDER_CUSTOM
1156        ) && !layout_speakers(&layout).contains(&Speaker::Unnamed);
1157        (layout.nb_channels, named)
1158    };
1159    if channels <= 0 {
1160        return Err(DecodeError::Adapter(
1161            "decoded audio frame has no channels".into(),
1162        ));
1163    }
1164    if named {
1165        return Ok(());
1166    }
1167    if channels == stream.nb_channels {
1168        let mut owned = AVChannelLayout::from_nb_channels(1);
1169        owned.copy(stream);
1170        frame.set_ch_layout(owned.into_inner());
1171        return Ok(());
1172    }
1173    let count = usize::try_from(channels).map_err(|_| DecodeError::OutOfRange)?;
1174    let speakers = SpeakerLayout::default_for(count).ok_or_else(|| unsupported_channels(count))?;
1175    frame.set_ch_layout(ffmpeg_layout(&speakers)?.into_inner());
1176    Ok(())
1177}
1178
1179pub(crate) fn audio_output_frame(sample_rate: i32, layout: &AVChannelLayout) -> AVFrame {
1180    let mut frame = AVFrame::new();
1181    frame.set_format(ffi::AV_SAMPLE_FMT_FLT);
1182    frame.set_sample_rate(sample_rate);
1183    let mut owned = AVChannelLayout::from_nb_channels(1);
1184    owned.copy(layout);
1185    frame.set_ch_layout(owned.into_inner());
1186    frame
1187}
1188
1189pub(crate) fn append_interleaved_audio(
1190    frame: &AVFrame,
1191    output: &mut [VecDeque<f32>],
1192) -> Result<(), DecodeError> {
1193    let packed = alpha2_ffmpeg_bridge::audio_f32_interleaved(frame).map_err(adapter_error)?;
1194    if output.is_empty() || packed.len() % output.len() != 0 {
1195        return Err(DecodeError::Adapter(
1196            "FFmpeg returned a partial audio frame".into(),
1197        ));
1198    }
1199    for frame in packed.chunks_exact(output.len()) {
1200        for (channel, sample) in output.iter_mut().zip(frame) {
1201            channel.push_back(*sample);
1202        }
1203    }
1204    Ok(())
1205}
1206
1207/// Whether an `AVCodecHWConfig`'s `methods` include `method`.
1208///
1209/// `bindgen` types `FFmpeg`'s enum constants by the target's C ABI: unsigned on
1210/// Linux, macOS and MinGW, `int` on MSVC. Widening both sides is the one
1211/// spelling that compiles, lint-free, on all of them.
1212pub(crate) fn has_hw_config_method(methods: i32, method: impl Into<i64>) -> bool {
1213    i64::from(methods) & method.into() != 0
1214}
1215
1216/// Where a seek starts that must read from a stream's first packet: below
1217/// any timestamp a demuxer indexes, with room for the ones that shift the
1218/// target first (`mov` subtracts its composition shift). MPEG-TS needs it:
1219/// its first packet's decode time is before the stream's start time, so a
1220/// seek to the start time lands after it. A demuxer that refuses a time
1221/// before its index (`FFmpeg`'s generic search, which AVI uses) is sought to
1222/// the stream's start time instead.
1223const STREAM_START_SEEK: i64 = i64::MIN / 2;
1224
1225/// What decoding from a seek's landing found for a time.
1226enum Landing {
1227    /// The frame presented at the time.
1228    Presented(DecodedFrame),
1229    /// The first frame decoded was already later than the time.
1230    Late(DecodedFrame),
1231    /// No frame before the end of the stream.
1232    Empty,
1233}
1234
1235impl NativeDecoder {
1236    /// Selects strict failure (default) or bounded, reported recovery.
1237    pub fn set_decode_policy(&mut self, policy: DecodePolicy) {
1238        self.recovery.policy = policy;
1239    }
1240
1241    /// Takes the color a project declares for this source (its
1242    /// `color_metadata`), which every decoded frame then carries in place of
1243    /// what the frame and the stream are tagged with: a declaration corrects
1244    /// missing or wrong tags. `None` leaves the tags in charge.
1245    pub fn declare_color(&mut self, color: Option<ColorMetadata>) {
1246        self.declared_color = color;
1247    }
1248
1249    /// Where `frame`'s chroma sits, as its codec defines it: what a hardware
1250    /// session's frames report, which no software path reads.
1251    #[cfg(all(
1252        any(target_os = "linux", target_os = "macos", target_os = "windows"),
1253        feature = "hwdecode"
1254    ))]
1255    pub(crate) fn picture_chroma_location(&self, frame: &AVFrame) -> alpha2_media::ChromaLocation {
1256        crate::convert::decoded_chroma_location(self.decoder.codec_id, frame.chroma_location)
1257    }
1258
1259    /// The color `frame` is shown with: the project's declaration, else the
1260    /// frame's own tags, else the stream's.
1261    pub(crate) fn picture_color(&self, frame: &AVFrame) -> Option<ColorMetadata> {
1262        self.declared_color
1263            .or_else(|| frame_color_metadata(frame))
1264            .or(self.color)
1265    }
1266
1267    /// Receives each diagnostic before recovery continues, including before errors.
1268    pub fn set_decode_reporter(
1269        &mut self,
1270        reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
1271    ) {
1272        self.recovery.reporter = Some(Box::new(reporter));
1273    }
1274
1275    /// Cumulative corruption and concealment diagnostics, including seeks.
1276    #[must_use]
1277    pub fn decode_diagnostics(&self) -> &DecodeDiagnostics {
1278        &self.recovery.diagnostics
1279    }
1280
1281    /// Opens the deterministically selected video stream.
1282    ///
1283    /// # Errors
1284    /// Returns an error when no decodable video stream exists.
1285    pub fn open(path: impl AsRef<Path>) -> Result<Self, DecodeError> {
1286        Self::open_with_threads(path, codec_thread_budget())
1287    }
1288
1289    /// Opens the selected stream with an explicit `FFmpeg` worker allocation.
1290    ///
1291    /// # Errors
1292    /// Returns an error when no decodable video stream exists.
1293    pub fn open_with_threads(path: impl AsRef<Path>, threads: i64) -> Result<Self, DecodeError> {
1294        if !(1..=64).contains(&threads) {
1295            return Err(DecodeError::OutOfRange);
1296        }
1297        let mut input = open_input(path.as_ref())?;
1298        let probed = probe_context(&input, path.as_ref())?;
1299        let video_index = probed.selected_video.ok_or_else(|| {
1300            DecodeError::Unsupported("resource has no decodable video stream".into())
1301        })?;
1302        input.discard_all_but(video_index);
1303        let selected = probed
1304            .streams
1305            .iter()
1306            .find(|stream| stream.index == video_index)
1307            .ok_or(DecodeError::OutOfRange)?;
1308        let orientation = selected.orientation;
1309        let sample_aspect_ratio = selected.sample_aspect_ratio;
1310        // HDR decodes like any 10-bit stream; the renderer tone-maps it or the project refuses it.
1311        let color = selected.color;
1312        let (time_base, decoder) = {
1313            let stream = &input.streams()[video_index];
1314            let parameters = stream.codecpar();
1315            let codec = software_video_decoder(parameters.codec_id)?;
1316            let mut decoder = AVCodecContext::new(&codec);
1317            decoder.apply_codecpar(&parameters).map_err(adapter_error)?;
1318            decoder.set_pkt_timebase(stream.time_base);
1319            let unused = alpha2_ffmpeg_bridge::open_codec_context(
1320                &mut decoder,
1321                video_decoder_options(threads),
1322            )
1323            .map_err(adapter_error)?;
1324            reject_unused_decode_options(unused)?;
1325            (stream.time_base, decoder)
1326        };
1327        let (prefetched, slack) = read_presentation_slack(&mut input, video_index, time_base);
1328        Ok(Self {
1329            time_base,
1330            input,
1331            container_duration: probed.duration,
1332            streams: probed.streams,
1333            video_index,
1334            decoder,
1335            orientation,
1336            sample_aspect_ratio,
1337            color,
1338            declared_color: None,
1339            video_scaler: crate::convert::FrameScaler::default(),
1340            pending: VecDeque::new(),
1341            demux_eof: false,
1342            decoder_flushed: false,
1343            stages: DecodeStageTimings::default(),
1344            first_video_pts: None,
1345            recovery: Recovery::new(time_base),
1346            slack,
1347            prefetched,
1348        })
1349    }
1350
1351    /// Opens one hardware decode session for `spec` on an existing device.
1352    ///
1353    /// The caller's thread budget is passed through unchanged; see
1354    /// `codec_thread_allocation` for the share a hardware session is given.
1355    /// Failures carry a typed reason so callers can record why a source fell
1356    /// back rather than guessing from an error string.
1357    #[cfg(all(
1358        any(target_os = "linux", target_os = "macos", target_os = "windows"),
1359        feature = "hwdecode"
1360    ))]
1361    #[allow(clippy::too_many_lines)]
1362    pub(crate) fn open_hardware(
1363        path: impl AsRef<Path>,
1364        threads: i64,
1365        device: AVHWDeviceContext,
1366        spec: &HardwareDecoderSpec,
1367    ) -> Result<Self, HardwareOpenError> {
1368        use crate::video_frame::HardwareDecodeFallback::{
1369            DecoderOpenFailed, NoHardwareConfig, SourceUnavailable, UnsupportedColor,
1370        };
1371
1372        let unavailable = |error| HardwareOpenError::new(SourceUnavailable, error);
1373        let mut input = open_input(path.as_ref()).map_err(unavailable)?;
1374        let probed = probe_context(&input, path.as_ref()).map_err(unavailable)?;
1375        let video_index = probed.selected_video.ok_or_else(|| {
1376            HardwareOpenError::new(
1377                SourceUnavailable,
1378                DecodeError::Unsupported("resource has no decodable video stream".into()),
1379            )
1380        })?;
1381        input.discard_all_but(video_index);
1382        let selected = probed
1383            .streams
1384            .iter()
1385            .find(|stream| stream.index == video_index)
1386            .ok_or_else(|| HardwareOpenError::new(SourceUnavailable, DecodeError::OutOfRange))?;
1387        let orientation = selected.orientation;
1388        let sample_aspect_ratio = selected.sample_aspect_ratio;
1389        let color = selected.color;
1390        if selected.color.is_some_and(ColorMetadata::is_hdr) {
1391            return Err(HardwareOpenError::new(
1392                UnsupportedColor,
1393                DecodeError::Unsupported("hardware export currently supports SDR only".into()),
1394            ));
1395        }
1396        let (time_base, decoder) = {
1397            let stream = &input.streams()[video_index];
1398            let parameters = stream.codecpar();
1399            let preferred = AVCodec::find_decoder(parameters.codec_id).ok_or_else(|| {
1400                HardwareOpenError::new(
1401                    NoHardwareConfig,
1402                    DecodeError::Unsupported("selected video codec has no decoder".into()),
1403                )
1404            })?;
1405            let supported = |codec: &AVCodecRef<'_>| {
1406                (0..)
1407                    .map_while(|index| codec.hw_config(index))
1408                    .any(|config| {
1409                        config.device_type == spec.device_type
1410                            && config.pix_fmt == spec.pixel_format
1411                            && (!spec.require_device_context
1412                                || has_hw_config_method(
1413                                    config.methods,
1414                                    ffi::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX,
1415                                ))
1416                    })
1417            };
1418            let Some(codec) =
1419                registered_video_decoders(parameters.codec_id).find(|codec| supported(codec))
1420            else {
1421                return Err(HardwareOpenError::new(
1422                    NoHardwareConfig,
1423                    DecodeError::Unsupported(format!(
1424                        "decoder {} has no {} hardware configuration",
1425                        preferred.name().to_string_lossy(),
1426                        spec.backend
1427                    )),
1428                ));
1429            };
1430            check_hardware_sample_profile(parameters.format, spec)?;
1431            check_hardware_crop(&parameters, spec)?;
1432            let open_failed = |error| HardwareOpenError::new(DecoderOpenFailed, error);
1433            let mut decoder = AVCodecContext::new(&codec);
1434            decoder
1435                .apply_codecpar(&parameters)
1436                .map_err(adapter_error)
1437                .map_err(open_failed)?;
1438            decoder.set_pkt_timebase(stream.time_base);
1439            decoder.set_get_format(Some(spec.get_format));
1440            decoder.set_hw_device_ctx(device);
1441            // A hardware decoder failing to open is the ordinary case here, so the open must fail
1442            // cleanly.
1443            let unused = alpha2_ffmpeg_bridge::open_codec_context(
1444                &mut decoder,
1445                video_decoder_options(threads),
1446            )
1447            .map_err(adapter_error)
1448            .map_err(open_failed)?;
1449            reject_unused_decode_options(unused).map_err(open_failed)?;
1450            (stream.time_base, decoder)
1451        };
1452        let (prefetched, slack) = read_presentation_slack(&mut input, video_index, time_base);
1453        Ok(Self {
1454            time_base,
1455            input,
1456            container_duration: probed.duration,
1457            streams: probed.streams,
1458            video_index,
1459            decoder,
1460            orientation,
1461            sample_aspect_ratio,
1462            color,
1463            declared_color: None,
1464            video_scaler: crate::convert::FrameScaler::default(),
1465            pending: VecDeque::new(),
1466            demux_eof: false,
1467            decoder_flushed: false,
1468            stages: DecodeStageTimings::default(),
1469            first_video_pts: None,
1470            recovery: Recovery::hardware(time_base),
1471            slack,
1472            prefetched,
1473        })
1474    }
1475
1476    /// Effective end of the selected video source.
1477    ///
1478    /// The container duration is included because browser media APIs expose
1479    /// that duration when authoring a clip, while `FFmpeg`'s selected stream can
1480    /// end a few ticks earlier because of time-base rounding.
1481    #[must_use]
1482    pub fn video_duration(&self) -> Option<MediaTime> {
1483        effective_media_duration(
1484            self.streams
1485                .iter()
1486                .find(|stream| stream.index == self.video_index)
1487                .and_then(|stream| stream.duration),
1488            self.container_duration,
1489        )
1490    }
1491
1492    /// Takes the spans accumulated since the last call.
1493    pub(crate) fn take_stage_timings(&mut self) -> DecodeStageTimings {
1494        std::mem::take(&mut self.stages)
1495    }
1496
1497    pub(crate) fn receive_available_raw(
1498        &mut self,
1499        cancel: &CancelToken,
1500    ) -> Result<Option<DecodedFrame>, DecodeError> {
1501        let mut budget = WorkBudget::new();
1502        loop {
1503            budget.step(cancel)?;
1504            let started = Instant::now();
1505            let received = DecodedFrame::receive(&mut self.decoder);
1506            self.stages.receive_frame = self.stages.receive_frame.saturating_add(started.elapsed());
1507            match received {
1508                Ok(frame) => {
1509                    if frame.flags
1510                        & i32::try_from(ffi::AV_FRAME_FLAG_CORRUPT).expect("flag fits i32")
1511                        != 0
1512                        || frame.decode_error_flags != 0
1513                    {
1514                        self.recovery.corrupt("video frame", Some(frame.pts))?;
1515                        continue;
1516                    }
1517                    #[cfg(test)]
1518                    VIDEO_FRAMES_DECODED.with(|decoded| decoded.set(decoded.get() + 1));
1519                    self.recovery.progress(
1520                        if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1521                            frame.pts
1522                        } else {
1523                            frame.best_effort_timestamp
1524                        },
1525                    )?;
1526                    return Ok(Some(frame));
1527                }
1528                Err(RsmpegError::DecoderDrainError | RsmpegError::DecoderFlushedError) => {
1529                    return Ok(None);
1530                }
1531                Err(error) => {
1532                    self.recovery.error(error, "video receive", None)?;
1533                }
1534            }
1535        }
1536    }
1537
1538    pub(crate) fn convert_received(
1539        &mut self,
1540        frame: &DecodedFrame,
1541    ) -> Result<DecodedVideoFrame, DecodeError> {
1542        // HDR frames keep their components and color: the renderer tone-maps them, or the project
1543        // refuses them.
1544        let declared = self.picture_color(frame);
1545        let started = Instant::now();
1546        let converted = convert_video_frame(
1547            frame,
1548            self.decoder.codec_id,
1549            self.time_base,
1550            declared.unwrap_or(ColorMetadata::UNDECLARED),
1551            &mut self.video_scaler,
1552        );
1553        self.stages.convert = self.stages.convert.saturating_add(started.elapsed());
1554        let mut converted = converted?;
1555        // Nothing declared the color: the frame goes without one, so the renderer applies the same
1556        // fallback and reports it. A repack to the identity matrix keeps the color it names.
1557        if declared.is_none() && converted.color == Some(ColorMetadata::UNDECLARED) {
1558            converted.color = None;
1559        }
1560        converted.orientation = self.orientation;
1561        converted.sample_aspect_ratio = self.picture_sample_aspect_ratio(frame);
1562        Ok(converted)
1563    }
1564
1565    /// The pixel aspect ratio `frame` is shown with: the container's
1566    /// declaration, else the picture's own (a broadcast stream may change it
1567    /// between pictures), else the stream's as probed.
1568    pub(crate) fn picture_sample_aspect_ratio(&self, frame: &AVFrame) -> SampleAspectRatio {
1569        // A ratio the container declares is the probed one, and overrides the picture's.
1570        if declared_sar(self.input.streams()[self.video_index].sample_aspect_ratio).is_some() {
1571            return self.sample_aspect_ratio;
1572        }
1573        declared_sar(frame.sample_aspect_ratio).unwrap_or(self.sample_aspect_ratio)
1574    }
1575
1576    pub(crate) fn raw_frame_pts(&self, frame: &AVFrame) -> Result<MediaTime, DecodeError> {
1577        let pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
1578            frame.pts
1579        } else {
1580            frame.best_effort_timestamp
1581        };
1582        timestamp(pts, self.time_base).ok_or(DecodeError::OutOfRange)
1583    }
1584
1585    /// Drops queued packets and frames, keeping the stream selection.
1586    pub(crate) fn flush_video(&mut self) {
1587        let started = Instant::now();
1588        self.recovery.reset_progress();
1589        self.decoder.flush_buffers();
1590        self.stages.seek = self.stages.seek.saturating_add(started.elapsed());
1591        self.pending.clear();
1592        self.prefetched.clear();
1593        self.demux_eof = false;
1594        self.decoder_flushed = false;
1595    }
1596
1597    /// Seeks to `target` and returns the raw frame that is presented at it:
1598    /// the last frame whose timestamp is at or before its presentation
1599    /// deadline ([`Self::presentation_deadline`]), a few ticks after `target`
1600    /// for the rounding its timestamps went through.
1601    ///
1602    /// Deciding that takes the frame after, which is kept for the next
1603    /// `decode_raw_next`. A stream that starts after `target` answers with
1604    /// its first frame, and one that ends before it with its last.
1605    ///
1606    /// The seek goes to the keyframe at or before the deadline, so a
1607    /// keyframe stored up to the slack after `target` is landed on, not
1608    /// decoded to from the one before. The demuxer's landing is checked, not
1609    /// trusted: MPEG-TS and MPEG-PS index every packet as a keyframe, so a
1610    /// seek lands mid-GOP and the codec shows nothing before the next one,
1611    /// and an open GOP's leading frames cannot be decoded from the I-frame a
1612    /// seek lands on. When the first frame decoded after a seek is later than
1613    /// the deadline and is not the stream's first frame, the seek starts
1614    /// again 1 s, 2 s, 4 s, ... earlier until a frame at or before the
1615    /// deadline comes out or the seek starts at the stream's first packet.
1616    ///
1617    /// # Errors
1618    /// Returns cancellation, seek, and decode failures, and
1619    /// [`DecodeError::OutOfRange`] for a stream with no frame at all.
1620    pub(crate) fn seek_raw(
1621        &mut self,
1622        target: MediaTime,
1623        cancel: &CancelToken,
1624    ) -> Result<DecodedFrame, DecodeError> {
1625        if cancel.is_cancelled() {
1626            return Err(DecodeError::Cancelled);
1627        }
1628        let start_time = self.input.streams()[self.video_index].start_time;
1629        let stream_start = timestamp(start_time, self.time_base).unwrap_or(MediaTime::ZERO);
1630        let deadline = self.presentation_deadline(target);
1631        let mut back_off = 0_i64;
1632        loop {
1633            // From the deadline: a keyframe stored up to the slack after the target is the frame
1634            // shown there, and seeking to the one before would decode a GOP for nothing.
1635            let from = MediaTime::from_ticks(deadline.ticks().saturating_sub(back_off));
1636            let from_start =
1637                from <= stream_start || self.first_video_pts.is_some_and(|first| deadline < first);
1638            if !from_start {
1639                // Rounded down, so a time just before a keyframe never seeks onto it.
1640                let position = av_rescale_q_rnd(
1641                    from.ticks(),
1642                    tick_base(),
1643                    self.time_base,
1644                    ffi::AV_ROUND_DOWN,
1645                );
1646                self.seek_video_to(position, cancel)?;
1647            } else if let Err(error) = self.seek_video_to(STREAM_START_SEEK, cancel) {
1648                if error == DecodeError::Cancelled {
1649                    return Err(error);
1650                }
1651                let start = if start_time == ffi::AV_NOPTS_VALUE {
1652                    0
1653                } else {
1654                    start_time
1655                };
1656                self.seek_video_to(start, cancel)?;
1657            }
1658            let mut first = None;
1659            let landing = self.present_after_seek(target, &mut first, cancel)?;
1660            if from_start {
1661                // Read from the first packet, the first frame out is the stream's first frame.
1662                self.first_video_pts = first.or(self.first_video_pts);
1663            }
1664            match landing {
1665                Landing::Presented(frame) => return Ok(frame),
1666                Landing::Late(frame) if from_start || first == self.first_video_pts => {
1667                    return Ok(frame);
1668                }
1669                Landing::Empty if from_start => return Err(DecodeError::OutOfRange),
1670                Landing::Late(_) | Landing::Empty => {}
1671            }
1672            back_off = if back_off == 0 {
1673                TICKS_PER_SECOND
1674            } else {
1675                back_off.saturating_mul(2)
1676            };
1677        }
1678    }
1679
1680    /// `av_seek_frame` to `position`, in the stream's time base, and a flush.
1681    fn seek_video_to(&mut self, position: i64, cancel: &CancelToken) -> Result<(), DecodeError> {
1682        interrupt_on_cancel(&self.input, cancel);
1683        let seek_started = Instant::now();
1684        let sought = self.input.seek(
1685            i32::try_from(self.video_index).map_err(|_| DecodeError::OutOfRange)?,
1686            position,
1687            i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).expect("seek flag fits i32"),
1688        );
1689        self.stages.seek = self.stages.seek.saturating_add(seek_started.elapsed());
1690        sought.map_err(|error| seek_failure(error, cancel))?;
1691        self.flush_video();
1692        Ok(())
1693    }
1694
1695    /// Decodes from where a seek landed to the frame presented at `target`,
1696    /// the last whose timestamp is at or before its presentation deadline
1697    /// ([`Self::presentation_deadline`]), noting in `first` when the
1698    /// first frame decoded there is presented.
1699    fn present_after_seek(
1700        &mut self,
1701        target: MediaTime,
1702        first: &mut Option<MediaTime>,
1703        cancel: &CancelToken,
1704    ) -> Result<Landing, DecodeError> {
1705        let deadline = self.presentation_deadline(target);
1706        let mut presented: Option<DecodedFrame> = None;
1707        let mut budget = WorkBudget::new();
1708        loop {
1709            budget.step(cancel)?;
1710            let Some(raw) = self.decode_raw_next(cancel)? else {
1711                return Ok(presented.map_or(Landing::Empty, Landing::Presented));
1712            };
1713            // A keyframe far before the target takes long to decode from, and is still progress.
1714            budget.progressed();
1715            #[cfg(test)]
1716            crate::recovery::take_frame_delay();
1717            let pts = self.raw_frame_pts(&raw)?;
1718            first.get_or_insert(pts);
1719            if pts > deadline {
1720                return Ok(match presented {
1721                    Some(frame) => {
1722                        self.pending.push_front(raw);
1723                        Landing::Presented(frame)
1724                    }
1725                    None => Landing::Late(raw),
1726                });
1727            }
1728            // A frame from the target to its deadline is the one shown: the next is a frame
1729            // interval later.
1730            if pts >= target {
1731                return Ok(Landing::Presented(raw));
1732            }
1733            presented = Some(raw);
1734        }
1735    }
1736
1737    /// The latest timestamp a frame of this stream may carry and still be
1738    /// the one shown at `time`: `time` plus the stream's
1739    /// [`alpha2_time::presentation_slack`], from its time base and the
1740    /// rounding its first timestamps show. The frame shown is the last one at
1741    /// or before it.
1742    #[must_use]
1743    pub fn presentation_deadline(&self, time: MediaTime) -> MediaTime {
1744        time.presentation_deadline(self.slack)
1745    }
1746
1747    /// When the frame the next [`Decoder::decode_next`] returns is
1748    /// presented, or `None` at the end of the stream.
1749    ///
1750    /// The frame is decoded now and kept for that call, so a caller can find
1751    /// the frame presented at a time (the last one at or before its
1752    /// [`Self::presentation_deadline`]) without consuming the one after.
1753    ///
1754    /// # Errors
1755    /// Returns cancellation and decode failures.
1756    pub fn peek_video_pts(
1757        &mut self,
1758        cancel: &CancelToken,
1759    ) -> Result<Option<MediaTime>, DecodeError> {
1760        self.peek_raw_pts(cancel)
1761    }
1762
1763    /// The presentation time of the frame `decode_raw_next` returns next,
1764    /// which is decoded now and kept for it; `None` at the end of the stream.
1765    ///
1766    /// # Errors
1767    /// Returns cancellation and decode failures.
1768    pub(crate) fn peek_raw_pts(
1769        &mut self,
1770        cancel: &CancelToken,
1771    ) -> Result<Option<MediaTime>, DecodeError> {
1772        let Some(raw) = self.decode_raw_next(cancel)? else {
1773            return Ok(None);
1774        };
1775        let pts = self.raw_frame_pts(&raw);
1776        self.pending.push_front(raw);
1777        pts.map(Some)
1778    }
1779
1780    pub(crate) fn decode_raw_next(
1781        &mut self,
1782        cancel: &CancelToken,
1783    ) -> Result<Option<DecodedFrame>, DecodeError> {
1784        if cancel.is_cancelled() {
1785            return Err(DecodeError::Cancelled);
1786        }
1787        if let Some(frame) = self.pending.pop_front() {
1788            return Ok(Some(frame));
1789        }
1790        interrupt_on_cancel(&self.input, cancel);
1791        let mut budget = WorkBudget::new();
1792        let mut foreign = false;
1793        loop {
1794            // Another stream's packet is skipped, not work (see `fill_pending`).
1795            if std::mem::take(&mut foreign) {
1796                if cancel.is_cancelled() {
1797                    return Err(DecodeError::Cancelled);
1798                }
1799            } else {
1800                budget.step(cancel)?;
1801            }
1802            if let Some(frame) = self.pending.pop_front() {
1803                return Ok(Some(frame));
1804            }
1805            if let Some(frame) = self.receive_available_raw(cancel)? {
1806                return Ok(Some(frame));
1807            }
1808            if self.demux_eof {
1809                if !self.decoder_flushed {
1810                    match self.decoder.send_packet(None) {
1811                        Ok(()) | Err(RsmpegError::DecoderFlushedError) => {}
1812                        Err(RsmpegError::DecoderFullError) => continue,
1813                        Err(error) => {
1814                            self.recovery.error(error, "flush", None)?;
1815                            continue;
1816                        }
1817                    }
1818                    self.decoder_flushed = true;
1819                    continue;
1820                }
1821                self.recovery.video_eof()?;
1822                return Ok(None);
1823            }
1824            let demux_started = Instant::now();
1825            let packet = self
1826                .prefetched
1827                .pop_front()
1828                .unwrap_or_else(|| self.input.read_packet());
1829            self.stages.demux = self.stages.demux.saturating_add(demux_started.elapsed());
1830            let packet = match packet {
1831                Ok(Some(packet)) => packet,
1832                Ok(None) => {
1833                    self.demux_eof = true;
1834                    continue;
1835                }
1836                Err(error) => {
1837                    let error = demux_failure(error, cancel)?;
1838                    self.recovery.error(error, "video demux", None)?;
1839                    continue;
1840                }
1841            };
1842            if usize::try_from(packet.stream_index).ok() != Some(self.video_index) {
1843                foreign = true;
1844                continue;
1845            }
1846            if packet.flags & i32::try_from(ffi::AV_PKT_FLAG_CORRUPT).expect("flag fits i32") != 0 {
1847                self.recovery.corrupt("video packet", Some(packet.pts))?;
1848                continue;
1849            }
1850            submit_packet(
1851                |drain| {
1852                    if drain {
1853                        let corrupt_before = self.recovery.diagnostics.corrupt_packets.len();
1854                        if let Some(frame) = self.receive_available_raw(cancel)? {
1855                            self.pending.push_back(frame);
1856                            return Ok(true);
1857                        }
1858                        return Ok(self.recovery.diagnostics.corrupt_packets.len() > corrupt_before);
1859                    }
1860                    let started = Instant::now();
1861                    let sent = self.decoder.send_packet(Some(&packet));
1862                    self.stages.send_packet += started.elapsed();
1863                    match sent {
1864                        Ok(()) => Ok(true),
1865                        Err(RsmpegError::DecoderFullError) => Ok(false),
1866                        Err(error) => {
1867                            self.recovery.error(error, "video send", Some(packet.pts))?;
1868                            Ok(true)
1869                        }
1870                    }
1871                },
1872                &mut budget,
1873                cancel,
1874            )?;
1875        }
1876    }
1877}
1878
1879impl Decoder for NativeDecoder {
1880    fn streams(&self) -> &[StreamInfo] {
1881        &self.streams
1882    }
1883
1884    fn flush(&mut self) -> Result<(), DecodeError> {
1885        self.flush_video();
1886        Ok(())
1887    }
1888
1889    fn seek(
1890        &mut self,
1891        request: DecodeRequest,
1892        cancel: &CancelToken,
1893    ) -> Result<DecodeResult, DecodeError> {
1894        let raw = self.seek_raw(request.time, cancel)?;
1895        let frame = self.convert_received(&raw)?;
1896        Ok(DecodeResult {
1897            request,
1898            event: DecodeEvent::Video(frame),
1899        })
1900    }
1901
1902    fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError> {
1903        if cancel.is_cancelled() {
1904            return Err(DecodeError::Cancelled);
1905        }
1906        match self.decode_raw_next(cancel)? {
1907            Some(frame) => Ok(DecodeEvent::Video(self.convert_received(&frame)?)),
1908            None => Ok(DecodeEvent::EndOfStream),
1909        }
1910    }
1911}
1912
1913/// Decoders `FFmpeg` registers that decode only through a hardware session.
1914/// Without one they open, and then fail every packet: `FFmpeg` 8.0's own AV1
1915/// decoder answers `ENOSYS` ("Your platform doesn't support hardware
1916/// accelerated AV1 decoding"). The pinned SDK decodes AV1 in software with
1917/// libdav1d instead; a runtime built without it has no software AV1 decoder.
1918const HWACCEL_ONLY_DECODERS: &[&str] = &["av1"];
1919
1920/// Every decoder `FFmpeg` registers for `codec_id`, the one it prefers first.
1921/// An external software decoder is preferred over `FFmpeg`'s own: with
1922/// libdav1d, `find_decoder` answers AV1 with it, and only `FFmpeg`'s own
1923/// `av1` decoder carries the hardware configurations. A hardware session
1924/// therefore looks through all of them.
1925pub(crate) fn registered_video_decoders(
1926    codec_id: ffi::AVCodecID,
1927) -> impl Iterator<Item = AVCodecRef<'static>> {
1928    AVCodec::iterate().filter(move |codec| {
1929        codec.id == codec_id
1930            && AVCodec::find_decoder_by_name(codec.name())
1931                .is_some_and(|found| found.as_ptr() == codec.as_ptr())
1932    })
1933}
1934
1935/// The decoder a software session opens for `codec_id`: the first one
1936/// `FFmpeg` would pick, passing over those that need a hardware session.
1937pub(crate) fn software_video_decoder(
1938    codec_id: ffi::AVCodecID,
1939) -> Result<AVCodecRef<'static>, DecodeError> {
1940    let Some(preferred) = AVCodec::find_decoder(codec_id) else {
1941        return Err(DecodeError::Unsupported(
1942            "selected video codec has no decoder".into(),
1943        ));
1944    };
1945    let hardware_only =
1946        |codec: &AVCodecRef<'_>| HWACCEL_ONLY_DECODERS.contains(&&*codec.name().to_string_lossy());
1947    if !hardware_only(&preferred) {
1948        return Ok(preferred);
1949    }
1950    let experimental = i32::try_from(ffi::AV_CODEC_CAP_EXPERIMENTAL).unwrap_or(0);
1951    AVCodec::iterate()
1952        .find(|codec| {
1953            codec.id == codec_id
1954                && AVCodec::find_decoder_by_name(codec.name())
1955                    .is_some_and(|found| found.as_ptr() == codec.as_ptr())
1956                && codec.capabilities & experimental == 0
1957                && !hardware_only(codec)
1958        })
1959        .ok_or_else(|| DecodeError::Unsupported(hardware_only_reason(&preferred)))
1960}
1961
1962fn hardware_only_reason(decoder: &AVCodecRef<'_>) -> String {
1963    let name = decoder.name().to_string_lossy();
1964    format!(
1965        "{name} needs a hardware decoder; this runtime has no software {} decoder",
1966        name.to_uppercase()
1967    )
1968}
1969
1970/// Why this runtime cannot decode `stream` without a hardware session, or
1971/// `None` when a software session can. A probe names the decoder `FFmpeg`
1972/// prefers (`StreamInfo::codec`); some of those decode only through a
1973/// hardware session, and a runtime without libdav1d has no software AV1
1974/// decoder.
1975#[must_use]
1976pub fn software_decode_unsupported(stream: &StreamInfo) -> Option<String> {
1977    if stream.kind != StreamKind::Video {
1978        return None;
1979    }
1980    let name = CString::new(stream.codec.as_str()).ok()?;
1981    let decoder = AVCodec::find_decoder_by_name(&name)?;
1982    software_video_decoder(decoder.id)
1983        .err()
1984        .map(|error| match error {
1985            DecodeError::Unsupported(reason) => reason,
1986            error => error.to_string(),
1987        })
1988}
1989
1990/// The longer of a stream's and its container's duration (see
1991/// [`NativeDecoder::video_duration`]).
1992pub(crate) fn effective_media_duration(
1993    stream_duration: Option<MediaTime>,
1994    container_duration: Option<MediaTime>,
1995) -> Option<MediaTime> {
1996    stream_duration.into_iter().chain(container_duration).max()
1997}
1998
1999/// Refuses a stream whose SPS crops its left or top edge on a backend whose
2000/// surfaces do not keep that crop ([`HardwareDecoderSpec::keeps_leading_crop`]).
2001/// A sample description whose SPS this cannot read is not refused.
2002#[cfg(all(
2003    any(target_os = "linux", target_os = "macos", target_os = "windows"),
2004    feature = "hwdecode"
2005))]
2006fn check_hardware_crop(
2007    parameters: &rsmpeg::avcodec::AVCodecParameters,
2008    spec: &HardwareDecoderSpec,
2009) -> Result<(), HardwareOpenError> {
2010    if spec.keeps_leading_crop {
2011        return Ok(());
2012    }
2013    let extradata = alpha2_ffmpeg_bridge::codec_parameters_extradata(parameters);
2014    match crate::parameter_sets::leading_crop(parameters.codec_id, &extradata) {
2015        Some((left, top)) if left > 0 || top > 0 => Err(HardwareOpenError::new(
2016            crate::video_frame::HardwareDecodeFallback::UnsupportedCrop,
2017            DecodeError::Unsupported(format!(
2018                "{} decode does not show a picture cropped {left} columns from the left and {top} rows from the top",
2019                spec.backend
2020            )),
2021        )),
2022        _ => Ok(()),
2023    }
2024}
2025
2026/// Rejects sample formats that have no resident presentation path.
2027///
2028/// Only 4:2:0 at eight bits, or ten bits where the renderer can present them,
2029/// reaches the GPU path. Deciding at open keeps an unsupported profile from
2030/// becoming a mid-export surface failure and reports the reason in one place.
2031/// A container that has not declared a format yet is left to the surface check
2032/// at import time.
2033#[cfg(all(
2034    any(target_os = "linux", target_os = "macos", target_os = "windows"),
2035    feature = "hwdecode"
2036))]
2037fn check_hardware_sample_profile(
2038    format: ffi::AVPixelFormat,
2039    spec: &HardwareDecoderSpec,
2040) -> Result<(), HardwareOpenError> {
2041    let Some(profile) = alpha2_ffmpeg_bridge::sample_profile(format) else {
2042        return Ok(());
2043    };
2044    let maximum_depth = if spec.ten_bit { 10 } else { 8 };
2045    if profile.is_yuv_420 && !profile.has_alpha && profile.bit_depth <= maximum_depth {
2046        return Ok(());
2047    }
2048    Err(HardwareOpenError::new(
2049        crate::video_frame::HardwareDecodeFallback::UnsupportedProfile,
2050        DecodeError::Unsupported(format!(
2051            "{} decode does not present {}-bit {} input",
2052            spec.backend,
2053            profile.bit_depth,
2054            if profile.is_yuv_420 {
2055                "4:2:0"
2056            } else {
2057                "non-4:2:0"
2058            }
2059        )),
2060    ))
2061}
2062
2063/// The demuxers a source may be opened with: the containers alpha2 reads,
2064/// by `FFmpeg` short name. Every one of them reads only the file it is given.
2065/// Left out are, above all, the demuxers that open further files a file
2066/// names (HLS playlists, `ffconcat` lists, image-sequence patterns), which
2067/// would let an asset read media a request never named, and containers no
2068/// alpha2 runtime claims to read.
2069pub(crate) const SOURCE_CONTAINERS: &CStr =
2070    c"mov,matroska,mpegts,avi,mp3,aac,wav,flac,ogg,png_pipe,jpeg_pipe,webp_pipe,gif";
2071
2072/// Opens a source with the input policy: [`SOURCE_CONTAINERS`] only, and
2073/// only the `file` protocol, so neither the file nor anything a demuxer
2074/// opens for it reaches past a local path.
2075pub(crate) fn open_input(path: &Path) -> Result<DemuxInput, DecodeError> {
2076    #[cfg(test)]
2077    INPUTS_OPENED.with(|opened| opened.set(opened.get() + 1));
2078    let text = path
2079        .to_str()
2080        .ok_or_else(|| DecodeError::Unsupported("media path is not UTF-8".into()))?;
2081    let path = CString::new(text)
2082        .map_err(|_| DecodeError::Unsupported("media path contains a NUL byte".into()))?;
2083    alpha2_ffmpeg_bridge::open_input(&path, SOURCE_CONTAINERS).map_err(|error| match error {
2084        OpenInputError::Refused(format) => DecodeError::Unsupported(format!(
2085            "the file is {format} input, which is not a container alpha2 reads"
2086        )),
2087        OpenInputError::Ffmpeg(error) => adapter_error(error),
2088    })
2089}
2090
2091#[cfg(test)]
2092thread_local! {
2093    /// How many inputs [`open_input`] has opened on this thread.
2094    pub(crate) static INPUTS_OPENED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2095    /// How many video frames the codec has given a [`NativeDecoder`] on this thread.
2096    pub(crate) static VIDEO_FRAMES_DECODED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2097}
2098
2099/// Lets `cancel` stop a blocking `FFmpeg` call on `input`, not only the next
2100/// call: a seek or a read that waits on the file returns `AVERROR_EXIT`.
2101pub(crate) fn interrupt_on_cancel(input: &DemuxInput, cancel: &CancelToken) {
2102    let cancel = cancel.clone();
2103    input.interrupt_when(move || cancel.is_cancelled());
2104}
2105
2106/// A demux or seek failure, as a cancel when the cancel stopped it.
2107fn demux_failure(error: RsmpegError, cancel: &CancelToken) -> Result<RsmpegError, DecodeError> {
2108    if cancel.is_cancelled() {
2109        Err(DecodeError::Cancelled)
2110    } else {
2111        Ok(error)
2112    }
2113}
2114
2115/// A seek failure, as a cancel when the cancel stopped it.
2116fn seek_failure(error: RsmpegError, cancel: &CancelToken) -> DecodeError {
2117    demux_failure(error, cancel).map_or_else(|cancelled| cancelled, adapter_error)
2118}
2119
2120/// Demuxers of one still picture, whose decoder, not the container, says how
2121/// the picture is shown: `FFmpeg` attaches a JPEG's Exif `Orientation` to the
2122/// decoded frame, never to the stream.
2123const STILL_IMAGE_FORMATS: [&CStr; 3] = [c"jpeg_pipe", c"png_pipe", c"webp_pipe"];
2124
2125fn is_still_image_format(input: &DemuxInput) -> bool {
2126    STILL_IMAGE_FORMATS.contains(&input.iformat().name())
2127}
2128
2129/// The orientation a still picture's decoder attaches to its frame, read by
2130/// decoding it once on its own input: `None` when it attaches none or the
2131/// picture does not decode, which the decode that shows it then reports.
2132///
2133/// A decoder that can decode at a fraction of the size does, since only the
2134/// frame's side data is read.
2135fn still_image_orientation(path: &Path, index: usize) -> Option<Orientation> {
2136    let mut input = open_input(path).ok()?;
2137    let (codec, mut decoder) = {
2138        let stream = input.streams().get(index)?;
2139        let parameters = stream.codecpar();
2140        let codec = software_video_decoder(parameters.codec_id).ok()?;
2141        let mut decoder = AVCodecContext::new(&codec);
2142        decoder.apply_codecpar(&parameters).ok()?;
2143        decoder.set_pkt_timebase(stream.time_base);
2144        (codec, decoder)
2145    };
2146    let mut options = video_decoder_options(1);
2147    if codec.max_lowres > 0 {
2148        options = options.set_int(c"lowres", i64::from(codec.max_lowres), 0);
2149    }
2150    alpha2_ffmpeg_bridge::open_codec_context(&mut decoder, options).ok()?;
2151    let mut sent = false;
2152    // One picture is one packet; a few more allow for a demuxer that splits it.
2153    for _ in 0..16 {
2154        match decoder.receive_frame() {
2155            Ok(frame) => {
2156                return alpha2_ffmpeg_bridge::frame_display_rotation(&frame)
2157                    .and_then(orientation_from_display);
2158            }
2159            Err(RsmpegError::DecoderDrainError) => {}
2160            Err(_) => return None,
2161        }
2162        match input.read_packet().ok()? {
2163            Some(packet) if usize::try_from(packet.stream_index).ok() == Some(index) => {
2164                decoder.send_packet(Some(&packet)).ok()?;
2165                sent = true;
2166            }
2167            Some(_) => {}
2168            None if sent => {
2169                decoder.send_packet(None).ok()?;
2170                sent = false;
2171            }
2172            None => return None,
2173        }
2174    }
2175    None
2176}
2177
2178#[allow(clippy::too_many_lines)]
2179/// Bytes from a file's end read for the end of its packets, at first: as
2180/// much as `FFmpeg` reads for its own estimate (`DURATION_MAX_READ_SIZE`),
2181/// doubled up to five times while no packet with a time is found there.
2182const PARSED_END_READ: u64 = 250_000;
2183
2184/// Where `input`'s packets end, in `AV_TIME_BASE` units on the media's own
2185/// timeline (the latest `pts + duration` of an audio or video packet), when
2186/// `FFmpeg` estimated its duration from timestamps; `None` otherwise or when
2187/// it cannot be read.
2188///
2189/// That estimate (MPEG-TS, MPEG-PS) reads the last raw packets, unparsed:
2190/// one PES packet of a transport stream carries several AAC frames, so it
2191/// ends at the first frame's end and leaves out the rest of the packet, as
2192/// much as 0.19 s of a 1.8 s stream. A browser demuxer, and
2193/// `ffprobe -show_packets`, end at the last frame. This reads the tail again
2194/// through the parsers `av_read_frame` splits packets with, from another
2195/// input on the same file, so `input`'s own position is untouched.
2196fn parsed_end(input: &DemuxInput, path: &Path) -> Option<i64> {
2197    if input.duration_estimation_method != ffi::AVFMT_DURATION_FROM_PTS {
2198        return None;
2199    }
2200    let size = std::fs::metadata(path).ok()?.len();
2201    let mut tail = open_input(path).ok()?;
2202    let streams = tail
2203        .streams()
2204        .iter()
2205        .map(|stream| {
2206            let kind = stream.codecpar().codec_type();
2207            (kind.is_audio() || kind.is_video()).then_some(stream.time_base)
2208        })
2209        .collect::<Vec<_>>();
2210    let av_time_base = ra(1, i32::try_from(ffi::AV_TIME_BASE).ok()?);
2211    for doubling in 0..6 {
2212        let from = size.saturating_sub(PARSED_END_READ << doubling);
2213        tail.seek(
2214            -1,
2215            i64::try_from(from).ok()?,
2216            i32::try_from(ffi::AVSEEK_FLAG_BYTE).ok()?,
2217        )
2218        .ok()?;
2219        let mut end = None::<i64>;
2220        while let Ok(Some(packet)) = tail.read_packet() {
2221            let Some(Some(time_base)) = usize::try_from(packet.stream_index)
2222                .ok()
2223                .and_then(|index| streams.get(index))
2224            else {
2225                continue;
2226            };
2227            if packet.pts == ffi::AV_NOPTS_VALUE {
2228                continue;
2229            }
2230            let packet_end = av_rescale_q(
2231                packet.pts.saturating_add(packet.duration.max(0)),
2232                *time_base,
2233                av_time_base,
2234            );
2235            end = Some(end.map_or(packet_end, |end: i64| end.max(packet_end)));
2236        }
2237        if end.is_some() || from == 0 {
2238            return end;
2239        }
2240    }
2241    None
2242}
2243
2244/// The [`ProbeResult`] of an opened input, with `FFmpeg`'s best video and audio streams selected.
2245pub(crate) fn probe_context(input: &DemuxInput, path: &Path) -> Result<ProbeResult, DecodeError> {
2246    let selected_video = input
2247        .best_stream(ffi::AVMEDIA_TYPE_VIDEO)
2248        .map_err(adapter_error)?;
2249    let selected_audio = input
2250        .best_stream(ffi::AVMEDIA_TYPE_AUDIO)
2251        .map_err(adapter_error)?;
2252    let streams = input
2253        .streams()
2254        .iter()
2255        .enumerate()
2256        .filter_map(|(index, stream)| {
2257            let parameters = stream.codecpar();
2258            let kind = if parameters.codec_type().is_video() {
2259                StreamKind::Video
2260            } else if parameters.codec_type().is_audio() {
2261                StreamKind::Audio
2262            } else {
2263                return None;
2264            };
2265            let codec = AVCodec::find_decoder(parameters.codec_id).map_or_else(
2266                || "unknown".to_owned(),
2267                |codec| codec.name().to_string_lossy().into_owned(),
2268            );
2269            let dimensions = (kind == StreamKind::Video).then(|| {
2270                (
2271                    u32::try_from(parameters.width).unwrap_or(0),
2272                    u32::try_from(parameters.height).unwrap_or(0),
2273                )
2274            });
2275            let frame_rate = stream
2276                .guess_framerate()
2277                .filter(|rate| rate.num > 0 && rate.den > 0)
2278                .and_then(|rate| {
2279                    FrameRate::new(u32::try_from(rate.num).ok()?, u32::try_from(rate.den).ok()?)
2280                        .ok()
2281                });
2282            Some(StreamInfo {
2283                index,
2284                kind,
2285                codec,
2286                duration: timestamp(stream.duration, stream.time_base),
2287                dimensions,
2288                frame_rate,
2289                sample_rate: (parameters.sample_rate > 0)
2290                    .then(|| u32::try_from(parameters.sample_rate).unwrap_or(0)),
2291                channels: (kind == StreamKind::Audio)
2292                    .then(|| u32::try_from(parameters.ch_layout.nb_channels).unwrap_or(0)),
2293                orientation: alpha2_ffmpeg_bridge::display_rotation(&parameters)
2294                    .and_then(orientation_from_display)
2295                    .or_else(|| {
2296                        (kind == StreamKind::Video
2297                            && Some(index) == selected_video
2298                            && is_still_image_format(input))
2299                        .then(|| still_image_orientation(path, index))
2300                        .flatten()
2301                    })
2302                    .unwrap_or_else(|| Orientation::rotated(stream_rotation(stream.metadata()))),
2303                sample_aspect_ratio: guess_sar(
2304                    stream.sample_aspect_ratio,
2305                    parameters.sample_aspect_ratio,
2306                ),
2307                color: if kind == StreamKind::Video {
2308                    color_metadata(&parameters)
2309                } else {
2310                    None
2311                },
2312            })
2313        })
2314        .collect();
2315    let av_time_base = ra(
2316        1,
2317        i32::try_from(ffi::AV_TIME_BASE).expect("time base fits i32"),
2318    );
2319    let duration = parsed_end(input, path)
2320        .filter(|_| input.start_time != ffi::AV_NOPTS_VALUE)
2321        .map(|end| end.saturating_sub(input.start_time))
2322        .map_or(input.duration, |parsed| parsed.max(input.duration));
2323    Ok(ProbeResult {
2324        format: input.iformat().name().to_string_lossy().into_owned(),
2325        duration: (duration > 0)
2326            .then(|| MediaTime::from_ticks(av_rescale_q(duration, av_time_base, tick_base()))),
2327        start: (input.start_time != ffi::AV_NOPTS_VALUE).then(|| {
2328            MediaTime::from_ticks(av_rescale_q(
2329                input.start_time,
2330                ra(
2331                    1,
2332                    i32::try_from(ffi::AV_TIME_BASE).expect("time base fits i32"),
2333                ),
2334                tick_base(),
2335            ))
2336        }),
2337        streams,
2338        selected_video,
2339        selected_audio,
2340    })
2341}