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    /// Per-level assignment entries for a `leva` (LevelAssignmentBox,
125    /// ISO/IEC 14496-12 §8.8.13) emitted inside the init `mvex`.
126    ///
127    /// When non-empty, the muxer writes one `leva` after the `trex` boxes
128    /// in `mvex`, advertising how the file's content is partitioned into
129    /// **levels** for partial-subsegment fetch. Each entry is a
130    /// [`demux::LevaEntry`](crate::demux::LevaEntry) (`track_id` +
131    /// `padding_flag` + `assignment_type` + the type-specific tail). The
132    /// level *order* in this slice is the level *number* a sibling `ssix`
133    /// SubsegmentIndexBox refers to (§8.16.4.2).
134    ///
135    /// The §8.8.13.3 conformance constraints (`level_count ≥ 2`, the
136    /// "zero or more of type 2/3 then zero or more of exactly one type"
137    /// ordering rule) are the caller's responsibility; the muxer
138    /// serialises whatever is supplied verbatim. Empty by default — most
139    /// fragmented files don't declare levels.
140    pub levels: Vec<crate::demux::LevaEntry>,
141    /// Emit an `ssix` (SubsegmentIndexBox, ISO/IEC 14496-12 §8.16.4)
142    /// immediately after each per-fragment `sidx`, partitioning the
143    /// fragment's single referenced subsegment into two level byte ranges
144    /// for partial-subsegment fetch (§8.16.4.1).
145    ///
146    /// Requires `emit_random_access_indexes == true` (the `ssix` documents
147    /// the preceding `sidx`); when `emit_random_access_indexes` is `false`,
148    /// this flag has no effect. Each emitted `ssix` carries
149    /// `subsegment_count == 1` (matching the one-reference `sidx`) with two
150    /// ranges that together cover the whole subsegment (`styp? + prft? +
151    /// moof + mdat`):
152    ///
153    /// 1. `level = ssix_levels.0` → the leading metadata bytes
154    ///    (`styp? + prft? + moof`),
155    /// 2. `level = ssix_levels.1` → the trailing media bytes (`mdat`).
156    ///
157    /// The two level numbers should match level numbers assigned by the
158    /// [`levels`](Self::levels) `leva`; the §8.16.4 "ranges partition the
159    /// subsegment / ≥ 2 ranges" constraint is satisfied by construction.
160    /// Default `false` (no `ssix`); when enabled the default
161    /// `ssix_levels` is `(1, 2)`.
162    pub emit_ssix: bool,
163    /// The two `ssix` level numbers `(metadata_level, media_level)` used
164    /// when [`emit_ssix`](Self::emit_ssix) is set: the first labels the
165    /// leading `styp? + prft? + moof` range, the second the trailing
166    /// `mdat` range. Defaults to `(1, 2)`. Ignored when `emit_ssix` is
167    /// `false`.
168    pub ssix_levels: (u8, u8),
169    /// Per-track `trep` (TrackExtensionPropertiesBox, ISO/IEC 14496-12
170    /// §8.8.15) records emitted inside the init `mvex`.
171    ///
172    /// When non-empty, the muxer writes one `trep` per record after the
173    /// `trex` boxes (and after any `leva`), in slice order. Each record
174    /// is a [`demux::TrepRecord`](crate::demux::TrepRecord) carrying its
175    /// `track_id` and child boxes; the one base-spec-defined child,
176    /// `assp` (AlternativeStartupSequencePropertiesBox, §8.8.16), is
177    /// serialised from its typed [`AsspRecord`](crate::demux::AsspRecord)
178    /// when present on a [`TrepChild`](crate::demux::TrepChild). The
179    /// records read back through the demuxer's `mvex` walk (`trep_<n>`
180    /// metadata + `Mp4Demuxer::treps()`).
181    ///
182    /// §8.8.15.1 fixes quantity at zero or one `trep` per track; the
183    /// muxer serialises whatever is supplied verbatim (the per-track
184    /// uniqueness is the caller's responsibility). Empty by default —
185    /// most fragmented files don't declare track extension properties.
186    pub treps: Vec<crate::demux::TrepRecord>,
187    /// Write a `mehd` (MovieExtendsHeaderBox, ISO/IEC 14496-12 §8.8.2)
188    /// as the first child of the init-segment `mvex`, sealing the
189    /// file's overall presentation duration at `write_trailer`.
190    ///
191    /// §8.8.2.3 defines `fragment_duration` as "the duration of the
192    /// longest track, including movie fragments" in the movie
193    /// timescale — a value only known once the last fragment is laid
194    /// down. The muxer therefore reserves a version-1 (64-bit) `mehd`
195    /// with `fragment_duration = 0` at `write_header` and patches the
196    /// eight duration bytes in place at `write_trailer` (the output is
197    /// `WriteSeek`, so the seek-back is always available). A sealed
198    /// file then demuxes with an authoritative `duration_micros` even
199    /// though its `mvhd.duration` is 0 (no moov-resident samples); the
200    /// demuxer surfaces the raw value as the `mehd_fragment_duration`
201    /// metadata key. If `write_trailer` is never reached (a truncated
202    /// live capture), the placeholder 0 is exactly the "value unknown"
203    /// posture readers already handle — §8.8.2.1 says the overall
204    /// duration must then be computed by examining each fragment.
205    ///
206    /// Default `false`: no `mehd` is written and the init segment is
207    /// byte-identical to before (the right choice for live/low-latency
208    /// output where the init segment ships before the stream ends).
209    pub write_mehd: bool,
210}
211
212impl Default for FragmentedOptions {
213    fn default() -> Self {
214        Self {
215            cadence: FragmentCadence::EverySeconds(2.0),
216            styp: Some(BrandPreset::Custom {
217                major: *b"msdh",
218                compatible: vec![*b"msdh", *b"msix"],
219            }),
220            emit_random_access_indexes: true,
221            levels: Vec::new(),
222            emit_ssix: false,
223            ssix_levels: (1, 2),
224            treps: Vec::new(),
225            write_mehd: false,
226        }
227    }
228}
229
230/// Per-track sample-group emission request.
231///
232/// Each entry attaches an `sbgp` (SampleToGroupBox), `csgp`
233/// (CompactSampleToGroupBox) and / or `sgpd` (SampleGroupDescriptionBox)
234/// box into one track's `stbl`. The halves share `grouping_type`; the
235/// writer simply serialises whatever the caller supplies — content
236/// interpretation belongs to a layer that knows the grouping-type
237/// semantics (per ISO/IEC 14496-12 §8.9).
238///
239/// Multiple `TrackSampleGroups` entries may target the same
240/// `stream_index`; they accumulate in encounter order. The muxer
241/// emits all `sgpd` boxes first, then all `sbgp` boxes, then all
242/// `csgp` boxes, after the chunk-offset table inside each track's
243/// `stbl`. `sbgp` and `csgp` are *alternative* encodings of the same
244/// per-sample → group mapping (§8.9.5: "at most one `csgp` *or* `sbgp`
245/// with a given `grouping_type` may exist per track"); a caller picks
246/// one form per `grouping_type` and never both.
247#[derive(Clone, Debug, Default)]
248pub struct TrackSampleGroups {
249    /// Index into the muxer's `streams` slice (the stream slot that
250    /// owns these groups).
251    pub stream_index: usize,
252    /// `sbgp` boxes to emit for this track. Order is preserved.
253    pub sbgp: Vec<crate::sample_groups::SampleToGroup>,
254    /// `sgpd` boxes to emit for this track. Order is preserved.
255    pub sgpd: Vec<crate::sample_groups::SampleGroupDescription>,
256    /// `csgp` (CompactSampleToGroupBox, §8.9.5) boxes to emit for this
257    /// track — the compact, bit-packed alternative to `sbgp` for tracks
258    /// whose per-sample group membership is periodic. Order is preserved
259    /// and they follow any `sbgp`. Use `csgp` *or* `sbgp` for a given
260    /// `grouping_type`, never both.
261    pub csgp: Vec<crate::sample_groups::CompactSampleToGroup>,
262}
263
264/// An explicit per-track edit list for the muxer (ISO/IEC 14496-12
265/// §8.6.5–6).
266///
267/// When a [`Mp4MuxerOptions::track_edit_lists`] entry targets a
268/// stream, the muxer emits that track's `edts/elst` from the given
269/// entries verbatim (serialised through `demux::build_elst_box`, so
270/// the §8.6.6.3 round-trip rules apply — `media_rate_integer` must be
271/// 0 or 1, the final entry may not be an empty edit, `media_time`
272/// may not sit below the `-1` empty-edit sentinel, and the entry list
273/// may not be empty; violations fail at `open` time). An explicit
274/// list *overrides* the automatic start-delay emission for that track
275/// and is written even when [`Mp4MuxerOptions::write_edit_list`] is
276/// `false` (the flag governs only the automatic behaviour).
277///
278/// This is the write-side dual of `Mp4Demuxer::edit_list`: a remuxer
279/// carries a source's elst across by feeding the demuxer's slice
280/// straight back in. Note the §8.6.6.3 unit split — each entry's
281/// `segment_duration` is in the *movie* timescale (this muxer writes
282/// movie timescale 1000) while `media_time` is in the track's media
283/// timescale.
284#[derive(Clone, Debug, Default)]
285pub struct TrackEditList {
286    /// Index into the muxer's `streams` slice (the stream slot that
287    /// owns this edit list).
288    pub stream_index: usize,
289    /// The `elst` entries, emitted in order.
290    pub entries: Vec<crate::demux::EditListEntry>,
291}
292
293/// Per-track CENC protection signalling for the muxer (ISO/IEC
294/// 14496-12 §8.12 envelope + ISO/IEC 23001-7 §4.1 carriage).
295///
296/// When a [`Mp4MuxerOptions::track_protection`] entry targets a
297/// stream, the muxer wraps that track's sample entry into its
298/// protected form: the FourCC becomes `encv` / `enca` / `enct` /
299/// `encs` (per the stream's media type) and a `sinf` box —
300/// `frma(original_format)` + `schm(scheme_type, scheme_version)` +
301/// `schi(tenc)` — is appended to the entry body, exactly the shape
302/// this crate's demuxer unwraps back to the original codec id plus
303/// `protection_scheme` / `cenc_default_*` options.
304///
305/// The muxer signals protection only — packet payloads are written
306/// as handed in. The caller encrypts each sample first (e.g. via
307/// `cenc_cipher::encrypt_sample_in_place` with a
308/// `CencSchemeDecision` built from this same `(scheme_type, tenc)`
309/// pair) and carries the per-sample IVs / subsample maps through its
310/// own `senc` / `saiz` / `saio` channel.
311#[derive(Clone, Debug)]
312pub struct TrackProtection {
313    /// Index into the muxer's `streams` slice (the stream to protect).
314    pub stream_index: usize,
315    /// §8.12.5 `scheme_type` FourCC — one of the ISO/IEC 23001-7 §10
316    /// schemes (`cenc` / `cbc1` / `cens` / `cbcs`) or a private
317    /// dialect FourCC (validated structurally only in that case).
318    pub scheme_type: [u8; 4],
319    /// §8.12.5 32-bit `scheme_version` word. Every ISO/IEC 23001-7
320    /// edition to date uses `0x0001_0000` (the [`Default`] here).
321    pub scheme_version: u32,
322    /// Track-default encryption parameters written into `schi/tenc`
323    /// (ISO/IEC 23001-7 §8.2). Must satisfy the same round-trip rules
324    /// as `cenc::build_tenc_box` plus scheme coherence (a §10 scheme
325    /// pins the `tenc` version; pattern schemes need a non-zero
326    /// pattern pair) — violations fail at `open`.
327    pub tenc: crate::cenc::TencBox,
328}
329
330/// Runtime options controlling how the MP4 muxer shapes its output.
331///
332/// Call [`Mp4MuxerOptions::default`] for the historical behavior of the
333/// plain `"mp4"` registry entry (major=`mp42`, no faststart, no fragmentation).
334#[derive(Clone, Debug)]
335pub struct Mp4MuxerOptions {
336    /// `ftyp` brand preset written at the beginning of the file.
337    pub brand: BrandPreset,
338    /// If `true`, rewrite the file at `write_trailer` time so `moov` precedes
339    /// `mdat` ("faststart" / "web-optimized" layout). Requires a seekable
340    /// output (which `WriteSeek` already provides). Mutually exclusive with
341    /// `fragmented`.
342    pub faststart: bool,
343    /// If `Some(...)`, switch the muxer to fragmented-MP4 mode (DASH / HLS /
344    /// Smooth-Streaming / CMAF). The first call to `write_header` emits
345    /// `ftyp + moov` (with `mvex+trex` defaults, no media samples); each
346    /// fragment cadence boundary emits `styp? + moof + mdat`. Mutually
347    /// exclusive with `faststart`.
348    pub fragmented: Option<FragmentedOptions>,
349    /// If `true` (the default), the muxer emits a per-track `edts/elst`
350    /// (EditBox/EditListBox, ISO/IEC 14496-12 §8.6.5–6) whenever a track's
351    /// first packet has a positive presentation timestamp. The edit list
352    /// carries a leading **empty edit** (`media_time = -1`) of that start
353    /// delay followed by a normal `media_time = 0` segment for the track's
354    /// duration, so a player offsets the track start instead of beginning
355    /// at presentation time 0 (the §8.6.5 "An empty edit is used to offset
356    /// the start time of a track" idiom).
357    ///
358    /// Tracks whose first PTS is zero (or absent) get no `edts` — the
359    /// implicit one-to-one timeline mapping applies. Set this to `false`
360    /// to suppress edit-list emission entirely.
361    pub write_edit_list: bool,
362    /// Per-track sample-group declarations (`sbgp` + `sgpd`, ISO/IEC
363    /// 14496-12 §8.9.2 / §8.9.3). Empty by default — most muxed files
364    /// don't need sample groups. When non-empty, each
365    /// [`TrackSampleGroups`] entry's `sbgp` / `sgpd` boxes are emitted
366    /// into the target track's `stbl` after the chunk-offset table.
367    pub track_sample_groups: Vec<TrackSampleGroups>,
368    /// Explicit per-track edit lists (`edts`/`elst`, ISO/IEC 14496-12
369    /// §8.6.5–6). Empty by default — the automatic start-delay
370    /// emission (see [`Self::write_edit_list`]) covers the common
371    /// case. A [`TrackEditList`] entry overrides the automatic elst
372    /// for its stream and is emitted even when `write_edit_list` is
373    /// `false`. Validated at `open` through `demux::build_elst_box`'s
374    /// §8.6.6.3 round-trip rules.
375    pub track_edit_lists: Vec<TrackEditList>,
376    /// Reserve a 64-bit `largesize` header for the `mdat` box so the
377    /// media payload may exceed 4 GiB (ISO/IEC 14496-12 §4.2 extended
378    /// size form: `size == 1` then an `unsigned int(64) largesize`).
379    ///
380    /// The plain 32-bit `mdat` header can only describe a box up to
381    /// `u32::MAX` bytes; without this flag the muxer errors at
382    /// `write_trailer` if the accumulated payload would overflow that.
383    /// Because the direct-write path streams `mdat` to the output before
384    /// the final size is known, the header form has to be chosen *up
385    /// front* — so a producer that expects a >4 GiB `mdat` (long
386    /// uncompressed captures, multi-hour high-bitrate masters) sets this
387    /// to `true` to reserve the 16-byte largesize header. The 8 extra
388    /// bytes are the only cost for files that stay under 4 GiB, so the
389    /// default is `false` (compact 32-bit header, byte-identical to the
390    /// historical output). `co64` chunk offsets are still chosen
391    /// automatically when any chunk offset itself exceeds `u32::MAX`,
392    /// independent of this flag.
393    pub large_mdat: bool,
394    /// Per-track CENC protection signalling (ISO/IEC 14496-12 §8.12 +
395    /// ISO/IEC 23001-7). Empty by default. Each [`TrackProtection`]
396    /// entry wraps the target stream's sample entry into its `enc*`
397    /// protected form with a `sinf`(`frma`+`schm`+`schi`/`tenc`)
398    /// envelope. See [`TrackProtection`] for the caller's encryption
399    /// responsibilities.
400    pub track_protection: Vec<TrackProtection>,
401    /// `pssh` (ProtectionSystemSpecificHeaderBox, ISO/IEC 23001-7
402    /// §8.1) boxes emitted at `moov` level, after the `trak` boxes —
403    /// one per DRM system the content keys are provisioned for.
404    /// Empty by default (no box). Serialised through
405    /// `cenc::build_pssh_box`, so a record that would not round-trip
406    /// (a v0 record carrying KIDs, oversize counts) fails at
407    /// `write_trailer` (non-fragmented) / `write_header` (fragmented
408    /// init segment) rather than emitting a malformed box.
409    pub pssh: Vec<crate::cenc::PsshBox>,
410}
411
412impl Default for Mp4MuxerOptions {
413    fn default() -> Self {
414        Self {
415            brand: BrandPreset::Mp4,
416            faststart: false,
417            fragmented: None,
418            write_edit_list: true,
419            track_sample_groups: Vec::new(),
420            track_edit_lists: Vec::new(),
421            large_mdat: false,
422            track_protection: Vec::new(),
423            pssh: Vec::new(),
424        }
425    }
426}