Skip to main content

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}