Skip to main content

zvidlib_container/
mp4.rs

1//! Deterministic seekable ISO BMFF/MP4 muxing.
2
3use crate::codec::{AudioGapless, EncodedSample, EncoderConfig, SampleDependency, TrackKind};
4use crate::io::ByteSink;
5use crate::media::{Codec, VideoDimensions};
6use crate::{Error, ErrorKind, Result};
7
8const MOVIE_TIMESCALE: u32 = 1_000;
9
10/// Media-specific fields used to create an MP4 sample entry.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Mp4TrackFormat {
13    Video(VideoDimensions),
14    Audio { channels: u16 },
15}
16
17/// Image encoding of an MP4 cover-art picture.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum CoverArtFormat {
20    Jpeg,
21    Png,
22}
23
24impl CoverArtFormat {
25    /// The iTunes metadata `data` box type indicator for this format.
26    pub(crate) fn data_type(self) -> u32 {
27        match self {
28            Self::Jpeg => 13,
29            Self::Png => 14,
30        }
31    }
32
33    pub(crate) fn from_data_type(data_type: u32) -> Option<Self> {
34        match data_type {
35            13 => Some(Self::Jpeg),
36            14 => Some(Self::Png),
37            _ => None,
38        }
39    }
40}
41
42/// An already-encoded cover-art image, written as iTunes-style
43/// `moov/udta/meta/ilst/covr` metadata that file browsers and players show as
44/// the file's thumbnail without decoding any video.
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CoverArt {
47    pub format: CoverArtFormat,
48    pub data: Vec<u8>,
49}
50
51/// A track declaration passed to [`Mp4Muxer`].
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct Mp4TrackConfig {
54    pub encoder: EncoderConfig,
55    pub format: Mp4TrackFormat,
56}
57
58impl Mp4TrackConfig {
59    pub fn kind(&self) -> TrackKind {
60        match self.format {
61            Mp4TrackFormat::Video(_) => TrackKind::Video,
62            Mp4TrackFormat::Audio { .. } => TrackKind::Audio,
63        }
64    }
65}
66
67#[derive(Clone, Debug)]
68struct SampleRecord {
69    offset: u64,
70    size: u32,
71    dts: i64,
72    pts: i64,
73    duration: u32,
74    is_sync: bool,
75    dependency: SampleDependency,
76}
77
78#[derive(Clone, Debug)]
79struct TrackState {
80    id: u32,
81    config: Mp4TrackConfig,
82    samples: Vec<SampleRecord>,
83    gapless: AudioGapless,
84}
85
86/// A seekable MP4 muxer that writes payload bytes immediately.
87///
88/// Each `write_sample` awaits the underlying sink before returning, so a slow
89/// sink naturally applies backpressure without an unbounded packet queue.
90pub struct Mp4Muxer<S> {
91    sink: S,
92    tracks: Vec<TrackState>,
93    mdat_start: u64,
94    max_samples_per_track: usize,
95    cover_art: Option<CoverArt>,
96    finished: bool,
97}
98
99impl<S: ByteSink> Mp4Muxer<S> {
100    pub async fn new(
101        mut sink: S,
102        configs: Vec<Mp4TrackConfig>,
103        max_samples_per_track: usize,
104    ) -> Result<Self> {
105        if !sink.is_seekable() {
106            return Err(Error::new(
107                ErrorKind::Unsupported,
108                "ordinary MP4 output requires a seekable sink",
109            ));
110        }
111        if configs.is_empty() {
112            return Err(invalid("an MP4 must declare at least one track"));
113        }
114        if max_samples_per_track == 0 {
115            return Err(invalid("the MP4 sample limit must be nonzero"));
116        }
117        for config in &configs {
118            validate_track_config(config)?;
119        }
120
121        sink.write(&ftyp_box()?).await?;
122        let mdat_start = sink.position();
123        let mut header = Vec::with_capacity(16);
124        header.extend_from_slice(&1_u32.to_be_bytes());
125        header.extend_from_slice(b"mdat");
126        header.extend_from_slice(&0_u64.to_be_bytes());
127        sink.write(&header).await?;
128
129        let tracks = configs
130            .into_iter()
131            .enumerate()
132            .map(|(index, config)| {
133                Ok(TrackState {
134                    id: u32::try_from(index + 1).map_err(|_| limit("too many MP4 tracks"))?,
135                    config,
136                    samples: Vec::new(),
137                    gapless: AudioGapless::default(),
138                })
139            })
140            .collect::<Result<Vec<_>>>()?;
141        Ok(Self {
142            sink,
143            tracks,
144            mdat_start,
145            max_samples_per_track,
146            cover_art: None,
147            finished: false,
148        })
149    }
150
151    pub fn track_count(&self) -> usize {
152        self.tracks.len()
153    }
154
155    pub async fn write_sample(&mut self, track: usize, sample: EncodedSample) -> Result<()> {
156        if self.finished {
157            return Err(invalid_state("cannot write an MP4 after finish"));
158        }
159        let state = self
160            .tracks
161            .get_mut(track)
162            .ok_or_else(|| invalid("MP4 track index is out of range"))?;
163        if state.samples.len() >= self.max_samples_per_track {
164            return Err(Error::new(
165                ErrorKind::ResourceLimit,
166                "MP4 track sample limit exceeded",
167            ));
168        }
169        validate_sample(state, &sample)?;
170        let size = u32::try_from(sample.data.len()).map_err(|_| {
171            Error::new(
172                ErrorKind::ResourceLimit,
173                "encoded sample is too large for MP4",
174            )
175        })?;
176        let record = SampleRecord {
177            offset: self.sink.position(),
178            size,
179            dts: sample.dts,
180            pts: sample.pts,
181            duration: sample.duration,
182            is_sync: sample.is_sync,
183            dependency: sample.dependency,
184        };
185        self.sink.write(&sample.data).await?;
186        state.samples.push(record);
187        Ok(())
188    }
189
190    pub fn set_audio_gapless(&mut self, track: usize, gapless: AudioGapless) -> Result<()> {
191        let state = self
192            .tracks
193            .get_mut(track)
194            .ok_or_else(|| invalid("MP4 track index is out of range"))?;
195        if state.config.kind() != TrackKind::Audio {
196            return Err(invalid("gapless metadata belongs to an audio track"));
197        }
198        state.gapless = gapless;
199        Ok(())
200    }
201
202    /// Sets or clears the cover art that [`Mp4Muxer::finish`] writes into the
203    /// movie box. It may be set any time before `finish`, so a recorder can
204    /// pick a frame after capture. Without cover art, output is unchanged.
205    pub fn set_cover_art(&mut self, cover_art: Option<CoverArt>) -> Result<()> {
206        if let Some(cover_art) = &cover_art {
207            validate_cover_art(cover_art)?;
208        }
209        self.cover_art = cover_art;
210        Ok(())
211    }
212
213    pub async fn finish(mut self) -> Result<S> {
214        if self.finished {
215            return Err(invalid_state("MP4 muxer was already finished"));
216        }
217        validate_gapless(&self.tracks)?;
218        let payload_end = self.sink.position();
219        let mdat_size = payload_end
220            .checked_sub(self.mdat_start)
221            .ok_or_else(|| internal("sink position moved before the MP4 media-data box"))?;
222        self.sink.seek(self.mdat_start + 8).await?;
223        self.sink.write(&mdat_size.to_be_bytes()).await?;
224        self.sink.seek(payload_end).await?;
225        let moov = moov_box(&self.tracks, self.cover_art.as_ref())?;
226        self.sink.write(&moov).await?;
227        self.sink.flush().await?;
228        self.finished = true;
229        Ok(self.sink)
230    }
231}
232
233fn validate_track_config(config: &Mp4TrackConfig) -> Result<()> {
234    let expected_config = match config.encoder.codec {
235        Codec::UncompressedVideo => {
236            return Err(invalid(
237                "the uncompressed conformance codec is not an MP4 output codec",
238            ));
239        }
240        Codec::H264 => b"avcC",
241        Codec::Hevc => b"hvcC",
242        Codec::Av1 => b"av1C",
243        Codec::Aac => b"esds",
244        Codec::Opus => b"dOps",
245        Codec::Vp9 => b"vpcC",
246        // VP8-in-MP4 is not a standard mapping; VP8 is carried in WebM.
247        Codec::Vp8 => return Err(invalid("VP8 is not an MP4 output codec")),
248        Codec::Vorbis => {
249            return Err(Error::new(
250                ErrorKind::Unsupported,
251                "Vorbis has no widely supported MP4 mapping; write it to a WebM container instead",
252            ));
253        }
254        _ => {
255            return Err(Error::new(
256                ErrorKind::Unsupported,
257                "unsupported MP4 output codec",
258            ));
259        }
260    };
261    if config.encoder.timescale == 0 {
262        return Err(invalid("an MP4 track timescale must be nonzero"));
263    }
264    if config.encoder.decoder_config.len() < 8 {
265        return Err(invalid(
266            "codec configuration must contain a complete MP4 box",
267        ));
268    }
269    let declared = u32::from_be_bytes(
270        config.encoder.decoder_config[..4]
271            .try_into()
272            .map_err(|_| invalid("invalid codec configuration box"))?,
273    );
274    if usize::try_from(declared).ok() != Some(config.encoder.decoder_config.len()) {
275        return Err(invalid("codec configuration box size is inconsistent"));
276    }
277    if &config.encoder.decoder_config[4..8] != expected_config {
278        return Err(invalid("codec configuration box type is incompatible"));
279    }
280    match (config.encoder.codec, config.format) {
281        (Codec::H264 | Codec::Hevc | Codec::Av1 | Codec::Vp9, Mp4TrackFormat::Video(_))
282        | (Codec::Aac, Mp4TrackFormat::Audio { .. }) => Ok(()),
283        (Codec::Opus, Mp4TrackFormat::Audio { channels }) => {
284            // "Encapsulation of Opus in ISO Base Media File Format" section
285            // 4.3: the sample entry's rate is always 48 kHz, and the media
286            // timescale should be too, so packet durations are exact.
287            if config.encoder.timescale != crate::OPUS_SAMPLE_RATE {
288                return Err(invalid("an Opus track's timescale must be 48000"));
289            }
290            let head = crate::OpusHead::from_dops(&config.encoder.decoder_config)?;
291            if u16::from(head.channels) != channels {
292                return Err(invalid(
293                    "the Opus dOps channel count disagrees with the track format",
294                ));
295            }
296            Ok(())
297        }
298        _ => Err(invalid("codec and MP4 track kind are incompatible")),
299    }
300}
301
302fn validate_cover_art(cover_art: &CoverArt) -> Result<()> {
303    if cover_art.data.is_empty() {
304        return Err(invalid("cover art data must be nonempty"));
305    }
306    Ok(())
307}
308
309fn validate_sample(track: &TrackState, sample: &EncodedSample) -> Result<()> {
310    if sample.duration == 0 {
311        return Err(invalid("encoded sample duration must be nonzero"));
312    }
313    if sample.data.is_empty() {
314        return Err(invalid("encoded sample data must be nonempty"));
315    }
316    if sample.dependency.is_leading > 3
317        || sample.dependency.depends_on > 3
318        || sample.dependency.is_depended_on > 3
319        || sample.dependency.has_redundancy > 3
320    {
321        return Err(invalid("sample dependency fields must fit in two bits"));
322    }
323    if sample.dts < 0 {
324        return Err(invalid("negative decode timestamps are not supported"));
325    }
326    let offset = sample
327        .pts
328        .checked_sub(sample.dts)
329        .ok_or_else(|| invalid("composition offset overflow"))?;
330    i32::try_from(offset).map_err(|_| invalid("composition offset exceeds MP4 version 1 range"))?;
331    if let Some(previous) = track.samples.last() {
332        let expected = previous
333            .dts
334            .checked_add(i64::from(previous.duration))
335            .ok_or_else(|| invalid("decode timestamp overflow"))?;
336        if sample.dts != expected {
337            return Err(invalid(
338                "encoded samples must have contiguous decode timestamps",
339            ));
340        }
341    } else if sample.dts != 0 {
342        return Err(invalid("the first encoded sample must start at DTS zero"));
343    }
344    Ok(())
345}
346
347fn validate_gapless(tracks: &[TrackState]) -> Result<()> {
348    for track in tracks {
349        if track.config.kind() != TrackKind::Audio {
350            continue;
351        }
352        let duration = track_duration(track)?;
353        let trim = u64::from(track.gapless.priming) + u64::from(track.gapless.padding);
354        if trim > duration {
355            return Err(invalid("audio priming and padding exceed encoded duration"));
356        }
357    }
358    Ok(())
359}
360
361fn ftyp_box() -> Result<Vec<u8>> {
362    let mut payload = Vec::new();
363    payload.extend_from_slice(b"isom");
364    payload.extend_from_slice(&0x200_u32.to_be_bytes());
365    payload.extend_from_slice(b"isomiso6mp41");
366    make_box(*b"ftyp", payload)
367}
368
369fn moov_box(tracks: &[TrackState], cover_art: Option<&CoverArt>) -> Result<Vec<u8>> {
370    let mut payload = mvhd_box(tracks)?;
371    for track in tracks {
372        payload.extend_from_slice(&trak_box(track)?);
373    }
374    if let Some(cover_art) = cover_art {
375        payload.extend_from_slice(&udta_box(cover_art)?);
376    }
377    make_box(*b"moov", payload)
378}
379
380/// `udta` > `meta` > `ilst` > `covr` > `data`, the iTunes metadata layout that
381/// Explorer, Finder, and most players read cover art from.
382fn udta_box(cover_art: &CoverArt) -> Result<Vec<u8>> {
383    let mut data = cover_art.format.data_type().to_be_bytes().to_vec();
384    data.extend_from_slice(&0_u32.to_be_bytes());
385    data.extend_from_slice(&cover_art.data);
386    let ilst = make_box(*b"ilst", make_box(*b"covr", make_box(*b"data", data)?)?)?;
387
388    let mut hdlr = vec![0; 4];
389    hdlr.extend_from_slice(b"mdir");
390    hdlr.extend_from_slice(b"appl");
391    hdlr.extend_from_slice(&[0; 8]);
392    hdlr.push(0);
393    let mut meta = full_box(*b"hdlr", 0, 0, hdlr)?;
394    meta.extend_from_slice(&ilst);
395    make_box(*b"udta", full_box(*b"meta", 0, 0, meta)?)
396}
397
398fn mvhd_box(tracks: &[TrackState]) -> Result<Vec<u8>> {
399    let duration = tracks.iter().try_fold(0_u64, |maximum, track| {
400        Ok::<_, Error>(maximum.max(presentation_duration(track, MOVIE_TIMESCALE)?))
401    })?;
402    let mut body = Vec::new();
403    body.extend_from_slice(&0_u32.to_be_bytes());
404    body.extend_from_slice(&0_u32.to_be_bytes());
405    body.extend_from_slice(&MOVIE_TIMESCALE.to_be_bytes());
406    body.extend_from_slice(&u32_checked(duration, "movie duration")?.to_be_bytes());
407    body.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
408    body.extend_from_slice(&0x0100_u16.to_be_bytes());
409    body.extend_from_slice(&[0; 10]);
410    append_identity_matrix(&mut body);
411    body.extend_from_slice(&[0; 24]);
412    body.extend_from_slice(
413        &u32::try_from(tracks.len() + 1)
414            .map_err(|_| limit("too many MP4 tracks"))?
415            .to_be_bytes(),
416    );
417    full_box(*b"mvhd", 0, 0, body)
418}
419
420fn trak_box(track: &TrackState) -> Result<Vec<u8>> {
421    let mut payload = tkhd_box(track)?;
422    if track.config.kind() == TrackKind::Audio
423        && (track.gapless.priming != 0 || track.gapless.padding != 0)
424    {
425        payload.extend_from_slice(&edts_box(track)?);
426    }
427    payload.extend_from_slice(&mdia_box(track)?);
428    make_box(*b"trak", payload)
429}
430
431fn tkhd_box(track: &TrackState) -> Result<Vec<u8>> {
432    let mut body = Vec::new();
433    body.extend_from_slice(&0_u32.to_be_bytes());
434    body.extend_from_slice(&0_u32.to_be_bytes());
435    body.extend_from_slice(&track.id.to_be_bytes());
436    body.extend_from_slice(&0_u32.to_be_bytes());
437    let duration = presentation_duration(track, MOVIE_TIMESCALE)?;
438    body.extend_from_slice(&u32_checked(duration, "track movie duration")?.to_be_bytes());
439    body.extend_from_slice(&[0; 8]);
440    body.extend_from_slice(&0_u16.to_be_bytes());
441    body.extend_from_slice(&0_u16.to_be_bytes());
442    let volume = if track.config.kind() == TrackKind::Audio {
443        0x0100_u16
444    } else {
445        0
446    };
447    body.extend_from_slice(&volume.to_be_bytes());
448    body.extend_from_slice(&0_u16.to_be_bytes());
449    append_identity_matrix(&mut body);
450    let (width, height) = match track.config.format {
451        Mp4TrackFormat::Video(dimensions) => (dimensions.width, dimensions.height),
452        Mp4TrackFormat::Audio { .. } => (0, 0),
453    };
454    body.extend_from_slice(
455        &width
456            .checked_shl(16)
457            .ok_or_else(|| limit("MP4 width overflow"))?
458            .to_be_bytes(),
459    );
460    body.extend_from_slice(
461        &height
462            .checked_shl(16)
463            .ok_or_else(|| limit("MP4 height overflow"))?
464            .to_be_bytes(),
465    );
466    full_box(*b"tkhd", 0, 7, body)
467}
468
469fn edts_box(track: &TrackState) -> Result<Vec<u8>> {
470    let presentation = presentation_duration(track, MOVIE_TIMESCALE)?;
471    let media_time = i32::try_from(track.gapless.priming)
472        .map_err(|_| limit("audio priming exceeds MP4 edit range"))?;
473    let mut body = Vec::new();
474    body.extend_from_slice(&1_u32.to_be_bytes());
475    body.extend_from_slice(&u32_checked(presentation, "audio edit duration")?.to_be_bytes());
476    body.extend_from_slice(&media_time.to_be_bytes());
477    body.extend_from_slice(&1_i16.to_be_bytes());
478    body.extend_from_slice(&0_i16.to_be_bytes());
479    make_box(*b"edts", full_box(*b"elst", 0, 0, body)?)
480}
481
482fn mdia_box(track: &TrackState) -> Result<Vec<u8>> {
483    let mut payload = mdhd_box(track)?;
484    payload.extend_from_slice(&hdlr_box(track.config.kind())?);
485    payload.extend_from_slice(&minf_box(track)?);
486    make_box(*b"mdia", payload)
487}
488
489fn mdhd_box(track: &TrackState) -> Result<Vec<u8>> {
490    let mut body = Vec::new();
491    body.extend_from_slice(&0_u32.to_be_bytes());
492    body.extend_from_slice(&0_u32.to_be_bytes());
493    body.extend_from_slice(&track.config.encoder.timescale.to_be_bytes());
494    body.extend_from_slice(&u32_checked(track_duration(track)?, "media duration")?.to_be_bytes());
495    body.extend_from_slice(&0x55c4_u16.to_be_bytes());
496    body.extend_from_slice(&0_u16.to_be_bytes());
497    full_box(*b"mdhd", 0, 0, body)
498}
499
500fn hdlr_box(kind: TrackKind) -> Result<Vec<u8>> {
501    let mut body = vec![0; 4];
502    body.extend_from_slice(match kind {
503        TrackKind::Video => b"vide",
504        TrackKind::Audio => b"soun",
505    });
506    body.extend_from_slice(&[0; 12]);
507    body.extend_from_slice(match kind {
508        TrackKind::Video => b"zvidlib video\0",
509        TrackKind::Audio => b"zvidlib audio\0",
510    });
511    full_box(*b"hdlr", 0, 0, body)
512}
513
514fn minf_box(track: &TrackState) -> Result<Vec<u8>> {
515    let header = match track.config.kind() {
516        TrackKind::Video => {
517            let mut body = Vec::new();
518            body.extend_from_slice(&0_u16.to_be_bytes());
519            body.extend_from_slice(&[0; 6]);
520            full_box(*b"vmhd", 0, 1, body)?
521        }
522        TrackKind::Audio => {
523            let mut body = Vec::new();
524            body.extend_from_slice(&0_i16.to_be_bytes());
525            body.extend_from_slice(&0_u16.to_be_bytes());
526            full_box(*b"smhd", 0, 0, body)?
527        }
528    };
529    let mut payload = header;
530    payload.extend_from_slice(&dinf_box()?);
531    payload.extend_from_slice(&stbl_box(track)?);
532    make_box(*b"minf", payload)
533}
534
535fn dinf_box() -> Result<Vec<u8>> {
536    let url = full_box(*b"url ", 0, 1, Vec::new())?;
537    let mut dref_body = 1_u32.to_be_bytes().to_vec();
538    dref_body.extend_from_slice(&url);
539    make_box(*b"dinf", full_box(*b"dref", 0, 0, dref_body)?)
540}
541
542fn stbl_box(track: &TrackState) -> Result<Vec<u8>> {
543    let mut payload = stsd_box(track)?;
544    payload.extend_from_slice(&stts_box(track)?);
545    if track.samples.iter().any(|sample| sample.pts != sample.dts) {
546        payload.extend_from_slice(&ctts_box(track)?);
547    }
548    payload.extend_from_slice(&stsc_box(track)?);
549    payload.extend_from_slice(&stsz_box(track)?);
550    payload.extend_from_slice(&co64_box(track)?);
551    if track.config.kind() == TrackKind::Video {
552        payload.extend_from_slice(&stss_box(track)?);
553    }
554    payload.extend_from_slice(&sdtp_box(track)?);
555    make_box(*b"stbl", payload)
556}
557
558fn stsd_box(track: &TrackState) -> Result<Vec<u8>> {
559    let entry = match track.config.format {
560        Mp4TrackFormat::Video(dimensions) => video_sample_entry(track, dimensions)?,
561        Mp4TrackFormat::Audio { channels } => audio_sample_entry(track, channels)?,
562    };
563    let mut body = 1_u32.to_be_bytes().to_vec();
564    body.extend_from_slice(&entry);
565    full_box(*b"stsd", 0, 0, body)
566}
567
568fn video_sample_entry(track: &TrackState, dimensions: VideoDimensions) -> Result<Vec<u8>> {
569    let mut body = vec![0; 6];
570    body.extend_from_slice(&1_u16.to_be_bytes());
571    body.extend_from_slice(&[0; 16]);
572    body.extend_from_slice(
573        &u16::try_from(dimensions.width)
574            .map_err(|_| limit("MP4 video width exceeds u16"))?
575            .to_be_bytes(),
576    );
577    body.extend_from_slice(
578        &u16::try_from(dimensions.height)
579            .map_err(|_| limit("MP4 video height exceeds u16"))?
580            .to_be_bytes(),
581    );
582    body.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
583    body.extend_from_slice(&0x0048_0000_u32.to_be_bytes());
584    body.extend_from_slice(&0_u32.to_be_bytes());
585    body.extend_from_slice(&1_u16.to_be_bytes());
586    body.extend_from_slice(&[0; 32]);
587    body.extend_from_slice(&0x0018_u16.to_be_bytes());
588    body.extend_from_slice(&u16::MAX.to_be_bytes());
589    body.extend_from_slice(&track.config.encoder.decoder_config);
590    make_box(
591        match track.config.encoder.codec {
592            Codec::UncompressedVideo => {
593                return Err(internal(
594                    "uncompressed conformance codec used for an MP4 video sample entry",
595                ));
596            }
597            Codec::H264 => *b"avc1",
598            Codec::Hevc => *b"hvc1",
599            Codec::Av1 => *b"av01",
600            Codec::Vp9 => *b"vp09",
601            Codec::Aac | Codec::Opus | Codec::Vorbis => {
602                return Err(internal("audio codec used for a video sample entry"));
603            }
604            Codec::Vp8 => return Err(internal("VP8 used for an MP4 video sample entry")),
605            _ => {
606                return Err(internal(
607                    "unsupported codec used for an MP4 video sample entry",
608                ));
609            }
610        },
611        body,
612    )
613}
614
615fn audio_sample_entry(track: &TrackState, channels: u16) -> Result<Vec<u8>> {
616    let mut body = vec![0; 6];
617    body.extend_from_slice(&1_u16.to_be_bytes());
618    body.extend_from_slice(&[0; 8]);
619    body.extend_from_slice(&channels.to_be_bytes());
620    body.extend_from_slice(&16_u16.to_be_bytes());
621    body.extend_from_slice(&0_u16.to_be_bytes());
622    body.extend_from_slice(&0_u16.to_be_bytes());
623    body.extend_from_slice(
624        &track
625            .config
626            .encoder
627            .timescale
628            .checked_shl(16)
629            .ok_or_else(|| limit("MP4 audio sample rate overflow"))?
630            .to_be_bytes(),
631    );
632    body.extend_from_slice(&track.config.encoder.decoder_config);
633    make_box(
634        match track.config.encoder.codec {
635            Codec::Aac => *b"mp4a",
636            Codec::Opus => *b"Opus",
637            _ => {
638                return Err(internal(
639                    "non-MP4 audio codec used for an audio sample entry",
640                ));
641            }
642        },
643        body,
644    )
645}
646
647fn stts_box(track: &TrackState) -> Result<Vec<u8>> {
648    let groups = run_lengths(track.samples.iter().map(|sample| sample.duration));
649    let mut body = u32_checked(groups.len() as u64, "stts entry count")?
650        .to_be_bytes()
651        .to_vec();
652    for (count, duration) in groups {
653        body.extend_from_slice(&count.to_be_bytes());
654        body.extend_from_slice(&duration.to_be_bytes());
655    }
656    full_box(*b"stts", 0, 0, body)
657}
658
659fn ctts_box(track: &TrackState) -> Result<Vec<u8>> {
660    let mut offsets = Vec::with_capacity(track.samples.len());
661    for sample in &track.samples {
662        offsets.push(
663            i32::try_from(sample.pts - sample.dts)
664                .map_err(|_| invalid("composition offset exceeds MP4 range"))?,
665        );
666    }
667    let groups = run_lengths(offsets.into_iter());
668    let mut body = u32_checked(groups.len() as u64, "ctts entry count")?
669        .to_be_bytes()
670        .to_vec();
671    for (count, offset) in groups {
672        body.extend_from_slice(&count.to_be_bytes());
673        body.extend_from_slice(&offset.to_be_bytes());
674    }
675    full_box(*b"ctts", 1, 0, body)
676}
677
678fn stsc_box(track: &TrackState) -> Result<Vec<u8>> {
679    let entry_count = u32::from(!track.samples.is_empty());
680    let mut body = entry_count.to_be_bytes().to_vec();
681    if !track.samples.is_empty() {
682        body.extend_from_slice(&1_u32.to_be_bytes());
683        body.extend_from_slice(&1_u32.to_be_bytes());
684        body.extend_from_slice(&1_u32.to_be_bytes());
685    }
686    full_box(*b"stsc", 0, 0, body)
687}
688
689fn stsz_box(track: &TrackState) -> Result<Vec<u8>> {
690    let mut body = 0_u32.to_be_bytes().to_vec();
691    body.extend_from_slice(&u32_checked(track.samples.len() as u64, "sample count")?.to_be_bytes());
692    for sample in &track.samples {
693        body.extend_from_slice(&sample.size.to_be_bytes());
694    }
695    full_box(*b"stsz", 0, 0, body)
696}
697
698fn co64_box(track: &TrackState) -> Result<Vec<u8>> {
699    let mut body = u32_checked(track.samples.len() as u64, "chunk count")?
700        .to_be_bytes()
701        .to_vec();
702    for sample in &track.samples {
703        body.extend_from_slice(&sample.offset.to_be_bytes());
704    }
705    full_box(*b"co64", 0, 0, body)
706}
707
708fn stss_box(track: &TrackState) -> Result<Vec<u8>> {
709    let sync: Vec<_> = track
710        .samples
711        .iter()
712        .enumerate()
713        .filter(|(_, sample)| sample.is_sync)
714        .map(|(index, _)| u32::try_from(index + 1).map_err(|_| limit("sync sample index overflow")))
715        .collect::<Result<_>>()?;
716    let mut body = u32_checked(sync.len() as u64, "sync sample count")?
717        .to_be_bytes()
718        .to_vec();
719    for index in sync {
720        body.extend_from_slice(&index.to_be_bytes());
721    }
722    full_box(*b"stss", 0, 0, body)
723}
724
725fn sdtp_box(track: &TrackState) -> Result<Vec<u8>> {
726    full_box(
727        *b"sdtp",
728        0,
729        0,
730        track
731            .samples
732            .iter()
733            .map(|sample| sample.dependency.to_sdtp())
734            .collect(),
735    )
736}
737
738fn track_duration(track: &TrackState) -> Result<u64> {
739    let Some(last) = track.samples.last() else {
740        return Ok(0);
741    };
742    let end = last
743        .dts
744        .checked_add(i64::from(last.duration))
745        .ok_or_else(|| limit("track duration overflow"))?;
746    u64::try_from(end).map_err(|_| invalid("track duration is negative"))
747}
748
749fn presentation_duration(track: &TrackState, timescale: u32) -> Result<u64> {
750    let mut duration = presentation_end(track)?;
751    if track.config.kind() == TrackKind::Audio {
752        duration = duration
753            .checked_sub(u64::from(track.gapless.priming) + u64::from(track.gapless.padding))
754            .ok_or_else(|| invalid("audio gapless trim exceeds its duration"))?;
755    }
756    scale_duration(duration, track.config.encoder.timescale, timescale)
757}
758
759fn presentation_end(track: &TrackState) -> Result<u64> {
760    track.samples.iter().try_fold(0_u64, |maximum, sample| {
761        let end = sample
762            .pts
763            .checked_add(i64::from(sample.duration))
764            .ok_or_else(|| limit("presentation duration overflow"))?;
765        let end = u64::try_from(end)
766            .map_err(|_| invalid("encoded presentation ends before time zero"))?;
767        Ok(maximum.max(end))
768    })
769}
770
771fn scale_duration(value: u64, source: u32, target: u32) -> Result<u64> {
772    let scaled = u128::from(value)
773        .checked_mul(u128::from(target))
774        .ok_or_else(|| limit("MP4 duration scaling overflow"))?
775        / u128::from(source);
776    u64::try_from(scaled).map_err(|_| limit("MP4 duration cannot be represented"))
777}
778
779fn run_lengths<T: Copy + Eq>(values: impl Iterator<Item = T>) -> Vec<(u32, T)> {
780    let mut groups: Vec<(u32, T)> = Vec::new();
781    for value in values {
782        if let Some((count, previous)) = groups.last_mut()
783            && *previous == value
784            && *count < u32::MAX
785        {
786            *count += 1;
787        } else {
788            groups.push((1, value));
789        }
790    }
791    groups
792}
793
794fn append_identity_matrix(output: &mut Vec<u8>) {
795    output.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
796    output.extend_from_slice(&0_u32.to_be_bytes());
797    output.extend_from_slice(&0_u32.to_be_bytes());
798    output.extend_from_slice(&0_u32.to_be_bytes());
799    output.extend_from_slice(&0x0001_0000_u32.to_be_bytes());
800    output.extend_from_slice(&0_u32.to_be_bytes());
801    output.extend_from_slice(&0_u32.to_be_bytes());
802    output.extend_from_slice(&0_u32.to_be_bytes());
803    output.extend_from_slice(&0x4000_0000_u32.to_be_bytes());
804}
805
806fn full_box(kind: [u8; 4], version: u8, flags: u32, mut body: Vec<u8>) -> Result<Vec<u8>> {
807    if flags > 0x00ff_ffff {
808        return Err(internal("full-box flags exceed 24 bits"));
809    }
810    let mut payload = Vec::with_capacity(body.len() + 4);
811    payload.push(version);
812    payload.extend_from_slice(&flags.to_be_bytes()[1..]);
813    payload.append(&mut body);
814    make_box(kind, payload)
815}
816
817fn make_box(kind: [u8; 4], payload: Vec<u8>) -> Result<Vec<u8>> {
818    let size = payload
819        .len()
820        .checked_add(8)
821        .ok_or_else(|| limit("MP4 box size overflow"))?;
822    let mut output = Vec::with_capacity(size);
823    output.extend_from_slice(
824        &u32::try_from(size)
825            .map_err(|_| limit("MP4 box exceeds 32-bit size"))?
826            .to_be_bytes(),
827    );
828    output.extend_from_slice(&kind);
829    output.extend_from_slice(&payload);
830    Ok(output)
831}
832
833fn u32_checked(value: u64, label: &str) -> Result<u32> {
834    u32::try_from(value).map_err(|_| limit(format!("{label} exceeds MP4 version 0 range")))
835}
836
837fn invalid(message: impl Into<String>) -> Error {
838    Error::new(ErrorKind::InvalidInput, message)
839}
840
841fn invalid_state(message: impl Into<String>) -> Error {
842    Error::new(ErrorKind::InvalidState, message)
843}
844
845fn limit(message: impl Into<String>) -> Error {
846    Error::new(ErrorKind::ResourceLimit, message)
847}
848
849fn internal(message: impl Into<String>) -> Error {
850    Error::new(ErrorKind::Internal, message)
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856    use crate::codec::EncoderConfig;
857    use crate::io::{IoFuture, NonSeekableSink};
858    use crate::media::{Codec, VideoDimensions};
859    use std::cell::Cell;
860    use std::future::Future;
861    use std::rc::Rc;
862    use std::task::{Context, Poll, Waker};
863
864    /// Wraps [`NonSeekableSink`] to record whether `write()` was ever called,
865    /// so the test can prove rejection happens before any bytes are written.
866    struct TrackingSink {
867        inner: NonSeekableSink,
868        wrote: Rc<Cell<bool>>,
869    }
870
871    impl ByteSink for TrackingSink {
872        fn position(&self) -> u64 {
873            self.inner.position()
874        }
875
876        fn is_seekable(&self) -> bool {
877            self.inner.is_seekable()
878        }
879
880        fn write<'a>(&'a mut self, bytes: &'a [u8]) -> IoFuture<'a, ()> {
881            self.wrote.set(true);
882            self.inner.write(bytes)
883        }
884
885        fn seek<'a>(&'a mut self, position: u64) -> IoFuture<'a, ()> {
886            self.inner.seek(position)
887        }
888    }
889
890    fn block_on<T>(future: impl Future<Output = T>) -> T {
891        let waker = Waker::noop();
892        let mut context = Context::from_waker(waker);
893        match Box::pin(future).as_mut().poll(&mut context) {
894            Poll::Ready(value) => value,
895            Poll::Pending => panic!("MP4 muxer test unexpectedly returned a pending future"),
896        }
897    }
898
899    fn codec_box(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
900        let mut output = u32::try_from(payload.len() + 8)
901            .unwrap()
902            .to_be_bytes()
903            .to_vec();
904        output.extend_from_slice(kind);
905        output.extend_from_slice(payload);
906        output
907    }
908
909    fn video_track_config() -> Mp4TrackConfig {
910        Mp4TrackConfig {
911            encoder: EncoderConfig {
912                codec: Codec::Av1,
913                timescale: 1_000,
914                decoder_config: codec_box(b"av1C", &[0x81, 0, 0, 0]),
915            },
916            format: Mp4TrackFormat::Video(VideoDimensions {
917                width: 16,
918                height: 16,
919            }),
920        }
921    }
922
923    #[test]
924    fn ordinary_mp4_creation_rejects_non_seekable_sink_before_writing() {
925        let wrote = Rc::new(Cell::new(false));
926        let sink = TrackingSink {
927            inner: NonSeekableSink::default(),
928            wrote: wrote.clone(),
929        };
930        let error = match block_on(Mp4Muxer::new(sink, vec![video_track_config()], 16)) {
931            Ok(_) => panic!("expected a non-seekable sink to be rejected"),
932            Err(error) => error,
933        };
934        assert_eq!(error.kind(), ErrorKind::Unsupported);
935        assert!(
936            !wrote.get(),
937            "sink must not be written to before the seekability check"
938        );
939    }
940}