Skip to main content

frame_source/
mp4_source.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::path::Path;
5
6use crate::{
7    Result,
8    h264_source::{H264Preparser, H264Source, SeekRead, SeekableH264Source},
9};
10use mp4::MediaType;
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct Mp4NalLocation {
14    track_id: u32,
15    sample_id: u32,
16}
17
18#[derive(thiserror::Error, Debug)]
19pub enum Mp4SourceError {
20    #[error("sample is empty")]
21    SampleEmpty,
22    #[error("sample in track disappeared")]
23    SampleDisappeared,
24    #[error("only MP4 files with a single H264 video track are supported")]
25    SingleH264TrackOnly,
26    #[error("No H264 video track found in MP4 file.")]
27    NoH264Track,
28    #[error("sample buffer is too short for NAL unit header")]
29    SampleBufferTooShort,
30    #[error("AVCC buffer length: {sz}+4 but buffer {cur_len}")]
31    LengthMismatch { sz: usize, cur_len: usize },
32}
33
34pub struct Mp4Source {
35    mp4_reader: mp4::Mp4Reader<Box<dyn SeekRead + Send>>,
36    nal_locations: Vec<Mp4NalLocation>,
37    first_sps: Vec<u8>,
38    first_pps: Vec<u8>,
39}
40
41impl SeekableH264Source for Mp4Source {
42    type NalLocation = Mp4NalLocation;
43    fn nal_boundaries(&mut self) -> &[Self::NalLocation] {
44        &self.nal_locations
45    }
46    fn read_nal_units_at_location(&mut self, location: &Self::NalLocation) -> Result<Vec<Vec<u8>>> {
47        match self
48            .mp4_reader
49            .read_sample(location.track_id, location.sample_id)?
50        {
51            Some(sample) => {
52                if !sample.bytes.is_empty() {
53                    let sample_nal_units = avcc_to_nalu_ebsp(sample.bytes.as_ref())?;
54                    Ok(sample_nal_units.iter().map(|x| x.to_vec()).collect())
55                } else {
56                    Err(Mp4SourceError::SampleEmpty.into())
57                }
58            }
59            _ => Err(Mp4SourceError::SampleDisappeared.into()),
60        }
61    }
62    fn first_sps(&self) -> Option<Vec<u8>> {
63        Some(self.first_sps.clone())
64    }
65    fn first_pps(&self) -> Option<Vec<u8>> {
66        Some(self.first_pps.clone())
67    }
68}
69
70/// Construct a frame source from an already-opened MP4 reader.
71pub fn from_reader_with_timestamp_source(
72    mut mp4_reader: mp4::Mp4Reader<Box<dyn SeekRead + Send>>,
73    do_decode_h264: bool,
74    timestamp_source: crate::TimestampSource,
75    srt_file_path: Option<std::path::PathBuf>,
76    show_progress: bool,
77    preparser: Option<Box<dyn H264Preparser>>,
78) -> Result<H264Source<Mp4Source>> {
79    let mut video_track = None;
80    for (track_id, track) in mp4_reader.tracks().iter() {
81        // ignore all tracks except H264
82        if let Ok(MediaType::H264) = track.media_type() {
83            if video_track.is_some() {
84                return Err(Mp4SourceError::SingleH264TrackOnly.into());
85            }
86            video_track = Some((track_id, track));
87        }
88    }
89
90    let (track_id, track) = if let Some(vt) = video_track {
91        vt
92    } else {
93        return Err(Mp4SourceError::NoH264Track.into());
94    };
95
96    let track_id = *track_id;
97
98    // Iterate over every sample in the track. Typically (always?) one such MP4
99    // sample corresponds to one frame of video (and often multiple NAL units).
100    // Here we assume this 1:1 mapping between MP4 samples and video frames. The
101    // `nal_locations` and `mp4_pts` each are indexed by sample number.
102    let mut nal_locations = Vec::new();
103    let mut mp4_pts = Vec::new();
104    let mut sample_timing = Vec::new();
105    let data_from_mp4_track = crate::h264_source::FromMp4Track {
106        sequence_parameter_set: track.sequence_parameter_set()?.to_vec(),
107        picture_parameter_set: track.picture_parameter_set()?.to_vec(),
108    };
109
110    // Per-sample composition-time offsets (ctts), expanded to one per sample.
111    // Computed here while the shared borrow of `track` is still held. These are
112    // needed to turn decode timestamps into presentation timestamps and to
113    // preserve the composition offset when re-muxing.
114    let comp_offsets = composition_offsets(track);
115    // Sample decode times and durations are in the *track* (media) timescale,
116    // which differs from the movie timescale returned by `mp4_reader.timescale()`.
117    let media_timescale = track.timescale();
118
119    let num_samples = mp4_reader.sample_count(track_id)?;
120
121    // mp4 uses 1 based indexing
122    for sample_id in 1..=num_samples {
123        // `sample_time_duration` returns the *decode* time and the sample
124        // duration. For streams encoded with B-frames the samples are stored in
125        // decode order with non-zero composition offsets, so the *presentation*
126        // time is decode time + composition offset. Reporting presentation time
127        // (not decode time) keeps consumers correct for reordered streams.
128        let (decode_time, duration) = mp4_reader.sample_time_duration(track_id, sample_id)?;
129        let offset = comp_offsets
130            .get((sample_id - 1) as usize)
131            .copied()
132            .unwrap_or(0);
133        let pts_raw = (decode_time as i64 + offset as i64).max(0) as u64;
134        mp4_pts.push(raw2dur(pts_raw, media_timescale));
135        sample_timing.push(crate::h264_source::Mp4SampleTiming {
136            decode_duration: raw2dur(duration as u64, media_timescale),
137            composition_offset: raw2signed_dur(offset, media_timescale),
138        });
139        nal_locations.push(Mp4NalLocation {
140            track_id,
141            sample_id,
142        });
143    }
144    assert_eq!(mp4_pts.len(), num_samples as usize);
145
146    let seekable_h264_source = Mp4Source {
147        mp4_reader,
148        nal_locations,
149        first_sps: data_from_mp4_track.sequence_parameter_set.clone(),
150        first_pps: data_from_mp4_track.picture_parameter_set.clone(),
151    };
152
153    let h264_source = H264Source::from_seekable_h264_source_with_timestamp_source(
154        seekable_h264_source,
155        do_decode_h264,
156        Some(mp4_pts),
157        Some(sample_timing),
158        Some(data_from_mp4_track),
159        timestamp_source,
160        srt_file_path,
161        show_progress,
162        preparser,
163    )?;
164    Ok(h264_source)
165}
166
167pub(crate) fn open_h264_in_mp4<P: AsRef<Path>>(
168    path: P,
169    do_decode_h264: bool,
170    timestamp_source: crate::TimestampSource,
171    srt_file_path: Option<std::path::PathBuf>,
172    show_progress: bool,
173    preparser: Option<Box<dyn H264Preparser>>,
174) -> Result<H264Source<Mp4Source>> {
175    let rdr = std::fs::File::open(path.as_ref())?;
176    let size = rdr.metadata()?.len();
177    let buf_reader: Box<dyn SeekRead + Send + 'static> = Box::new(std::io::BufReader::new(rdr));
178    let mp4_reader = mp4::Mp4Reader::read_header(buf_reader, size)?;
179
180    let result = from_reader_with_timestamp_source(
181        mp4_reader,
182        do_decode_h264,
183        timestamp_source,
184        srt_file_path,
185        show_progress,
186        preparser,
187    )?;
188    Ok(result)
189}
190
191/// Parse sample from MP4 as NAL units.
192///
193/// In MP4 files, each sample buffer is multiple NAL units consisting of a
194/// 4-byte length header and the data.
195///
196/// This function is not capable of parsing on non-NALU boundaries and must
197/// contain complete NALUs. For well-formed MP4 files, this should be the case.
198fn avcc_to_nalu_ebsp(mp4_sample_buffer: &[u8]) -> Result<Vec<&[u8]>> {
199    let mut result = vec![];
200    let mut cur_buf = mp4_sample_buffer;
201    let mut total_nal_sizes = 0;
202    while !cur_buf.is_empty() {
203        if cur_buf.len() < 4 {
204            return Err(Mp4SourceError::SampleBufferTooShort.into());
205        }
206        let header = [cur_buf[0], cur_buf[1], cur_buf[2], cur_buf[3]];
207        let sz: usize = u32::from_be_bytes(header).try_into().unwrap();
208        let used = sz + 4;
209        if cur_buf.len() < used {
210            return Err(Mp4SourceError::LengthMismatch {
211                sz,
212                cur_len: cur_buf.len(),
213            }
214            .into());
215        }
216        total_nal_sizes += used;
217        result.push(&cur_buf[4..used]);
218        cur_buf = &cur_buf[used..];
219    }
220    if total_nal_sizes != mp4_sample_buffer.len() {
221        tracing::warn!(
222            "MP4 sample was {} bytes, but H264 NAL units totaled {} bytes.",
223            mp4_sample_buffer.len(),
224            total_nal_sizes
225        );
226    }
227    Ok(result)
228}
229
230fn raw2dur(raw: u64, timescale: u32) -> std::time::Duration {
231    std::time::Duration::from_secs_f64(raw as f64 / timescale as f64)
232}
233
234/// Convert a (possibly negative) composition offset in `timescale` units into a
235/// signed duration.
236fn raw2signed_dur(raw: i32, timescale: u32) -> chrono::Duration {
237    let nanos = (raw as i64 * 1_000_000_000i64) / timescale as i64;
238    chrono::Duration::nanoseconds(nanos)
239}
240
241/// Per-sample composition-time offset (ctts), expanded from the run-length
242/// `ctts` box into one entry per sample. Returns an empty vec when there is no
243/// `ctts` box (i.e. no B-frame reordering, so all offsets are zero); callers
244/// treat a missing index as offset 0.
245fn composition_offsets(track: &mp4::Mp4Track) -> Vec<i32> {
246    let mut offsets = Vec::new();
247    if let Some(ctts) = track.trak.mdia.minf.stbl.ctts.as_ref() {
248        for entry in &ctts.entries {
249            for _ in 0..entry.sample_count {
250                offsets.push(entry.sample_offset);
251            }
252        }
253    }
254    offsets
255}
256
257#[test]
258fn test_raw_duration() {
259    const TIMESCALE: u32 = 90_000;
260    fn dur2raw(dur: &std::time::Duration) -> u64 {
261        (dur.as_secs_f64() * TIMESCALE as f64).round() as u64
262    }
263
264    fn roundtrip(orig: u64) {
265        let actual = dur2raw(&raw2dur(orig, TIMESCALE));
266        assert_eq!(orig, actual);
267    }
268    roundtrip(0);
269    roundtrip(100);
270    roundtrip(1_000_000);
271    roundtrip(1_000_000_000);
272    roundtrip(1_000_000_000_000);
273}