1use 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Mp4TrackFormat {
13 Video(VideoDimensions),
14 Audio { channels: u16 },
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum CoverArtFormat {
20 Jpeg,
21 Png,
22}
23
24impl CoverArtFormat {
25 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#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CoverArt {
47 pub format: CoverArtFormat,
48 pub data: Vec<u8>,
49}
50
51#[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
86pub 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 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 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 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
380fn 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 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}