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}