Skip to main content

zencodec_media_testkit/
checks.rs

1//! Conformance checks. Each takes factories for the implementation under
2//! test and inputs it should handle, and returns `Err` with the first rule
3//! broken.
4
5use alloc::format;
6use alloc::string::{String, ToString};
7use alloc::sync::Arc;
8use alloc::vec::Vec;
9use core::fmt;
10
11use enough::{Stop, StopReason};
12use zencodec::decode::{AnimationFrameDecoder, Input};
13use zencodec::encode::AnimationFrameEncoder;
14use zencodec::how_far::NoPulse;
15use zencodec::source::{ReadAt, SourceError};
16use zencodec::time::{FrameDuration, TimeBase, Timestamp};
17use zencodec_io::{DriveFailure, DriveOptions, ReadAtCursor, SliceCursor, Yield};
18use zencodec_media::adapt::{
19    AnimationVideoEncoder, CaptureFrame, RenderFrame, VideoAnimationDecoder,
20};
21use zencodec_media::container::{
22    DemuxEvent, DemuxStage, Demuxer, MuxHint, Muxer, SeekStatus, SeekTarget,
23};
24use zencodec_media::frame::{Frame, PlaneSamples};
25use zencodec_media::packet::{Discard, Packet, RandomAccess};
26use zencodec_media::session::{
27    CodecConfig, ProtocolViolation, Receive, SendStatus, SessionError, UnsupportedMedia,
28    VideoDecoder, VideoEncoder, erase_video_decoder,
29};
30use zencodec_media::track::TrackInfo;
31use zenpixels::PixelBuffer;
32
33/// A broken rule.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Failure {
36    /// The check that failed.
37    pub check: &'static str,
38    /// What went wrong.
39    pub detail: String,
40}
41
42impl fmt::Display for Failure {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "[{}] {}", self.check, self.detail)
45    }
46}
47
48impl core::error::Error for Failure {}
49
50/// The result of a check.
51pub type Conformance = Result<(), Failure>;
52
53fn fail(check: &'static str, detail: impl Into<String>) -> Failure {
54    Failure {
55        check,
56        detail: detail.into(),
57    }
58}
59
60/// A stop that has tripped.
61struct Tripped;
62
63impl Stop for Tripped {
64    fn check(&self) -> Result<(), StopReason> {
65        Err(StopReason::Cancelled)
66    }
67}
68
69/// Every plane's samples in order, as bytes: what a frame shows.
70fn frame_pixels(frame: &Frame) -> Result<Vec<u8>, String> {
71    let mapped = frame.map().map_err(|e| e.to_string())?;
72    let mut out = Vec::new();
73    for i in 0..mapped.plane_count() {
74        let plane = mapped.plane(i).ok_or("plane disappeared")?;
75        for y in 0..plane.height() {
76            match plane.row(y).ok_or("row out of range")? {
77                PlaneSamples::U8(r) => out.extend_from_slice(r),
78                PlaneSamples::U16(r) => {
79                    for v in r {
80                        out.extend_from_slice(&v.to_le_bytes());
81                    }
82                }
83            }
84        }
85    }
86    Ok(out)
87}
88
89/// What a decoded frame is, for comparing two decodes.
90#[derive(Debug, PartialEq, Eq)]
91struct Shown {
92    pts: Option<Timestamp>,
93    keyframe: bool,
94    show_existing: bool,
95    pixels: Vec<u8>,
96}
97
98fn shown(frame: &Frame) -> Result<Shown, String> {
99    Ok(Shown {
100        pts: frame.timing().pts,
101        keyframe: frame.presentation().keyframe,
102        show_existing: frame.presentation().show_existing,
103        pixels: frame_pixels(frame)?,
104    })
105}
106
107/// Decode `packets` receiving everything as soon as it is ready.
108fn decode_eager<D: VideoDecoder>(dec: &mut D, packets: &[Packet]) -> Result<Vec<Frame>, String> {
109    let mut frames = Vec::new();
110    for p in packets {
111        let mut p = p.clone();
112        zencodec_media::session::send_packet_until_accepted(dec, &mut p, Some(&NoPulse), |f| {
113            frames.push(f);
114            Ok::<(), ()>(())
115        })
116        .map_err(|e| format!("{e:?}"))?;
117    }
118    zencodec_media::session::drain_decoder(dec, Some(&NoPulse), |f| {
119        frames.push(f);
120        Ok::<(), ()>(())
121    })
122    .map_err(|e| format!("{e:?}"))?;
123    Ok(frames)
124}
125
126/// Verify a [`VideoDecoder`] against the session contract.
127///
128/// `make` builds a fresh decoder; `config` configures it; `packets` is a
129/// valid stream for it. Checked:
130///
131/// - Receiving lazily (only when the decoder says `ReceivePending`, then
132///   draining) and eagerly (after every packet) yields the same frames:
133///   timestamps, presentation flags and pixels.
134/// - `ReceivePending` leaves the packet untaken and is followed by a
135///   `Ready`.
136/// - `end_input` is idempotent, `EndOfStream` is stable, and `send` after
137///   `end_input` is [`ProtocolViolation::SendAfterEnd`].
138/// - A packet from another configuration epoch is
139///   [`ProtocolViolation::EpochMismatch`].
140/// - A frame and a mapping of it stay valid while the decoder decodes more,
141///   resets and is dropped; after `reset` the same packets decode to the
142///   same frames.
143/// - Behind [`erase_video_decoder`], a tripped stop is `Cancelled`, every
144///   call after it `Finished`, until `reset`.
145pub fn check_video_decoder_contract<D, F>(
146    mut make: F,
147    config: &CodecConfig,
148    packets: &[Packet],
149) -> Conformance
150where
151    D: VideoDecoder + Send + 'static,
152    F: FnMut() -> D,
153{
154    const CHECK: &str = "video_decoder_contract";
155    let configured = |make: &mut F| -> Result<D, Failure> {
156        let mut d = make();
157        d.configure(config, Some(&NoPulse))
158            .map_err(|e| fail(CHECK, format!("configure: {e:?}")))?;
159        Ok(d)
160    };
161    if packets.is_empty() {
162        return Err(fail(CHECK, "no packets supplied"));
163    }
164
165    let mut dec = configured(&mut make)?;
166    let eager = decode_eager(&mut dec, packets).map_err(|e| fail(CHECK, e))?;
167    if eager.is_empty() {
168        return Err(fail(CHECK, "the stream decoded to no frames"));
169    }
170    let want: Vec<Shown> = eager
171        .iter()
172        .map(shown)
173        .collect::<Result<_, _>>()
174        .map_err(|e| fail(CHECK, e))?;
175
176    // Lazy: receive only when pushed back, then at the end.
177    let mut dec = configured(&mut make)?;
178    let mut lazy = Vec::new();
179    for (i, p) in packets.iter().enumerate() {
180        let mut p = p.clone();
181        loop {
182            match dec.send(&mut p, Some(&NoPulse)) {
183                Ok(SendStatus::Accepted) => break,
184                Ok(SendStatus::ReceivePending) => {
185                    if p.is_taken() {
186                        return Err(fail(
187                            CHECK,
188                            format!("packet {i}: ReceivePending took the payload"),
189                        ));
190                    }
191                    match dec.receive(Some(&NoPulse)) {
192                        Ok(Receive::Ready(f)) => lazy.push(f),
193                        other => {
194                            return Err(fail(
195                                CHECK,
196                                format!("packet {i}: ReceivePending, then {other:?}"),
197                            ));
198                        }
199                    }
200                }
201                Ok(other) => return Err(fail(CHECK, format!("packet {i}: send {other:?}"))),
202                Err(e) => return Err(fail(CHECK, format!("packet {i}: send {e:?}"))),
203            }
204        }
205    }
206    dec.end_input()
207        .map_err(|e| fail(CHECK, format!("end_input: {e:?}")))?;
208    dec.end_input()
209        .map_err(|e| fail(CHECK, format!("second end_input: {e:?}")))?;
210    loop {
211        match dec.receive(Some(&NoPulse)) {
212            Ok(Receive::Ready(f)) => lazy.push(f),
213            Ok(Receive::EndOfStream) => break,
214            other => return Err(fail(CHECK, format!("drain: {other:?}"))),
215        }
216    }
217    if !matches!(dec.receive(Some(&NoPulse)), Ok(Receive::EndOfStream)) {
218        return Err(fail(CHECK, "EndOfStream is not stable"));
219    }
220    let got: Vec<Shown> = lazy
221        .iter()
222        .map(shown)
223        .collect::<Result<_, _>>()
224        .map_err(|e| fail(CHECK, e))?;
225    if got != want {
226        return Err(fail(
227            CHECK,
228            format!(
229                "lazy receive gave {} frames, eager {}, or they differ",
230                got.len(),
231                want.len()
232            ),
233        ));
234    }
235    let mut again = packets[0].clone();
236    match dec.send(&mut again, Some(&NoPulse)) {
237        Err(SessionError::Protocol(ProtocolViolation::SendAfterEnd)) => {}
238        other => return Err(fail(CHECK, format!("send after end_input: {other:?}"))),
239    }
240
241    // Epochs.
242    let mut dec = configured(&mut make)?;
243    let mut other_epoch = packets[0]
244        .clone()
245        .with_config_epoch(config.epoch.wrapping_add(1));
246    match dec.send(&mut other_epoch, Some(&NoPulse)) {
247        Err(SessionError::Protocol(ProtocolViolation::EpochMismatch)) => {}
248        other => {
249            return Err(fail(
250                CHECK,
251                format!("a packet from another epoch: {other:?}"),
252            ));
253        }
254    }
255
256    // Ownership: a frame and its mapping outlive more decoding, reset and
257    // the decoder.
258    let mut dec = configured(&mut make)?;
259    let first = decode_eager(&mut dec, packets).map_err(|e| fail(CHECK, e))?;
260    let held = first[0].clone();
261    let mapping = held.map().map_err(|e| fail(CHECK, e.to_string()))?;
262    dec.reset()
263        .map_err(|e| fail(CHECK, format!("reset: {e:?}")))?;
264    let after_reset = decode_eager(&mut dec, packets).map_err(|e| fail(CHECK, e))?;
265    drop(dec);
266    drop(mapping);
267    let after: Vec<Shown> = after_reset
268        .iter()
269        .map(shown)
270        .collect::<Result<_, _>>()
271        .map_err(|e| fail(CHECK, e))?;
272    if after != want {
273        return Err(fail(
274            CHECK,
275            "after reset the same packets decode to different frames",
276        ));
277    }
278    if shown(&held).map_err(|e| fail(CHECK, e))? != want[0] {
279        return Err(fail(
280            CHECK,
281            "a frame changed after its decoder decoded more, reset and dropped",
282        ));
283    }
284
285    // Cancellation is terminal until reset, behind the erased wrapper.
286    let mut dec = erase_video_decoder(configured(&mut make)?);
287    match dec.receive(Some(&zencodec::how_far::StopOnly::borrowed(&Tripped))) {
288        Err(SessionError::Cancelled(_)) => {}
289        other => return Err(fail(CHECK, format!("a tripped stop: {other:?}"))),
290    }
291    match dec.receive(Some(&NoPulse)) {
292        Err(SessionError::Finished) => {}
293        other => return Err(fail(CHECK, format!("after cancellation: {other:?}"))),
294    }
295    dec.reset()
296        .map_err(|e| fail(CHECK, format!("reset after cancellation: {e:?}")))?;
297    if !matches!(dec.receive(Some(&NoPulse)), Ok(Receive::NeedInput)) {
298        return Err(fail(CHECK, "reset did not reopen the session"));
299    }
300    Ok(())
301}
302
303/// Verify a [`VideoEncoder`] against the session contract.
304///
305/// `make` builds a fresh encoder; `frames` are valid input for it, with
306/// timestamps on its clock. Checked:
307///
308/// - `output_config` is available before the first frame.
309/// - Receiving lazily and eagerly yields the same packets (bytes,
310///   timestamps, random-access flags); every packet has a timestamp and the
311///   timestamps are exactly the frames'.
312/// - `ReceivePending` is followed by a `Ready`; `end_input` is idempotent,
313///   `EndOfStream` stable, `send` after it refused.
314/// - A frame on another clock is [`ProtocolViolation::ClockMismatch`].
315pub fn check_video_encoder_contract<E, F>(mut make: F, frames: &[Frame]) -> Conformance
316where
317    E: VideoEncoder,
318    F: FnMut() -> E,
319{
320    const CHECK: &str = "video_encoder_contract";
321    if frames.is_empty() {
322        return Err(fail(CHECK, "no frames supplied"));
323    }
324    type Out = (Vec<u8>, Option<Timestamp>, RandomAccess);
325    let run = |enc: &mut E, eager: bool| -> Result<Vec<Out>, Failure> {
326        let mut out = Vec::new();
327        let keep = |p: Packet| -> Out { (p.bytes().to_vec(), p.pts(), p.random_access()) };
328        for (i, f) in frames.iter().enumerate() {
329            loop {
330                match enc.send(f, Some(&NoPulse)) {
331                    Ok(SendStatus::Accepted) => break,
332                    Ok(SendStatus::ReceivePending) => match enc.receive(Some(&NoPulse)) {
333                        Ok(Receive::Ready(p)) => out.push(keep(p)),
334                        other => {
335                            return Err(fail(
336                                CHECK,
337                                format!("frame {i}: ReceivePending, then {other:?}"),
338                            ));
339                        }
340                    },
341                    other => return Err(fail(CHECK, format!("frame {i}: send {other:?}"))),
342                }
343            }
344            if eager {
345                while let Ok(Receive::Ready(p)) = enc.receive(Some(&NoPulse)) {
346                    out.push(keep(p));
347                }
348            }
349        }
350        enc.end_input()
351            .and_then(|()| enc.end_input())
352            .map_err(|e| fail(CHECK, format!("end_input: {e:?}")))?;
353        loop {
354            match enc.receive(Some(&NoPulse)) {
355                Ok(Receive::Ready(p)) => out.push(keep(p)),
356                Ok(Receive::EndOfStream) => break,
357                other => return Err(fail(CHECK, format!("drain: {other:?}"))),
358            }
359        }
360        if !matches!(enc.receive(Some(&NoPulse)), Ok(Receive::EndOfStream)) {
361            return Err(fail(CHECK, "EndOfStream is not stable"));
362        }
363        match enc.send(&frames[0], Some(&NoPulse)) {
364            Err(SessionError::Protocol(ProtocolViolation::SendAfterEnd)) => {}
365            other => return Err(fail(CHECK, format!("send after end_input: {other:?}"))),
366        }
367        Ok(out)
368    };
369    let mut enc = make();
370    let Some(config) = enc.output_config().cloned() else {
371        return Err(fail(CHECK, "no output_config before the first frame"));
372    };
373    let eager = run(&mut enc, true)?;
374    let lazy = run(&mut make(), false)?;
375    if eager != lazy {
376        return Err(fail(CHECK, "lazy and eager receive gave different packets"));
377    }
378    let mut got: Vec<Timestamp> = Vec::new();
379    for (i, (_, pts, _)) in eager.iter().enumerate() {
380        got.push(pts.ok_or_else(|| fail(CHECK, format!("packet {i} has no timestamp")))?);
381    }
382    let mut want: Vec<Timestamp> = frames.iter().filter_map(|f| f.timing().pts).collect();
383    got.sort_by(|a, b| a.compare(*b));
384    want.sort_by(|a, b| a.compare(*b));
385    if got != want {
386        return Err(fail(
387            CHECK,
388            "packet timestamps are not exactly the frames' timestamps",
389        ));
390    }
391    // A frame on another clock.
392    let mut enc = make();
393    let other_base = if config.time_base == TimeBase::MILLIS {
394        TimeBase::SECONDS
395    } else {
396        TimeBase::MILLIS
397    };
398    let mut timing = frames[0].timing();
399    timing.pts = Some(Timestamp::new(0, other_base));
400    let stray = frames[0].represent(timing, frames[0].presentation());
401    match enc.send(&stray, Some(&NoPulse)) {
402        Err(SessionError::Protocol(ProtocolViolation::ClockMismatch)) => Ok(()),
403        other => Err(fail(CHECK, format!("a frame on another clock: {other:?}"))),
404    }
405}
406
407/// A [`ReadAt`] over bytes that does not admit being in memory, so the
408/// windowed and range paths of the drivers run.
409struct Remote<'a>(&'a [u8]);
410
411impl ReadAt for Remote<'_> {
412    type Error = SourceError;
413
414    fn len(&mut self) -> Result<u64, SourceError> {
415        Ok(self.0.len() as u64)
416    }
417
418    fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), SourceError> {
419        if self.read_at(offset, out)? == out.len() {
420            Ok(())
421        } else {
422            Err(SourceError::UnexpectedEof {
423                offset,
424                len: out.len() as u64,
425                total: Some(self.0.len() as u64),
426            })
427        }
428    }
429
430    fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, SourceError> {
431        let start = (offset as usize).min(self.0.len());
432        let n = out.len().min(self.0.len() - start);
433        out[..n].copy_from_slice(&self.0[start..start + n]);
434        Ok(n)
435    }
436}
437
438/// A packet as data, for comparing demuxes.
439#[derive(Debug, PartialEq, Eq)]
440struct Demuxed {
441    track: u32,
442    bytes: Vec<u8>,
443    pts: Option<Timestamp>,
444    dts: Option<Timestamp>,
445    duration_ticks: Option<u64>,
446    discard: Option<Discard>,
447    random_access: RandomAccess,
448    epoch: u32,
449}
450
451#[derive(Debug, PartialEq, Eq)]
452enum Seen {
453    Headers(usize),
454    Packet(Demuxed),
455    Config(u32, u32),
456}
457
458fn seen(ev: DemuxEvent, tracks: usize) -> Option<Seen> {
459    match ev {
460        DemuxEvent::Headers => Some(Seen::Headers(tracks)),
461        DemuxEvent::Packet(p) => Some(Seen::Packet(Demuxed {
462            track: p.track(),
463            bytes: p.bytes().to_vec(),
464            pts: p.pts(),
465            dts: p.dts(),
466            duration_ticks: p.duration_ticks(),
467            discard: p.discard(),
468            random_access: p.random_access(),
469            epoch: p.config_epoch(),
470        })),
471        DemuxEvent::ConfigChanged { track, epoch } => Some(Seen::Config(track, epoch)),
472        _ => None,
473    }
474}
475
476type DemuxErr<D> = DriveFailure<SessionError<<D as Demuxer>::Error>, SourceError>;
477
478fn demux_all<D: Demuxer>(
479    dem: &mut D,
480    mut next: impl FnMut(&mut D) -> Result<Yield<DemuxEvent>, DemuxErr<D>>,
481) -> Result<Vec<Seen>, DemuxErr<D>> {
482    let mut out = Vec::new();
483    loop {
484        let y = next(dem)?;
485        let finished = y.is_finished();
486        if let Some(s) = seen(y.into_inner(), dem.tracks().len()) {
487            out.push(s);
488        }
489        if finished {
490            return Ok(out);
491        }
492    }
493}
494
495/// Verify a [`Demuxer`] against the container contract.
496///
497/// `make` builds a fresh demuxer; `file` is a valid container for it.
498/// Checked:
499///
500/// - The in-memory driver at windows of 1, 7 and 64 bytes and the whole
501///   file, and the random-access driver at 64 bytes and 4 KiB, give the
502///   same events: headers, packets (bytes, track, timestamps, random-access
503///   evidence, epoch), configuration changes.
504/// - Input at an offset the demuxer did not ask for is not consumed, and
505///   `requested` is never empty after `Need`.
506/// - Every truncation (at 13 evenly spread cut points) either fails as the
507///   demuxer's own error or succeeds with a prefix of the events; it never
508///   breaks the driver protocol.
509///
510/// For seeking, see [`check_demuxer_seek`].
511pub fn check_demuxer_contract<D, F>(mut make: F, file: &[u8]) -> Conformance
512where
513    D: Demuxer,
514    F: FnMut() -> D,
515{
516    const CHECK: &str = "demuxer_contract";
517    let slice = |make: &mut F, bytes: &[u8], window: usize| {
518        let mut dem = make();
519        let mut cursor = SliceCursor::new(window);
520        demux_all(&mut dem, |d| {
521            cursor
522                .next(bytes, &mut DemuxStage(d), Some(&NoPulse))
523                .map_err(DriveFailure::widen)
524        })
525    };
526    let want =
527        slice(&mut make, file, file.len().max(1)).map_err(|e| fail(CHECK, format!("{e:?}")))?;
528    if !want.iter().any(|s| matches!(s, Seen::Packet(_))) {
529        return Err(fail(CHECK, "the file yielded no packets"));
530    }
531    for window in [1usize, 7, 64] {
532        let got = slice(&mut make, file, window)
533            .map_err(|e| fail(CHECK, format!("window {window}: {e:?}")))?;
534        if got != want {
535            return Err(fail(
536                CHECK,
537                format!("window {window} gave different events than the whole file"),
538            ));
539        }
540    }
541    for read_size in [64usize, 4096] {
542        let mut dem = make();
543        let mut src = Remote(file);
544        let mut cursor = ReadAtCursor::new(DriveOptions::new(0, read_size));
545        let got = demux_all(&mut dem, |d| {
546            cursor.next(&mut src, &mut DemuxStage(d), Some(&NoPulse))
547        })
548        .map_err(|e| fail(CHECK, format!("read_at {read_size}: {e:?}")))?;
549        if got != want {
550            return Err(fail(
551                CHECK,
552                format!("the ReadAt driver at {read_size} bytes gave different events"),
553            ));
554        }
555    }
556
557    // Unrequested input is not consumed.
558    let mut dem = make();
559    let at = file.len() / 2;
560    let p = dem
561        .step(Input::new(at as u64 + 1, &file[at..]), Some(&NoPulse))
562        .map_err(|e| fail(CHECK, format!("an unrequested input: {e:?}")))?;
563    if p.consumed != 0 || !matches!(p.event, DemuxEvent::Need) || dem.requested().is_empty() {
564        return Err(fail(
565            CHECK,
566            "input at an offset nobody asked for was used, or no request was named",
567        ));
568    }
569
570    // Truncations.
571    for k in 1..=13usize {
572        let cut = file.len() * k / 14;
573        match slice(&mut make, &file[..cut], 7) {
574            Ok(got) => {
575                if got.len() > want.len() || got[..] != want[..got.len()] {
576                    return Err(fail(
577                        CHECK,
578                        format!("a file cut at {cut} gave events the whole file does not"),
579                    ));
580                }
581            }
582            Err(DriveFailure::Step(_)) => {}
583            Err(e) => {
584                return Err(fail(
585                    CHECK,
586                    format!("a file cut at {cut} broke the protocol: {e:?}"),
587                ));
588            }
589        }
590    }
591    Ok(())
592}
593
594/// Verify a demuxer's [`seek`](Demuxer::seek) against `targets`.
595///
596/// After the headers, each target is sought (stepping through
597/// `SeekStatus::Scanning` until `Seeked`). Checked: the reported position
598/// is the next packet's actual timestamp and random-access evidence, never
599/// the request echoed back; a `RandomAccessPoints` seek lands on a packet
600/// the container marks `RandomAccess::Yes`.
601pub fn check_demuxer_seek<D, F>(mut make: F, file: &[u8], targets: &[SeekTarget]) -> Conformance
602where
603    D: Demuxer,
604    F: FnMut() -> D,
605{
606    const CHECK: &str = "demuxer_seek";
607    let mut dem = make();
608    let mut cursor = SliceCursor::new(16);
609    loop {
610        match cursor
611            .next(file, &mut DemuxStage(&mut dem), Some(&NoPulse))
612            .map_err(|e| fail(CHECK, format!("{e:?}")))?
613            .into_inner()
614        {
615            DemuxEvent::Headers => break,
616            DemuxEvent::Done => return Err(fail(CHECK, "no headers")),
617            _ => {}
618        }
619    }
620    for (i, target) in targets.iter().enumerate() {
621        let result = match dem.seek(target) {
622            Ok(SeekStatus::Positioned(r)) => r,
623            Ok(SeekStatus::Scanning) => loop {
624                match cursor
625                    .next(file, &mut DemuxStage(&mut dem), Some(&NoPulse))
626                    .map_err(|e| fail(CHECK, format!("target {i}: {e:?}")))?
627                    .into_inner()
628                {
629                    DemuxEvent::Seeked(r) => break r,
630                    DemuxEvent::Done => {
631                        return Err(fail(CHECK, format!("target {i}: scan ended unpositioned")));
632                    }
633                    _ => {}
634                }
635            },
636            Ok(other) => return Err(fail(CHECK, format!("target {i}: {other:?}"))),
637            Err(SessionError::Unsupported(UnsupportedMedia::Seek)) => {
638                return Err(fail(CHECK, "seek is unsupported"));
639            }
640            Err(e) => return Err(fail(CHECK, format!("target {i}: {e:?}"))),
641        };
642        let next = loop {
643            match cursor
644                .next(file, &mut DemuxStage(&mut dem), Some(&NoPulse))
645                .map_err(|e| fail(CHECK, format!("target {i}: {e:?}")))?
646                .into_inner()
647            {
648                DemuxEvent::Packet(p) if p.track() == target.track => break p,
649                DemuxEvent::Done => {
650                    return Err(fail(CHECK, format!("target {i}: no packet after the seek")));
651                }
652                _ => {}
653            }
654        };
655        if next.pts().is_none_or(|p| !p.same_instant(result.pts)) {
656            return Err(fail(
657                CHECK,
658                format!(
659                    "target {i}: reported {:?} but the next packet is at {:?}",
660                    result.pts,
661                    next.pts()
662                ),
663            ));
664        }
665        if next.random_access() != result.random_access {
666            return Err(fail(
667                CHECK,
668                format!("target {i}: reported random access differs from the packet's"),
669            ));
670        }
671        if target.scope == zencodec_media::container::SeekScope::RandomAccessPoints
672            && result.random_access != RandomAccess::Yes
673        {
674            return Err(fail(
675                CHECK,
676                format!("target {i}: a random-access seek landed elsewhere"),
677            ));
678        }
679    }
680    Ok(())
681}
682
683/// How a muxer check drains output.
684#[derive(Clone, Copy, PartialEq, Eq)]
685enum Drain {
686    Never,
687    All,
688    Partial,
689    Take,
690}
691
692/// Verify a [`Muxer`]'s pulled output.
693///
694/// `make` builds a fresh muxer; `tracks` and `packets` are valid input for
695/// it. Checked:
696///
697/// - The finished file is identical whether output is never drained,
698///   drained fully after every packet, drained in uneven prefixes
699///   (sometimes asking for more than is pending, which clamps), or taken
700///   with `take_output`; patches land inside the file.
701/// - A second `begin` is [`ProtocolViolation::BeginTwice`].
702/// - With [`MuxHint::forward_only`], `begin` either refuses with
703///   [`UnsupportedMedia::ForwardOnlyLayout`] or produces no patches, and
704///   the timing it reports says so.
705pub fn check_muxer_output_honesty<M, F>(
706    mut make: F,
707    tracks: &[TrackInfo],
708    packets: &[Packet],
709) -> Conformance
710where
711    M: Muxer,
712    F: FnMut() -> M,
713{
714    const CHECK: &str = "muxer_output_honesty";
715    let run = |make: &mut F, drain: Drain, hint: MuxHint| -> Result<Vec<u8>, Failure> {
716        let mut m = make();
717        let timing = m
718            .begin(tracks, hint)
719            .map_err(|e| fail(CHECK, format!("begin: {e:?}")))?;
720        let mut out = Vec::new();
721        let mut rng = 0x2545_f491u32;
722        for (i, p) in packets.iter().enumerate() {
723            m.write_packet(p.clone(), Some(&NoPulse))
724                .map_err(|e| fail(CHECK, format!("packet {i}: {e:?}")))?;
725            let pending = m.pending_output().len();
726            match drain {
727                Drain::Never => {}
728                Drain::All => {
729                    out.extend_from_slice(m.pending_output());
730                    m.consume_output(pending);
731                }
732                Drain::Partial => {
733                    rng ^= rng << 13;
734                    rng ^= rng >> 17;
735                    rng ^= rng << 5;
736                    let n = rng as usize % (pending + 2);
737                    out.extend_from_slice(&m.pending_output()[..n.min(pending)]);
738                    m.consume_output(n);
739                    if m.pending_output().len() != pending - n.min(pending) {
740                        return Err(fail(CHECK, "consume_output did not clamp"));
741                    }
742                }
743                Drain::Take => m.take_output(&mut out),
744            }
745            if !timing.streams() && !m.pending_output().is_empty() {
746                return Err(fail(
747                    CHECK,
748                    "pending output before finish from a muxer that reported AT_FINISH",
749                ));
750            }
751        }
752        let fin = m
753            .finish(Some(&NoPulse))
754            .map_err(|e| fail(CHECK, format!("finish: {e:?}")))?;
755        if !timing.patches_at_finish() && !fin.patches.is_empty() {
756            return Err(fail(
757                CHECK,
758                "patches from a muxer whose timing said it would not patch",
759            ));
760        }
761        fin.apply_to(&mut out)
762            .map_err(|e| fail(CHECK, e.to_string()))?;
763        Ok(out)
764    };
765    let hint = MuxHint::default();
766    let want = run(&mut make, Drain::Never, hint)?;
767    for (name, drain) in [
768        ("draining all", Drain::All),
769        ("uneven prefixes", Drain::Partial),
770        ("take_output", Drain::Take),
771    ] {
772        if run(&mut make, drain, hint)? != want {
773            return Err(fail(CHECK, format!("{name} changed the file")));
774        }
775    }
776    let mut m = make();
777    m.begin(tracks, hint)
778        .map_err(|e| fail(CHECK, format!("begin: {e:?}")))?;
779    match m.begin(tracks, hint) {
780        Err(SessionError::Protocol(ProtocolViolation::BeginTwice)) => {}
781        other => return Err(fail(CHECK, format!("a second begin: {other:?}"))),
782    }
783    let mut m = make();
784    match m.begin(tracks, MuxHint::forward_only()) {
785        Err(SessionError::Unsupported(UnsupportedMedia::ForwardOnlyLayout)) => {}
786        Ok(t) if t.patches_at_finish() => {
787            return Err(fail(
788                CHECK,
789                "a forward-only destination got a layout that patches",
790            ));
791        }
792        Ok(_) => {
793            run(&mut make, Drain::Take, MuxHint::forward_only())?;
794        }
795        Err(e) => return Err(fail(CHECK, format!("forward-only begin: {e:?}"))),
796    }
797    Ok(())
798}
799
800/// Verify the animation adapters over a video codec and a container.
801///
802/// Encodes `frames` with `durations` through [`AnimationVideoEncoder`]
803/// (the encoder from `make_encoder`, the muxer from `make_muxer`, pixels
804/// captured by `capture`), drained and undrained, then decodes the file
805/// through [`VideoAnimationDecoder`] (`make_demuxer`, `make_decoder`,
806/// `render`). Checked: draining never changes the bytes; with a streaming
807/// muxer, bytes become pending across every `lag + 1` frames; the decoded
808/// animation has the same frames, pixels and exact durations; a declared
809/// frame count is binding.
810#[allow(clippy::too_many_arguments)]
811pub fn check_animation_over_video<E, M, D, C, P, R>(
812    mut make_encoder: impl FnMut() -> E,
813    mut make_muxer: impl FnMut() -> M,
814    lag_frames: u32,
815    mut capture: impl FnMut() -> P,
816    make_demuxer: impl FnOnce() -> D,
817    make_decoder: impl FnOnce() -> C,
818    render: R,
819    frames: &[PixelBuffer],
820    durations: &[FrameDuration],
821) -> Conformance
822where
823    E: VideoEncoder + Send,
824    M: Muxer + Send,
825    D: Demuxer + Send,
826    C: VideoDecoder + Send,
827    P: CaptureFrame + Send,
828    R: RenderFrame + Send,
829{
830    const CHECK: &str = "animation_over_video";
831    if frames.is_empty() || frames.len() != durations.len() {
832        return Err(fail(
833            CHECK,
834            "supply as many durations as frames, at least one",
835        ));
836    }
837    let (w, h, desc) = (
838        frames[0].width(),
839        frames[0].height(),
840        frames[0].descriptor(),
841    );
842    let count = frames.len() as u32;
843    // The file, bytes available after each push, and the reported lag.
844    type Encoded = (Vec<u8>, Vec<usize>, Option<u32>);
845    let mut encode = |drain: bool| -> Result<Encoded, Failure> {
846        let mut a = AnimationVideoEncoder::new(make_encoder(), make_muxer(), capture(), lag_frames)
847            .map_err(|e| fail(CHECK, format!("new: {e:?}")))?;
848        let timing = a
849            .begin(w, h, desc, Some(count))
850            .map_err(|e| fail(CHECK, format!("begin: {e:?}")))?;
851        let mut out = Vec::new();
852        let mut points = Vec::new();
853        if drain {
854            a.take_output(&mut out);
855        }
856        for (f, d) in frames.iter().zip(durations) {
857            a.push_frame_exact(f.as_slice(), *d, None)
858                .map_err(|e| fail(CHECK, format!("push: {e:?}")))?;
859            points.push(out.len() + a.pending_output().len());
860            if drain {
861                a.take_output(&mut out);
862            }
863        }
864        out.extend_from_slice(
865            a.finish(None)
866                .map_err(|e| fail(CHECK, format!("finish: {e:?}")))?
867                .data(),
868        );
869        Ok((out, points, timing.lag_frames()))
870    };
871    let (drained, points, lag) = encode(true)?;
872    let (undrained, _, _) = encode(false)?;
873    if drained != undrained {
874        return Err(fail(CHECK, "draining changed the animation's bytes"));
875    }
876    if let Some(lag) = lag {
877        let lag = lag as usize;
878        let bounded = &points[..points.len().saturating_sub(1)];
879        for i in 0..bounded.len() {
880            if let Some(&later) = bounded.get(i + lag + 1)
881                && later <= bounded[i]
882            {
883                return Err(fail(
884                    CHECK,
885                    format!(
886                        "a lag of {lag} frames, but no bytes from frame {i} to {}",
887                        i + lag + 1
888                    ),
889                ));
890            }
891        }
892    }
893
894    let mut anim = VideoAnimationDecoder::new(
895        Arc::<[u8]>::from(drained),
896        make_demuxer(),
897        make_decoder(),
898        render,
899        durations[durations.len() - 1],
900        7,
901    );
902    let mut index = 0usize;
903    while let Some(f) = anim
904        .render_next_frame(None)
905        .map_err(|e| fail(CHECK, format!("decode: {e:?}")))?
906    {
907        let Some(want) = frames.get(index) else {
908            return Err(fail(CHECK, "more frames decoded than encoded"));
909        };
910        if f.duration() != durations[index] {
911            return Err(fail(
912                CHECK,
913                format!(
914                    "frame {index} lasts {:?}, encoded {:?}",
915                    f.duration(),
916                    durations[index]
917                ),
918            ));
919        }
920        for y in 0..h {
921            if f.pixels().row(y) != want.as_slice().row(y) {
922                return Err(fail(CHECK, format!("frame {index} row {y} differs")));
923            }
924        }
925        if f.pts().is_none() {
926            return Err(fail(CHECK, format!("frame {index} has no timestamp")));
927        }
928        index += 1;
929    }
930    if index != frames.len() {
931        return Err(fail(
932            CHECK,
933            format!("{index} frames decoded, {} encoded", frames.len()),
934        ));
935    }
936
937    // A declared count binds.
938    let mut a = AnimationVideoEncoder::new(make_encoder(), make_muxer(), capture(), lag_frames)
939        .map_err(|e| fail(CHECK, format!("new: {e:?}")))?;
940    a.begin(w, h, desc, Some(count + 1))
941        .map_err(|e| fail(CHECK, format!("begin: {e:?}")))?;
942    for (f, d) in frames.iter().zip(durations) {
943        a.push_frame_exact(f.as_slice(), *d, None)
944            .map_err(|e| fail(CHECK, format!("push: {e:?}")))?;
945    }
946    if a.finish(None).is_ok() {
947        return Err(fail(
948            CHECK,
949            "finish short of the declared frame count was accepted",
950        ));
951    }
952    Ok(())
953}