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