Skip to main content

mp4_track/
writer.rs

1use crate::parse::validate_tree;
2use crate::*;
3use std::io::{Seek, SeekFrom, Write};
4
5/// A progressive MP4 writer.
6pub struct Mp4Writer<W: Write + Seek> {
7    writer: W,
8    config: WriterConfig,
9    tracks: Vec<WriterTrack>,
10    first_sample_written: bool,
11    current_track: Option<TrackId>,
12    ftyp_offset: u64,
13    free_offset: u64,
14    mdat_offset: u64,
15    data_offset: u64,
16}
17
18struct WriterTrack {
19    id: TrackId,
20    config: TrackConfig,
21    samples: Vec<WriterSample>,
22    chunks: Vec<WriterChunk>,
23    force_co64: bool,
24}
25
26#[derive(Clone, Copy)]
27struct WriterSample {
28    size: u32,
29    duration: u32,
30    cts_offset: i32,
31    is_sync: bool,
32}
33
34#[derive(Clone, Copy)]
35struct WriterChunk {
36    offset: u64,
37    sample_count: u32,
38}
39
40impl<W: Write + Seek> Mp4Writer<W> {
41    /// Creates a writer and emits the file-type and media-data headers.
42    pub fn new(mut writer: W, config: WriterConfig) -> Result<Self, Error> {
43        if config.movie_timescale == 0 {
44            return Err(Error::InvalidTimescale { track: None });
45        }
46        let ftyp_offset = writer.stream_position()?;
47        let ftyp = make_ftyp(&config, false)?;
48        writer.write_all(&ftyp)?;
49        let free_offset = writer.stream_position()?;
50        writer.write_all(&8u32.to_be_bytes())?;
51        writer.write_all(b"free")?;
52        let mdat_offset = writer.stream_position()?;
53        writer.write_all(&0u32.to_be_bytes())?;
54        writer.write_all(b"mdat")?;
55        let data_offset = writer.stream_position()?;
56        Ok(Self {
57            writer,
58            config,
59            tracks: Vec::new(),
60            first_sample_written: false,
61            current_track: None,
62            ftyp_offset,
63            free_offset,
64            mdat_offset,
65            data_offset,
66        })
67    }
68
69    /// Adds a track before the first sample and returns its identifier.
70    pub fn add_track(&mut self, track: TrackConfig) -> Result<TrackId, Error> {
71        if self.first_sample_written {
72            return Err(Error::WriterState("tracks must be added before samples"));
73        }
74        validate_track_config(&track)?;
75        let next = self
76            .tracks
77            .len()
78            .checked_add(1)
79            .ok_or(Error::WriterState("track count overflow"))?;
80        let id = TrackId(
81            u32::try_from(next).map_err(|_| Error::WriterState("track count exceeds u32"))?,
82        );
83        self.tracks.push(WriterTrack {
84            id,
85            config: track,
86            samples: Vec::new(),
87            chunks: Vec::new(),
88            force_co64: self.config.chunk_offsets == ChunkOffsetChoice::Co64,
89        });
90        Ok(id)
91    }
92
93    /// Writes one encoded sample.
94    pub fn write_sample(&mut self, id: TrackId, sample: &SampleInput<'_>) -> Result<(), Error> {
95        let position = self
96            .tracks
97            .iter()
98            .position(|track| track.id == id)
99            .ok_or(Error::NoSuchTrack(id))?;
100        let size = u32::try_from(sample.data.len()).map_err(|_| Error::LimitExceeded {
101            limit: "sample_size",
102            value: u64::try_from(sample.data.len()).unwrap_or(u64::MAX),
103            max: u64::from(u32::MAX),
104        })?;
105        let offset = self.writer.stream_position()?;
106        self.writer.write_all(sample.data)?;
107        let track = self
108            .tracks
109            .get_mut(position)
110            .ok_or(Error::NoSuchTrack(id))?;
111        if self.current_track != Some(id) {
112            track.chunks.push(WriterChunk {
113                offset,
114                sample_count: 0,
115            });
116            self.current_track = Some(id);
117        }
118        let chunk = track
119            .chunks
120            .last_mut()
121            .ok_or(Error::WriterState("sample chunk is missing"))?;
122        chunk.sample_count = chunk
123            .sample_count
124            .checked_add(1)
125            .ok_or(Error::WriterState("chunk sample count overflow"))?;
126        track.samples.push(WriterSample {
127            size,
128            duration: sample.duration,
129            cts_offset: sample.cts_offset,
130            is_sync: sample.is_sync,
131        });
132        self.first_sample_written = true;
133        Ok(())
134    }
135
136    /// Finalizes tables, fixes the media-data header, and returns the stream.
137    pub fn finish(mut self) -> Result<W, Error> {
138        if self.tracks.is_empty() {
139            return Err(Error::WriterState("at least one track is required"));
140        }
141        let media_end = self.writer.stream_position()?;
142        let payload_size = media_end
143            .checked_sub(self.data_offset)
144            .ok_or(Error::WriterState("media-data size underflow"))?;
145        if payload_size <= u64::from(u32::MAX).saturating_sub(8) {
146            let size = u32::try_from(payload_size.saturating_add(8))
147                .map_err(|_| Error::WriterState("media-data size exceeds u32"))?;
148            self.writer.seek(SeekFrom::Start(self.mdat_offset))?;
149            self.writer.write_all(&size.to_be_bytes())?;
150            self.writer.write_all(b"mdat")?;
151        } else {
152            let size = payload_size
153                .checked_add(16)
154                .ok_or(Error::WriterState("media-data size overflow"))?;
155            self.writer.seek(SeekFrom::Start(self.free_offset))?;
156            self.writer.write_all(&1u32.to_be_bytes())?;
157            self.writer.write_all(b"mdat")?;
158            self.writer.write_all(&size.to_be_bytes())?;
159        }
160        let has_avc = self
161            .tracks
162            .iter()
163            .any(|track| matches!(track.config.codec, Codec::H264 { .. }));
164        self.writer.seek(SeekFrom::Start(self.ftyp_offset))?;
165        self.writer.write_all(&make_ftyp(&self.config, has_avc)?)?;
166        self.writer.seek(SeekFrom::Start(media_end))?;
167        let moov = make_moov(&self.config, &self.tracks)?;
168        validate_tree(
169            moov.get(8..).ok_or(Error::WriterState(
170                "encoded movie is smaller than its header",
171            ))?,
172            FourCc(*b"moov"),
173            0,
174            Limits::default(),
175        )?;
176        self.writer.write_all(&moov)?;
177        self.writer.flush()?;
178        Ok(self.writer)
179    }
180}
181
182fn validate_track_config(track: &TrackConfig) -> Result<(), Error> {
183    if track.timescale == 0 {
184        return Err(Error::InvalidTimescale { track: None });
185    }
186    let bytes = track.language.as_bytes();
187    if bytes.len() != 3 || !bytes.iter().all(|byte| byte.is_ascii_lowercase()) {
188        return Err(Error::WriterState(
189            "language must be three lowercase ASCII letters",
190        ));
191    }
192    if !track.width.is_finite()
193        || !track.height.is_finite()
194        || track.width < 0.0
195        || track.height < 0.0
196    {
197        return Err(Error::WriterState(
198            "track dimensions must be finite and nonnegative",
199        ));
200    }
201    if track.width > 65_535.0 || track.height > 65_535.0 {
202        return Err(Error::WriterState("track dimensions exceed 16.16 range"));
203    }
204    Ok(())
205}
206
207fn make_box(kind: FourCc, body: Vec<u8>) -> Result<Vec<u8>, Error> {
208    let size = body
209        .len()
210        .checked_add(8)
211        .ok_or(Error::WriterState("box size overflow"))?;
212    let size = u32::try_from(size).map_err(|_| Error::WriterState("box exceeds u32 size"))?;
213    let mut result = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
214    result.extend_from_slice(&size.to_be_bytes());
215    result.extend_from_slice(&kind.0);
216    result.extend_from_slice(&body);
217    Ok(result)
218}
219
220fn full_box(version: u8, flags: u32, mut body: Vec<u8>) -> Vec<u8> {
221    let mut result = Vec::with_capacity(body.len().saturating_add(4));
222    result.push(version);
223    let bytes = flags.to_be_bytes();
224    result.extend_from_slice(bytes.get(1..).unwrap_or_default());
225    result.append(&mut body);
226    result
227}
228
229fn make_ftyp(config: &WriterConfig, has_avc: bool) -> Result<Vec<u8>, Error> {
230    let mut brands = config.compatible_brands.clone();
231    for required in [FourCc(*b"isom"), FourCc(*b"iso2"), FourCc(*b"mp41")] {
232        if !brands.contains(&required) {
233            brands.push(required);
234        }
235    }
236    brands.push(if has_avc {
237        FourCc(*b"avc1")
238    } else {
239        config.major_brand
240    });
241    let mut body = Vec::with_capacity(8usize.saturating_add(brands.len().saturating_mul(4)));
242    body.extend_from_slice(&config.major_brand.0);
243    body.extend_from_slice(&512u32.to_be_bytes());
244    for brand in brands {
245        body.extend_from_slice(&brand.0);
246    }
247    make_box(FourCc(*b"ftyp"), body)
248}
249
250fn make_moov(config: &WriterConfig, tracks: &[WriterTrack]) -> Result<Vec<u8>, Error> {
251    let movie_duration = tracks.iter().try_fold(0u64, |maximum, track| {
252        Ok::<u64, Error>(maximum.max(track_movie_duration(config.movie_timescale, track)?))
253    })?;
254    let mut body = make_mvhd(config.movie_timescale, movie_duration, tracks.len())?;
255    for track in tracks {
256        body.extend_from_slice(&make_trak(config.movie_timescale, track)?);
257    }
258    make_box(FourCc(*b"moov"), body)
259}
260
261fn track_media_duration(track: &WriterTrack) -> Result<u64, Error> {
262    track.samples.iter().try_fold(0u64, |sum, sample| {
263        sum.checked_add(u64::from(sample.duration))
264            .ok_or(Error::WriterState("track duration overflow"))
265    })
266}
267
268fn track_movie_duration(movie_timescale: u32, track: &WriterTrack) -> Result<u64, Error> {
269    if !track.config.edit_list.is_empty() {
270        return track.config.edit_list.iter().try_fold(0u64, |sum, edit| {
271            sum.checked_add(edit.segment_duration)
272                .ok_or(Error::WriterState("edit duration overflow"))
273        });
274    }
275    let media = u128::from(track_media_duration(track)?);
276    let scaled = media
277        .checked_mul(u128::from(movie_timescale))
278        .ok_or(Error::WriterState("movie duration overflow"))?
279        .checked_div(u128::from(track.config.timescale))
280        .ok_or(Error::InvalidTimescale {
281            track: Some(track.id),
282        })?;
283    u64::try_from(scaled).map_err(|_| Error::WriterState("movie duration exceeds u64"))
284}
285
286fn make_mvhd(timescale: u32, duration: u64, track_count: usize) -> Result<Vec<u8>, Error> {
287    let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
288    let mut body = Vec::new();
289    if version == 1 {
290        body.extend_from_slice(&0u64.to_be_bytes());
291        body.extend_from_slice(&0u64.to_be_bytes());
292        body.extend_from_slice(&timescale.to_be_bytes());
293        body.extend_from_slice(&duration.to_be_bytes());
294    } else {
295        body.extend_from_slice(&0u32.to_be_bytes());
296        body.extend_from_slice(&0u32.to_be_bytes());
297        body.extend_from_slice(&timescale.to_be_bytes());
298        body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
299    }
300    body.extend_from_slice(&0x0001_0000u32.to_be_bytes());
301    body.extend_from_slice(&0x0100u16.to_be_bytes());
302    body.extend_from_slice(&[0; 10]);
303    append_matrix(&mut body);
304    body.extend_from_slice(&[0; 24]);
305    let next = u32::try_from(track_count)
306        .unwrap_or(u32::MAX)
307        .saturating_add(1);
308    body.extend_from_slice(&next.to_be_bytes());
309    make_box(FourCc(*b"mvhd"), full_box(version, 0, body))
310}
311
312fn append_matrix(body: &mut Vec<u8>) {
313    for value in [0x0001_0000u32, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000] {
314        body.extend_from_slice(&value.to_be_bytes());
315    }
316}
317
318fn make_trak(movie_timescale: u32, track: &WriterTrack) -> Result<Vec<u8>, Error> {
319    let duration = track_movie_duration(movie_timescale, track)?;
320    let mut body = make_tkhd(duration, track)?;
321    if !track.config.edit_list.is_empty() {
322        body.extend_from_slice(&make_edts(&track.config.edit_list)?);
323    }
324    body.extend_from_slice(&make_mdia(track)?);
325    make_box(FourCc(*b"trak"), body)
326}
327
328fn fixed_16_16(value: f64) -> Result<u32, Error> {
329    let scaled = value * 65_536.0;
330    if !(0.0..=f64::from(u32::MAX)).contains(&scaled) {
331        return Err(Error::WriterState("fixed-point value is out of range"));
332    }
333    Ok(scaled.round() as u32)
334}
335
336fn make_tkhd(duration: u64, track: &WriterTrack) -> Result<Vec<u8>, Error> {
337    let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
338    let mut body = Vec::new();
339    if version == 1 {
340        body.extend_from_slice(&[0; 16]);
341        body.extend_from_slice(&track.id.0.to_be_bytes());
342        body.extend_from_slice(&0u32.to_be_bytes());
343        body.extend_from_slice(&duration.to_be_bytes());
344    } else {
345        body.extend_from_slice(&[0; 8]);
346        body.extend_from_slice(&track.id.0.to_be_bytes());
347        body.extend_from_slice(&0u32.to_be_bytes());
348        body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
349    }
350    body.extend_from_slice(&[0; 8]);
351    body.extend_from_slice(&0i16.to_be_bytes());
352    body.extend_from_slice(&0i16.to_be_bytes());
353    let volume = if track.config.kind == TrackKind::Audio {
354        0x0100u16
355    } else {
356        0
357    };
358    body.extend_from_slice(&volume.to_be_bytes());
359    body.extend_from_slice(&0u16.to_be_bytes());
360    append_matrix(&mut body);
361    body.extend_from_slice(&fixed_16_16(track.config.width)?.to_be_bytes());
362    body.extend_from_slice(&fixed_16_16(track.config.height)?.to_be_bytes());
363    make_box(FourCc(*b"tkhd"), full_box(version, 3, body))
364}
365
366fn make_edts(edits: &[Edit]) -> Result<Vec<u8>, Error> {
367    let version = if edits.iter().any(|edit| {
368        edit.segment_duration > u64::from(u32::MAX)
369            || edit
370                .media_time
371                .is_some_and(|value| value > i64::from(i32::MAX) as u64)
372    }) {
373        1
374    } else {
375        0
376    };
377    let mut body = Vec::new();
378    body.extend_from_slice(
379        &u32::try_from(edits.len())
380            .map_err(|_| Error::WriterState("edit count exceeds u32"))?
381            .to_be_bytes(),
382    );
383    for edit in edits {
384        if version == 1 {
385            body.extend_from_slice(&edit.segment_duration.to_be_bytes());
386            let media_time = edit
387                .media_time
388                .map(|value| i64::try_from(value).unwrap_or(i64::MAX))
389                .unwrap_or(-1);
390            body.extend_from_slice(&media_time.to_be_bytes());
391        } else {
392            body.extend_from_slice(
393                &u32::try_from(edit.segment_duration)
394                    .unwrap_or(u32::MAX)
395                    .to_be_bytes(),
396            );
397            let media_time = edit
398                .media_time
399                .map(|value| i32::try_from(value).unwrap_or(i32::MAX))
400                .unwrap_or(-1);
401            body.extend_from_slice(&media_time.to_be_bytes());
402        }
403        let integer = edit.media_rate.trunc();
404        let fraction = ((edit.media_rate - integer) * 65_536.0).round();
405        body.extend_from_slice(&(integer as i16).to_be_bytes());
406        body.extend_from_slice(&(fraction as u16).to_be_bytes());
407    }
408    let elst = make_box(FourCc(*b"elst"), full_box(version, 0, body))?;
409    make_box(FourCc(*b"edts"), elst)
410}
411
412fn make_mdia(track: &WriterTrack) -> Result<Vec<u8>, Error> {
413    let mut body = make_mdhd(track)?;
414    body.extend_from_slice(&make_hdlr(track)?);
415    body.extend_from_slice(&make_minf(track)?);
416    make_box(FourCc(*b"mdia"), body)
417}
418
419fn pack_language(language: &str) -> Result<u16, Error> {
420    let bytes = language.as_bytes();
421    let first = u16::from(
422        *bytes
423            .first()
424            .ok_or(Error::WriterState("language is empty"))?,
425    )
426    .saturating_sub(0x60);
427    let second = u16::from(
428        *bytes
429            .get(1)
430            .ok_or(Error::WriterState("language is too short"))?,
431    )
432    .saturating_sub(0x60);
433    let third = u16::from(
434        *bytes
435            .get(2)
436            .ok_or(Error::WriterState("language is too short"))?,
437    )
438    .saturating_sub(0x60);
439    Ok((first << 10) | (second << 5) | third)
440}
441
442fn make_mdhd(track: &WriterTrack) -> Result<Vec<u8>, Error> {
443    let duration = track_media_duration(track)?;
444    let version = if duration > u64::from(u32::MAX) { 1 } else { 0 };
445    let mut body = Vec::new();
446    if version == 1 {
447        body.extend_from_slice(&[0; 16]);
448        body.extend_from_slice(&track.config.timescale.to_be_bytes());
449        body.extend_from_slice(&duration.to_be_bytes());
450    } else {
451        body.extend_from_slice(&[0; 8]);
452        body.extend_from_slice(&track.config.timescale.to_be_bytes());
453        body.extend_from_slice(&u32::try_from(duration).unwrap_or(u32::MAX).to_be_bytes());
454    }
455    body.extend_from_slice(&pack_language(&track.config.language)?.to_be_bytes());
456    body.extend_from_slice(&0u16.to_be_bytes());
457    make_box(FourCc(*b"mdhd"), full_box(version, 0, body))
458}
459
460fn handler_type(kind: TrackKind) -> FourCc {
461    match kind {
462        TrackKind::Video => FourCc(*b"vide"),
463        TrackKind::Audio => FourCc(*b"soun"),
464        TrackKind::Subtitle => FourCc(*b"subt"),
465        TrackKind::Other(value) => value,
466    }
467}
468
469fn make_hdlr(track: &WriterTrack) -> Result<Vec<u8>, Error> {
470    let mut body = Vec::new();
471    body.extend_from_slice(&0u32.to_be_bytes());
472    body.extend_from_slice(&handler_type(track.config.kind).0);
473    body.extend_from_slice(&[0; 12]);
474    body.extend_from_slice(track.config.handler_name.as_bytes());
475    body.push(0);
476    make_box(FourCc(*b"hdlr"), full_box(0, 0, body))
477}
478
479fn make_minf(track: &WriterTrack) -> Result<Vec<u8>, Error> {
480    let mut body = match track.config.kind {
481        TrackKind::Video => {
482            let mut fields = Vec::new();
483            fields.extend_from_slice(&0u16.to_be_bytes());
484            fields.extend_from_slice(&[0; 6]);
485            make_box(FourCc(*b"vmhd"), full_box(0, 1, fields))?
486        }
487        TrackKind::Audio => {
488            let mut fields = Vec::new();
489            fields.extend_from_slice(&0i16.to_be_bytes());
490            fields.extend_from_slice(&0u16.to_be_bytes());
491            make_box(FourCc(*b"smhd"), full_box(0, 0, fields))?
492        }
493        TrackKind::Subtitle | TrackKind::Other(_) => {
494            make_box(FourCc(*b"nmhd"), full_box(0, 0, Vec::new()))?
495        }
496    };
497    body.extend_from_slice(&make_dinf()?);
498    body.extend_from_slice(&make_stbl(track)?);
499    make_box(FourCc(*b"minf"), body)
500}
501
502fn make_dinf() -> Result<Vec<u8>, Error> {
503    let url = make_box(FourCc(*b"url "), full_box(0, 1, Vec::new()))?;
504    let mut dref_body = Vec::new();
505    dref_body.extend_from_slice(&1u32.to_be_bytes());
506    dref_body.extend_from_slice(&url);
507    let dref = make_box(FourCc(*b"dref"), full_box(0, 0, dref_body))?;
508    make_box(FourCc(*b"dinf"), dref)
509}
510
511fn make_stbl(track: &WriterTrack) -> Result<Vec<u8>, Error> {
512    let mut body = make_stsd(track)?;
513    body.extend_from_slice(&make_stts(track)?);
514    if track.samples.iter().any(|sample| sample.cts_offset != 0) {
515        body.extend_from_slice(&make_ctts(track)?);
516    }
517    if track.samples.iter().any(|sample| !sample.is_sync) {
518        body.extend_from_slice(&make_stss(track)?);
519    }
520    body.extend_from_slice(&make_stsc(track)?);
521    body.extend_from_slice(&make_stsz(track)?);
522    body.extend_from_slice(&make_chunk_offsets(track)?);
523    make_box(FourCc(*b"stbl"), body)
524}
525
526fn make_stsd(track: &WriterTrack) -> Result<Vec<u8>, Error> {
527    let entry = make_sample_entry(track)?;
528    let mut body = Vec::new();
529    body.extend_from_slice(&1u32.to_be_bytes());
530    body.extend_from_slice(&entry);
531    make_box(FourCc(*b"stsd"), full_box(0, 0, body))
532}
533
534fn visual_fields(width: f64, height: f64) -> Result<Vec<u8>, Error> {
535    let mut body = Vec::with_capacity(78);
536    body.extend_from_slice(&[0; 6]);
537    body.extend_from_slice(&1u16.to_be_bytes());
538    body.extend_from_slice(&[0; 16]);
539    body.extend_from_slice(
540        &u16::try_from(width.round() as u64)
541            .map_err(|_| Error::WriterState("video width exceeds u16"))?
542            .to_be_bytes(),
543    );
544    body.extend_from_slice(
545        &u16::try_from(height.round() as u64)
546            .map_err(|_| Error::WriterState("video height exceeds u16"))?
547            .to_be_bytes(),
548    );
549    body.extend_from_slice(&0x0048_0000u32.to_be_bytes());
550    body.extend_from_slice(&0x0048_0000u32.to_be_bytes());
551    body.extend_from_slice(&0u32.to_be_bytes());
552    body.extend_from_slice(&1u16.to_be_bytes());
553    body.extend_from_slice(&[0; 32]);
554    body.extend_from_slice(&0x0018u16.to_be_bytes());
555    body.extend_from_slice(&u16::MAX.to_be_bytes());
556    Ok(body)
557}
558
559fn audio_fields(channels: u16, sample_rate: u32) -> Vec<u8> {
560    let mut body = Vec::with_capacity(28);
561    body.extend_from_slice(&[0; 6]);
562    body.extend_from_slice(&1u16.to_be_bytes());
563    body.extend_from_slice(&[0; 8]);
564    body.extend_from_slice(&channels.to_be_bytes());
565    body.extend_from_slice(&16u16.to_be_bytes());
566    body.extend_from_slice(&0u16.to_be_bytes());
567    body.extend_from_slice(&0u16.to_be_bytes());
568    // The sound sample entry stores a 16.16 rate. A rate above 65535 does
569    // not fit that field and stays in the media timescale, so the entry
570    // stores zero.
571    let fixed_rate = if sample_rate > 65_535 {
572        0
573    } else {
574        sample_rate.saturating_mul(65_536)
575    };
576    body.extend_from_slice(&fixed_rate.to_be_bytes());
577    body
578}
579
580fn append_sidecars(body: &mut Vec<u8>, sidecars: &[SidecarBox]) -> Result<(), Error> {
581    for sidecar in sidecars {
582        body.extend_from_slice(&make_box(sidecar.kind, sidecar.payload.clone())?);
583    }
584    Ok(())
585}
586
587fn make_sample_entry(track: &WriterTrack) -> Result<Vec<u8>, Error> {
588    match &track.config.codec {
589        Codec::H264 { entry, config } => {
590            let mut body = visual_fields(track.config.width, track.config.height)?;
591            body.extend_from_slice(&make_box(FourCc(*b"avcC"), config.raw.clone())?);
592            append_sidecars(&mut body, &config.sidecars)?;
593            make_box(*entry, body)
594        }
595        Codec::H265 { entry, config } => {
596            let mut body = visual_fields(track.config.width, track.config.height)?;
597            body.extend_from_slice(&make_box(FourCc(*b"hvcC"), config.raw.clone())?);
598            append_sidecars(&mut body, &config.sidecars)?;
599            make_box(*entry, body)
600        }
601        Codec::Aac { config } => {
602            let mut body = audio_fields(config.channels, config.sample_rate);
603            body.extend_from_slice(&make_box(FourCc(*b"esds"), config.raw_esds.clone())?);
604            append_sidecars(&mut body, &config.sidecars)?;
605            make_box(FourCc(*b"mp4a"), body)
606        }
607        Codec::Opus { config } => {
608            let mut body = audio_fields(u16::from(config.channels), track.config.timescale);
609            body.extend_from_slice(&make_box(FourCc(*b"dOps"), config.raw.clone())?);
610            append_sidecars(&mut body, &config.sidecars)?;
611            make_box(FourCc(*b"Opus"), body)
612        }
613        Codec::Other { entry, raw } => make_box(*entry, raw.clone()),
614    }
615}
616
617fn rle_u32<I>(values: I) -> Vec<(u32, u32)>
618where
619    I: IntoIterator<Item = u32>,
620{
621    let mut result: Vec<(u32, u32)> = Vec::new();
622    for value in values {
623        if let Some(last) = result.last_mut() {
624            if last.1 == value {
625                last.0 = last.0.saturating_add(1);
626                continue;
627            }
628        }
629        result.push((1, value));
630    }
631    result
632}
633
634fn make_stts(track: &WriterTrack) -> Result<Vec<u8>, Error> {
635    let entries = rle_u32(track.samples.iter().map(|sample| sample.duration));
636    let mut body = Vec::new();
637    body.extend_from_slice(
638        &u32::try_from(entries.len())
639            .map_err(|_| Error::WriterState("time entry count exceeds u32"))?
640            .to_be_bytes(),
641    );
642    for (count, delta) in entries {
643        body.extend_from_slice(&count.to_be_bytes());
644        body.extend_from_slice(&delta.to_be_bytes());
645    }
646    make_box(FourCc(*b"stts"), full_box(0, 0, body))
647}
648
649fn make_ctts(track: &WriterTrack) -> Result<Vec<u8>, Error> {
650    let version = if track.samples.iter().any(|sample| sample.cts_offset < 0) {
651        1
652    } else {
653        0
654    };
655    let mut entries: Vec<(u32, i32)> = Vec::new();
656    for sample in &track.samples {
657        if let Some(last) = entries.last_mut() {
658            if last.1 == sample.cts_offset {
659                last.0 = last.0.saturating_add(1);
660                continue;
661            }
662        }
663        entries.push((1, sample.cts_offset));
664    }
665    let mut body = Vec::new();
666    body.extend_from_slice(
667        &u32::try_from(entries.len())
668            .map_err(|_| Error::WriterState("composition entry count exceeds u32"))?
669            .to_be_bytes(),
670    );
671    for (count, offset) in entries {
672        body.extend_from_slice(&count.to_be_bytes());
673        if version == 1 {
674            body.extend_from_slice(&offset.to_be_bytes());
675        } else {
676            body.extend_from_slice(
677                &u32::try_from(offset)
678                    .map_err(|_| Error::WriterState("negative version-zero composition offset"))?
679                    .to_be_bytes(),
680            );
681        }
682    }
683    make_box(FourCc(*b"ctts"), full_box(version, 0, body))
684}
685
686fn make_stss(track: &WriterTrack) -> Result<Vec<u8>, Error> {
687    let count = track.samples.iter().filter(|sample| sample.is_sync).count();
688    let mut body = Vec::new();
689    body.extend_from_slice(
690        &u32::try_from(count)
691            .map_err(|_| Error::WriterState("sync sample count exceeds u32"))?
692            .to_be_bytes(),
693    );
694    for (index, sample) in track.samples.iter().enumerate() {
695        if sample.is_sync {
696            let number = u32::try_from(index)
697                .map_err(|_| Error::WriterState("sample index exceeds u32"))?
698                .saturating_add(1);
699            body.extend_from_slice(&number.to_be_bytes());
700        }
701    }
702    make_box(FourCc(*b"stss"), full_box(0, 0, body))
703}
704
705fn make_stsc(track: &WriterTrack) -> Result<Vec<u8>, Error> {
706    let mut entries: Vec<(u32, u32)> = Vec::new();
707    for (index, chunk) in track.chunks.iter().enumerate() {
708        if entries
709            .last()
710            .is_some_and(|entry| entry.1 == chunk.sample_count)
711        {
712            continue;
713        }
714        entries.push((
715            u32::try_from(index)
716                .map_err(|_| Error::WriterState("chunk index exceeds u32"))?
717                .saturating_add(1),
718            chunk.sample_count,
719        ));
720    }
721    let mut body = Vec::new();
722    body.extend_from_slice(
723        &u32::try_from(entries.len())
724            .map_err(|_| Error::WriterState("chunk map count exceeds u32"))?
725            .to_be_bytes(),
726    );
727    for (first_chunk, samples_per_chunk) in entries {
728        body.extend_from_slice(&first_chunk.to_be_bytes());
729        body.extend_from_slice(&samples_per_chunk.to_be_bytes());
730        body.extend_from_slice(&1u32.to_be_bytes());
731    }
732    make_box(FourCc(*b"stsc"), full_box(0, 0, body))
733}
734
735fn make_stsz(track: &WriterTrack) -> Result<Vec<u8>, Error> {
736    let fixed = track
737        .samples
738        .first()
739        .map(|first| first.size)
740        .filter(|size| track.samples.iter().all(|sample| sample.size == *size))
741        .unwrap_or(0);
742    let mut body = Vec::new();
743    body.extend_from_slice(&fixed.to_be_bytes());
744    body.extend_from_slice(
745        &u32::try_from(track.samples.len())
746            .map_err(|_| Error::WriterState("sample count exceeds u32"))?
747            .to_be_bytes(),
748    );
749    if fixed == 0 {
750        for sample in &track.samples {
751            body.extend_from_slice(&sample.size.to_be_bytes());
752        }
753    }
754    make_box(FourCc(*b"stsz"), full_box(0, 0, body))
755}
756
757fn make_chunk_offsets(track: &WriterTrack) -> Result<Vec<u8>, Error> {
758    let use_co64 = track.force_co64
759        || track
760            .chunks
761            .iter()
762            .any(|chunk| chunk.offset > u64::from(u32::MAX));
763    let mut body = Vec::new();
764    body.extend_from_slice(
765        &u32::try_from(track.chunks.len())
766            .map_err(|_| Error::WriterState("chunk count exceeds u32"))?
767            .to_be_bytes(),
768    );
769    if use_co64 {
770        for chunk in &track.chunks {
771            body.extend_from_slice(&chunk.offset.to_be_bytes());
772        }
773        make_box(FourCc(*b"co64"), full_box(0, 0, body))
774    } else {
775        for chunk in &track.chunks {
776            body.extend_from_slice(
777                &u32::try_from(chunk.offset)
778                    .unwrap_or(u32::MAX)
779                    .to_be_bytes(),
780            );
781        }
782        make_box(FourCc(*b"stco"), full_box(0, 0, body))
783    }
784}