Skip to main content

zvidlib_core/
codec.rs

1use crate::{
2    AudioBuffer, Codec, ColorRange, Error, ErrorKind, FrameIndex, FrameSource, Limits, PixelFormat,
3    Plane, Result, VideoDimensions, VideoFrame,
4};
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::Duration;
11
12/// A non-`Send` encoder future that works on native and single-threaded WASM.
13pub type EncoderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + 'a>>;
14
15/// The kind of media carried by an encoded track.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum TrackKind {
18    Video,
19    Audio,
20}
21
22/// Dependency information written to the MP4 sample dependency table.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct SampleDependency {
25    pub is_leading: u8,
26    pub depends_on: u8,
27    pub is_depended_on: u8,
28    pub has_redundancy: u8,
29}
30
31impl SampleDependency {
32    pub const INDEPENDENT: Self = Self {
33        is_leading: 0,
34        depends_on: 2,
35        is_depended_on: 0,
36        has_redundancy: 0,
37    };
38
39    pub const DEPENDENT: Self = Self {
40        is_leading: 0,
41        depends_on: 1,
42        is_depended_on: 0,
43        has_redundancy: 0,
44    };
45
46    #[doc(hidden)]
47    pub fn to_sdtp(self) -> u8 {
48        ((self.is_leading & 3) << 6)
49            | ((self.depends_on & 3) << 4)
50            | ((self.is_depended_on & 3) << 2)
51            | (self.has_redundancy & 3)
52    }
53}
54
55/// One encoded access unit with exact decode and presentation timing.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct EncodedSample {
58    pub data: Vec<u8>,
59    pub dts: i64,
60    pub pts: i64,
61    pub duration: u32,
62    pub is_sync: bool,
63    pub dependency: SampleDependency,
64}
65
66/// Codec and sample-entry information needed to declare an MP4 track.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct EncoderConfig {
69    pub codec: Codec,
70    pub timescale: u32,
71    /// Complete codec configuration box, including its size and fourcc.
72    pub decoder_config: Vec<u8>,
73}
74
75/// Video format accepted by a video encoder.
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub struct VideoEncoderFormat {
78    pub dimensions: VideoDimensions,
79    pub pixel_format: PixelFormat,
80}
81
82/// Audio format accepted by an audio encoder.
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub struct AudioEncoderFormat {
85    pub sample_rate: u32,
86    pub channels: u16,
87}
88
89/// Gapless audio metadata produced when an encoder is drained.
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct AudioGapless {
92    pub priming: u32,
93    pub padding: u32,
94}
95
96/// Packets and metadata emitted while draining an audio encoder.
97#[derive(Clone, Debug, Default, Eq, PartialEq)]
98pub struct AudioDrain {
99    pub samples: Vec<EncodedSample>,
100    pub gapless: AudioGapless,
101}
102
103/// Backend-neutral video encoder contract.
104pub trait VideoEncoder {
105    fn config(&self) -> &EncoderConfig;
106    fn format(&self) -> VideoEncoderFormat;
107    /// Whether this encoder runs on dedicated hardware or in software.
108    ///
109    /// A factory's [`VideoEncoderFactory::capability`] answers the same
110    /// question before an encoder exists; this is the answer for the encoder
111    /// actually created, which can differ when a backend that probed as
112    /// available fails to start and a `Prefer` factory falls back. The
113    /// default is `Software`, which is what every encoder that does not
114    /// override it is.
115    fn implementation(&self) -> CodecImplementation {
116        CodecImplementation::Software
117    }
118    /// A human-readable name for the backend doing the encoding, such as a
119    /// Media Foundation encoder's friendly name. For logs and diagnostics
120    /// only; it is not stable across platforms, drivers or releases.
121    fn backend_name(&self) -> &str {
122        "zvidlib"
123    }
124    fn encode<'a>(
125        &'a mut self,
126        index: FrameIndex,
127        frame: FrameSource<'a>,
128    ) -> EncoderFuture<'a, Vec<EncodedSample>>;
129    fn finish<'a>(&'a mut self) -> EncoderFuture<'a, Vec<EncodedSample>>;
130}
131
132/// Backend-neutral audio encoder contract.
133///
134/// # Where the audio encoders come from
135///
136/// zvidlib writes no audio codec of its own design. Each encoder behind this
137/// trait is either the platform's or a faithful port of the codec's reference
138/// encoder, so the quality a caller gets is the reference's rather than a
139/// reimplementation's:
140///
141/// * AAC-LC: `native_aac_audio_encoder_factory` delegates to the operating
142///   system's encoder, AudioToolbox on macOS and Media Foundation on Windows,
143///   and reports `HardwareUnavailable` elsewhere; the browser build encodes
144///   through `WebCodecs`. A pure-Rust AAC-LC encoder is a large subsystem in
145///   its own right -- filter bank, psychoacoustic model, quantization and rate
146///   control -- with no open reference implementation to port, and the
147///   platforms already have good ones (issue #174).
148/// * Opus: `native_opus_audio_encoder_factory` runs `opus-pure`, a pure-Rust
149///   port of libopus, on every target; the browser build can also encode
150///   through `WebCodecs`.
151/// * Vorbis: `native_vorbis_audio_encoder_factory` runs zvidlib's own port of
152///   the libvorbis 1.3.7 encoder, whose packets are bit-identical to
153///   libvorbis's for the same input and quality.
154///
155/// The concern behind issue #174 was that a mediocre encoder shipped under
156/// this crate's name is worse for callers than none, because it is harder to
157/// route around. A port of the reference encoder answers that concern where an
158/// original design could not. `crate::MediaOutput` muxes an audio track
159/// given any `AudioEncoder`, so a caller with an encoder of its own still
160/// plugs it in here.
161pub trait AudioEncoder {
162    fn config(&self) -> &EncoderConfig;
163    fn format(&self) -> AudioEncoderFormat;
164    fn encode<'a>(
165        &'a mut self,
166        index: FrameIndex,
167        buffer: AudioBuffer,
168    ) -> EncoderFuture<'a, Vec<EncodedSample>>;
169    fn finish<'a>(&'a mut self) -> EncoderFuture<'a, AudioDrain>;
170}
171
172/// A normalized codec profile that can be compared across backend implementations.
173#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
174#[non_exhaustive]
175pub enum CodecProfile {
176    UncompressedGray8,
177    HevcMain,
178    HevcMain10,
179    Av1Main,
180    Av1High,
181    Av1Professional,
182    /// VP8 has a single profile; the bitstream version each frame carries
183    /// selects its reconstruction and loop filters.
184    Vp8,
185    /// VP9 profile 0: 8-bit 4:2:0.
186    Vp9Profile0,
187    /// VP9 profile 1: 8-bit 4:2:2, 4:4:0 or 4:4:4.
188    Vp9Profile1,
189    /// VP9 profile 2: 10- or 12-bit 4:2:0.
190    Vp9Profile2,
191    /// VP9 profile 3: 10- or 12-bit 4:2:2, 4:4:0 or 4:4:4.
192    Vp9Profile3,
193    AacLowComplexity,
194    /// Opus has a single profile.
195    Opus,
196    /// Vorbis I has a single profile.
197    Vorbis,
198}
199
200/// Whether a caller permits or requires a hardware codec implementation.
201#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
202#[non_exhaustive]
203pub enum HardwarePreference {
204    Require,
205    Prefer,
206    Avoid,
207}
208
209/// The implementation class selected by a codec factory.
210#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
211#[non_exhaustive]
212pub enum CodecImplementation {
213    Software,
214    Hardware,
215}
216
217/// The result of querying a factory with a complete normalized configuration.
218#[derive(Clone, Debug, Eq, PartialEq)]
219#[non_exhaustive]
220pub enum CodecSupport {
221    Supported { implementation: CodecImplementation },
222    UnsupportedCodec,
223    UnsupportedProfile,
224    InvalidConfiguration { reason: String },
225    HardwareUnavailable,
226}
227
228impl CodecSupport {
229    pub const fn is_supported(&self) -> bool {
230        matches!(self, Self::Supported { .. })
231    }
232}
233
234/// Backend-neutral video decoder configuration.
235#[derive(Clone, Debug, Eq, PartialEq)]
236pub struct VideoDecoderConfig {
237    pub codec: Codec,
238    pub profile: CodecProfile,
239    pub coded_dimensions: VideoDimensions,
240    pub output_format: PixelFormat,
241    pub color_range: ColorRange,
242    pub hardware: HardwarePreference,
243    /// Container configuration bytes in the codec's standardized format.
244    pub configuration: Vec<u8>,
245}
246
247/// Backend-neutral video encoder configuration.
248#[derive(Clone, Debug, Eq, PartialEq)]
249pub struct VideoEncoderConfig {
250    pub codec: Codec,
251    pub profile: CodecProfile,
252    pub coded_dimensions: VideoDimensions,
253    pub input_format: PixelFormat,
254    pub color_range: ColorRange,
255    pub hardware: HardwarePreference,
256    /// Exact media-clock timescale used for emitted DTS and PTS values.
257    pub timescale: u32,
258    /// Exact duration, in `timescale` ticks, of every submitted frame.
259    pub frame_duration: u32,
260    pub configuration: Vec<u8>,
261}
262
263/// One owned compressed sample in decode order with its presentation identity.
264#[derive(Clone, Debug, Eq, PartialEq)]
265pub struct EncodedVideoSample {
266    pub presentation_index: FrameIndex,
267    pub random_access: bool,
268    pub data: Vec<u8>,
269}
270
271/// One normalized decoder output, independent of backend-private image types.
272#[derive(Clone, Debug, Eq, PartialEq)]
273pub struct DecodedVideoFrame {
274    pub presentation_index: FrameIndex,
275    pub frame: VideoFrame,
276}
277
278/// A cheap cloneable cancellation signal shared with a running codec operation.
279#[derive(Clone, Debug, Default)]
280pub struct CancellationToken {
281    cancelled: Arc<AtomicBool>,
282}
283
284impl CancellationToken {
285    pub fn new() -> Self {
286        Self::default()
287    }
288
289    pub fn cancel(&self) {
290        self.cancelled.store(true, Ordering::Release);
291    }
292
293    pub fn is_cancelled(&self) -> bool {
294        self.cancelled.load(Ordering::Acquire)
295    }
296
297    #[doc(hidden)]
298    pub fn check(&self) -> Result<()> {
299        if self.is_cancelled() {
300            Err(Error::new(
301                ErrorKind::Cancelled,
302                "codec operation cancelled",
303            ))
304        } else {
305            Ok(())
306        }
307    }
308}
309
310/// A stateful video decoder that may retain reference and reorder state.
311///
312/// Requires `Send` so an [`ExactFrameReader`] (and the decoder it owns) can be moved to a
313/// background thread, which native examples rely on to decode without stalling rendering.
314pub trait VideoDecoder: Send {
315    fn submit(
316        &mut self,
317        sample: &EncodedVideoSample,
318        cancellation: &CancellationToken,
319    ) -> Result<Vec<DecodedVideoFrame>>;
320    fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>>;
321    fn reset(&mut self) -> Result<()>;
322
323    /// Whether the caller wants the pictures the next samples decode to, or is only decoding
324    /// them because a later frame references them.
325    ///
326    /// Reaching a frame in the middle of a long group of pictures means decoding every sample
327    /// before it, and on the way there nothing looks at those pictures. What they cost is not
328    /// the decoding: a hardware backend hands back NV12 and every one of these decoders then
329    /// converts a whole picture to RGBA on the CPU and allocates the frame that holds it. On
330    /// the bundled 1080p sample that conversion is 6.6 ms of the 9.6 ms a frame takes through
331    /// VideoToolbox, so a seek that skips it for the frames it passes costs a third of what it
332    /// did.
333    ///
334    /// A decoder must still *decode* a suppressed sample - the frames after it reference the
335    /// picture - and must still account for its presentation identity; what it may skip is
336    /// producing the [`DecodedVideoFrame`], which it reports by returning fewer frames than it
337    /// was given samples.
338    ///
339    /// The default implementation ignores the hint, which is always correct: a decoder that
340    /// keeps producing every frame is a decoder that is merely no faster. [`ExactFrameReader`]
341    /// tracks what it was actually handed rather than what it asked for, so a backend that
342    /// implements this and one that does not answer the same frames.
343    fn set_output_wanted(&mut self, _wanted: bool) {}
344}
345
346/// Discovers and creates video decoders without exposing backend-specific types.
347pub trait VideoDecoderFactory {
348    fn capability(&self, configuration: &VideoDecoderConfig) -> CodecSupport;
349    fn create(
350        &self,
351        configuration: &VideoDecoderConfig,
352        limits: &Limits,
353    ) -> Result<Box<dyn VideoDecoder>>;
354}
355
356/// Discovers and creates video encoders using the normalized capability model.
357pub trait VideoEncoderFactory {
358    fn capability(&self, configuration: &VideoEncoderConfig) -> CodecSupport;
359    fn create(
360        &self,
361        configuration: &VideoEncoderConfig,
362        limits: &Limits,
363    ) -> Result<Box<dyn VideoEncoder>>;
364}
365
366/// Backend-neutral audio encoder configuration.
367#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct AudioEncoderConfig {
369    pub codec: Codec,
370    pub profile: CodecProfile,
371    pub sample_rate: u32,
372    pub channels: u16,
373    /// Exact media-clock timescale used for emitted DTS and PTS values.
374    pub timescale: u32,
375    pub configuration: Vec<u8>,
376}
377
378/// Discovers and creates audio encoders using the normalized capability model,
379/// the audio counterpart to [`VideoEncoderFactory`].
380///
381/// The native AAC, Opus and Vorbis factories implement it (see
382/// [`AudioEncoder`] for where each encoder comes from), and a caller's own
383/// adapter can too, so a caller that wants discoverable capability reporting
384/// is not left writing the `Result<Box<dyn AudioEncoder>>` construction by
385/// hand.
386pub trait AudioEncoderFactory {
387    fn capability(&self, configuration: &AudioEncoderConfig) -> CodecSupport;
388    fn create(
389        &self,
390        configuration: &AudioEncoderConfig,
391        limits: &Limits,
392    ) -> Result<Box<dyn AudioEncoder>>;
393}
394
395/// Reads an audio encoder's backend-private configuration: `Some(None)` for
396/// an empty one (the backend's default bit rate), `Some(Some(bits_per_second))`
397/// for four big-endian bytes naming a nonzero rate, and `None` for anything
398/// else. Zero is rejected rather than read as "no target", as the HEVC encoder
399/// does. Every native audio encoder takes this form.
400#[doc(hidden)]
401pub fn parse_audio_bit_rate(configuration: &[u8]) -> Option<Option<u32>> {
402    match configuration {
403        [] => Some(None),
404        [a, b, c, d] => match u32::from_be_bytes([*a, *b, *c, *d]) {
405            0 => None,
406            bits_per_second => Some(Some(bits_per_second)),
407        },
408        _ => None,
409    }
410}
411
412/// Observable counters for validating cache and decoder reuse behavior.
413#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
414pub struct DecodeStatistics {
415    pub samples_submitted: u64,
416    /// Of those, the ones submitted only so a later frame could reference them, with the
417    /// decoder told not to produce their pictures.
418    pub samples_skipped: u64,
419    pub cache_hits: u64,
420    pub resets: u64,
421    pub drains: u64,
422}
423
424/// Presentation-indexed exact-frame access over decode-order compressed samples.
425pub struct ExactFrameReader {
426    configuration: VideoDecoderConfig,
427    decoder: Box<dyn VideoDecoder>,
428    samples: Vec<EncodedVideoSample>,
429    decode_position_by_presentation: HashMap<FrameIndex, usize>,
430    cache: BTreeMap<FrameIndex, VideoFrame>,
431    lru: VecDeque<FrameIndex>,
432    /// Presentation frames the decoder has emitted since the last reset, which is how far
433    /// continuing the open session is assumed to reach: one of these that has since been
434    /// evicted from the cache is assumed unreachable without a reset. It is only that
435    /// assumption. A decoder that emits one of them again is published again and cached, not
436    /// refused (see [`publish`]).
437    ///
438    /// [`publish`]: Self::publish
439    published_since_reset: HashSet<FrameIndex>,
440    /// Frames whose sample was submitted while the decoder was told nothing wanted its picture,
441    /// so the decoder passed through them without producing one. Reaching one again needs the
442    /// same reset an evicted published frame does.
443    suppressed_since_reset: HashSet<FrameIndex>,
444    /// Frames whose sample has been submitted and whose picture has not come back yet, because a
445    /// reordering decoder holds one until the samples after it arrive. They are the frames a
446    /// decoder may drop the moment output stops being wanted, so turning it off writes them off
447    /// with the rest.
448    in_flight_since_reset: HashSet<FrameIndex>,
449    /// What the decoder was last told, so a walk toggles it once at the target rather than
450    /// on every sample.
451    output_wanted: bool,
452    next_decode_position: Option<usize>,
453    /// The random-access position the open decode session started from. Only frames at or
454    /// after it have been, or can still be, submitted without a reset.
455    session_start: Option<usize>,
456    /// The fast tier [`ExactFrameReader::seek`] answers from, when the caller has attached one.
457    seek_previews: Option<Arc<dyn SeekPreviewSource>>,
458    limits: Limits,
459    statistics: DecodeStatistics,
460}
461
462/// The longest a seek to any position of any track may take.
463///
464/// This is the requirement `ARCHITECTURE.md` section 3.2 states, kept here as a value so the
465/// tests that hold the library to it and the callers that budget against it quote one number.
466pub const SEEK_LATENCY_BUDGET: Duration = Duration::from_millis(50);
467
468/// Already-decoded pictures spread over a track, which a seek may answer from.
469///
470/// This is a trait rather than the concrete index because [`ExactFrameReader`] is portable and
471/// `zvidlib::previews::PreviewIndex` is not - its pass owns a thread, so it is native-only - and a
472/// reader that named the concrete type could not be built for the browser at all, which is the
473/// target that needs the tier most. An implementation must not decode and must not wait on a
474/// decoder: what it returns is a picture it already had, or nothing.
475///
476/// Issue #432 gave the browser its own implementation. What answers a seek on either target is
477/// now the same `zvidlib::previews::PreviewStore`; what differs is only what fills it, a thread in
478/// `zvidlib::previews::PreviewIndex` against an idle callback in `zvidlib::web_previews`.
479pub trait SeekPreviewSource: Send + Sync {
480    /// The kept picture closest to `frame` and the frame it is actually of, or `None` while
481    /// nothing near that position has been decoded.
482    fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)>;
483}
484
485/// What a constant-time [`ExactFrameReader::seek`] could answer with.
486///
487/// A seek is a question about a position on the timeline, and it has to be answered now: the
488/// exact frame at an arbitrary point of a long group of pictures is a decode of every frame
489/// before it, which on a 1080p track is seconds rather than the [`SEEK_LATENCY_BUDGET`]
490/// `ARCHITECTURE.md` section 3.2 allows. So a seek returns the best picture already decoded and
491/// the caller walks to the exact one behind it with [`ExactFrameReader::get`] if it still wants
492/// it.
493#[derive(Clone, Debug, Eq, PartialEq)]
494pub enum Seek {
495    /// The frame asked for, already decoded and cached. Nothing further is needed.
496    Exact(VideoFrame),
497    /// A picture of a different frame, the closest one the seek preview index holds.
498    Preview {
499        picture: VideoFrame,
500        /// The frame `picture` is actually of, which is within one preview stride of the frame
501        /// asked for once the pass has covered that part of the track.
502        frame: FrameIndex,
503    },
504    /// Nothing is decoded yet for anywhere near this position: no cached frame, and either no
505    /// preview index attached or an index whose pass has not produced a picture.
506    Pending,
507}
508
509/// What a caller wants out of a request, which is what decides whether the reader keeps the
510/// frames behind the one it was asked for.
511///
512/// The frames a walk passes are decoded either way - a picture cannot be decoded without its
513/// references - so this only decides which of them are converted to RGBA, which is the part of
514/// a walk that costs.
515#[derive(Clone, Copy, Debug, Eq, PartialEq)]
516enum Request {
517    /// Somewhere to be: keep the frames behind it that the cache can hold, so coming back to
518    /// one of them is free.
519    Destination,
520    /// Somewhere to pass through: keep only the frame asked for, and the reordered frames at or
521    /// after it that the next step needs anyway.
522    Step,
523}
524
525impl Request {
526    /// How far behind the target this request keeps pictures, in presentation frames.
527    fn cache_tail(self, limits: &Limits) -> u64 {
528        match self {
529            Self::Destination => u64::from(limits.max_cached_frames),
530            Self::Step => 0,
531        }
532    }
533}
534
535impl ExactFrameReader {
536    pub fn new(
537        factory: &dyn VideoDecoderFactory,
538        configuration: VideoDecoderConfig,
539        samples: Vec<EncodedVideoSample>,
540        limits: Limits,
541    ) -> Result<Self> {
542        if limits.max_cached_frames == 0 || limits.max_decode_samples_per_seek == 0 {
543            return Err(Error::new(
544                ErrorKind::ResourceLimit,
545                "exact-frame limits must permit cached frames and decode work",
546            ));
547        }
548        let capability = factory.capability(&configuration);
549        if !capability.is_supported() {
550            return Err(capability_error(capability));
551        }
552        if samples.is_empty() {
553            return Err(Error::new(
554                ErrorKind::InvalidInput,
555                "an exact-frame reader requires at least one sample",
556            ));
557        }
558        if !samples[0].random_access {
559            return Err(Error::new(
560                ErrorKind::InvalidInput,
561                "the first decode-order sample must be a random-access point",
562            ));
563        }
564        let mut positions = HashMap::with_capacity(samples.len());
565        for (position, sample) in samples.iter().enumerate() {
566            if positions
567                .insert(sample.presentation_index, position)
568                .is_some()
569            {
570                return Err(Error::new(
571                    ErrorKind::InvalidInput,
572                    "presentation frame identities must be unique",
573                ));
574            }
575        }
576        let decoder = factory.create(&configuration, &limits)?;
577        Ok(Self {
578            configuration,
579            decoder,
580            samples,
581            decode_position_by_presentation: positions,
582            cache: BTreeMap::new(),
583            lru: VecDeque::new(),
584            published_since_reset: HashSet::new(),
585            suppressed_since_reset: HashSet::new(),
586            in_flight_since_reset: HashSet::new(),
587            output_wanted: true,
588            next_decode_position: None,
589            session_start: None,
590            seek_previews: None,
591            limits,
592            statistics: DecodeStatistics::default(),
593        })
594    }
595
596    /// Attaches (or, with `None`, detaches) the preview source [`Self::seek`] answers from.
597    ///
598    /// The source is normally a `zvidlib::previews::PreviewIndex` filling itself on a decoder of its
599    /// own, and it is shared rather than owned so a reader can start answering seeks from it
600    /// while its pass is still walking the track.
601    pub fn set_seek_previews(&mut self, previews: Option<Arc<dyn SeekPreviewSource>>) {
602        self.seek_previews = previews;
603    }
604
605    /// Answers "what is at this position of the timeline" without decoding anything.
606    ///
607    /// This is the seek path, and it is bounded by `ARCHITECTURE.md` section 3.2: it takes the
608    /// same time whichever position of whatever track it is asked about, because it only ever
609    /// looks at pictures that are already decoded. It reads the presentation cache first - a
610    /// seek back to somewhere the reader has just been is exact for nothing - and then the
611    /// attached seek preview index, and it returns [`Seek::Pending`] rather than blocking when
612    /// neither holds anything.
613    ///
614    /// Reaching the exact frame is [`Self::get`], which is unbounded by construction: the frame
615    /// depends on every frame back to its random-access point and they all have to be decoded.
616    /// A caller that wants both draws what this returns and walks to the exact frame behind it.
617    pub fn seek(&mut self, presentation_index: FrameIndex) -> Seek {
618        if let Some(frame) = self.cache.get(&presentation_index).cloned() {
619            self.statistics.cache_hits = self.statistics.cache_hits.saturating_add(1);
620            self.touch(presentation_index);
621            return Seek::Exact(frame);
622        }
623        match self
624            .seek_previews
625            .as_ref()
626            .and_then(|previews| previews.nearest_at(presentation_index))
627        {
628            Some((frame, picture)) => Seek::Preview { picture, frame },
629            None => Seek::Pending,
630        }
631    }
632
633    /// Returns exactly the requested presentation frame, and the frames behind it that the
634    /// cache can hold, so a request that comes back for one of those is answered for nothing.
635    ///
636    /// This is the destination of a seek. A walk that is going to keep walking forwards should
637    /// ask for its intermediate frames with [`get_step`] instead, which converts the frame it
638    /// was asked for and nothing else.
639    ///
640    /// [`get_step`]: Self::get_step
641    pub fn get(
642        &mut self,
643        presentation_index: FrameIndex,
644        cancellation: &CancellationToken,
645    ) -> Result<VideoFrame> {
646        self.get_request(presentation_index, Request::Destination, cancellation)
647    }
648
649    /// Returns exactly the requested presentation frame and converts nothing else.
650    ///
651    /// A step is a picture on the way to somewhere, not somewhere: the caller is going to ask
652    /// for a frame further forward next, so the frames behind this one that [`get`] would keep
653    /// are converted for a request that never comes. That tail is bounded per call and so is
654    /// affordable for one seek, but a walk that publishes as it goes pays it once per published
655    /// picture, and once the stride is shorter than the tail the tails overlap and the walk
656    /// converts every picture it passes rather than the few it publishes (issue #402).
657    ///
658    /// Frames at or after the target in presentation order are still kept, exactly as [`get`]
659    /// keeps them: on a stream with B-pictures those are the reordered frames the next request
660    /// asks for, and dropping them would reset the decoder and re-walk the group of pictures.
661    ///
662    /// [`get`]: Self::get
663    pub fn get_step(
664        &mut self,
665        presentation_index: FrameIndex,
666        cancellation: &CancellationToken,
667    ) -> Result<VideoFrame> {
668        self.get_request(presentation_index, Request::Step, cancellation)
669    }
670
671    fn get_request(
672        &mut self,
673        presentation_index: FrameIndex,
674        request: Request,
675        cancellation: &CancellationToken,
676    ) -> Result<VideoFrame> {
677        cancellation.check()?;
678        if let Some(frame) = self.cache.get(&presentation_index).cloned() {
679            self.statistics.cache_hits = self.statistics.cache_hits.saturating_add(1);
680            self.touch(presentation_index);
681            return Ok(frame);
682        }
683        let target_position = *self
684            .decode_position_by_presentation
685            .get(&presentation_index)
686            .ok_or_else(|| {
687                Error::new(ErrorKind::InvalidInput, "presentation frame is not indexed")
688            })?;
689        let random_access_position = self.nearest_random_access(target_position);
690        // A decoder with output reordering (e.g. hierarchical B-frames) may need to be fed
691        // samples *past* `target_position` before the reordered frame at `target_position` is
692        // actually emitted. So `next_decode_position > target_position` does not by itself mean
693        // the frame is unreachable without a reset: it may simply still be buffered inside the
694        // decoder, pending release as more samples are submitted. The only case that truly
695        // requires a reset is when the frame was already published once and evicted from the
696        // cache, since a decoder is not expected to emit the same presentation frame twice
697        // without an intervening reset. That is an assumption about what continuing the session
698        // can reach, not a rule the decoder is held to: one that does emit the frame again is
699        // published a second time and cached rather than refused (see `publish`).
700        //
701        // A frame the decoder walked past without producing is in the same position as one that
702        // was published and evicted: the decoder will not emit it again, so only a reset can
703        // reach it.
704        //
705        // And a session that started at a later random-access point than the target's never
706        // submitted the target at all, however far it has since walked. On a track whose frames
707        // are all random-access points, a request for frame 2 opens the session at 2, and a
708        // request for frame 0 after it would otherwise walk on from 3 to the end of the track.
709        let can_reuse = self
710            .next_decode_position
711            .is_some_and(|position| position >= random_access_position)
712            && self
713                .session_start
714                .is_some_and(|start| start <= random_access_position)
715            && !self.published_since_reset.contains(&presentation_index)
716            && !self.suppressed_since_reset.contains(&presentation_index);
717        if !can_reuse {
718            self.decoder.reset()?;
719            self.statistics.resets = self.statistics.resets.saturating_add(1);
720            self.published_since_reset.clear();
721            self.suppressed_since_reset.clear();
722            self.in_flight_since_reset.clear();
723            self.next_decode_position = Some(random_access_position);
724            self.session_start = Some(random_access_position);
725        }
726
727        let mut work = 0_u32;
728        while let Some(position) = self.next_decode_position {
729            cancellation.check()?;
730            if work >= self.limits.max_decode_samples_per_seek {
731                return Err(Error::new(
732                    ErrorKind::ResourceLimit,
733                    "exact-frame request exceeded the configured decode-work limit",
734                ));
735            }
736            if position == self.samples.len() {
737                self.set_output_wanted(true);
738                self.drain_internal(cancellation)?;
739                break;
740            }
741            // Nothing looks at a picture decoded on the way to the target, and skipping the
742            // colour conversion it would otherwise pay is most of what a long walk costs. Two
743            // kinds of frame are kept anyway, both because the request after this one is very
744            // likely to be for them.
745            //
746            // The first is anything at or after the target in *presentation* order. A walk
747            // arrives at its target from behind and carries on forwards, and deciding this by
748            // decode position instead discards exactly the reordered frames a stream with
749            // B-pictures asks for next: on the bundled sample every fourth request would then
750            // reset the decoder and walk the whole group of pictures again.
751            //
752            // The second is the frames immediately behind the target, as many as the cache can
753            // hold, and that one belongs to the request rather than to the reader. Walking here
754            // fills the cache with them as a side effect, and that is what makes stepping
755            // backwards a frame at a time cost nothing after a seek; skipping them would leave
756            // the cache empty behind the target and turn every backward step into another walk
757            // from the random-access point. For a destination they are a bounded charge - the
758            // last `max_cached_frames` conversions of a walk however long - against a saving
759            // that grows with the distance. For a step they are a charge with no saving behind
760            // it at all: the caller is walking forwards and will never ask for them, and paying
761            // the tail once per step is what made a 150 ms preview cadence convert 765 of the
762            // bundled sample's 768 pictures (issue #402).
763            let cache_tail = request.cache_tail(&self.limits);
764            let wanted = position >= target_position
765                || self.samples[position].presentation_index.0
766                    >= presentation_index.0.saturating_sub(cache_tail);
767            self.set_output_wanted(wanted);
768            let suppressed = !self.output_wanted;
769            let outputs = self.decoder.submit(&self.samples[position], cancellation)?;
770            self.statistics.samples_submitted = self.statistics.samples_submitted.saturating_add(1);
771            if suppressed {
772                self.statistics.samples_skipped = self.statistics.samples_skipped.saturating_add(1);
773                self.suppressed_since_reset
774                    .insert(self.samples[position].presentation_index);
775            } else {
776                self.in_flight_since_reset
777                    .insert(self.samples[position].presentation_index);
778            }
779            self.next_decode_position = Some(position + 1);
780            work += 1;
781            self.publish(outputs)?;
782            if let Some(frame) = self.cache.get(&presentation_index).cloned() {
783                self.touch(presentation_index);
784                return Ok(frame);
785            }
786        }
787        if let Some(frame) = self.cache.get(&presentation_index).cloned() {
788            self.touch(presentation_index);
789            Ok(frame)
790        } else {
791            Err(Error::new(
792                ErrorKind::MalformedMedia,
793                "decoder did not produce the requested presentation frame",
794            ))
795        }
796    }
797
798    /// Drains delayed output into the bounded presentation cache.
799    pub fn drain(&mut self, cancellation: &CancellationToken) -> Result<usize> {
800        cancellation.check()?;
801        self.set_output_wanted(true);
802        self.drain_internal(cancellation)
803    }
804
805    /// Tells the decoder whether the next samples' pictures are wanted, when that has changed.
806    ///
807    /// Turning output off writes off the frames the decoder is still holding as well as the ones
808    /// it is about to be handed. A reordering decoder releases a picture several samples after
809    /// the one that carried it, so a frame submitted while output was wanted can come out - and
810    /// be dropped - during the suppressed stretch that follows. Not writing those off is a
811    /// decoder that will never produce them and a reader that still believes it can ask, which
812    /// ends in `decoder did not produce the requested presentation frame`. A picture that does
813    /// arrive after all takes itself back off the list in [`publish`].
814    ///
815    /// [`publish`]: Self::publish
816    fn set_output_wanted(&mut self, wanted: bool) {
817        if self.output_wanted == wanted {
818            return;
819        }
820        if !wanted {
821            for frame in self.in_flight_since_reset.drain() {
822                self.suppressed_since_reset.insert(frame);
823            }
824        }
825        self.decoder.set_output_wanted(wanted);
826        self.output_wanted = wanted;
827    }
828
829    /// Clears decoder, reorder, and frame-cache state.
830    pub fn reset(&mut self) -> Result<()> {
831        self.set_output_wanted(true);
832        self.decoder.reset()?;
833        self.statistics.resets = self.statistics.resets.saturating_add(1);
834        self.next_decode_position = None;
835        self.session_start = None;
836        self.cache.clear();
837        self.lru.clear();
838        self.published_since_reset.clear();
839        self.suppressed_since_reset.clear();
840        self.in_flight_since_reset.clear();
841        Ok(())
842    }
843
844    pub const fn statistics(&self) -> DecodeStatistics {
845        self.statistics
846    }
847
848    pub fn cached_frames(&self) -> usize {
849        self.cache.len()
850    }
851
852    /// The random-access point a decode reaching `target_position` starts from.
853    ///
854    /// A random-access point presented *after* the target cannot be it, even when it comes
855    /// first in decode order: the target is then one of its leading pictures, and an open-GOP
856    /// stream's leading pictures (HEVC RASL pictures) reference pictures from before the
857    /// random-access point, so a decode that starts there cannot reconstruct them (issue #506).
858    /// The walk goes back to a random-access point the target is not leading.
859    fn nearest_random_access(&self, target_position: usize) -> usize {
860        let target = self.samples[target_position].presentation_index;
861        (0..=target_position)
862            .rev()
863            .find(|position| {
864                let sample = &self.samples[*position];
865                sample.random_access && sample.presentation_index <= target
866            })
867            .unwrap_or(0)
868    }
869
870    fn drain_internal(&mut self, cancellation: &CancellationToken) -> Result<usize> {
871        let outputs = self.decoder.drain(cancellation)?;
872        let count = outputs.len();
873        self.statistics.drains = self.statistics.drains.saturating_add(1);
874        self.next_decode_position = Some(self.samples.len());
875        self.publish(outputs)?;
876        Ok(count)
877    }
878
879    fn publish(&mut self, outputs: Vec<DecodedVideoFrame>) -> Result<()> {
880        for output in outputs {
881            // A decoder that ignores the output hint, or one that only emits a suppressed
882            // sample's picture later, hands back a frame this reader had written off. What it
883            // was handed is what counts, so it is a cached frame again rather than a reset.
884            self.suppressed_since_reset
885                .remove(&output.presentation_index);
886            self.in_flight_since_reset
887                .remove(&output.presentation_index);
888            // A repeat of a frame already published since the last reset is not an error, and
889            // was one until issue #415. Nothing here depends on a decoder emitting each
890            // presentation frame at most once per session: `published_since_reset` only decides
891            // whether a re-request can continue the open session or has to reset, and a decoder
892            // that hands the frame back anyway makes that answer conservative rather than wrong.
893            // Refusing it rejected the whole stream as `MalformedMedia` over a backend's
894            // behaviour, and rejected it for a picture the reader was about to cache and could
895            // have answered the very request from. The `WebCodecs` backend already re-caches
896            // such a frame, so this is also what makes the two backends agree.
897            self.published_since_reset.insert(output.presentation_index);
898            if !self
899                .decode_position_by_presentation
900                .contains_key(&output.presentation_index)
901            {
902                return Err(Error::new(
903                    ErrorKind::MalformedMedia,
904                    "decoder produced an unindexed presentation frame",
905                ));
906            }
907            if output.frame.dimensions != self.configuration.coded_dimensions
908                || output.frame.pixel_format != self.configuration.output_format
909                || output.frame.color_range != self.configuration.color_range
910            {
911                return Err(Error::new(
912                    ErrorKind::MalformedMedia,
913                    "decoder output does not match its normalized configuration",
914                ));
915            }
916            self.insert_cache(output.presentation_index, output.frame);
917        }
918        Ok(())
919    }
920
921    fn insert_cache(&mut self, index: FrameIndex, frame: VideoFrame) {
922        self.cache.insert(index, frame);
923        self.touch(index);
924        while self.cache.len() > self.limits.max_cached_frames as usize {
925            if let Some(evicted) = self.lru.pop_front() {
926                self.cache.remove(&evicted);
927            }
928        }
929    }
930
931    fn touch(&mut self, index: FrameIndex) {
932        self.lru.retain(|candidate| *candidate != index);
933        self.lru.push_back(index);
934    }
935}
936
937fn capability_error(capability: CodecSupport) -> Error {
938    let (kind, message) = match capability {
939        CodecSupport::UnsupportedCodec => (
940            ErrorKind::Unsupported,
941            "decoder does not support the requested codec".into(),
942        ),
943        CodecSupport::UnsupportedProfile => (
944            ErrorKind::Unsupported,
945            "decoder does not support the requested profile".into(),
946        ),
947        CodecSupport::InvalidConfiguration { reason } => (ErrorKind::InvalidInput, reason),
948        CodecSupport::HardwareUnavailable => (
949            ErrorKind::Unsupported,
950            "requested hardware decoder is unavailable".into(),
951        ),
952        CodecSupport::Supported { .. } => (
953            ErrorKind::Internal,
954            "decoder capability changed unexpectedly".into(),
955        ),
956    };
957    Error::new(kind, message)
958}
959
960/// Returns the portable, software-only uncompressed Gray8 decoder backend.
961///
962/// Each packet contains one tightly packed Gray8 image. The concrete backend
963/// remains private while this factory provides an end-to-end reference path.
964pub fn uncompressed_video_decoder_factory() -> impl VideoDecoderFactory {
965    UncompressedVideoDecoderFactory
966}
967
968struct UncompressedVideoDecoderFactory;
969
970impl VideoDecoderFactory for UncompressedVideoDecoderFactory {
971    fn capability(&self, configuration: &VideoDecoderConfig) -> CodecSupport {
972        if configuration.codec != Codec::UncompressedVideo {
973            return CodecSupport::UnsupportedCodec;
974        }
975        if configuration.profile != CodecProfile::UncompressedGray8 {
976            return CodecSupport::UnsupportedProfile;
977        }
978        if configuration.output_format != PixelFormat::Gray8
979            || !configuration.configuration.is_empty()
980        {
981            return CodecSupport::InvalidConfiguration {
982                reason: "uncompressed Gray8 requires Gray8 output and no configuration bytes"
983                    .into(),
984            };
985        }
986        if configuration.hardware == HardwarePreference::Require {
987            return CodecSupport::HardwareUnavailable;
988        }
989        CodecSupport::Supported {
990            implementation: CodecImplementation::Software,
991        }
992    }
993
994    fn create(
995        &self,
996        configuration: &VideoDecoderConfig,
997        limits: &Limits,
998    ) -> Result<Box<dyn VideoDecoder>> {
999        let capability = self.capability(configuration);
1000        if !capability.is_supported() {
1001            return Err(capability_error(capability));
1002        }
1003        if configuration.coded_dimensions.width > limits.max_width
1004            || configuration.coded_dimensions.height > limits.max_height
1005        {
1006            return Err(Error::new(
1007                ErrorKind::ResourceLimit,
1008                "decoded dimensions exceed the configured limits",
1009            ));
1010        }
1011        let required = u64::from(configuration.coded_dimensions.width)
1012            .checked_mul(u64::from(configuration.coded_dimensions.height))
1013            .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "decoded frame size overflow"))?;
1014        if required > limits.max_allocation_bytes {
1015            return Err(Error::new(
1016                ErrorKind::ResourceLimit,
1017                "decoded frame exceeds the configured allocation limit",
1018            ));
1019        }
1020        Ok(Box::new(UncompressedVideoDecoder {
1021            configuration: configuration.clone(),
1022            limits: *limits,
1023        }))
1024    }
1025}
1026
1027struct UncompressedVideoDecoder {
1028    configuration: VideoDecoderConfig,
1029    limits: Limits,
1030}
1031
1032impl VideoDecoder for UncompressedVideoDecoder {
1033    fn submit(
1034        &mut self,
1035        sample: &EncodedVideoSample,
1036        cancellation: &CancellationToken,
1037    ) -> Result<Vec<DecodedVideoFrame>> {
1038        cancellation.check()?;
1039        let width = usize::try_from(self.configuration.coded_dimensions.width)
1040            .map_err(|_| Error::new(ErrorKind::ResourceLimit, "decoded width is too large"))?;
1041        let height = usize::try_from(self.configuration.coded_dimensions.height)
1042            .map_err(|_| Error::new(ErrorKind::ResourceLimit, "decoded height is too large"))?;
1043        let length = width
1044            .checked_mul(height)
1045            .ok_or_else(|| Error::new(ErrorKind::ResourceLimit, "decoded frame size overflow"))?;
1046        if sample.data.len() != length {
1047            return Err(Error::new(
1048                ErrorKind::MalformedMedia,
1049                "uncompressed Gray8 packet size does not match coded dimensions",
1050            ));
1051        }
1052        let frame = VideoFrame::new(
1053            self.configuration.coded_dimensions,
1054            PixelFormat::Gray8,
1055            self.configuration.color_range,
1056            vec![Plane {
1057                data: sample.data.clone(),
1058                stride: width,
1059            }],
1060            &self.limits,
1061        )?;
1062        Ok(vec![DecodedVideoFrame {
1063            presentation_index: sample.presentation_index,
1064            frame,
1065        }])
1066    }
1067
1068    fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
1069        cancellation.check()?;
1070        Ok(Vec::new())
1071    }
1072
1073    fn reset(&mut self) -> Result<()> {
1074        Ok(())
1075    }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use std::sync::Mutex;
1082
1083    fn config() -> VideoDecoderConfig {
1084        VideoDecoderConfig {
1085            codec: Codec::UncompressedVideo,
1086            profile: CodecProfile::UncompressedGray8,
1087            coded_dimensions: VideoDimensions::new(1, 1, &Limits::default()).unwrap(),
1088            output_format: PixelFormat::Gray8,
1089            color_range: ColorRange::Full,
1090            hardware: HardwarePreference::Avoid,
1091            configuration: Vec::new(),
1092        }
1093    }
1094
1095    fn sample(index: u64, value: u8, random_access: bool) -> EncodedVideoSample {
1096        EncodedVideoSample {
1097            presentation_index: FrameIndex(index),
1098            random_access,
1099            data: vec![value],
1100        }
1101    }
1102
1103    fn value(frame: &VideoFrame) -> u8 {
1104        frame.planes[0].data[0]
1105    }
1106
1107    /// A reader over a single-group track: one random-access point at frame zero, so reaching
1108    /// any frame exactly means decoding every frame before it. This is the shape the bundled
1109    /// sample has and the shape the seek budget is hard for.
1110    fn single_group_reader(frames: u64) -> ExactFrameReader {
1111        let samples = (0..frames)
1112            .map(|index| sample(index, (index % 251) as u8, index == 0))
1113            .collect();
1114        ExactFrameReader::new(
1115            &uncompressed_video_decoder_factory(),
1116            config(),
1117            samples,
1118            Limits::default(),
1119        )
1120        .unwrap()
1121    }
1122
1123    /// A preview source holding one picture every `stride` frames, standing in for the pass a
1124    /// `previews::PreviewIndex` runs. The reader's contract is that a seek reads pictures and
1125    /// never decodes, and asserting that does not need a decoder behind the source.
1126    struct StridedPreviews {
1127        stride: u64,
1128        frames: u64,
1129    }
1130
1131    impl SeekPreviewSource for StridedPreviews {
1132        fn nearest_at(&self, frame: FrameIndex) -> Option<(FrameIndex, VideoFrame)> {
1133            if self.frames == 0 {
1134                return None;
1135            }
1136            let at = (frame.0 / self.stride * self.stride).min(self.frames - 1);
1137            let picture = VideoFrame::new(
1138                config().coded_dimensions,
1139                PixelFormat::Gray8,
1140                ColorRange::Full,
1141                vec![Plane {
1142                    data: vec![(at % 251) as u8],
1143                    stride: 1,
1144                }],
1145                &Limits::default(),
1146            )
1147            .ok()?;
1148            Some((FrameIndex(at), picture))
1149        }
1150    }
1151
1152    /// A seek with nothing decoded anywhere near it says so rather than deciding to decode: the
1153    /// caller falls through to its own exact request instead of the seek path silently becoming
1154    /// the walk it is bounded against.
1155    #[test]
1156    fn a_seek_with_nothing_decoded_is_pending_and_submits_no_samples() {
1157        let mut reader = single_group_reader(512);
1158        assert_eq!(reader.seek(FrameIndex(511)), Seek::Pending);
1159        assert_eq!(reader.statistics().samples_submitted, 0);
1160        assert_eq!(reader.statistics().resets, 0);
1161    }
1162
1163    /// A seek back to somewhere the reader has just been is the frame itself, from the cache.
1164    #[test]
1165    fn a_seek_into_the_cache_answers_exactly() {
1166        let mut reader = single_group_reader(64);
1167        let cancellation = CancellationToken::new();
1168        reader.get(FrameIndex(40), &cancellation).unwrap();
1169        let submitted = reader.statistics().samples_submitted;
1170        let Seek::Exact(frame) = reader.seek(FrameIndex(40)) else {
1171            panic!("a cached frame seeks exactly");
1172        };
1173        assert_eq!(value(&frame), 40);
1174        assert_eq!(
1175            reader.statistics().samples_submitted,
1176            submitted,
1177            "an exact seek decodes nothing"
1178        );
1179    }
1180
1181    /// The requirement in `ARCHITECTURE.md` section 3.2: a seek to any position of a track that
1182    /// is one group of pictures answers from an already-decoded picture and decodes nothing, so
1183    /// what it costs does not grow with how far along the track the position is.
1184    #[test]
1185    fn a_seek_anywhere_on_a_single_group_track_decodes_nothing() {
1186        let mut reader = single_group_reader(512);
1187        reader.set_seek_previews(Some(Arc::new(StridedPreviews {
1188            stride: 12,
1189            frames: 512,
1190        })));
1191        for target in [0_u64, 1, 255, 400, 511] {
1192            match reader.seek(FrameIndex(target)) {
1193                Seek::Preview { frame, .. } => assert!(
1194                    frame.0.abs_diff(target) < 12,
1195                    "frame {target} answered by {frame:?}, further than one stride away"
1196                ),
1197                Seek::Exact(_) => {}
1198                Seek::Pending => panic!("frame {target} had no answer"),
1199            }
1200        }
1201        assert_eq!(
1202            reader.statistics().samples_submitted,
1203            0,
1204            "seeking is not decoding"
1205        );
1206        assert_eq!(reader.statistics().resets, 0);
1207    }
1208
1209    /// The 50 ms half of the same requirement, held on every run rather than only in the
1210    /// host-specific measurement over the bundled sample: a hundred positions of a single-group
1211    /// track, the far end no slower than the near one.
1212    #[test]
1213    fn a_seek_to_any_position_answers_inside_the_latency_budget() {
1214        let frames = 768;
1215        let mut reader = single_group_reader(frames);
1216        reader.set_seek_previews(Some(Arc::new(StridedPreviews { stride: 12, frames })));
1217        let mut worst = Duration::ZERO;
1218        for step in 0..=100_u64 {
1219            let target = FrameIndex(step * (frames - 1) / 100);
1220            let started = std::time::Instant::now();
1221            let answer = reader.seek(target);
1222            worst = worst.max(started.elapsed());
1223            assert_ne!(answer, Seek::Pending, "frame {} had no answer", target.0);
1224        }
1225        assert!(
1226            worst < SEEK_LATENCY_BUDGET,
1227            "the worst seek took {worst:?}, over the {SEEK_LATENCY_BUDGET:?} budget"
1228        );
1229    }
1230
1231    /// Detaching the source puts the reader back to answering out of its own cache alone, so a
1232    /// caller that drops a preview index does not keep serving its pictures.
1233    #[test]
1234    fn detaching_the_preview_source_leaves_only_the_cache() {
1235        let mut reader = single_group_reader(64);
1236        reader.set_seek_previews(Some(Arc::new(StridedPreviews {
1237            stride: 12,
1238            frames: 64,
1239        })));
1240        assert!(matches!(reader.seek(FrameIndex(63)), Seek::Preview { .. }));
1241        reader.set_seek_previews(None);
1242        assert_eq!(reader.seek(FrameIndex(63)), Seek::Pending);
1243    }
1244
1245    /// Issue #506: frame 3 follows the random-access point at frame 4 in decode order but is
1246    /// presented before it, the shape of an open-GOP stream's leading pictures. A decoder that
1247    /// starts at frame 4 cannot reconstruct it, so the reader starts from frame 0 instead. Frame
1248    /// 5 is not leading, so it is still decoded from frame 4.
1249    #[test]
1250    fn a_leading_picture_is_decoded_from_the_random_access_point_before_it() {
1251        let samples = vec![
1252            sample(0, 10, true),
1253            sample(1, 11, false),
1254            sample(2, 12, false),
1255            sample(4, 14, true),
1256            sample(3, 13, false),
1257            sample(5, 15, false),
1258        ];
1259        let mut reader = ExactFrameReader::new(
1260            &uncompressed_video_decoder_factory(),
1261            config(),
1262            samples.clone(),
1263            Limits::default(),
1264        )
1265        .unwrap();
1266        let cancellation = CancellationToken::new();
1267        assert_eq!(
1268            value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
1269            13
1270        );
1271        assert_eq!(reader.statistics().samples_submitted, 5);
1272
1273        let mut reader = ExactFrameReader::new(
1274            &uncompressed_video_decoder_factory(),
1275            config(),
1276            samples,
1277            Limits::default(),
1278        )
1279        .unwrap();
1280        assert_eq!(
1281            value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
1282            15
1283        );
1284        assert_eq!(reader.statistics().samples_submitted, 3);
1285    }
1286
1287    #[test]
1288    fn capability_distinguishes_codec_profile_configuration_and_hardware() {
1289        let factory = uncompressed_video_decoder_factory();
1290        assert!(factory.capability(&config()).is_supported());
1291        let mut candidate = config();
1292        candidate.codec = Codec::Av1;
1293        assert_eq!(
1294            factory.capability(&candidate),
1295            CodecSupport::UnsupportedCodec
1296        );
1297        candidate = config();
1298        candidate.profile = CodecProfile::Av1Main;
1299        assert_eq!(
1300            factory.capability(&candidate),
1301            CodecSupport::UnsupportedProfile
1302        );
1303        candidate = config();
1304        candidate.output_format = PixelFormat::Rgba8;
1305        assert!(matches!(
1306            factory.capability(&candidate),
1307            CodecSupport::InvalidConfiguration { .. }
1308        ));
1309        candidate = config();
1310        candidate.hardware = HardwarePreference::Require;
1311        assert_eq!(
1312            factory.capability(&candidate),
1313            CodecSupport::HardwareUnavailable
1314        );
1315
1316        candidate = config();
1317        candidate.output_format = PixelFormat::Rgba8;
1318        assert_eq!(
1319            factory
1320                .create(&candidate, &Limits::default())
1321                .err()
1322                .unwrap()
1323                .kind(),
1324            ErrorKind::InvalidInput
1325        );
1326
1327        let restrictive = Limits {
1328            max_width: 0,
1329            ..Limits::default()
1330        };
1331        assert_eq!(
1332            factory
1333                .create(&config(), &restrictive)
1334                .err()
1335                .unwrap()
1336                .kind(),
1337            ErrorKind::ResourceLimit
1338        );
1339    }
1340
1341    /// Issue #504: every frame of an all-intra track is a random-access point, so a request for
1342    /// frame 2 opens its decode session at frame 2. Frame 0 was never submitted in that session,
1343    /// so reaching it needs a reset rather than a walk onwards from frame 3.
1344    #[test]
1345    fn a_request_before_the_open_session_start_resets_the_decoder() {
1346        let samples = (0..4)
1347            .map(|index| sample(index, index as u8 + 10, true))
1348            .collect();
1349        let mut reader = ExactFrameReader::new(
1350            &uncompressed_video_decoder_factory(),
1351            config(),
1352            samples,
1353            Limits {
1354                max_cached_frames: 1,
1355                ..Limits::default()
1356            },
1357        )
1358        .unwrap();
1359        let cancellation = CancellationToken::new();
1360        assert_eq!(
1361            value(&reader.get(FrameIndex(2), &cancellation).unwrap()),
1362            12
1363        );
1364        assert_eq!(
1365            value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
1366            10
1367        );
1368        assert_eq!(reader.statistics().resets, 2);
1369    }
1370
1371    #[test]
1372    fn conformance_vector_reorders_exact_frames_and_reuses_bounded_state() {
1373        let samples = vec![
1374            sample(0, 10, true),
1375            sample(3, 40, false),
1376            sample(1, 20, false),
1377            sample(2, 30, false),
1378            sample(4, 50, true),
1379            sample(6, 70, false),
1380            sample(5, 60, false),
1381        ];
1382        let factory = uncompressed_video_decoder_factory();
1383        let mut reader = ExactFrameReader::new(
1384            &factory,
1385            config(),
1386            samples,
1387            Limits {
1388                max_cached_frames: 3,
1389                ..Limits::default()
1390            },
1391        )
1392        .unwrap();
1393        let cancellation = CancellationToken::new();
1394        assert_eq!(
1395            value(&reader.get(FrameIndex(1), &cancellation).unwrap()),
1396            20
1397        );
1398        let first = reader.statistics();
1399        assert_eq!(
1400            value(&reader.get(FrameIndex(2), &cancellation).unwrap()),
1401            30
1402        );
1403        assert_eq!(reader.statistics().resets, first.resets);
1404        assert_eq!(
1405            value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
1406            40
1407        );
1408        assert!(reader.statistics().cache_hits > 0);
1409        assert_eq!(
1410            value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
1411            60
1412        );
1413        assert_eq!(
1414            value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
1415            10
1416        );
1417        assert!(reader.statistics().resets > first.resets);
1418        assert!(reader.cached_frames() <= 3);
1419    }
1420
1421    /// Records what each sample was asked to produce, so a test can see the hint the reader
1422    /// actually gave rather than only its effect.
1423    struct HintFactory {
1424        wanted: Arc<Mutex<Vec<(u64, bool)>>>,
1425    }
1426
1427    struct HintDecoder {
1428        configuration: VideoDecoderConfig,
1429        limits: Limits,
1430        wanted: Arc<Mutex<Vec<(u64, bool)>>>,
1431        output_wanted: bool,
1432    }
1433
1434    impl VideoDecoderFactory for HintFactory {
1435        fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
1436            CodecSupport::Supported {
1437                implementation: CodecImplementation::Software,
1438            }
1439        }
1440
1441        fn create(
1442            &self,
1443            configuration: &VideoDecoderConfig,
1444            limits: &Limits,
1445        ) -> Result<Box<dyn VideoDecoder>> {
1446            Ok(Box::new(HintDecoder {
1447                configuration: configuration.clone(),
1448                limits: *limits,
1449                wanted: Arc::clone(&self.wanted),
1450                output_wanted: true,
1451            }))
1452        }
1453    }
1454
1455    impl VideoDecoder for HintDecoder {
1456        fn submit(
1457            &mut self,
1458            sample: &EncodedVideoSample,
1459            cancellation: &CancellationToken,
1460        ) -> Result<Vec<DecodedVideoFrame>> {
1461            cancellation.check()?;
1462            self.wanted
1463                .lock()
1464                .expect("hint log poisoned")
1465                .push((sample.presentation_index.0, self.output_wanted));
1466            if !self.output_wanted {
1467                return Ok(Vec::new());
1468            }
1469            Ok(vec![DecodedVideoFrame {
1470                presentation_index: sample.presentation_index,
1471                frame: VideoFrame::new(
1472                    self.configuration.coded_dimensions,
1473                    PixelFormat::Gray8,
1474                    ColorRange::Full,
1475                    vec![Plane {
1476                        data: sample.data.clone(),
1477                        stride: 1,
1478                    }],
1479                    &self.limits,
1480                )?,
1481            }])
1482        }
1483
1484        fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
1485            Ok(Vec::new())
1486        }
1487
1488        fn reset(&mut self) -> Result<()> {
1489            Ok(())
1490        }
1491
1492        fn set_output_wanted(&mut self, wanted: bool) {
1493            self.output_wanted = wanted;
1494        }
1495    }
1496
1497    /// A decoder that hands back the previous sample's picture again alongside the current
1498    /// one, as a backend that does not deduplicate its output across a session does. Both
1499    /// pictures are genuine and correctly identified; the repeat is only a frame the reader has
1500    /// already seen since its last reset.
1501    struct ReplayFactory;
1502
1503    struct ReplayDecoder {
1504        configuration: VideoDecoderConfig,
1505        limits: Limits,
1506        previous: Option<(FrameIndex, u8)>,
1507    }
1508
1509    impl ReplayDecoder {
1510        fn produce(&self, presentation_index: FrameIndex, value: u8) -> Result<DecodedVideoFrame> {
1511            Ok(DecodedVideoFrame {
1512                presentation_index,
1513                frame: VideoFrame::new(
1514                    self.configuration.coded_dimensions,
1515                    PixelFormat::Gray8,
1516                    ColorRange::Full,
1517                    vec![Plane {
1518                        data: vec![value],
1519                        stride: 1,
1520                    }],
1521                    &self.limits,
1522                )?,
1523            })
1524        }
1525    }
1526
1527    impl VideoDecoderFactory for ReplayFactory {
1528        fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
1529            CodecSupport::Supported {
1530                implementation: CodecImplementation::Software,
1531            }
1532        }
1533
1534        fn create(
1535            &self,
1536            configuration: &VideoDecoderConfig,
1537            limits: &Limits,
1538        ) -> Result<Box<dyn VideoDecoder>> {
1539            Ok(Box::new(ReplayDecoder {
1540                configuration: configuration.clone(),
1541                limits: *limits,
1542                previous: None,
1543            }))
1544        }
1545    }
1546
1547    impl VideoDecoder for ReplayDecoder {
1548        fn submit(
1549            &mut self,
1550            sample: &EncodedVideoSample,
1551            cancellation: &CancellationToken,
1552        ) -> Result<Vec<DecodedVideoFrame>> {
1553            cancellation.check()?;
1554            let value = sample.data[0];
1555            let mut outputs = Vec::new();
1556            if let Some((index, previous)) = self.previous {
1557                outputs.push(self.produce(index, previous)?);
1558            }
1559            outputs.push(self.produce(sample.presentation_index, value)?);
1560            self.previous = Some((sample.presentation_index, value));
1561            Ok(outputs)
1562        }
1563
1564        fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
1565            Ok(Vec::new())
1566        }
1567
1568        fn reset(&mut self) -> Result<()> {
1569            self.previous = None;
1570            Ok(())
1571        }
1572    }
1573
1574    /// Issue #415: publishing a presentation frame twice within one decode session used to fail
1575    /// the whole request with `MalformedMedia`, which held the backend to a rule the reader does
1576    /// not depend on and threw away a picture it was about to cache.
1577    #[test]
1578    fn a_frame_the_decoder_emits_twice_is_cached_again_rather_than_refused() {
1579        let mut reader = ExactFrameReader::new(
1580            &ReplayFactory,
1581            config(),
1582            single_group_of_pictures(),
1583            Limits::default(),
1584        )
1585        .unwrap();
1586        let cancellation = CancellationToken::new();
1587
1588        // Reaching frame 3 submits samples 0 through 3, and every sample after the first
1589        // republishes its predecessor, so frames 0, 1, and 2 are each published twice.
1590        assert_eq!(
1591            value(&reader.get(FrameIndex(3), &cancellation).unwrap()),
1592            30
1593        );
1594        assert_eq!(
1595            value(&reader.get(FrameIndex(6), &cancellation).unwrap()),
1596            60
1597        );
1598
1599        // The repeats were cached rather than refused, so the frames behind the target are
1600        // still answered from the cache instead of costing another reset.
1601        let resets = reader.statistics().resets;
1602        let hits = reader.statistics().cache_hits;
1603        assert_eq!(
1604            value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
1605            50
1606        );
1607        assert_eq!(reader.statistics().resets, resets);
1608        assert!(reader.statistics().cache_hits > hits);
1609    }
1610
1611    /// Ten frames behind one random-access point, as the bundled sample's single group of
1612    /// pictures is, with `value == index * 10`.
1613    fn single_group_of_pictures() -> Vec<EncodedVideoSample> {
1614        (0..10)
1615            .map(|index| sample(index, (index * 10) as u8, index == 0))
1616            .collect()
1617    }
1618
1619    /// A cache small enough that a walk over ten frames is longer than the tail the reader
1620    /// keeps behind its target; the default cache would hold the whole group of pictures.
1621    fn small_cache() -> Limits {
1622        Limits {
1623            max_cached_frames: 2,
1624            ..Limits::default()
1625        }
1626    }
1627
1628    #[test]
1629    fn walking_to_a_distant_frame_does_not_ask_for_the_pictures_it_passes() {
1630        let log = Arc::new(Mutex::new(Vec::new()));
1631        let factory = HintFactory {
1632            wanted: Arc::clone(&log),
1633        };
1634        let mut reader = ExactFrameReader::new(
1635            &factory,
1636            config(),
1637            single_group_of_pictures(),
1638            small_cache(),
1639        )
1640        .unwrap();
1641        let cancellation = CancellationToken::new();
1642
1643        assert_eq!(
1644            value(&reader.get(FrameIndex(8), &cancellation).unwrap()),
1645            80
1646        );
1647        assert_eq!(
1648            *log.lock().unwrap(),
1649            vec![
1650                (0, false),
1651                (1, false),
1652                (2, false),
1653                (3, false),
1654                (4, false),
1655                (5, false),
1656                (6, true),
1657                (7, true),
1658                (8, true),
1659            ],
1660            "only the target and the tail the cache can hold are produced"
1661        );
1662        let statistics = reader.statistics();
1663        assert_eq!(statistics.samples_submitted, 9);
1664        assert_eq!(statistics.samples_skipped, 6);
1665        assert_eq!(statistics.resets, 1, "the walk itself is one decode");
1666    }
1667
1668    #[test]
1669    fn a_frame_the_walk_went_past_is_decoded_again_rather_than_reported_missing() {
1670        let log = Arc::new(Mutex::new(Vec::new()));
1671        let factory = HintFactory {
1672            wanted: Arc::clone(&log),
1673        };
1674        let mut reader = ExactFrameReader::new(
1675            &factory,
1676            config(),
1677            single_group_of_pictures(),
1678            small_cache(),
1679        )
1680        .unwrap();
1681        let cancellation = CancellationToken::new();
1682        reader.get(FrameIndex(8), &cancellation).unwrap();
1683        let resets = reader.statistics().resets;
1684
1685        // Frame 4 was submitted with nothing wanting its picture, so this decoder will never
1686        // emit it again. The reader has to notice that and start over, exactly as it does for a
1687        // frame it published and then evicted.
1688        assert_eq!(
1689            value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
1690            40
1691        );
1692        assert_eq!(reader.statistics().resets, resets + 1);
1693        // And having decoded it, it is a cached frame again rather than a second reset.
1694        assert_eq!(
1695            value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
1696            40
1697        );
1698        assert_eq!(reader.statistics().resets, resets + 1);
1699    }
1700
1701    #[test]
1702    fn a_reordered_frame_at_or_past_the_target_is_kept_for_the_request_that_follows() {
1703        // Decode order 0, 4, 2, 1, 3: reaching frame 4 means submitting samples whose own
1704        // frames are 2, 1 and 3, which is a walk *backwards* in presentation order. Skipping by
1705        // decode position would discard them and make every following request a fresh walk.
1706        let samples = vec![
1707            sample(0, 0, true),
1708            sample(4, 40, false),
1709            sample(2, 20, false),
1710            sample(1, 10, false),
1711            sample(3, 30, false),
1712            sample(8, 80, false),
1713            sample(6, 60, false),
1714            sample(5, 50, false),
1715            sample(7, 70, false),
1716        ];
1717        let log = Arc::new(Mutex::new(Vec::new()));
1718        let factory = HintFactory {
1719            wanted: Arc::clone(&log),
1720        };
1721        let mut reader = ExactFrameReader::new(&factory, config(), samples, small_cache()).unwrap();
1722        let cancellation = CancellationToken::new();
1723
1724        assert_eq!(
1725            value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
1726            40
1727        );
1728        let resets = reader.statistics().resets;
1729        for (index, expected) in [(1_u64, 10_u8), (2, 20), (3, 30)] {
1730            assert_eq!(
1731                value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
1732                expected,
1733                "frame {index} is still available"
1734            );
1735        }
1736        assert_eq!(
1737            reader.statistics().resets,
1738            resets,
1739            "a frame after the target in decode order is not skipped"
1740        );
1741        assert_eq!(
1742            reader.statistics().samples_skipped,
1743            1,
1744            "only frame 0, the random-access point the walk starts from, is behind frame 4"
1745        );
1746    }
1747
1748    #[test]
1749    fn a_walk_leaves_the_frames_behind_its_target_cached_so_stepping_back_is_free() {
1750        // Walking fills the cache with the frames just before the target as a side effect, and
1751        // that is what makes the example's previous-frame key cheap. Skipping those too would
1752        // send every backward step all the way back to the random-access point.
1753        let log = Arc::new(Mutex::new(Vec::new()));
1754        let factory = HintFactory {
1755            wanted: Arc::clone(&log),
1756        };
1757        let samples: Vec<_> = (0..40)
1758            .map(|index| sample(index, index as u8, index == 0))
1759            .collect();
1760        let mut reader = ExactFrameReader::new(
1761            &factory,
1762            config(),
1763            samples,
1764            Limits {
1765                max_cached_frames: 4,
1766                ..Limits::default()
1767            },
1768        )
1769        .unwrap();
1770        let cancellation = CancellationToken::new();
1771        reader.get(FrameIndex(32), &cancellation).unwrap();
1772        let after_walk = reader.statistics();
1773        assert_eq!(after_walk.samples_skipped, 28, "frames 0 to 27 are skipped");
1774
1775        for index in [31_u64, 30, 29] {
1776            assert_eq!(
1777                value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
1778                index as u8
1779            );
1780        }
1781        assert_eq!(
1782            reader.statistics().resets,
1783            after_walk.resets,
1784            "the tail the walk kept is in the cache, so stepping back decodes nothing"
1785        );
1786        assert_eq!(
1787            reader.statistics().samples_submitted,
1788            after_walk.samples_submitted
1789        );
1790    }
1791
1792    #[test]
1793    fn a_step_converts_its_own_picture_and_no_tail_behind_it() {
1794        // The charge issue #402 is about. A destination keeps the frames behind it because the
1795        // request after it is very likely to be for one of them; a step is passed through on
1796        // the way somewhere else, so those frames are converted for a request that never comes,
1797        // and a walk that steps more often than the tail is long converts everything it passes.
1798        let log = Arc::new(Mutex::new(Vec::new()));
1799        let factory = HintFactory {
1800            wanted: Arc::clone(&log),
1801        };
1802        let samples: Vec<_> = (0..40)
1803            .map(|index| sample(index, index as u8, index == 0))
1804            .collect();
1805        let mut reader = ExactFrameReader::new(
1806            &factory,
1807            config(),
1808            samples,
1809            Limits {
1810                max_cached_frames: 4,
1811                ..Limits::default()
1812            },
1813        )
1814        .unwrap();
1815        let cancellation = CancellationToken::new();
1816
1817        assert_eq!(
1818            value(&reader.get_step(FrameIndex(32), &cancellation).unwrap()),
1819            32
1820        );
1821        let after_step = reader.statistics();
1822        assert_eq!(
1823            after_step.samples_skipped, 32,
1824            "frames 0 to 31 are all passed through, tail included"
1825        );
1826        assert_eq!(
1827            reader.cached_frames(),
1828            1,
1829            "only the picture the step asked for was converted"
1830        );
1831    }
1832
1833    #[test]
1834    fn a_walk_of_steps_converts_what_it_asks_for_rather_than_what_it_passes() {
1835        // The shape of a drag's preview walk: repeated forward steps towards a far target. Each
1836        // step used to pay its own cache tail, and with a stride shorter than the tail the tails
1837        // overlapped and covered the whole span. As steps, the walk converts one picture per
1838        // step however long the span is.
1839        let log = Arc::new(Mutex::new(Vec::new()));
1840        let factory = HintFactory {
1841            wanted: Arc::clone(&log),
1842        };
1843        let samples: Vec<_> = (0..64)
1844            .map(|index| sample(index, index as u8, index == 0))
1845            .collect();
1846        let mut reader = ExactFrameReader::new(
1847            &factory,
1848            config(),
1849            samples,
1850            Limits {
1851                max_cached_frames: 8,
1852                ..Limits::default()
1853            },
1854        )
1855        .unwrap();
1856        let cancellation = CancellationToken::new();
1857
1858        // A stride of 4 against a tail of 8: every frame in the span is within some step's tail.
1859        let steps: Vec<u64> = (4..60).step_by(4).collect();
1860        for step in &steps {
1861            reader.get_step(FrameIndex(*step), &cancellation).unwrap();
1862        }
1863        let converted = reader.statistics().samples_submitted - reader.statistics().samples_skipped;
1864        assert_eq!(
1865            converted,
1866            steps.len() as u64,
1867            "one conversion per published step, not one per frame passed"
1868        );
1869        assert_eq!(
1870            reader.statistics().resets,
1871            1,
1872            "only the cold start resets; a forward walk of steps never starts over again"
1873        );
1874    }
1875
1876    #[test]
1877    fn a_step_walk_that_ends_in_a_destination_still_leaves_a_cache_tail_to_step_back_through() {
1878        // What the request split must not cost: a committed scrub is a walk of steps that ends
1879        // at the frame under the pointer, and stepping backwards from there is still expected
1880        // to come out of the cache rather than walking from the random-access point again.
1881        let log = Arc::new(Mutex::new(Vec::new()));
1882        let factory = HintFactory {
1883            wanted: Arc::clone(&log),
1884        };
1885        let samples: Vec<_> = (0..40)
1886            .map(|index| sample(index, index as u8, index == 0))
1887            .collect();
1888        let mut reader = ExactFrameReader::new(
1889            &factory,
1890            config(),
1891            samples,
1892            Limits {
1893                max_cached_frames: 4,
1894                ..Limits::default()
1895            },
1896        )
1897        .unwrap();
1898        let cancellation = CancellationToken::new();
1899
1900        for step in [8_u64, 16, 24] {
1901            reader.get_step(FrameIndex(step), &cancellation).unwrap();
1902        }
1903        reader.get(FrameIndex(32), &cancellation).unwrap();
1904        let after_walk = reader.statistics();
1905
1906        for index in [31_u64, 30, 29] {
1907            assert_eq!(
1908                value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
1909                index as u8
1910            );
1911        }
1912        assert_eq!(
1913            reader.statistics().resets,
1914            after_walk.resets,
1915            "the destination kept its tail, so stepping back decodes nothing"
1916        );
1917        assert_eq!(
1918            reader.statistics().samples_submitted,
1919            after_walk.samples_submitted
1920        );
1921    }
1922
1923    /// A decoder that releases each picture one sample late, and drops whatever it is holding
1924    /// when nothing wants its output - which is what a reordering decoder does.
1925    struct LateFactory;
1926
1927    struct LateDecoder {
1928        configuration: VideoDecoderConfig,
1929        limits: Limits,
1930        held: Option<EncodedVideoSample>,
1931        output_wanted: bool,
1932    }
1933
1934    impl VideoDecoderFactory for LateFactory {
1935        fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
1936            CodecSupport::Supported {
1937                implementation: CodecImplementation::Software,
1938            }
1939        }
1940
1941        fn create(
1942            &self,
1943            configuration: &VideoDecoderConfig,
1944            limits: &Limits,
1945        ) -> Result<Box<dyn VideoDecoder>> {
1946            Ok(Box::new(LateDecoder {
1947                configuration: configuration.clone(),
1948                limits: *limits,
1949                held: None,
1950                output_wanted: true,
1951            }))
1952        }
1953    }
1954
1955    impl LateDecoder {
1956        fn release(&self, sample: EncodedVideoSample) -> Result<Vec<DecodedVideoFrame>> {
1957            if !self.output_wanted {
1958                return Ok(Vec::new());
1959            }
1960            Ok(vec![DecodedVideoFrame {
1961                presentation_index: sample.presentation_index,
1962                frame: VideoFrame::new(
1963                    self.configuration.coded_dimensions,
1964                    PixelFormat::Gray8,
1965                    ColorRange::Full,
1966                    vec![Plane {
1967                        data: sample.data,
1968                        stride: 1,
1969                    }],
1970                    &self.limits,
1971                )?,
1972            }])
1973        }
1974    }
1975
1976    impl VideoDecoder for LateDecoder {
1977        fn submit(
1978            &mut self,
1979            sample: &EncodedVideoSample,
1980            cancellation: &CancellationToken,
1981        ) -> Result<Vec<DecodedVideoFrame>> {
1982            cancellation.check()?;
1983            match self.held.replace(sample.clone()) {
1984                Some(previous) => self.release(previous),
1985                None => Ok(Vec::new()),
1986            }
1987        }
1988
1989        fn drain(&mut self, _: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
1990            match self.held.take() {
1991                Some(held) => self.release(held),
1992                None => Ok(Vec::new()),
1993            }
1994        }
1995
1996        fn reset(&mut self) -> Result<()> {
1997            self.held = None;
1998            Ok(())
1999        }
2000
2001        fn set_output_wanted(&mut self, wanted: bool) {
2002            self.output_wanted = wanted;
2003        }
2004    }
2005
2006    #[test]
2007    fn a_frame_still_inside_a_reordering_decoder_when_output_stops_is_written_off_too() {
2008        // The frame a decoder is holding when output stops being wanted comes out during the
2009        // suppressed stretch and is dropped, even though its own sample was submitted while
2010        // output was still wanted. A reader that does not write it off believes it can still be
2011        // asked for and reports the decoder as malformed when it never arrives.
2012        let mut reader = ExactFrameReader::new(
2013            &LateFactory,
2014            config(),
2015            single_group_of_pictures(),
2016            small_cache(),
2017        )
2018        .unwrap();
2019        let cancellation = CancellationToken::new();
2020
2021        // Reaching frame 4 means submitting frame 5's sample to release it, so the decoder is
2022        // left holding frame 5 - which it releases, and drops, on the first suppressed submit of
2023        // the request that follows.
2024        assert_eq!(
2025            value(&reader.get(FrameIndex(4), &cancellation).unwrap()),
2026            40
2027        );
2028        assert_eq!(
2029            value(&reader.get(FrameIndex(9), &cancellation).unwrap()),
2030            90
2031        );
2032        assert_eq!(
2033            value(&reader.get(FrameIndex(5), &cancellation).unwrap()),
2034            50,
2035            "the frame that was dropped in flight is decoded again, not reported missing"
2036        );
2037    }
2038
2039    #[test]
2040    fn a_decoder_that_ignores_the_hint_still_answers_every_frame() {
2041        // `set_output_wanted` is advisory: `uncompressed_video_decoder_factory` does not
2042        // implement it, and the reader has to be no less correct for that.
2043        let factory = uncompressed_video_decoder_factory();
2044        let mut reader = ExactFrameReader::new(
2045            &factory,
2046            config(),
2047            single_group_of_pictures(),
2048            small_cache(),
2049        )
2050        .unwrap();
2051        let cancellation = CancellationToken::new();
2052        reader.get(FrameIndex(8), &cancellation).unwrap();
2053        assert_eq!(reader.statistics().samples_skipped, 6);
2054        for index in 0..9_u64 {
2055            assert_eq!(
2056                value(&reader.get(FrameIndex(index), &cancellation).unwrap()),
2057                (index * 10) as u8,
2058                "frame {index} came back from the decoder that ignored the hint"
2059            );
2060        }
2061    }
2062
2063    #[test]
2064    fn malformed_packets_and_cancellation_are_reported() {
2065        let factory = uncompressed_video_decoder_factory();
2066        let mut reader = ExactFrameReader::new(
2067            &factory,
2068            config(),
2069            vec![EncodedVideoSample {
2070                presentation_index: FrameIndex(0),
2071                random_access: true,
2072                data: Vec::new(),
2073            }],
2074            Limits::default(),
2075        )
2076        .unwrap();
2077        assert_eq!(
2078            reader
2079                .get(FrameIndex(0), &CancellationToken::new())
2080                .unwrap_err()
2081                .kind(),
2082            ErrorKind::MalformedMedia
2083        );
2084        let cancelled = CancellationToken::new();
2085        cancelled.cancel();
2086        assert_eq!(
2087            reader.get(FrameIndex(0), &cancelled).unwrap_err().kind(),
2088            ErrorKind::Cancelled
2089        );
2090    }
2091
2092    #[test]
2093    fn random_access_decode_work_obeys_the_configured_bound() {
2094        let factory = uncompressed_video_decoder_factory();
2095        let mut reader = ExactFrameReader::new(
2096            &factory,
2097            config(),
2098            vec![sample(0, 1, true), sample(1, 2, false), sample(2, 3, false)],
2099            Limits {
2100                max_decode_samples_per_seek: 2,
2101                ..Limits::default()
2102            },
2103        )
2104        .unwrap();
2105        let error = reader
2106            .get(FrameIndex(2), &CancellationToken::new())
2107            .unwrap_err();
2108        assert_eq!(error.kind(), ErrorKind::ResourceLimit);
2109        assert_eq!(reader.statistics().samples_submitted, 2);
2110    }
2111
2112    struct DelayedFactory;
2113    impl VideoDecoderFactory for DelayedFactory {
2114        fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
2115            CodecSupport::Supported {
2116                implementation: CodecImplementation::Software,
2117            }
2118        }
2119        fn create(
2120            &self,
2121            configuration: &VideoDecoderConfig,
2122            limits: &Limits,
2123        ) -> Result<Box<dyn VideoDecoder>> {
2124            Ok(Box::new(DelayedDecoder {
2125                configuration: configuration.clone(),
2126                limits: *limits,
2127                delayed: None,
2128            }))
2129        }
2130    }
2131    struct DelayedDecoder {
2132        configuration: VideoDecoderConfig,
2133        limits: Limits,
2134        delayed: Option<EncodedVideoSample>,
2135    }
2136    impl DelayedDecoder {
2137        fn decode(&self, sample: EncodedVideoSample) -> Result<DecodedVideoFrame> {
2138            Ok(DecodedVideoFrame {
2139                presentation_index: sample.presentation_index,
2140                frame: VideoFrame::new(
2141                    self.configuration.coded_dimensions,
2142                    PixelFormat::Gray8,
2143                    ColorRange::Full,
2144                    vec![Plane {
2145                        data: sample.data,
2146                        stride: 1,
2147                    }],
2148                    &self.limits,
2149                )?,
2150            })
2151        }
2152    }
2153    impl VideoDecoder for DelayedDecoder {
2154        fn submit(
2155            &mut self,
2156            sample: &EncodedVideoSample,
2157            cancellation: &CancellationToken,
2158        ) -> Result<Vec<DecodedVideoFrame>> {
2159            cancellation.check()?;
2160            let previous = self.delayed.replace(sample.clone());
2161            previous
2162                .map(|sample| self.decode(sample).map(|frame| vec![frame]))
2163                .unwrap_or(Ok(Vec::new()))
2164        }
2165        fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
2166            cancellation.check()?;
2167            self.delayed
2168                .take()
2169                .map(|sample| self.decode(sample).map(|frame| vec![frame]))
2170                .unwrap_or(Ok(Vec::new()))
2171        }
2172        fn reset(&mut self) -> Result<()> {
2173            self.delayed = None;
2174            Ok(())
2175        }
2176    }
2177
2178    #[test]
2179    fn delayed_output_drains_and_reset_clears_state() {
2180        let mut reader = ExactFrameReader::new(
2181            &DelayedFactory,
2182            config(),
2183            vec![sample(0, 11, true), sample(1, 22, false)],
2184            Limits::default(),
2185        )
2186        .unwrap();
2187        let cancellation = CancellationToken::new();
2188        assert_eq!(
2189            value(&reader.get(FrameIndex(1), &cancellation).unwrap()),
2190            22
2191        );
2192        assert_eq!(reader.statistics().drains, 1);
2193        reader.reset().unwrap();
2194        assert_eq!(reader.cached_frames(), 0);
2195        assert_eq!(
2196            value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
2197            11
2198        );
2199    }
2200
2201    #[test]
2202    fn sequential_playback_through_a_pipelined_decoder_does_not_reset_every_frame() {
2203        // `DelayedDecoder` only emits a frame's output on the *next* `submit` call (or on
2204        // `drain`), modeling the one-frame pipeline latency a real reordering decoder exhibits.
2205        // Even though `next_decode_position` therefore always runs one step ahead of the frame
2206        // a straightforward sequential `get(0), get(1), get(2), ...` walk is actually waiting
2207        // on, that lag must not force a full reset-and-redecode-from-keyframe on every call.
2208        let mut reader = ExactFrameReader::new(
2209            &DelayedFactory,
2210            config(),
2211            vec![
2212                sample(0, 11, true),
2213                sample(1, 22, false),
2214                sample(2, 33, false),
2215                sample(3, 44, false),
2216                sample(4, 55, false),
2217            ],
2218            Limits::default(),
2219        )
2220        .unwrap();
2221        let cancellation = CancellationToken::new();
2222
2223        assert_eq!(
2224            value(&reader.get(FrameIndex(0), &cancellation).unwrap()),
2225            11
2226        );
2227        assert_eq!(reader.statistics().resets, 1);
2228
2229        for (index, expected) in [(1, 22_u8), (2, 33), (3, 44), (4, 55)] {
2230            let frame = reader.get(FrameIndex(index), &cancellation).unwrap();
2231            assert_eq!(value(&frame), expected);
2232            assert_eq!(
2233                reader.statistics().resets,
2234                1,
2235                "sequential forward playback must not reset once decoding has started"
2236            );
2237        }
2238        assert_eq!(reader.statistics().samples_submitted, 5);
2239        assert_eq!(reader.statistics().drains, 1);
2240    }
2241
2242    struct MalformedOutputFactory;
2243
2244    impl VideoDecoderFactory for MalformedOutputFactory {
2245        fn capability(&self, _: &VideoDecoderConfig) -> CodecSupport {
2246            CodecSupport::Supported {
2247                implementation: CodecImplementation::Software,
2248            }
2249        }
2250
2251        fn create(
2252            &self,
2253            configuration: &VideoDecoderConfig,
2254            limits: &Limits,
2255        ) -> Result<Box<dyn VideoDecoder>> {
2256            let factory = UncompressedVideoDecoderFactory;
2257            factory.create(configuration, limits).map(|decoder| {
2258                Box::new(MalformedOutputDecoder {
2259                    decoder,
2260                    previous: None,
2261                }) as Box<dyn VideoDecoder>
2262            })
2263        }
2264    }
2265
2266    struct MalformedOutputDecoder {
2267        decoder: Box<dyn VideoDecoder>,
2268        previous: Option<FrameIndex>,
2269    }
2270
2271    impl VideoDecoder for MalformedOutputDecoder {
2272        fn submit(
2273            &mut self,
2274            sample: &EncodedVideoSample,
2275            cancellation: &CancellationToken,
2276        ) -> Result<Vec<DecodedVideoFrame>> {
2277            let mut output = self.decoder.submit(sample, cancellation)?;
2278            if let (Some(previous), Some(frame)) = (self.previous, output.first_mut()) {
2279                frame.presentation_index = previous;
2280            }
2281            self.previous = Some(sample.presentation_index);
2282            Ok(output)
2283        }
2284
2285        fn drain(&mut self, cancellation: &CancellationToken) -> Result<Vec<DecodedVideoFrame>> {
2286            self.decoder.drain(cancellation)
2287        }
2288
2289        fn reset(&mut self) -> Result<()> {
2290            self.previous = None;
2291            self.decoder.reset()
2292        }
2293    }
2294
2295    #[test]
2296    fn malformed_backend_output_is_rejected() {
2297        let mut reader = ExactFrameReader::new(
2298            &MalformedOutputFactory,
2299            config(),
2300            vec![sample(0, 1, true), sample(1, 2, false)],
2301            Limits::default(),
2302        )
2303        .unwrap();
2304        let error = reader
2305            .get(FrameIndex(1), &CancellationToken::new())
2306            .unwrap_err();
2307        assert_eq!(error.kind(), ErrorKind::MalformedMedia);
2308    }
2309}