Skip to main content

frame_source/
h264_source.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::{
5    io::{BufReader, Read, Seek},
6    path::Path,
7};
8
9use chrono::{DateTime, FixedOffset, Utc};
10use h264_reader::{
11    Context as H264ParsingContext,
12    nal::{
13        Nal, RefNal, UnitType,
14        sei::{HeaderType, SeiMessage, SeiReader},
15    },
16    rbsp::BitReaderError,
17};
18use serde::{Deserialize, Serialize};
19
20use strand_cam_remote_control::{H264_METADATA_UUID, H264_METADATA_VERSION, H264Metadata};
21
22#[cfg(feature = "openh264")]
23use machine_vision_formats::owned::OImage;
24
25use crate::{
26    EncodedH264, Error, FrameData, FrameDataSource, H264EncodingVariant, ImageData, MyAsStr,
27    Result, Timestamp, TimestampSource,
28    h264_poc::{self, PocStrategy},
29    ntp_timestamp::NtpTimestamp,
30    srt_reader::{self, Stanza},
31};
32
33struct SrtData {
34    stanzas: Vec<Stanza>,
35    frame0_time: DateTime<FixedOffset>,
36    idx: usize,
37    /// Line at which the SRT file's parse stopped early (see
38    /// [`srt_reader::SrtParseOutcome::truncated_at_line`]), if that's why
39    /// `stanzas` might be shorter than the video's frame count.
40    truncated_at_line: Option<usize>,
41}
42
43/// Reports that an [`H264Source`] built from an SRT-file timestamp source
44/// covers fewer frames than the underlying container, because the SRT ran
45/// out of usable stanzas partway through. The source is truncated to the last
46/// complete group of pictures (GOP) that a stanza is available for every
47/// frame of, so reordered (B-frame) streams remain fully resolvable.
48#[derive(Debug, Clone)]
49pub struct SrtTruncation {
50    /// Number of frames retained (a prefix, rounded down to the last
51    /// complete GOP boundary at or before `usable_stanzas`).
52    pub kept_frames: usize,
53    /// Total number of frames in the underlying container.
54    pub total_frames: usize,
55    /// Number of stanzas the SRT file yielded successfully.
56    pub usable_stanzas: usize,
57    /// Line in the SRT file where parsing stopped due to malformed content.
58    /// `None` if the SRT file simply had fewer stanzas than the video has
59    /// frames (no parse error, just ran out).
60    pub malformed_at_line: Option<usize>,
61}
62
63#[derive(serde::Deserialize)]
64struct SrtMsg {
65    timestamp: DateTime<chrono::FixedOffset>,
66}
67
68impl SrtData {
69    fn parse_time(stanza: &Stanza) -> DateTime<FixedOffset> {
70        let msg: SrtMsg = serde_json::from_str(&stanza.lines).unwrap();
71        msg.timestamp
72    }
73    fn next_pts(&mut self) -> Result<std::time::Duration> {
74        let stanza = &self.stanzas[self.idx];
75        self.idx += 1;
76        let tnow = Self::parse_time(stanza);
77        Ok(tnow.signed_duration_since(self.frame0_time).to_std()?)
78    }
79    /// Capture time (relative to frame0) for stanza `idx`, where `idx` is the
80    /// frame's presentation (display) rank. Frames are handed to us in decode
81    /// order, which differs from presentation order for B-frame streams, so we
82    /// must index the (presentation-order) stanzas by display rank rather than
83    /// by decode index.
84    fn time_at(&self, idx: usize) -> Result<std::time::Duration> {
85        let stanza = &self.stanzas[idx];
86        let tnow = Self::parse_time(stanza);
87        Ok(tnow.signed_duration_since(self.frame0_time).to_std()?)
88    }
89    fn frame0_time(&self) -> DateTime<FixedOffset> {
90        self.frame0_time
91    }
92    /// Wall-clock span from the first to the last stanza (presentation order).
93    fn span(&self) -> Result<std::time::Duration> {
94        let first = Self::parse_time(&self.stanzas[0]);
95        let last = Self::parse_time(&self.stanzas[self.stanzas.len() - 1]);
96        Ok(last.signed_duration_since(first).to_std()?)
97    }
98}
99
100/// Per-sample timing carried through from an MP4 source, so it can be
101/// preserved verbatim when re-muxing (rather than re-derived from
102/// presentation-time deltas). This is what lets reordered (B-frame) streams be
103/// re-muxed correctly.
104#[derive(Debug, Clone, Copy)]
105pub struct Mp4SampleTiming {
106    /// The sample's own decode duration (stts delta).
107    pub decode_duration: std::time::Duration,
108    /// The sample's composition time offset (ctts); `presentation = decode +
109    /// composition_offset`. Non-zero for reordered streams.
110    pub composition_offset: chrono::Duration,
111}
112
113pub trait H264Preparser {
114    fn put_seq_param_set(&mut self, nalu: &RefNal<'_>) -> eyre::Result<()>;
115    fn put_pic_param_set(&mut self, nalu: &RefNal<'_>) -> eyre::Result<()>;
116    fn put_sei_nalu(&mut self, nalu: &RefNal<'_>) -> eyre::Result<()>;
117    fn put_slice_layer_nalu(&mut self, nalu: &RefNal<'_>, is_i_frame: bool) -> eyre::Result<()>;
118    fn set_num_positions(&mut self, num_positions: usize) -> eyre::Result<()>;
119    fn set_position(&mut self, pos: usize) -> eyre::Result<()>;
120    fn close(self) -> eyre::Result<()>;
121}
122
123// Found in libx264-encoded h264 streams. See
124// https://code.videolan.org/videolan/x264/-/blob/da14df5535/encoder/set.c#L598
125const X264_UUID: &[u8; 16] = uuid::uuid!("dc45e9bd-e6d9-48b7-962c-d820d923eeef").as_bytes();
126
127// Found in videotoolbox-encoded h264 streams.
128const VIDEOTOOLBOX_UUID: &[u8; 16] = uuid::uuid!("47564adc-5c4c-433f-94ef-c5113cd143a8").as_bytes();
129
130/// H264 data source. Can come directly from an "Annex B" format .h264 file or
131/// from an MP4 file.
132///
133/// This should be as general purpose H264 file reader as possible.
134///
135/// Strand Camera specific features are supported if present: metadata at the
136/// H264 stream start (UUID 0ba99cc7-f607-3851-b35e-8c7d8c04da0a) is parsed, as
137/// are precision time stamps (specified by MISB ST 0604.3).
138///
139/// ## Timestamp handling:
140///
141/// ### Case 1, H264 in MP4
142///
143/// Tracks in MP4 files are composed of samples. Each sample has a presentation
144/// timestamp (PTS), the time elapsed from the start. A sample contains multiple
145/// (0, 1, 2 or more) H264 NAL units. A sample can contain zero, one, two or
146/// more image frames of data. (There exist MP4 files in which a sample contains
147/// zero NAL units and thus zero image frames as well as MP4 files in which a
148/// single sample contains many NAL units and many image frames.) Samples also
149/// carry a duration, which is informational to assist with playback. The
150/// duration of frame N should be the PTS of frame N+1 - the PTS of frame N.
151/// There seems to be a general assumption that samples should be equi-distant
152/// in time and thus that the file has a constant frame rate, although I have
153/// not found this in any specification.
154///
155/// ### Case 2, raw H264
156///
157/// A raw .h264 file, which is defined as the "Annex B format", (or simply the
158/// H264 data inside an MP4 file) may have no explicit timing information in it.
159/// Alternatively, for example with the `timing_info_present_flag` in the VUI
160/// parameters, timing information may be present. So far we ignore these flags.
161/// In this case, the timestamp data for the frame is simply returned as a
162/// fraction of complete (in the interval from 0.0 to 1.0).
163///
164/// ### Case 3, H264 files with `MISPmicrosectime` supplemental enhancement information
165///
166/// H264 can contain additional NAL units, called supplemental enhancement
167/// information (SEI) which is ignored by decoders but can provide additional
168/// information such as metadata at the start of H264 data and per-frame
169/// timestamps as specified in MISB ST 0604.3.
170pub struct H264Source<H: SeekableH264Source> {
171    seekable_h264_source: H,
172    /// For every NAL unit, the coordinates in the source to read it.
173    nal_locations: Vec<H::NalLocation>,
174    /// timestamps from MP4 files, one per MP4 sample (which we assume to be one per frame)
175    mp4_pts: Option<Vec<std::time::Duration>>,
176    frame_time_info: Vec<FrameTimeInfo>,
177    pub h264_metadata: Option<H264Metadata>,
178    frame0_precision_time: Option<chrono::DateTime<chrono::FixedOffset>>,
179    frame0_frameinfo: Option<FrameInfo>,
180    width: u32,
181    height: u32,
182    do_decode_h264: bool,
183    timestamp_source: Option<crate::TimestampSource>,
184    has_timestamps: bool,
185    srt_data: Option<SrtData>,
186    /// Per-sample decode duration + composition offset (MP4 sources only), so a
187    /// re-mux can preserve the source's timing (stts + ctts) verbatim.
188    mp4_sample_timing: Option<Vec<Mp4SampleTiming>>,
189    /// For SRT timestamps: the display rank of each decode-order frame, i.e.
190    /// `srt_display_rank[decode_index]` is the SRT stanza index (stanzas are in
191    /// presentation order) for that frame. `None` when SRT timestamps or
192    /// per-sample PTS are unavailable, in which case stanzas are consumed
193    /// sequentially.
194    srt_display_rank: Option<Vec<usize>>,
195    /// The display (presentation) rank of each decode-order frame:
196    /// `presentation_rank[decode_index]` is that frame's position in display
197    /// order. Derived from per-sample PTS (`mp4_pts`) when present, else from
198    /// the bitstream POC. `None` when presentation order cannot be recovered
199    /// (no PTS and no decodable POC), which makes [`Self::presentation_order_iter`]
200    /// fail loudly.
201    presentation_rank: Option<Vec<usize>>,
202    /// Whether each decode-order frame is an IDR (starts a new coded video
203    /// sequence). Used to bound the presentation-order reorder buffer to one
204    /// coded video sequence.
205    is_idr: Vec<bool>,
206    /// Set when an SRT-file timestamp source ran out of usable stanzas
207    /// before covering every frame, and the source was truncated to the last
208    /// complete GOP a stanza is available for every frame of.
209    srt_truncation: Option<SrtTruncation>,
210    average_fps: Option<f64>,
211    /// Chroma subsampling of the stream, read from the first SPS. The built-in
212    /// OpenH264 decoder only handles 4:2:0, so on a decode failure this is used
213    /// to explain that a 4:2:2 / 4:4:4 stream (e.g. from a macOS / FaceTime
214    /// camera) is the likely cause, rather than surfacing only OpenH264's opaque
215    /// native error code. Only read when compiled with the `openh264` feature.
216    #[cfg_attr(not(feature = "openh264"), allow(dead_code))]
217    chroma_format: h264_reader::nal::sps::ChromaFormat,
218    /// Human-readable H.264 profile of the stream (e.g. "High444"), read from
219    /// the first SPS. Used only for diagnostics.
220    #[cfg_attr(not(feature = "openh264"), allow(dead_code))]
221    profile: String,
222}
223
224impl<H: SeekableH264Source> H264Source<H> {
225    pub fn as_seekable_h264_source(&self) -> &H {
226        &self.seekable_h264_source
227    }
228
229    /// Per-sample timing (decode duration + composition offset) for MP4
230    /// sources, indexed by frame number (see [`FrameData::idx`]). `None` for
231    /// non-MP4 sources.
232    pub fn mp4_sample_timing(&self) -> Option<&[Mp4SampleTiming]> {
233        self.mp4_sample_timing.as_deref()
234    }
235
236    /// Set when an SRT-file timestamp source ran out of usable stanzas
237    /// before covering every frame in the container; `None` otherwise.
238    pub fn srt_truncation(&self) -> Option<&SrtTruncation> {
239        self.srt_truncation.as_ref()
240    }
241
242    fn create_iter_unchecked<'a>(
243        &'a mut self,
244        frame_idx: usize,
245    ) -> Box<dyn Iterator<Item = Result<FrameData>> + 'a> {
246        let openh264_decoder_state = if self.do_decode_h264 {
247            Some(crate::opt_openh264_decoder::new_stream_decoder().unwrap())
248        } else {
249            None
250        };
251        // Invert `presentation_rank` to get the decode index of each
252        // display-order position, used to pair decoder output pictures (which
253        // emerge in display order) with the input frames they decode.
254        let display_order = self.presentation_rank.as_ref().map(|rank| {
255            let mut order = vec![0usize; rank.len()];
256            for (decode_idx, &r) in rank.iter().enumerate() {
257                order[r] = decode_idx;
258            }
259            order
260        });
261        Box::new(RawH264Iter {
262            parent: self,
263            frame_idx,
264            openh264_decoder_state,
265            display_order,
266            n_pictures_out: 0,
267            pending_meta: std::collections::HashMap::new(),
268            decoded_ready: std::collections::BTreeMap::new(),
269            next_emit: frame_idx,
270            flushed: false,
271            poisoned: false,
272        })
273    }
274}
275
276/// Timing information for a frame of video.
277pub struct FrameTimeInfo {
278    /// The NAL unit location.
279    ///
280    /// This is an index into the slice &[SeekableH264Source::NalLocation]
281    /// returned by [SeekableH264Source::nal_boundaries]. In the case of MP4
282    /// files, each index corresponds to multiple NAL units.
283    nal_location_index: usize,
284    precise_timestamp: Option<DateTime<Utc>>,
285    frameinfo: Option<FrameInfo>,
286    /// Picture order count reconstructed from the bitstream, when available.
287    /// `None` if the POC could not be determined (e.g. `pic_order_cnt_type 1`).
288    poc: Option<i64>,
289    /// Whether this frame is an IDR picture (starts a new coded video sequence).
290    is_idr: bool,
291}
292
293impl<H: SeekableH264Source> FrameDataSource for H264Source<H> {
294    fn width(&self) -> u32 {
295        self.width
296    }
297    fn height(&self) -> u32 {
298        self.height
299    }
300    fn camera_name(&self) -> Option<&str> {
301        self.h264_metadata
302            .as_ref()
303            .and_then(|x| x.camera_name.as_deref())
304    }
305    fn gamma(&self) -> Option<f32> {
306        self.h264_metadata.as_ref().and_then(|x| x.gamma)
307    }
308    fn frame0_time(&self) -> Option<chrono::DateTime<chrono::FixedOffset>> {
309        match &self.timestamp_source {
310            Some(TimestampSource::BestGuess) => unreachable!(),
311            Some(TimestampSource::FixedFramerate) => {
312                if let Some(t) = &self.frame0_precision_time {
313                    Some(*t)
314                } else {
315                    self.frame0_frameinfo.as_ref().map(|fi| fi.recv.into())
316                }
317            }
318            Some(TimestampSource::MispMicrosectime) => self.frame0_precision_time,
319            Some(TimestampSource::FrameInfoRecvTime) | Some(TimestampSource::FrameInfoRtp) => {
320                Some(self.frame0_frameinfo.as_ref().unwrap().recv.into())
321            }
322            Some(TimestampSource::Mp4Pts) | None => None,
323            Some(TimestampSource::SrtFile) => self.srt_data.as_ref().map(|x| x.frame0_time()),
324        }
325    }
326    fn average_framerate(&self) -> Option<f64> {
327        self.average_fps
328    }
329    fn skip_n_frames(&mut self, n_frames: usize) -> Result<()> {
330        if n_frames > 0 {
331            return Err(Error::SkippingFramesNotSupported);
332            // Doing so would require finding I frames and only skipping to
333            // those (or decoding and interpolating a new I frame).
334            // Also: caching SPS and PPS would be required.
335            // We do this in the MKV reader, so we should use that
336            // implementation for inspiration.
337        }
338        Ok(())
339    }
340    fn estimate_luminance_range(&mut self) -> Result<(u16, u16)> {
341        Err(Error::NotImplemented("h264 luminance scanning"))
342    }
343    fn decode_order_iter<'a>(&'a mut self) -> Box<dyn Iterator<Item = Result<FrameData>> + 'a> {
344        self.create_iter_unchecked(0)
345    }
346    fn presentation_order_iter<'a>(
347        &'a mut self,
348    ) -> Result<Box<dyn Iterator<Item = Result<FrameData>> + 'a>> {
349        let rank = self.presentation_rank.clone().ok_or_else(|| {
350            Error::H264Poc(
351                "cannot recover presentation order: source has neither per-sample \
352                 timestamps nor a decodable picture order count"
353                    .to_string(),
354            )
355        })?;
356        let is_idr = self.is_idr.clone();
357        let total = rank.len();
358        // Decode/parse in decode order (required to feed a decoder and to
359        // reconstruct POC correctly), then reorder into display order.
360        let inner = self.create_iter_unchecked(0);
361        Ok(Box::new(PresentationReorderIter {
362            inner,
363            rank,
364            is_idr,
365            total,
366            pending: Vec::new(),
367            ready: std::collections::VecDeque::new(),
368            done: false,
369        }))
370    }
371    fn timestamp_source(&self) -> &str {
372        self.timestamp_source.as_str()
373    }
374    fn has_timestamps(&self) -> bool {
375        self.has_timestamps
376    }
377    fn srt_truncation(&self) -> Option<SrtTruncation> {
378        self.srt_truncation.clone()
379    }
380}
381
382pub(crate) struct FromMp4Track {
383    pub(crate) sequence_parameter_set: Vec<u8>,
384    pub(crate) picture_parameter_set: Vec<u8>,
385}
386
387pub trait SeekRead: Seek + Read {}
388impl<T> SeekRead for T where T: Seek + Read {}
389
390pub trait SeekableH264Source {
391    type NalLocation;
392    fn nal_boundaries(&mut self) -> &[Self::NalLocation];
393    /// Read multiple NAL units at the specified location
394    fn read_nal_units_at_location(&mut self, location: &Self::NalLocation) -> Result<Vec<Vec<u8>>>;
395    /// Read multiple NAL units at multiple specified locations
396    fn read_nal_units_at_locations(
397        &mut self,
398        locations: &[Self::NalLocation],
399    ) -> Result<Vec<Vec<u8>>> {
400        let mut result = Vec::with_capacity(locations.len() * 3);
401        for location in locations.iter() {
402            let nal_units = self.read_nal_units_at_location(location)?;
403            result.extend(nal_units);
404        }
405        Ok(result)
406    }
407
408    /// Return the first SPS
409    fn first_sps(&self) -> Option<Vec<u8>>;
410    /// Return the first PPS
411    fn first_pps(&self) -> Option<Vec<u8>>;
412}
413
414#[derive(Debug, PartialEq, Clone)]
415pub struct AnnexBLocation {
416    pub(crate) start: u64,
417    pub(crate) sz: usize,
418}
419
420pub struct H264AnnexBSource {
421    inner: Box<dyn SeekRead + Send>,
422    my_nal_boundaries: Vec<AnnexBLocation>,
423}
424
425impl H264AnnexBSource {
426    pub fn from_file(fd: std::fs::File) -> Result<Self> {
427        let inner = Box::new(BufReader::new(fd));
428        Self::from_readseek(inner)
429    }
430    pub fn from_readseek(mut inner: Box<dyn SeekRead + Send>) -> Result<Self> {
431        inner.seek(std::io::SeekFrom::Start(0))?;
432        let my_nal_boundaries = crate::h264_annexb_splitter::find_nals(&mut inner)?;
433        inner.seek(std::io::SeekFrom::Start(0))?;
434        Ok(Self {
435            inner,
436            my_nal_boundaries,
437        })
438    }
439}
440
441impl SeekableH264Source for H264AnnexBSource {
442    type NalLocation = AnnexBLocation;
443    fn nal_boundaries(&mut self) -> &[Self::NalLocation] {
444        &self.my_nal_boundaries
445    }
446    fn read_nal_units_at_location(&mut self, location: &Self::NalLocation) -> Result<Vec<Vec<u8>>> {
447        self.inner.seek(std::io::SeekFrom::Start(location.start))?;
448        let mut buf = vec![0u8; location.sz];
449        self.inner.read_exact(&mut buf)?;
450        Ok(vec![buf])
451    }
452
453    fn first_sps(&self) -> Option<Vec<u8>> {
454        None
455    }
456    fn first_pps(&self) -> Option<Vec<u8>> {
457        None
458    }
459}
460
461impl<H> H264Source<H>
462where
463    H: SeekableH264Source,
464    <H as SeekableH264Source>::NalLocation: Clone,
465{
466    #[expect(
467        clippy::too_many_arguments,
468        reason = "we grudgingly accept this ugliness"
469    )]
470    pub(crate) fn from_seekable_h264_source_with_timestamp_source(
471        mut seekable_h264_source: H,
472        do_decode_h264: bool,
473        mut mp4_pts: Option<Vec<std::time::Duration>>,
474        mut mp4_sample_timing: Option<Vec<Mp4SampleTiming>>,
475        data_from_mp4_track: Option<FromMp4Track>,
476        timestamp_source: crate::TimestampSource,
477        srt_file_path: Option<std::path::PathBuf>,
478        show_progress: bool,
479        mut preparser: Option<Box<dyn H264Preparser>>,
480    ) -> Result<Self> {
481        let nal_locations: Vec<H::NalLocation> = seekable_h264_source.nal_boundaries().to_vec();
482
483        let mut parsing_ctx = H264ParsingContext::default();
484
485        // open SRT file
486        if timestamp_source == crate::TimestampSource::SrtFile && srt_file_path.is_none() {
487            return Err(Error::NoSrtPathGiven);
488        }
489
490        let mut srt_data = if let Some(srt_file_path) = srt_file_path {
491            let outcome = srt_reader::read_srt_file(&srt_file_path)?;
492            if outcome.stanzas.is_empty() {
493                return Err(Error::SrtParseError {
494                    path: srt_file_path,
495                    line: outcome.truncated_at_line.unwrap_or(1),
496                });
497            }
498            let frame0_time = SrtData::parse_time(&outcome.stanzas[0]);
499            Some(SrtData {
500                stanzas: outcome.stanzas,
501                idx: 0,
502                frame0_time,
503                truncated_at_line: outcome.truncated_at_line,
504            })
505        } else {
506            None
507        };
508
509        // Use data from container if present
510        if let Some(dfc) = data_from_mp4_track {
511            tracing::debug!("Using SPS and PPS data from mp4 track.");
512            {
513                // SPS
514                let sps_nal = RefNal::new(&dfc.sequence_parameter_set, &[], true);
515                if sps_nal.header().unwrap().nal_unit_type() != UnitType::SeqParameterSet {
516                    return Err(Error::ExpectedSpsNotFound);
517                }
518
519                let isps =
520                    h264_reader::nal::sps::SeqParameterSet::from_bits(sps_nal.rbsp_bits()).unwrap();
521                if let Some(preparser) = preparser.as_mut() {
522                    preparser
523                        .put_seq_param_set(&sps_nal)
524                        .map_err(Error::PreParserError)?;
525                }
526                parsing_ctx.put_seq_param_set(isps);
527            }
528
529            {
530                // PPS
531                let pps_nal = RefNal::new(&dfc.picture_parameter_set, &[], true);
532                if pps_nal.header().unwrap().nal_unit_type() != UnitType::PicParameterSet {
533                    return Err(Error::ExpectedPpsNotFound);
534                }
535
536                let ipps = h264_reader::nal::pps::PicParameterSet::from_bits(
537                    &parsing_ctx,
538                    pps_nal.rbsp_bits(),
539                )
540                .unwrap();
541                if let Some(preparser) = preparser.as_mut() {
542                    preparser
543                        .put_pic_param_set(&pps_nal)
544                        .map_err(Error::PreParserError)?;
545                }
546                parsing_ctx.put_pic_param_set(ipps);
547            }
548        }
549
550        // iterate through all NAL units.
551        let timing_data = load_timing_data(
552            &nal_locations,
553            &mut seekable_h264_source,
554            &mut parsing_ctx,
555            show_progress,
556            preparser,
557        )?;
558
559        let TimingData {
560            mut frame_time_info,
561            frame0_precision_time,
562            frame0_frameinfo,
563            h264_metadata,
564            tz_offset,
565        } = timing_data;
566
567        let mut widthheight = None;
568        let mut chroma_format = h264_reader::nal::sps::ChromaFormat::YUV420;
569        let mut profile = "Unknown".to_string();
570        for sps in parsing_ctx.sps() {
571            if let Ok(wh) = sps.pixel_dimensions() {
572                widthheight = Some(wh);
573            }
574            chroma_format = sps.chroma_info.chroma_format;
575            profile = format!("{:?}", sps.profile());
576        }
577
578        let (width, height) = widthheight.ok_or_else(|| crate::Error::ExpectedSpsNotFound)?;
579
580        let timezone = tz_offset.unwrap_or_else(|| chrono::FixedOffset::east_opt(0).unwrap());
581
582        let frame0_precision_time = frame0_precision_time
583            .as_ref()
584            .map(|dt| dt.with_timezone(&timezone));
585
586        let (timestamp_source, has_timestamps) = match timestamp_source {
587            crate::TimestampSource::BestGuess => {
588                if frame0_precision_time.is_some() {
589                    (Some(crate::TimestampSource::MispMicrosectime), true)
590                } else if frame0_frameinfo.is_some() {
591                    (Some(crate::TimestampSource::FrameInfoRtp), true)
592                } else if mp4_pts.is_some() {
593                    (Some(crate::TimestampSource::Mp4Pts), true)
594                } else {
595                    (None, false)
596                }
597            }
598            crate::TimestampSource::FixedFramerate => (Some(timestamp_source), true),
599            crate::TimestampSource::FrameInfoRecvTime | crate::TimestampSource::FrameInfoRtp => {
600                if frame0_frameinfo.is_none() {
601                    return Err(Error::H264TimestampError(
602                        "Requested timestamp that requires FrameInfo, but this information is not present."
603                            .into(),
604                    ));
605                }
606                (Some(timestamp_source), true)
607            }
608            crate::TimestampSource::MispMicrosectime => {
609                if frame0_precision_time.is_none() {
610                    return Err(Error::H264TimestampError(
611                        "Requested timestamp source MispMicrosectime, but frame0_precision_time not present."
612                            .into(),
613                    ));
614                }
615                (Some(timestamp_source), true)
616            }
617            crate::TimestampSource::Mp4Pts => {
618                if mp4_pts.is_none() {
619                    return Err(Error::H264TimestampError(
620                        "Requested timestamp source Mp4Pts, but MP4 PTS not present.".into(),
621                    ));
622                }
623                (Some(timestamp_source), true)
624            }
625            crate::TimestampSource::SrtFile => (Some(timestamp_source), true),
626        };
627
628        if let Some(mp4_pts) = mp4_pts.as_ref()
629            && mp4_pts.len() != frame_time_info.len()
630        {
631            return Err(Error::H264TimestampError(format!(
632                "We have {} frames of MP4 PTS timing, but computed {} frames of video.",
633                mp4_pts.len(),
634                frame_time_info.len()
635            )));
636        }
637        // Precompute the display (presentation) rank of each decode-order frame.
638        // Ranking by per-sample PTS is robust to a constant encoder composition
639        // delay (unlike matching absolute PTS values against stanza timecodes);
640        // when there is no PTS we fall back to the bitstream POC, keyed within
641        // each coded video sequence so GOPs stay in order.
642        let mut presentation_rank = compute_presentation_rank(mp4_pts.as_deref(), &frame_time_info);
643
644        let mut is_idr: Vec<bool> = frame_time_info.iter().map(|fti| fti.is_idr).collect();
645
646        // If the SRT ran out before covering every frame -- either it was
647        // malformed partway through, or it simply has fewer stanzas than the
648        // video has frames -- salvage what we can rather than failing
649        // outright: keep only whole GOPs fully covered by a usable stanza,
650        // dropping the incomplete tail. SRT stanzas are handed to frames by
651        // *presentation* rank, so a partial trailing GOP can't be resolved
652        // for reordered (B-frame) streams; rounding down to the last
653        // complete GOP boundary keeps every kept frame's timing intact.
654        let mut srt_truncation = None;
655        if let Some(srt) = srt_data.as_mut() {
656            let total_frames = frame_time_info.len();
657            let usable_stanzas = srt.stanzas.len();
658            if usable_stanzas < total_frames {
659                let rank = presentation_rank.as_ref().ok_or_else(|| {
660                    Error::H264TimestampError(
661                        "SRT file has fewer entries than the video has frames, and \
662                         presentation order cannot be recovered to safely truncate \
663                         (no per-sample PTS and no decodable picture order count)."
664                            .to_string(),
665                    )
666                })?;
667                let mut gop_starts: Vec<usize> = std::iter::once(0)
668                    .chain((0..total_frames).filter(|&i| is_idr[i]).map(|i| rank[i]))
669                    .collect();
670                gop_starts.sort_unstable();
671                gop_starts.dedup();
672                let kept_frames = gop_starts
673                    .into_iter()
674                    .filter(|&g| g <= usable_stanzas)
675                    .max()
676                    .unwrap_or(0);
677
678                if kept_frames == 0 {
679                    return Err(Error::H264TimestampError(format!(
680                        "SRT file only has usable timestamps for {usable_stanzas} of \
681                         {total_frames} frames, not even one complete group of pictures."
682                    )));
683                }
684
685                let keep: Vec<bool> = (0..total_frames).map(|i| rank[i] < kept_frames).collect();
686                retain_by_mask(&mut frame_time_info, &keep);
687                if let Some(pts) = mp4_pts.as_mut() {
688                    retain_by_mask(pts, &keep);
689                }
690                if let Some(timing) = mp4_sample_timing.as_mut() {
691                    retain_by_mask(timing, &keep);
692                }
693                retain_by_mask(&mut is_idr, &keep);
694                if let Some(rank) = presentation_rank.as_mut() {
695                    retain_by_mask(rank, &keep);
696                }
697                srt.stanzas.truncate(kept_frames);
698
699                srt_truncation = Some(SrtTruncation {
700                    kept_frames,
701                    total_frames,
702                    usable_stanzas,
703                    malformed_at_line: srt.truncated_at_line,
704                });
705            }
706        }
707
708        let average_fps = calc_avg_fps(&frame_time_info[..]);
709
710        // Rescale the source's per-sample timing (stts/ctts) to the real
711        // capture cadence given by the SRT. The intermediate encoder uses a
712        // fixed nominal framerate unrelated to the true capture rate, so its
713        // absolute durations are meaningless; only its *relative* reorder
714        // structure (the ratio of composition offset to frame duration) is.
715        // Scaling by real_span/source_span fixes the playback rate while
716        // preserving valid decode/composition ordering (and cancels any
717        // timescale discrepancy in the source durations).
718        let mp4_sample_timing = match (mp4_sample_timing, srt_data.as_ref()) {
719            (Some(mut timing), Some(srt)) if timing.len() >= 2 => {
720                let source_span: f64 = timing.iter().map(|t| t.decode_duration.as_secs_f64()).sum();
721                let real_span = srt.span().map(|d| d.as_secs_f64()).unwrap_or(0.0);
722                if source_span > 0.0 && real_span > 0.0 {
723                    let scale = real_span / source_span;
724                    for t in timing.iter_mut() {
725                        t.decode_duration = t.decode_duration.mul_f64(scale);
726                        let off_ns = t.composition_offset.num_nanoseconds().unwrap_or(0) as f64;
727                        t.composition_offset =
728                            chrono::Duration::nanoseconds((off_ns * scale) as i64);
729                    }
730                }
731                Some(timing)
732            }
733            (other, _) => other,
734        };
735
736        // SRT stanzas are in presentation order, so pair them with reordered
737        // (B-frame) streams by display rank. Fall back to sequential consumption
738        // when presentation order is unknown.
739        let srt_display_rank = if srt_data.is_some() {
740            presentation_rank.clone()
741        } else {
742            None
743        };
744
745        Ok(Self {
746            seekable_h264_source,
747            nal_locations,
748            mp4_pts,
749            mp4_sample_timing,
750            srt_display_rank,
751            presentation_rank,
752            is_idr,
753            frame_time_info,
754            h264_metadata,
755            frame0_precision_time,
756            frame0_frameinfo,
757            width,
758            height,
759            do_decode_h264,
760            timestamp_source,
761            has_timestamps,
762            srt_data,
763            srt_truncation,
764            average_fps,
765            chroma_format,
766            profile,
767        })
768    }
769}
770
771/// Keep only the elements of `items` at positions where `keep` is `true`,
772/// preserving order. `keep` must be the same length as `items`.
773fn retain_by_mask<T>(items: &mut Vec<T>, keep: &[bool]) {
774    let mut keep = keep.iter();
775    items.retain(|_| *keep.next().unwrap());
776}
777
778/// Compute the display (presentation) rank of each decode-order frame.
779///
780/// `presentation_rank[decode_index]` is that frame's position in presentation
781/// order. Ranking uses per-sample PTS when available (`mp4_pts`), else the
782/// bitstream POC keyed by coded video sequence (so GOPs stay ordered). Returns
783/// `None` when neither signal is available for every frame, i.e. presentation
784/// order cannot be recovered.
785fn compute_presentation_rank(
786    mp4_pts: Option<&[std::time::Duration]>,
787    frame_time_info: &[FrameTimeInfo],
788) -> Option<Vec<usize>> {
789    let n = frame_time_info.len();
790    if n == 0 {
791        return Some(Vec::new());
792    }
793    // Display sort key per decode index: (coded-video-sequence index, tie-break
794    // within that sequence). For PTS the whole stream is one global ordering; for
795    // POC we bump the sequence index at each IDR so GOPs stay contiguous and in
796    // order even though POC resets to 0 at every IDR.
797    let keys: Vec<(i64, i64)> = if let Some(pts) = mp4_pts {
798        pts.iter().map(|d| (0i64, d.as_nanos() as i64)).collect()
799    } else {
800        if !frame_time_info.iter().all(|fti| fti.poc.is_some()) {
801            return None;
802        }
803        let mut cvs = 0i64;
804        let mut keys = Vec::with_capacity(n);
805        for (i, fti) in frame_time_info.iter().enumerate() {
806            if fti.is_idr && i != 0 {
807                cvs += 1;
808            }
809            keys.push((cvs, fti.poc.unwrap()));
810        }
811        keys
812    };
813    let mut order: Vec<usize> = (0..n).collect();
814    order.sort_by_key(|&i| keys[i]);
815    let mut rank = vec![0usize; n];
816    for (display_rank, &decode_index) in order.iter().enumerate() {
817        rank[decode_index] = display_rank;
818    }
819    Some(rank)
820}
821
822fn calc_avg_fps(fti: &[FrameTimeInfo]) -> Option<f64> {
823    if fti.len() <= 1 {
824        return None;
825    }
826    let frames = (fti.len() - 1) as f64;
827    if let Some(t0) = fti[0].precise_timestamp {
828        // prefer precise_timestamps
829        let tend = fti[fti.len() - 1].precise_timestamp.unwrap();
830        let secs = (tend - t0).to_std().unwrap().as_secs_f64();
831        Some(frames / secs)
832    } else if let Some(fi) = &fti[0].frameinfo {
833        // else use FrameInfo
834        let t0: chrono::DateTime<chrono::Utc> = fi.recv.into();
835        let tend: chrono::DateTime<chrono::Utc> =
836            fti[fti.len() - 1].frameinfo.as_ref().unwrap().recv.into();
837        let secs = (tend - t0).to_std().unwrap().as_secs_f64();
838        Some(frames / secs)
839    } else {
840        // final resort
841        None
842    }
843}
844
845struct TimingData {
846    frame_time_info: Vec<FrameTimeInfo>,
847    frame0_precision_time: Option<DateTime<Utc>>,
848    frame0_frameinfo: Option<FrameInfo>,
849    h264_metadata: Option<H264Metadata>,
850    tz_offset: Option<FixedOffset>,
851}
852
853fn load_timing_data<H>(
854    nal_locations: &[H::NalLocation],
855    seekable_h264_source: &mut H,
856    parsing_ctx: &mut H264ParsingContext,
857    show_progress: bool,
858    mut preparser: Option<Box<dyn H264Preparser>>,
859) -> Result<TimingData>
860where
861    H: SeekableH264Source,
862    <H as SeekableH264Source>::NalLocation: Clone,
863{
864    let mut scratch = Vec::new();
865
866    let mut tz_offset = None;
867
868    let mut h264_metadata = None;
869
870    // One entry per frame. Can refer to multiple multiple NAL units, e.g.
871    // in MP4 files where a frame is an MP4 sample containing multiple NAL
872    // units.
873    let mut frame_time_info = Vec::new();
874
875    let mut frame0_precision_time = None;
876    let mut frame0_frameinfo = None;
877
878    tracing::debug!(
879        "Iterating through NAL units at {} locations to load timing data.",
880        nal_locations.len()
881    );
882
883    let mut pb = if show_progress {
884        // Custom progress bar with space at right end to prevent obscuring last
885        // digit with cursor.
886        let style = indicatif::ProgressStyle::with_template(
887            "Iterating NAL units in h264 source {wide_bar} {pos}/{len} ETA: {eta} ",
888        )
889        .unwrap();
890        Some(indicatif::ProgressBar::new(nal_locations.len().try_into().unwrap()).with_style(style))
891    } else {
892        None
893    };
894
895    if let Some(preparser) = preparser.as_mut() {
896        preparser
897            .set_num_positions(nal_locations.len())
898            .map_err(Error::PreParserError)?;
899    }
900    // Cached value of MISP time data for the frame whose data is being accumulated.
901    let mut precise_timestamp = None;
902    // Cached value of frame number as we accumluate data.
903    let mut next_frame_num = 0;
904
905    // POC (picture order count) reconstruction. The strategy is fixed once the
906    // first SPS is seen; `None` means the POC could not be determined (e.g.
907    // `pic_order_cnt_type 1`), in which case per-frame `poc` stays `None`.
908    let mut poc_strategy: Option<PocStrategy> = None;
909
910    // Cached value of FrameInfo time data for the frame whose data is being
911    // accumulated.
912    let mut frameinfo = None;
913
914    for (nal_location_index, nal_location) in nal_locations.iter().enumerate() {
915        if let Some(preparser) = preparser.as_mut() {
916            preparser
917                .set_position(nal_location_index)
918                .map_err(Error::PreParserError)?;
919        }
920
921        if let Some(pb) = pb.as_mut() {
922            pb.set_position(nal_location_index.try_into().unwrap());
923        }
924
925        // Read all NAL units from this location. (For MP4 files, this means
926        // read all NAL units from this sample. For H264 AnnexB files, this
927        // will read a single NAL unit.)
928        let nal_units = seekable_h264_source.read_nal_units_at_location(nal_location)?;
929        for nal_unit in nal_units.iter() {
930            // Note, there are multiple NAL units per `nal_location_index`
931            // in MP4 files because in that case, `nal_location_index`
932            // refers to the MP4 sample which has multiple NAL units.
933            let nal = RefNal::new(nal_unit.as_slice(), &[], true);
934            let nal_unit_type = nal.header().unwrap().nal_unit_type();
935            tracing::trace!("NAL unit location index {nal_location_index}, {nal_unit_type:?}");
936            match nal_unit_type {
937                UnitType::SEI => {
938                    if let Some(preparser) = preparser.as_mut() {
939                        preparser
940                            .put_sei_nalu(&nal)
941                            .map_err(Error::PreParserError)?;
942                    }
943                    let mut sei_reader = SeiReader::from_rbsp_bytes(nal.rbsp_bytes(), &mut scratch);
944                    loop {
945                        match sei_reader.next() {
946                            Ok(Some(sei_message)) => {
947                                tracing::trace!("SEI payload type: {:?}", sei_message.payload_type);
948                                match &sei_message.payload_type {
949                                    HeaderType::UserDataUnregistered => {
950                                        let udu = UserDataUnregistered::read(&sei_message)?;
951                                        match udu.uuid {
952                                            &H264_METADATA_UUID => {
953                                                let md: H264Metadata =
954                                                    serde_json::from_slice(udu.payload)?;
955                                                if md.version != H264_METADATA_VERSION {
956                                                    return Err(Error::H264Error(
957                                                        "unexpected version in h264 metadata",
958                                                    ));
959                                                }
960                                                if h264_metadata.is_some() {
961                                                    return Err(Error::H264Error(
962                                                        "multiple SEI messages, but expected exactly one",
963                                                    ));
964                                                }
965
966                                                tracing::debug!("Found H264_METADATA_UUID: {md:?}");
967                                                tz_offset = Some(*md.creation_time.offset());
968                                                h264_metadata = Some(md);
969                                            }
970                                            X264_UUID => {
971                                                let payload_str =
972                                                    String::from_utf8_lossy(udu.payload);
973                                                tracing::trace!(
974                                                    "Ignoring SEI UserDataUnregistered x264 payload: {}",
975                                                    payload_str,
976                                                );
977                                            }
978                                            VIDEOTOOLBOX_UUID => {
979                                                tracing::trace!(
980                                                    "Ignoring SEI UserDataUnregistered from videotoolbox."
981                                                );
982                                            }
983                                            b"MISPmicrosectime" => {
984                                                let ts = parse_precision_time(udu.payload)?;
985                                                tracing::trace!("Found MISPmicrosectime: {ts:?}");
986                                                precise_timestamp = Some(ts);
987                                                if next_frame_num == 0 {
988                                                    frame0_precision_time = Some(ts);
989                                                }
990                                            }
991                                            b"strawlab.org/89H" => {
992                                                let fi: FrameInfo =
993                                                    serde_json::from_slice(udu.payload)?;
994                                                tracing::trace!("Found 89H FrameInfo: {fi:?}");
995                                                frameinfo = Some(fi.clone());
996                                                if next_frame_num == 0 {
997                                                    frame0_frameinfo = Some(fi);
998                                                }
999                                            }
1000                                            _uuid => {
1001                                                tracing::trace!(
1002                                                    "Ignoring SEI UserDataUnregistered uuid: {}",
1003                                                    uuid::Uuid::from_bytes(*udu.uuid).to_string(),
1004                                                );
1005                                            }
1006                                        }
1007                                    }
1008                                    _ => {
1009                                        // handle other SEI types.
1010                                    }
1011                                }
1012                            }
1013                            Ok(None) => {
1014                                break;
1015                            }
1016                            Err(BitReaderError::ReaderErrorFor(what, io_err)) => {
1017                                tracing::error!(
1018                                    "Ignoring error when reading SEI NAL unit {what}: {io_err:?}"
1019                                );
1020                                // We do not process this NAL unit but nor do we
1021                                // propagate the error further. FFMPEG also
1022                                // skips this error except writing "SEI type 5
1023                                // size X truncated at Y" where Y is less than
1024                                // X.
1025                            }
1026                            Err(e) => {
1027                                return Err(Error::H264Nal {
1028                                    nal_location_index,
1029                                    e,
1030                                });
1031                            }
1032                        }
1033                    }
1034                }
1035                UnitType::SeqParameterSet => {
1036                    let isps =
1037                        h264_reader::nal::sps::SeqParameterSet::from_bits(nal.rbsp_bits()).unwrap();
1038                    if let Some(preparser) = preparser.as_mut() {
1039                        preparser
1040                            .put_seq_param_set(&nal)
1041                            .map_err(Error::PreParserError)?;
1042                    }
1043                    // Fix the POC strategy from the first SPS. `strategy_from_sps`
1044                    // errors on the unsupported `pic_order_cnt_type 1`; treat that
1045                    // as "POC unavailable" rather than a hard failure.
1046                    if poc_strategy.is_none() {
1047                        poc_strategy = h264_poc::strategy_from_sps(&isps).ok();
1048                    }
1049                    parsing_ctx.put_seq_param_set(isps);
1050                }
1051                UnitType::PicParameterSet => {
1052                    match h264_reader::nal::pps::PicParameterSet::from_bits(
1053                        parsing_ctx,
1054                        nal.rbsp_bits(),
1055                    ) {
1056                        Ok(ipps) => {
1057                            if let Some(preparser) = preparser.as_mut() {
1058                                preparser
1059                                    .put_pic_param_set(&nal)
1060                                    .map_err(Error::PreParserError)?;
1061                            }
1062                            parsing_ctx.put_pic_param_set(ipps);
1063                        }
1064                        Err(h264_reader::nal::pps::PpsError::BadPicParamSetId(
1065                            h264_reader::nal::pps::PicParamSetIdError::IdTooLarge(_id),
1066                        )) => {
1067                            // While this is open, ignore the error.
1068                            // https://github.com/dholroyd/h264-reader/issues/56
1069                        }
1070                        Err(e) => {
1071                            return Err(Error::H264Pps(format!("reading PPS: {e:?}")));
1072                        }
1073                    }
1074                }
1075                UnitType::SliceLayerWithoutPartitioningIdr
1076                | UnitType::SliceLayerWithoutPartitioningNonIdr => {
1077                    let is_i_frame = nal_unit_type == UnitType::SliceLayerWithoutPartitioningIdr;
1078                    if let Some(preparser) = preparser.as_mut() {
1079                        preparser
1080                            .put_slice_layer_nalu(&nal, is_i_frame)
1081                            .map_err(Error::PreParserError)?;
1082                    }
1083                    // Reconstruct this frame's picture order count from the
1084                    // bitstream. Advancing the strategy is stateful and must
1085                    // happen once per frame in decode order.
1086                    let poc = poc_strategy.as_mut().and_then(|strategy| {
1087                        h264_poc::advance_poc(strategy, parsing_ctx, std::slice::from_ref(nal_unit))
1088                            .ok()
1089                    });
1090                    // The NAL unit with the video frames comes after the
1091                    // timing into NAL unit(s) so we gather them now.
1092                    frame_time_info.push(FrameTimeInfo {
1093                        nal_location_index,
1094                        precise_timestamp,
1095                        frameinfo,
1096                        poc,
1097                        is_idr: is_i_frame,
1098                    });
1099                    // Reset temporary values.
1100                    precise_timestamp = None;
1101                    frameinfo = None;
1102                    next_frame_num += 1;
1103                }
1104                _nal_unit_type => {}
1105            }
1106        }
1107    }
1108
1109    if let Some(pb) = pb.as_mut() {
1110        pb.finish_and_clear();
1111    }
1112
1113    tracing::debug!("Done iterating through all NAL units.");
1114
1115    Ok(TimingData {
1116        frame_time_info,
1117        frame0_precision_time,
1118        frame0_frameinfo,
1119        h264_metadata,
1120        tz_offset,
1121    })
1122}
1123
1124struct RawH264Iter<'parent, H: SeekableH264Source> {
1125    parent: &'parent mut H264Source<H>,
1126    /// frame index (not NAL unit index)
1127    frame_idx: usize,
1128    openh264_decoder_state: Option<crate::opt_openh264_decoder::DecoderType>,
1129    /// `display_order[k]` is the decode index of the k-th frame in display
1130    /// order (the inverse of `parent.presentation_rank`). The decoder outputs
1131    /// pictures in display order, so this pairs each output picture with the
1132    /// input frame it decodes. `None` when presentation order is unknown, in
1133    /// which case output order is assumed to equal decode order (true for
1134    /// streams without B-frames). Only used when decoding.
1135    display_order: Option<Vec<usize>>,
1136    /// Number of pictures the decoder has output so far, i.e. the display rank
1137    /// of the next picture it will output. Only used when decoding.
1138    n_pictures_out: usize,
1139    /// Metadata of frames already fed to the decoder whose picture has not yet
1140    /// been output, keyed by decode index. Bounded by the stream's reorder
1141    /// depth (the decoded picture buffer, at most 16 frames). Only used when
1142    /// decoding.
1143    pending_meta: std::collections::HashMap<usize, PendingMeta>,
1144    /// Decoded frames awaiting emission in decode order, keyed by decode
1145    /// index. Bounded by the stream's reorder depth. Only used when decoding.
1146    decoded_ready: std::collections::BTreeMap<usize, FrameData>,
1147    /// Decode index of the next frame to emit. Only used when decoding.
1148    next_emit: usize,
1149    /// Whether the end-of-stream drain of the decoder has run.
1150    flushed: bool,
1151    /// Set after a decode error: the picture↔frame pairing may be lost, so
1152    /// stop iterating rather than emit frames with wrong timestamps.
1153    poisoned: bool,
1154}
1155
1156/// Metadata of an input frame fed to the decoder, held until the decoder
1157/// outputs the corresponding picture.
1158#[cfg_attr(not(feature = "openh264"), allow(dead_code))]
1159struct PendingMeta {
1160    timestamp: Timestamp,
1161    poc: Option<i64>,
1162    buf_len: usize,
1163}
1164
1165/// One frame read from the source in decode order: its raw NAL units plus
1166/// timestamp and POC metadata.
1167struct InputFrame {
1168    nal_units: Vec<Vec<u8>>,
1169    timestamp: Timestamp,
1170    poc: Option<i64>,
1171    is_idr: bool,
1172}
1173
1174impl<H: SeekableH264Source> RawH264Iter<'_, H> {
1175    /// Read input frame `frame_number` (decode order).
1176    fn read_input_frame(&mut self, frame_number: usize) -> Result<InputFrame> {
1177        let nti = &self.parent.frame_time_info[frame_number];
1178        // Slice of all NAL units belonging to this frame: everything after
1179        // the previous frame's slice NAL up to and including this frame's.
1180        // `nal_location_index` is indexed per NAL unit (Annex B) or per
1181        // sample (MP4); for MP4 each frame is one location so this reduces
1182        // to `[frame_number..=frame_number]`, but for Annex B a frame spans
1183        // several NAL units (SEI/SPS/PPS + slice), so we must start after
1184        // the previous slice rather than at `frame_number`.
1185        let start = if frame_number == 0 {
1186            0
1187        } else {
1188            self.parent.frame_time_info[frame_number - 1].nal_location_index + 1
1189        };
1190        let nal_locations = &self.parent.nal_locations[start..=(nti.nal_location_index)];
1191        let mp4_pts = self.parent.mp4_pts.as_ref().map(|x| x[frame_number]); // one per mp4 sample
1192        let fraction_done = frame_number as f32 / self.parent.nal_locations.len() as f32;
1193
1194        let frame_timestamp = match self.parent.timestamp_source {
1195            Some(TimestampSource::BestGuess) => unreachable!(),
1196            Some(TimestampSource::MispMicrosectime) => {
1197                let f0 = self.parent.frame0_precision_time.as_ref().unwrap();
1198                Timestamp::Duration(
1199                    nti.precise_timestamp
1200                        .unwrap()
1201                        .signed_duration_since(*f0)
1202                        .to_std()
1203                        .unwrap(),
1204                )
1205            }
1206            Some(TimestampSource::FrameInfoRecvTime) => {
1207                let t0 = self.parent.frame0_frameinfo.as_ref().unwrap().recv;
1208                let t0: chrono::DateTime<chrono::Utc> = t0.into();
1209                let this_frame: chrono::DateTime<chrono::Utc> =
1210                    nti.frameinfo.as_ref().unwrap().recv.into();
1211                Timestamp::Duration(this_frame.signed_duration_since(t0).to_std().unwrap())
1212            }
1213            Some(TimestampSource::FrameInfoRtp) => {
1214                let fi0 = self.parent.frame0_frameinfo.as_ref().unwrap();
1215                let rtp0 = fi0.rtp;
1216                let rtp_now = nti.frameinfo.as_ref().unwrap().rtp;
1217                let rtp_dur = rtp_now.wrapping_sub(rtp0);
1218                let rtp_dur_secs = rtp_dur as f64 / 90000.0; // nominally 90 kHz
1219                Timestamp::Duration(std::time::Duration::from_secs_f64(rtp_dur_secs))
1220            }
1221            Some(TimestampSource::FixedFramerate) => {
1222                let dur_secs = nti.nal_location_index as f64 / self.parent.average_fps.unwrap();
1223                Timestamp::Duration(std::time::Duration::from_secs_f64(dur_secs))
1224            }
1225            Some(TimestampSource::Mp4Pts) => Timestamp::Duration(mp4_pts.unwrap()),
1226            Some(TimestampSource::SrtFile) => {
1227                // Pair this decode-order frame with the SRT stanza at its
1228                // display rank, so B-frame reordering (decode order !=
1229                // presentation order) does not misassign timestamps. For raw
1230                // Annex B (no per-sample PTS) fall back to sequential order.
1231                let rank_idx = self
1232                    .parent
1233                    .srt_display_rank
1234                    .as_ref()
1235                    .map(|rank| rank[frame_number]);
1236                let srt_data = self.parent.srt_data.as_mut().unwrap();
1237                let pts = match rank_idx {
1238                    Some(idx) => srt_data.time_at(idx).unwrap(),
1239                    None => srt_data.next_pts().unwrap(),
1240                };
1241                Timestamp::Duration(pts)
1242            }
1243            None => Timestamp::Fraction(fraction_done),
1244        };
1245
1246        let nal_units = self
1247            .parent
1248            .seekable_h264_source
1249            .read_nal_units_at_locations(nal_locations)?;
1250
1251        Ok(InputFrame {
1252            nal_units,
1253            timestamp: frame_timestamp,
1254            poc: nti.poc,
1255            is_idr: nti.is_idr,
1256        })
1257    }
1258
1259    /// Feed input frame `frame_number` to the decoder and, if a picture comes
1260    /// out, pair it with its input frame and store it in `decoded_ready`.
1261    fn feed_decoder(&mut self, frame_number: usize) -> Result<()> {
1262        let InputFrame {
1263            nal_units,
1264            timestamp,
1265            poc,
1266            is_idr,
1267        } = self.read_input_frame(frame_number)?;
1268
1269        // MP4 stores the SPS/PPS in the `avcC` box rather than inline in
1270        // the sample data, so the NAL units for an IDR frame contain only
1271        // the coded slice. Without parameter sets OpenH264 fails with
1272        // `dsNoParamSets` (native error 16). Prepend the stream's SPS/PPS
1273        // ahead of each IDR so the decoder is configured. Annex B sources
1274        // carry SPS/PPS inline and return `None` here, so this is a no-op
1275        // for them (their slice already includes the parameter sets).
1276        let annex_b = if is_idr {
1277            let mut prefix = Vec::with_capacity(nal_units.len() + 2);
1278            prefix.extend(self.parent.seekable_h264_source.first_sps());
1279            prefix.extend(self.parent.seekable_h264_source.first_pps());
1280            prefix.extend_from_slice(nal_units.as_slice());
1281            copy_nalus_to_annex_b(&prefix)
1282        } else {
1283            copy_nalus_to_annex_b(nal_units.as_slice())
1284        };
1285
1286        let buf_len = nal_units.iter().map(|x| x.len()).sum();
1287        self.pending_meta.insert(
1288            frame_number,
1289            PendingMeta {
1290                timestamp,
1291                poc,
1292                buf_len,
1293            },
1294        );
1295
1296        // Attempt the decode first, so that if OpenH264 ever gains
1297        // 4:2:2 / 4:4:4 support the stream simply works. Only when the
1298        // decode actually fails do we enrich its opaque native error
1299        // with the stream's chroma/profile as the likely cause (the
1300        // built-in decoder handles only 4:2:0).
1301        let decoder = self.openh264_decoder_state.as_mut().unwrap();
1302        let decode_result = decoder.decode(&annex_b[..]);
1303        #[cfg(feature = "openh264")]
1304        let decode_result = decode_result.map_err(|source| Error::H264DecodeFailed {
1305            frame: frame_number,
1306            hint: decode_failure_hint(self.parent.chroma_format, &self.parent.profile),
1307            source,
1308        });
1309
1310        // A decode call returning no picture is not an error: the decoder
1311        // buffers frames to reorder streams with B-frames into display order,
1312        // and the picture comes out on a later call (or the end-of-stream
1313        // drain).
1314        if let Some(decoded_yuv) = decode_result? {
1315            accept_decoded_picture(
1316                decoded_yuv,
1317                self.display_order.as_deref(),
1318                &mut self.n_pictures_out,
1319                &mut self.pending_meta,
1320                &mut self.decoded_ready,
1321            )?;
1322        }
1323        Ok(())
1324    }
1325
1326    /// Drain the pictures still buffered in the decoder at end of stream (for
1327    /// B-frame streams the decoder holds back frames to reorder them).
1328    fn drain_decoder(&mut self) -> Result<()> {
1329        let decoder = self.openh264_decoder_state.as_mut().unwrap();
1330        let pictures = decoder.flush_remaining();
1331        #[cfg(feature = "openh264")]
1332        let pictures = pictures.map_err(|source| Error::H264DecodeFailed {
1333            frame: self.next_emit,
1334            hint: decode_failure_hint(self.parent.chroma_format, &self.parent.profile),
1335            source,
1336        });
1337        for decoded_yuv in pictures? {
1338            accept_decoded_picture(
1339                decoded_yuv,
1340                self.display_order.as_deref(),
1341                &mut self.n_pictures_out,
1342                &mut self.pending_meta,
1343                &mut self.decoded_ready,
1344            )?;
1345        }
1346        Ok(())
1347    }
1348
1349    /// `next()` for the decoding path: feed input frames (decode order) until
1350    /// the frame with the next decode index has been decoded, draining the
1351    /// decoder at end of stream.
1352    fn next_decoded(&mut self) -> Option<Result<FrameData>> {
1353        loop {
1354            if let Some(frame_data) = self.decoded_ready.remove(&self.next_emit) {
1355                self.next_emit += 1;
1356                return Some(Ok(frame_data));
1357            }
1358            if self.poisoned {
1359                return None;
1360            }
1361            if self.frame_idx < self.parent.frame_time_info.len() {
1362                let frame_number = self.frame_idx;
1363                self.frame_idx += 1;
1364                if let Err(e) = self.feed_decoder(frame_number) {
1365                    self.poisoned = true;
1366                    return Some(Err(e));
1367                }
1368            } else if !self.flushed {
1369                self.flushed = true;
1370                if let Err(e) = self.drain_decoder() {
1371                    self.poisoned = true;
1372                    return Some(Err(e));
1373                }
1374            } else {
1375                if !self.pending_meta.is_empty() {
1376                    self.poisoned = true;
1377                    return Some(Err(Error::H264Error(
1378                        "decoder produced fewer pictures than input frames",
1379                    )));
1380                }
1381                return None;
1382            }
1383        }
1384    }
1385}
1386
1387impl<H: SeekableH264Source> Iterator for RawH264Iter<'_, H> {
1388    type Item = Result<FrameData>;
1389    fn next(&mut self) -> Option<Self::Item> {
1390        if self.openh264_decoder_state.is_some() {
1391            return self.next_decoded();
1392        }
1393        let frame_number = self.frame_idx;
1394        if frame_number >= self.parent.frame_time_info.len() {
1395            return None;
1396        }
1397        self.frame_idx += 1;
1398
1399        Some(self.read_input_frame(frame_number).map(|input| {
1400            let buf_len = input.nal_units.iter().map(|x| x.len()).sum();
1401            let buf = EncodedH264 {
1402                data: H264EncodingVariant::RawEbsp(input.nal_units),
1403                has_precision_timestamp: self.parent.frame0_precision_time.is_some(),
1404            };
1405            FrameData {
1406                timestamp: input.timestamp,
1407                image: ImageData::EncodedH264(buf),
1408                buf_len,
1409                idx: frame_number,
1410                poc: input.poc,
1411            }
1412        }))
1413    }
1414
1415    fn size_hint(&self) -> (usize, Option<usize>) {
1416        let total = self.parent.frame_time_info.len();
1417        // The cursor advances past the end once the iterator is exhausted, so
1418        // saturate to avoid underflow. When decoding, frames are emitted by
1419        // `next_emit` (input frames can be consumed ahead of emission while
1420        // the decoder reorders); otherwise by `frame_idx`.
1421        let remaining = if self.openh264_decoder_state.is_some() {
1422            if self.poisoned {
1423                0
1424            } else {
1425                total.saturating_sub(self.next_emit)
1426            }
1427        } else {
1428            total.saturating_sub(self.frame_idx)
1429        };
1430        (remaining, Some(remaining))
1431    }
1432}
1433
1434/// Pair a picture output by the decoder with the input frame it decodes and
1435/// queue the resulting [`FrameData`] for emission.
1436///
1437/// The decoder outputs pictures in display order, so the picture's display
1438/// rank (`n_pictures_out`) is mapped back to a decode index via
1439/// `display_order`.
1440#[cfg(feature = "openh264")]
1441fn accept_decoded_picture(
1442    decoded_yuv: openh264::decoder::DecodedYUV<'_>,
1443    display_order: Option<&[usize]>,
1444    n_pictures_out: &mut usize,
1445    pending_meta: &mut std::collections::HashMap<usize, PendingMeta>,
1446    decoded_ready: &mut std::collections::BTreeMap<usize, FrameData>,
1447) -> Result<()> {
1448    let display_rank = *n_pictures_out;
1449    *n_pictures_out += 1;
1450    let decode_idx = match display_order {
1451        Some(order) => *order.get(display_rank).ok_or(Error::H264Error(
1452            "decoder produced more pictures than input frames",
1453        ))?,
1454        None => display_rank,
1455    };
1456    let meta = pending_meta.remove(&decode_idx).ok_or(Error::H264Error(
1457        "decoder output picture does not correspond to a pending input frame",
1458    ))?;
1459    let frame_data = yuv2rgb(
1460        decoded_yuv,
1461        decode_idx,
1462        meta.poc,
1463        meta.buf_len,
1464        meta.timestamp,
1465    )?;
1466    decoded_ready.insert(decode_idx, frame_data);
1467    Ok(())
1468}
1469
1470#[cfg(not(feature = "openh264"))]
1471fn accept_decoded_picture(
1472    _decoded_yuv: (),
1473    _display_order: Option<&[usize]>,
1474    _n_pictures_out: &mut usize,
1475    _pending_meta: &mut std::collections::HashMap<usize, PendingMeta>,
1476    _decoded_ready: &mut std::collections::BTreeMap<usize, FrameData>,
1477) -> Result<()> {
1478    Err(Error::H264Error("No H264 decoder support at compile time"))
1479}
1480
1481/// Reorders a decode-order H.264 frame iterator into presentation (display)
1482/// order by buffering one coded video sequence at a time.
1483///
1484/// Frames arrive from `inner` in decode order. Each coded video sequence
1485/// (delimited by IDR pictures) is buffered in `pending`; when the next IDR
1486/// arrives (or the stream ends) the buffered frames are sorted by their display
1487/// rank and moved to `ready` for emission. Because coded video sequences are
1488/// contiguous in display order for the closed GOPs produced by strand-cam /
1489/// braid / ffmpeg, this yields globally correct presentation order with memory
1490/// bounded by one GOP.
1491///
1492/// Open-GOP streams (leading pictures following a non-IDR recovery point) are
1493/// not handled specially; the encode paths in this workspace use closed GOPs.
1494struct PresentationReorderIter<'a> {
1495    inner: Box<dyn Iterator<Item = Result<FrameData>> + 'a>,
1496    /// `rank[decode_idx]` is the frame's position in display order.
1497    rank: Vec<usize>,
1498    /// `is_idr[decode_idx]` marks the start of a coded video sequence.
1499    is_idr: Vec<bool>,
1500    /// Total number of frames, used to restamp fraction-done timestamps.
1501    total: usize,
1502    /// Frames of the current coded video sequence, buffered in decode order.
1503    pending: Vec<FrameData>,
1504    /// Frames flushed and ready to emit, already in display order.
1505    ready: std::collections::VecDeque<FrameData>,
1506    /// Whether `inner` has been exhausted (or errored).
1507    done: bool,
1508}
1509
1510impl PresentationReorderIter<'_> {
1511    /// Sort the buffered coded video sequence by display rank and move it to
1512    /// `ready`, restamping fraction-done timestamps (which count decode
1513    /// position) to display position so they stay monotonic.
1514    fn flush(&mut self) {
1515        let rank = &self.rank;
1516        self.pending
1517            .sort_by_key(|f| rank.get(f.idx()).copied().unwrap_or(usize::MAX));
1518        let denom = self.total.max(1) as f32;
1519        for mut frame in self.pending.drain(..).collect::<Vec<_>>() {
1520            if let Timestamp::Fraction(_) = frame.timestamp {
1521                let pos = self.rank.get(frame.idx()).copied().unwrap_or(0);
1522                frame.timestamp = Timestamp::Fraction(pos as f32 / denom);
1523            }
1524            self.ready.push_back(frame);
1525        }
1526    }
1527}
1528
1529impl Iterator for PresentationReorderIter<'_> {
1530    type Item = Result<FrameData>;
1531    fn next(&mut self) -> Option<Self::Item> {
1532        loop {
1533            if let Some(frame) = self.ready.pop_front() {
1534                return Some(Ok(frame));
1535            }
1536            if self.done {
1537                return None;
1538            }
1539            match self.inner.next() {
1540                Some(Ok(frame)) => {
1541                    // A new coded video sequence begins: flush the previous one
1542                    // (now complete) before buffering this IDR.
1543                    if self.is_idr.get(frame.idx()).copied().unwrap_or(false)
1544                        && !self.pending.is_empty()
1545                    {
1546                        self.flush();
1547                    }
1548                    self.pending.push(frame);
1549                }
1550                Some(Err(e)) => {
1551                    self.done = true;
1552                    self.pending.clear();
1553                    return Some(Err(e));
1554                }
1555                None => {
1556                    self.done = true;
1557                    self.flush();
1558                }
1559            }
1560        }
1561    }
1562
1563    fn size_hint(&self) -> (usize, Option<usize>) {
1564        // Reordering neither adds nor drops frames, so the total is the inner
1565        // iterator's remaining count plus whatever we are currently holding.
1566        let (lo, hi) = self.inner.size_hint();
1567        let buffered = self.ready.len() + self.pending.len();
1568        (lo + buffered, hi.map(|h| h + buffered))
1569    }
1570}
1571
1572/// Build a likely-cause hint appended to an OpenH264 decode failure. The
1573/// built-in decoder handles only 4:2:0 (YUV420) chroma, so a stream with any
1574/// other chroma subsampling is a likely cause. Names the unsupported chroma
1575/// the stream uses, the profile, and how to re-encode. Returns an empty
1576/// string when the stream is 4:2:0 (the failure lies elsewhere).
1577#[cfg(feature = "openh264")]
1578fn decode_failure_hint(chroma: h264_reader::nal::sps::ChromaFormat, profile: &str) -> String {
1579    use h264_reader::nal::sps::ChromaFormat::*;
1580    let feature = match chroma {
1581        YUV420 => return String::new(),
1582        Monochrome => "4:0:0 (monochrome) chroma subsampling".to_string(),
1583        YUV422 => "4:2:2 chroma subsampling".to_string(),
1584        YUV444 => "4:4:4 chroma subsampling".to_string(),
1585        Invalid(idc) => format!("unknown chroma subsampling (idc={idc})"),
1586    };
1587    format!(
1588        " This stream uses {feature} (profile {profile}), which the built-in OpenH264 decoder \
1589         does not support — it decodes only 4:2:0 (YUV420). Re-encode first, \
1590         e.g. `ffmpeg -i INPUT -c:v libx264 -pix_fmt yuv420p OUTPUT.mp4`."
1591    )
1592}
1593
1594#[cfg(feature = "openh264")]
1595fn yuv2rgb(
1596    decoded_yuv: openh264::decoder::DecodedYUV<'_>,
1597    frame_number: usize,
1598    poc: Option<i64>,
1599    buf_len: usize,
1600    timestamp: Timestamp,
1601) -> Result<FrameData> {
1602    use openh264::formats::YUVSource;
1603    let dim = decoded_yuv.dimensions();
1604
1605    let stride = dim.0 * 3;
1606    let mut image_data = vec![0u8; stride * dim.1];
1607    decoded_yuv.write_rgb8(&mut image_data);
1608
1609    let dynamic_frame = strand_dynamic_frame::DynamicFrameOwned::from_static(
1610        OImage::<machine_vision_formats::pixel_format::RGB8>::new(
1611            dim.0.try_into().unwrap(),
1612            dim.1.try_into().unwrap(),
1613            stride,
1614            image_data,
1615        )
1616        .unwrap(),
1617    );
1618
1619    let idx = frame_number;
1620    let image = ImageData::Decoded(dynamic_frame);
1621    Ok(FrameData {
1622        timestamp,
1623        image,
1624        buf_len,
1625        idx,
1626        poc,
1627    })
1628}
1629
1630pub(crate) fn from_annexb_path_with_timestamp_source<P: AsRef<Path>>(
1631    path: P,
1632    do_decode_h264: bool,
1633    timestamp_source: crate::TimestampSource,
1634    srt_file_path: Option<std::path::PathBuf>,
1635    show_progress: bool,
1636) -> Result<H264Source<H264AnnexBSource>> {
1637    let rdr = std::fs::File::open(path.as_ref())?;
1638    let seekable_h264_source = H264AnnexBSource::from_file(rdr)?;
1639    from_annexb_reader_with_timestamp_source(
1640        seekable_h264_source,
1641        do_decode_h264,
1642        timestamp_source,
1643        srt_file_path,
1644        show_progress,
1645    )
1646}
1647
1648fn from_annexb_reader_with_timestamp_source(
1649    annex_b_source: H264AnnexBSource,
1650    do_decode_h264: bool,
1651    timestamp_source: crate::TimestampSource,
1652    srt_file_path: Option<std::path::PathBuf>,
1653    show_progress: bool,
1654) -> Result<H264Source<H264AnnexBSource>> {
1655    H264Source::from_seekable_h264_source_with_timestamp_source(
1656        annex_b_source,
1657        do_decode_h264,
1658        None, // mp4_pts
1659        None, // mp4_sample_timing
1660        None, // data_from_mp4_track
1661        timestamp_source,
1662        srt_file_path,
1663        show_progress,
1664        None,
1665    )
1666}
1667
1668pub(crate) struct UserDataUnregistered<'a> {
1669    pub uuid: &'a [u8; 16],
1670    pub payload: &'a [u8],
1671}
1672
1673impl<'a> UserDataUnregistered<'a> {
1674    pub fn read(msg: &SeiMessage<'a>) -> Result<UserDataUnregistered<'a>> {
1675        if msg.payload_type != HeaderType::UserDataUnregistered {
1676            return Err(Error::UduError(format!(
1677                "expected UserDataUnregistered message, found {:?}",
1678                msg.payload_type
1679            )));
1680        }
1681        if msg.payload.len() < 16 {
1682            return Err(Error::UduError(
1683                "SEI payload too short to contain UserDataUnregistered message".to_string(),
1684            ));
1685        }
1686        let uuid = (&msg.payload[0..16]).try_into().unwrap();
1687
1688        let payload = &msg.payload[16..];
1689        Ok(UserDataUnregistered { uuid, payload })
1690    }
1691}
1692
1693pub(crate) fn parse_precision_time(payload: &[u8]) -> Result<chrono::DateTime<chrono::Utc>> {
1694    if payload.len() != 12 {
1695        return Err(Error::UnexpectedPayloadLength);
1696    }
1697
1698    // // Time Stamp Status byte from MISB Standard 0603.
1699    // // Could parse Locked/Unlocked (bit 7), Normal/Discontinuity (bit 6),
1700    // // Forward/Reverse (bit 5).
1701
1702    // let time_stamp_status = payload[0];
1703    // if time_stamp_status & 0x1F != 0x1F {
1704    //     anyhow::bail!(
1705    //         "unexpected time stamp status byte. Full payload: {{{}}}",
1706    //         pretty_hex::simple_hex(&payload),
1707    //     );
1708    // }
1709
1710    let mut precision_time_stamp_bytes = [0u8; 8];
1711    for i in &[3, 6, 9] {
1712        if payload[*i] != 0xFF {
1713            return Err(Error::UnexpectedStartCodeByte);
1714        }
1715    }
1716    precision_time_stamp_bytes[0..2].copy_from_slice(&payload[1..3]);
1717    precision_time_stamp_bytes[2..4].copy_from_slice(&payload[4..6]);
1718    precision_time_stamp_bytes[4..6].copy_from_slice(&payload[7..9]);
1719    precision_time_stamp_bytes[6..8].copy_from_slice(&payload[10..12]);
1720    let precision_time_stamp: i64 = i64::from_be_bytes(precision_time_stamp_bytes);
1721    let dur = chrono::Duration::microseconds(precision_time_stamp);
1722
1723    let epoch_start = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)
1724        .unwrap()
1725        .and_hms_micro_opt(0, 0, 0, 0)
1726        .unwrap()
1727        .and_local_timezone(chrono::Utc)
1728        .unwrap();
1729
1730    Ok(epoch_start + dur)
1731}
1732
1733/// Copy raw headerless EBSP NAL units to Annex B
1734fn copy_nalus_to_annex_b(nalus: &[Vec<u8>]) -> Vec<u8> {
1735    let sz = nalus.iter().fold(0, |acc, x| acc + x.len() + 4);
1736    let mut result = vec![0u8; sz];
1737    let mut start_idx = 0;
1738    for src in nalus.iter() {
1739        let dest = &mut result[start_idx..start_idx + 4 + src.len()];
1740        dest[3] = 0x01;
1741        dest[4..].copy_from_slice(src);
1742        start_idx += src.len() + 4;
1743    }
1744    result
1745}
1746
1747/// Timing information associated with each video frame
1748///
1749/// UUID strawlab.org/89H
1750#[derive(Debug, Clone, Serialize, Deserialize)]
1751struct FrameInfo {
1752    /// Receive timestamp as NTP (Network Time Protocol) timestamp
1753    recv: NtpTimestamp,
1754    /// RTP (Real Time Protocol) timestamp as reported by the sender
1755    rtp: u32,
1756}
1757
1758// ----
1759
1760#[cfg(test)]
1761mod test {
1762    /// The decode-failure hint names the OpenH264-unsupported chroma the
1763    /// stream uses, and is empty for a 4:2:0 stream (where the failure lies
1764    /// elsewhere).
1765    #[cfg(feature = "openh264")]
1766    #[test]
1767    fn decode_failure_hint_flags_features() {
1768        use super::decode_failure_hint;
1769        use h264_reader::nal::sps::ChromaFormat::*;
1770
1771        // The unsupported feature named in the "uses ... (profile" clause.
1772        fn features(chroma: h264_reader::nal::sps::ChromaFormat) -> String {
1773            let h = decode_failure_hint(chroma, "P");
1774            if h.is_empty() {
1775                return String::new();
1776            }
1777            let start = h.find("uses ").unwrap() + "uses ".len();
1778            let end = h.find(" (profile").unwrap();
1779            h[start..end].to_string()
1780        }
1781
1782        // 4:2:0: nothing to explain.
1783        assert!(decode_failure_hint(YUV420, "High").is_empty());
1784
1785        // Non-4:2:0 chroma: flag it.
1786        assert_eq!(features(YUV444), "4:4:4 chroma subsampling");
1787        assert_eq!(features(YUV422), "4:2:2 chroma subsampling");
1788    }
1789
1790    #[cfg(feature = "openh264")]
1791    #[test]
1792    fn parse_h264() -> crate::Result<()> {
1793        use super::*;
1794
1795        {
1796            let file_buf = include_bytes!("test-data/test_less-avc_mono8_15x14.h264");
1797            let cursor = std::io::Cursor::new(file_buf);
1798            let seekable_h264_source = H264AnnexBSource::from_readseek(Box::new(cursor))?;
1799
1800            let do_decode_h264 = true;
1801            let mut h264_src = from_annexb_reader_with_timestamp_source(
1802                seekable_h264_source,
1803                do_decode_h264,
1804                TimestampSource::BestGuess,
1805                None,
1806                false,
1807            )?;
1808            assert_eq!(h264_src.width(), 15);
1809            assert_eq!(h264_src.height(), 14);
1810            let frames: Vec<_> = h264_src.decode_order_iter().collect();
1811            assert_eq!(frames.len(), 1);
1812        }
1813
1814        {
1815            let file_buf = include_bytes!("test-data/test_less-avc_rgb8_16x16.h264");
1816            let cursor = std::io::Cursor::new(file_buf);
1817            let seekable_h264_source = H264AnnexBSource::from_readseek(Box::new(cursor))?;
1818            let do_decode_h264 = true;
1819            let mut h264_src = from_annexb_reader_with_timestamp_source(
1820                seekable_h264_source,
1821                do_decode_h264,
1822                TimestampSource::BestGuess,
1823                None,
1824                false,
1825            )?;
1826            assert_eq!(h264_src.width(), 16);
1827            assert_eq!(h264_src.height(), 16);
1828            let frames: Vec<_> = h264_src.decode_order_iter().collect();
1829            assert_eq!(frames.len(), 1);
1830        }
1831        Ok(())
1832    }
1833}