oxideav_mp4/options.rs
1//! Muxer configuration for the MP4 / ISOBMFF writer.
2//!
3//! The default [`Mp4MuxerOptions`] matches what `muxer::open` has always done:
4//! major brand `mp42`, no faststart, no fragmentation. Three convenience
5//! presets are provided via [`BrandPreset`] for the common `mp4`, `mov`, and
6//! `ismv` registry entries; a `Custom` variant lets callers supply any
7//! major + compatible brand list directly.
8//!
9//! Setting [`Mp4MuxerOptions::fragmented`] to `Some(...)` switches the muxer
10//! into fragmented-MP4 mode (DASH / HLS / Smooth-Streaming / CMAF output).
11
12/// Brand preset controlling the `ftyp` box written at the start of the file.
13///
14/// The four-byte codes follow ISO/IEC 14496-12 and the de-facto QuickTime /
15/// Smooth Streaming conventions:
16///
17/// * [`Mp4`](BrandPreset::Mp4): `mp42` / `isom mp42 mp41 iso2`
18/// * [`Mov`](BrandPreset::Mov): `qt ` / `qt `
19/// * [`Ismv`](BrandPreset::Ismv): `iso4` / `iso4 piff iso6 isml`
20/// * [`Custom`](BrandPreset::Custom): caller-supplied major + compatible list
21#[derive(Clone, Debug)]
22pub enum BrandPreset {
23 /// Standard MP4 — `major=mp42`, compatible=`isom mp42 mp41 iso2`.
24 Mp4,
25 /// Apple QuickTime — `major=qt `, compatible=`qt `.
26 Mov,
27 /// Microsoft Smooth Streaming / ISMV — `major=iso4`, compatible=`iso4 piff iso6 isml`.
28 Ismv,
29 /// Custom brand with an explicit major + compatible list.
30 Custom {
31 major: [u8; 4],
32 compatible: Vec<[u8; 4]>,
33 },
34}
35
36impl BrandPreset {
37 /// Return the major brand for this preset.
38 pub fn major_brand(&self) -> [u8; 4] {
39 match self {
40 BrandPreset::Mp4 => *b"mp42",
41 BrandPreset::Mov => *b"qt ",
42 BrandPreset::Ismv => *b"iso4",
43 BrandPreset::Custom { major, .. } => *major,
44 }
45 }
46
47 /// Return the list of compatible brands for this preset.
48 pub fn compatible_brands(&self) -> Vec<[u8; 4]> {
49 match self {
50 BrandPreset::Mp4 => vec![*b"isom", *b"mp42", *b"mp41", *b"iso2"],
51 BrandPreset::Mov => vec![*b"qt "],
52 BrandPreset::Ismv => vec![*b"iso4", *b"piff", *b"iso6", *b"isml"],
53 BrandPreset::Custom { compatible, .. } => compatible.clone(),
54 }
55 }
56}
57
58/// Cadence policy controlling when the fragmented muxer emits a `moof+mdat`
59/// pair (one segment / fragment per flush).
60///
61/// In a true CMAF / DASH `init+seg*` workflow each `moof+mdat` becomes one
62/// addressable HTTP range; the cadence picks how big each one is.
63#[derive(Clone, Copy, Debug)]
64pub enum FragmentCadence {
65 /// Flush whenever the running fragment duration of the *first* track
66 /// (typically video) reaches `seconds`. Falls back to per-track total
67 /// when there is no first track. Compressed audio samples are tiny
68 /// (~20 ms each) so picking 2..6 s yields reasonable fragment sizes.
69 EverySeconds(f64),
70 /// Flush at every keyframe of the *first* track (typically video). The
71 /// run before the first keyframe is held until one arrives. Audio-only
72 /// inputs (every audio sample is a keyframe) effectively get one
73 /// fragment per audio sample with this — pair with seconds/N for
74 /// audio-only output.
75 EveryKeyframe,
76 /// Flush every `n` packets of the first track. Useful for testing
77 /// (predictable cadence without timing dependence).
78 EveryNPackets(u32),
79}
80
81/// Fragmented-MP4 muxer options.
82///
83/// When [`Mp4MuxerOptions::fragmented`] is `Some(FragmentedOptions { .. })`,
84/// the muxer writes the file as
85///
86/// ```text
87/// ftyp
88/// moov (mvex+trex; no media samples in moov)
89/// sidx? (one per fragment, references the next moof+mdat)
90/// styp? + moof + mdat (per fragment, repeated)
91/// sidx? + styp? + moof + mdat
92/// ...
93/// mfra? (at end: per-track tfra + mfro size trailer)
94/// ```
95///
96/// matching ISO/IEC 14496-12 §8.8 (Movie Fragments) + §8.16 (sidx) + §8.8.10
97/// (mfra) + DASH-IF Interop guidelines for `styp` brands.
98#[derive(Clone, Debug)]
99pub struct FragmentedOptions {
100 /// When to flush a fragment; see [`FragmentCadence`].
101 pub cadence: FragmentCadence,
102 /// Emit a `styp` SegmentTypeBox before each `moof+mdat` pair (CMAF
103 /// segment marker). When `None`, no `styp` is written and the file is
104 /// a plain fragmented ISOBMFF (still valid for any DASH parser, but
105 /// not a CMAF-conformant addressable segment).
106 ///
107 /// DASH-IF Interop §6.2 recommends `styp(major=msdh, compat=msdh msix)`
108 /// for an indexed media segment, or `cmfs` / `cmff` for CMAF brand
109 /// signalling. The default `Some(BrandPreset::Custom { major: msdh,
110 /// compatible: [msdh, msix] })` is the broadly-interop choice.
111 pub styp: Option<BrandPreset>,
112 /// Emit `sidx` (SegmentIndexBox §8.16.3) before each `moof+mdat` and
113 /// an `mfra` (MovieFragmentRandomAccessBox §8.8.10) trailer with
114 /// per-track `tfra` random-access tables + the size-of-mfra `mfro`
115 /// at end of file. Required for the DASH on-demand profile (single
116 /// file with embedded byte-range index) and for fast random-access
117 /// without scanning every moof. Default `true`.
118 ///
119 /// The emitted `sidx` is a single-entry index covering the immediately-
120 /// following moof+mdat (the simplest legal form per §8.16.3); a
121 /// multi-segment top-level sidx can be layered on by an outer
122 /// segmenter if needed.
123 pub emit_random_access_indexes: bool,
124}
125
126impl Default for FragmentedOptions {
127 fn default() -> Self {
128 Self {
129 cadence: FragmentCadence::EverySeconds(2.0),
130 styp: Some(BrandPreset::Custom {
131 major: *b"msdh",
132 compatible: vec![*b"msdh", *b"msix"],
133 }),
134 emit_random_access_indexes: true,
135 }
136 }
137}
138
139/// Per-track sample-group emission request.
140///
141/// Each entry attaches an `sbgp` (SampleToGroupBox) and / or `sgpd`
142/// (SampleGroupDescriptionBox) pair into one track's `stbl`. The two
143/// halves share `grouping_type`; the writer simply serialises whatever
144/// the caller supplies — content interpretation belongs to a layer
145/// that knows the grouping-type semantics (per ISO/IEC 14496-12 §8.9).
146///
147/// Multiple `TrackSampleGroups` entries may target the same
148/// `stream_index`; they accumulate in encounter order. The muxer
149/// emits all `sgpd` boxes first, then all `sbgp` boxes, after the
150/// chunk-offset table inside each track's `stbl`.
151#[derive(Clone, Debug, Default)]
152pub struct TrackSampleGroups {
153 /// Index into the muxer's `streams` slice (the stream slot that
154 /// owns these groups).
155 pub stream_index: usize,
156 /// `sbgp` boxes to emit for this track. Order is preserved.
157 pub sbgp: Vec<crate::sample_groups::SampleToGroup>,
158 /// `sgpd` boxes to emit for this track. Order is preserved.
159 pub sgpd: Vec<crate::sample_groups::SampleGroupDescription>,
160}
161
162/// Runtime options controlling how the MP4 muxer shapes its output.
163///
164/// Call [`Mp4MuxerOptions::default`] for the historical behavior of the
165/// plain `"mp4"` registry entry (major=`mp42`, no faststart, no fragmentation).
166#[derive(Clone, Debug)]
167pub struct Mp4MuxerOptions {
168 /// `ftyp` brand preset written at the beginning of the file.
169 pub brand: BrandPreset,
170 /// If `true`, rewrite the file at `write_trailer` time so `moov` precedes
171 /// `mdat` ("faststart" / "web-optimized" layout). Requires a seekable
172 /// output (which `WriteSeek` already provides). Mutually exclusive with
173 /// `fragmented`.
174 pub faststart: bool,
175 /// If `Some(...)`, switch the muxer to fragmented-MP4 mode (DASH / HLS /
176 /// Smooth-Streaming / CMAF). The first call to `write_header` emits
177 /// `ftyp + moov` (with `mvex+trex` defaults, no media samples); each
178 /// fragment cadence boundary emits `styp? + moof + mdat`. Mutually
179 /// exclusive with `faststart`.
180 pub fragmented: Option<FragmentedOptions>,
181 /// If `true` (the default), the muxer emits a per-track `edts/elst`
182 /// (EditBox/EditListBox, ISO/IEC 14496-12 §8.6.5–6) whenever a track's
183 /// first packet has a positive presentation timestamp. The edit list
184 /// carries a leading **empty edit** (`media_time = -1`) of that start
185 /// delay followed by a normal `media_time = 0` segment for the track's
186 /// duration, so a player offsets the track start instead of beginning
187 /// at presentation time 0 (the §8.6.5 "An empty edit is used to offset
188 /// the start time of a track" idiom).
189 ///
190 /// Tracks whose first PTS is zero (or absent) get no `edts` — the
191 /// implicit one-to-one timeline mapping applies. Set this to `false`
192 /// to suppress edit-list emission entirely.
193 pub write_edit_list: bool,
194 /// Per-track sample-group declarations (`sbgp` + `sgpd`, ISO/IEC
195 /// 14496-12 §8.9.2 / §8.9.3). Empty by default — most muxed files
196 /// don't need sample groups. When non-empty, each
197 /// [`TrackSampleGroups`] entry's `sbgp` / `sgpd` boxes are emitted
198 /// into the target track's `stbl` after the chunk-offset table.
199 pub track_sample_groups: Vec<TrackSampleGroups>,
200}
201
202impl Default for Mp4MuxerOptions {
203 fn default() -> Self {
204 Self {
205 brand: BrandPreset::Mp4,
206 faststart: false,
207 fragmented: None,
208 write_edit_list: true,
209 track_sample_groups: Vec::new(),
210 }
211 }
212}