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