Skip to main content

mp4forge/mux/
mod.rs

1//! Feature-gated mux planning, real MP4 container assembly, and sample-reader helpers.
2//!
3//! The additive `mux` feature exposes two layers:
4//! - low-level staged media-item planning plus payload-copy helpers
5//! - higher-level real MP4 mux helpers that assemble `ftyp`, `moov`, and `mdat`
6//!
7//! Internally, both layers build on one mux event graph that carries stream descriptions, ordered
8//! sample events, and boundary events. The task-level sample-reader helpers live under
9//! [`crate::mux::sample_reader`], the public direct-ingest inspection and export helpers plus the
10//! additive packet-focused report surface live under [`crate::mux::inspect`], the public
11//! elementary sample rewrite helpers and elementary export helpers live under
12//! [`crate::mux::rewrite`], and the real file-backed mux surface builds actual MP4 container
13//! output on top of the same internal event flow.
14
15use std::collections::BTreeMap;
16use std::error::Error;
17use std::fmt;
18use std::fs::File;
19use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
20use std::path::{Path, PathBuf};
21use std::str::FromStr;
22
23#[cfg(feature = "async")]
24use tokio::fs::File as TokioFile;
25#[cfg(feature = "async")]
26use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
27
28use crate::FourCc;
29#[cfg(feature = "async")]
30use crate::async_io::{AsyncReadForward, AsyncReadSeek, AsyncWrite, AsyncWriteForward};
31use crate::codec::CodecError;
32use crate::header::HeaderError;
33use crate::queue::{OrderedWorkQueue, QueueWorkItem};
34use crate::writer::WriterError;
35
36mod coordination;
37mod demux;
38pub(crate) mod event;
39mod import;
40/// Feature-gated direct-ingest inspection and export helpers built on native mux parsing.
41#[cfg_attr(docsrs, doc(cfg(feature = "mux")))]
42pub mod inspect;
43mod mp4;
44/// Feature-gated elementary sample rewrite helpers built on landed mux codec logic.
45#[cfg_attr(docsrs, doc(cfg(feature = "mux")))]
46pub mod rewrite;
47/// Feature-gated planned sample-reader helpers built on mux plans.
48#[cfg_attr(docsrs, doc(cfg(feature = "mux")))]
49pub mod sample_reader;
50
51use coordination::MuxCoordinationPlan;
52pub(crate) use coordination::{
53    MuxDurationBoundaryKind, TrackCoordinationDirective, build_capped_duration_chunk_sample_counts,
54    build_duration_chunk_sample_counts, build_duration_chunk_sample_counts_with_start_time,
55    build_fragmented_duration_chunk_sample_counts_with_start_time,
56    build_sync_aligned_fragmented_duration_chunk_sample_counts,
57    build_sync_aligned_segment_chunk_sample_counts,
58    rebalance_small_multi_audio_chunk_sample_counts,
59};
60pub(crate) use event::{MuxEventCursor, MuxEventGraph, MuxSampleEvent};
61pub use import::mux_fragmented_to_paths;
62#[cfg(feature = "async")]
63pub use import::mux_fragmented_to_paths_async;
64pub use import::mux_into_path;
65#[cfg(feature = "async")]
66pub use import::mux_into_path_async;
67pub use import::mux_to_path;
68#[cfg(feature = "async")]
69pub use import::mux_to_path_async;
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
72pub(crate) enum MuxRawCodec {
73    /// AV1 elementary input.
74    Av1,
75    /// MPEG-2 elementary video input.
76    Mpeg2v,
77    /// MPEG-4 Part 2 elementary input.
78    Mp4v,
79    /// H.263 elementary input.
80    H263,
81    /// H.264 or AVC elementary input.
82    H264,
83    /// H.265 or HEVC elementary input.
84    H265,
85    /// H.266 or VVC elementary input.
86    Vvc,
87    /// VP8 elementary input.
88    Vp8,
89    /// VP9 elementary input.
90    Vp9,
91    /// VP10 elementary input.
92    Vp10,
93    /// AAC input.
94    Aac,
95    /// AAC LATM input.
96    Latm,
97    /// MP3 input.
98    Mp3,
99    /// AC-3 input.
100    Ac3,
101    /// E-AC-3 input.
102    Eac3,
103    /// AC-4 input.
104    Ac4,
105    /// AMR narrowband input.
106    Amr,
107    /// AMR wideband input.
108    AmrWb,
109    /// QCP-wrapped voice input carrying QCELP, EVRC, or SMV frames.
110    Qcp,
111    /// JPEG still-image input.
112    Jpeg,
113    /// PNG still-image input.
114    Png,
115    /// BMP still-image input.
116    Bmp,
117    /// Raw ProRes input.
118    Prores,
119    /// Self-describing YUV4MPEG input.
120    Y4m,
121    /// JPEG 2000 image or codestream input.
122    J2k,
123    /// WAVE or PCM input.
124    Pcm,
125    /// DTS core input.
126    Dts,
127    /// Dolby TrueHD input.
128    Truehd,
129    /// ALAC input.
130    Alac,
131    /// FLAC input.
132    Flac,
133    /// IAMF elementary input.
134    Iamf,
135    /// MPEG-H AudioMux input.
136    MpegH,
137    /// Opus input.
138    Opus,
139    /// Vorbis input.
140    Vorbis,
141    /// Speex input.
142    Speex,
143    /// Theora input.
144    Theora,
145}
146
147impl MuxRawCodec {
148    pub const fn prefix(&self) -> &'static str {
149        match self {
150            Self::Av1 => "av1",
151            Self::Mpeg2v => "mpeg2v",
152            Self::Mp4v => "mp4v",
153            Self::H263 => "h263",
154            Self::H264 => "h264",
155            Self::H265 => "h265",
156            Self::Vvc => "vvc",
157            Self::Vp8 => "vp8",
158            Self::Vp9 => "vp9",
159            Self::Vp10 => "vp10",
160            Self::Aac => "aac",
161            Self::Latm => "latm",
162            Self::Mp3 => "mp3",
163            Self::Ac3 => "ac3",
164            Self::Eac3 => "ec3",
165            Self::Ac4 => "ac4",
166            Self::Amr => "amr",
167            Self::AmrWb => "amr-wb",
168            Self::Qcp => "qcp",
169            Self::Jpeg => "jpeg",
170            Self::Png => "png",
171            Self::Bmp => "bmp",
172            Self::Prores => "prores",
173            Self::Y4m => "y4m",
174            Self::J2k => "j2k",
175            Self::Pcm => "pcm",
176            Self::Dts => "dts",
177            Self::Truehd => "truehd",
178            Self::Alac => "alac",
179            Self::Flac => "flac",
180            Self::Iamf => "iamf",
181            Self::MpegH => "mhas",
182            Self::Opus => "opus",
183            Self::Vorbis => "vorbis",
184            Self::Speex => "speex",
185            Self::Theora => "theora",
186        }
187    }
188}
189
190/// One MP4-side track selector accepted by widened `mux` track specs.
191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
192pub enum MuxMp4TrackSelector {
193    /// Select the first video track from one MP4 source.
194    Video,
195    /// Select one audio track occurrence from one MP4 source.
196    ///
197    /// The occurrence index is one-based in the public surface, so `1` means the first audio
198    /// track in file order and `2` means the second.
199    Audio { occurrence: u32 },
200    /// Select one text-track occurrence from one MP4 source.
201    ///
202    /// The occurrence index is one-based in the public surface.
203    Text { occurrence: u32 },
204    /// Select one specific track identifier from one MP4 source.
205    TrackId { track_id: u32 },
206}
207
208/// One validated public track specification for the mux task surface.
209///
210/// The current path-first `mux` grammar uses one repeated track-spec model for both CLI and
211/// library callers:
212/// - path-only imports: `PATH`
213/// - path plus selector: `PATH#video`, `PATH#audio`, `PATH#audio:N`, `PATH#text`,
214///   `PATH#text:N`, `PATH#track:ID`
215/// - explicit bare raw-video imports: `PATH#rawvideo:size=WIDTHxHEIGHT,spfmt=PIXFMT,fps=NUM/DEN`
216///
217/// The raw-video form is intentionally explicit. Unlike self-describing YUV4MPEG streams, bare
218/// raw video needs out-of-band geometry, pixel-format, and frame-rate metadata before `mp4forge`
219/// can author a truthful `uncv` sample entry.
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
221pub enum MuxRawVideoPixelFormat {
222    /// Planar 8-bit YUV 4:2:0.
223    Yuv420p8,
224    /// Planar 8-bit YVU 4:2:0.
225    Yvu420p8,
226    /// Planar 10-bit YUV 4:2:0 stored in 16-bit words.
227    Yuv420p10,
228    /// Planar 8-bit YUV 4:2:2.
229    Yuv422p8,
230    /// Planar 10-bit YUV 4:2:2 stored in 16-bit words.
231    Yuv422p10,
232    /// Planar 8-bit YUV 4:4:4.
233    Yuv444p8,
234    /// Planar 10-bit YUV 4:4:4 stored in 16-bit words.
235    Yuv444p10,
236    /// Planar 8-bit YUV 4:2:0 with alpha.
237    Yuva420p8,
238    /// Planar 8-bit YUV 4:2:0 with depth.
239    Yuvd420p8,
240    /// Planar 8-bit YUV 4:4:4 with alpha.
241    Yuva444p8,
242    /// Semi-planar 8-bit NV12.
243    Nv12p8,
244    /// Semi-planar 8-bit NV21.
245    Nv21p8,
246    /// Semi-planar 10-bit NV12 stored in 16-bit words.
247    Nv12p10,
248    /// Semi-planar 10-bit NV21 stored in 16-bit words.
249    Nv21p10,
250    /// Packed 8-bit UYVY 4:2:2.
251    Uyvy422p8,
252    /// Packed 8-bit VYUY 4:2:2.
253    Vyuy422p8,
254    /// Packed 8-bit YUYV 4:2:2.
255    Yuyv422p8,
256    /// Packed 8-bit YVYU 4:2:2.
257    Yvyu422p8,
258    /// Packed 10-bit UYVY 4:2:2 stored in 16-bit words.
259    Uyvy422p10,
260    /// Packed 10-bit VYUY 4:2:2 stored in 16-bit words.
261    Vyuy422p10,
262    /// Packed 10-bit YUYV 4:2:2 stored in 16-bit words.
263    Yuyv422p10,
264    /// Packed 10-bit YVYU 4:2:2 stored in 16-bit words.
265    Yvyu422p10,
266    /// Packed 8-bit YUV 4:4:4.
267    Yuv444Packed8,
268    /// Packed 8-bit VYU 4:4:4.
269    Vyu444Packed8,
270    /// Packed 8-bit YUV 4:4:4 with alpha.
271    Yuva444Packed8,
272    /// Packed 8-bit UYV 4:4:4 with alpha.
273    Uyva444Packed8,
274    /// Packed 10-bit UYV 4:4:4 little-endian.
275    Yuv444Packed10,
276    /// Packed 10-bit v210 4:2:2 little-endian.
277    V210,
278    /// 8-bit greyscale.
279    Grey8,
280    /// 8-bit alpha followed by 8-bit greyscale.
281    AlphaGrey8,
282    /// 8-bit greyscale followed by 8-bit alpha.
283    GreyAlpha8,
284    /// Packed RGB 3:3:2.
285    Rgb332,
286    /// Packed RGB 4:4:4 stored in 16 bits.
287    Rgb444,
288    /// Packed RGB 5:5:5 stored in 16 bits.
289    Rgb555,
290    /// Packed RGB 5:6:5 stored in 16 bits.
291    Rgb565,
292    /// Packed 24-bit RGB in byte order `R-G-B`.
293    Rgb24,
294    /// Packed 24-bit RGB in byte order `B-G-R`.
295    Bgr24,
296    /// Packed 32-bit RGB in byte order `R-G-B-X`.
297    Rgbx32,
298    /// Packed 32-bit RGB in byte order `B-G-R-X`.
299    Bgrx32,
300    /// Packed 32-bit RGB in byte order `X-R-G-B`.
301    Xrgb32,
302    /// Packed 32-bit RGB in byte order `X-B-G-R`.
303    Xbgr32,
304    /// Packed 32-bit RGBA in byte order `A-R-G-B`.
305    Argb32,
306    /// Packed 32-bit RGBA in byte order `R-G-B-A`.
307    Rgba32,
308    /// Packed 32-bit RGBA in byte order `B-G-R-A`.
309    Bgra32,
310    /// Packed 32-bit RGBA in byte order `A-B-G-R`.
311    Abgr32,
312    /// Packed 32-bit RGB with depth.
313    Rgbd32,
314    /// Packed 32-bit RGB with depth and bit-shape.
315    Rgbds32,
316}
317
318impl MuxRawVideoPixelFormat {
319    /// Returns the canonical raw-video pixel-format label.
320    pub const fn canonical_name(self) -> &'static str {
321        match self {
322            Self::Yuv420p8 => "yuv420",
323            Self::Yvu420p8 => "yvu420",
324            Self::Yuv420p10 => "yuv420_10",
325            Self::Yuv422p8 => "yuv422",
326            Self::Yuv422p10 => "yuv422_10",
327            Self::Yuv444p8 => "yuv444",
328            Self::Yuv444p10 => "yuv444_10",
329            Self::Yuva420p8 => "yuva",
330            Self::Yuvd420p8 => "yuvd",
331            Self::Yuva444p8 => "yuv444a",
332            Self::Nv12p8 => "nv12",
333            Self::Nv21p8 => "nv21",
334            Self::Nv12p10 => "nv12_10",
335            Self::Nv21p10 => "nv21_10",
336            Self::Uyvy422p8 => "uyvy",
337            Self::Vyuy422p8 => "vyuy",
338            Self::Yuyv422p8 => "yuyv",
339            Self::Yvyu422p8 => "yvyu",
340            Self::Uyvy422p10 => "uyvl",
341            Self::Vyuy422p10 => "vyul",
342            Self::Yuyv422p10 => "yuyl",
343            Self::Yvyu422p10 => "yvyl",
344            Self::Yuv444Packed8 => "yuv444p",
345            Self::Vyu444Packed8 => "v308",
346            Self::Yuva444Packed8 => "yuv444ap",
347            Self::Uyva444Packed8 => "v408",
348            Self::Yuv444Packed10 => "v410",
349            Self::V210 => "v210",
350            Self::Grey8 => "grey",
351            Self::AlphaGrey8 => "algr",
352            Self::GreyAlpha8 => "gral",
353            Self::Rgb332 => "rgb8",
354            Self::Rgb444 => "rgb4",
355            Self::Rgb555 => "rgb5",
356            Self::Rgb565 => "rgb6",
357            Self::Rgb24 => "rgb",
358            Self::Bgr24 => "bgr",
359            Self::Rgbx32 => "rgbx",
360            Self::Bgrx32 => "bgrx",
361            Self::Xrgb32 => "xrgb",
362            Self::Xbgr32 => "xbgr",
363            Self::Argb32 => "argb",
364            Self::Rgba32 => "rgba",
365            Self::Bgra32 => "bgra",
366            Self::Abgr32 => "abgr",
367            Self::Rgbd32 => "rgbd",
368            Self::Rgbds32 => "rgbds",
369        }
370    }
371
372    fn parse(spec: &str, value: &str) -> Result<Self, MuxError> {
373        match value {
374            "yuv420" | "yuv" => Ok(Self::Yuv420p8),
375            "yvu420" | "yvu" => Ok(Self::Yvu420p8),
376            "yuv420_10" | "yuvl" => Ok(Self::Yuv420p10),
377            "yuv422" | "yuv2" => Ok(Self::Yuv422p8),
378            "yuv422_10" | "yp2l" => Ok(Self::Yuv422p10),
379            "yuv444" | "yuv4" => Ok(Self::Yuv444p8),
380            "yuv444_10" | "yp4l" => Ok(Self::Yuv444p10),
381            "yuva" => Ok(Self::Yuva420p8),
382            "yuvd" => Ok(Self::Yuvd420p8),
383            "yuv444a" | "yp4a" => Ok(Self::Yuva444p8),
384            "nv12" => Ok(Self::Nv12p8),
385            "nv21" => Ok(Self::Nv21p8),
386            "nv12_10" | "nv1l" => Ok(Self::Nv12p10),
387            "nv21_10" | "nv2l" => Ok(Self::Nv21p10),
388            "uyvy" => Ok(Self::Uyvy422p8),
389            "vyuy" => Ok(Self::Vyuy422p8),
390            "yuyv" => Ok(Self::Yuyv422p8),
391            "yvyu" => Ok(Self::Yvyu422p8),
392            "uyvl" => Ok(Self::Uyvy422p10),
393            "vyul" => Ok(Self::Vyuy422p10),
394            "yuyl" => Ok(Self::Yuyv422p10),
395            "yvyl" => Ok(Self::Yvyu422p10),
396            "yuv444p" | "yv4p" => Ok(Self::Yuv444Packed8),
397            "v308" => Ok(Self::Vyu444Packed8),
398            "yuv444ap" | "y4ap" => Ok(Self::Yuva444Packed8),
399            "v408" => Ok(Self::Uyva444Packed8),
400            "v410" => Ok(Self::Yuv444Packed10),
401            "v210" => Ok(Self::V210),
402            "grey" => Ok(Self::Grey8),
403            "algr" => Ok(Self::AlphaGrey8),
404            "gral" => Ok(Self::GreyAlpha8),
405            "rgb8" => Ok(Self::Rgb332),
406            "rgb4" => Ok(Self::Rgb444),
407            "rgb5" => Ok(Self::Rgb555),
408            "rgb6" => Ok(Self::Rgb565),
409            "rgb" => Ok(Self::Rgb24),
410            "bgr" => Ok(Self::Bgr24),
411            "rgbx" => Ok(Self::Rgbx32),
412            "bgrx" => Ok(Self::Bgrx32),
413            "xrgb" => Ok(Self::Xrgb32),
414            "xbgr" => Ok(Self::Xbgr32),
415            "argb" => Ok(Self::Argb32),
416            "rgba" => Ok(Self::Rgba32),
417            "bgra" => Ok(Self::Bgra32),
418            "abgr" => Ok(Self::Abgr32),
419            "rgbd" => Ok(Self::Rgbd32),
420            "rgbds" => Ok(Self::Rgbds32),
421            _ => Err(MuxError::InvalidTrackSpec {
422                spec: spec.to_string(),
423                message: format!(
424                    "unsupported rawvideo `spfmt={value}`; expected one of the rawvideo pixel formats supported by mp4forge"
425                ),
426            }),
427        }
428    }
429}
430
431/// One explicit bare raw-video import description for the mux surface.
432#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
433pub struct MuxRawVideoParams {
434    width: u32,
435    height: u32,
436    pixel_format: MuxRawVideoPixelFormat,
437    fps_num: u32,
438    fps_den: u32,
439}
440
441impl MuxRawVideoParams {
442    /// Validates one explicit raw-video import description.
443    pub fn new(
444        width: u32,
445        height: u32,
446        pixel_format: MuxRawVideoPixelFormat,
447        fps_num: u32,
448        fps_den: u32,
449    ) -> Result<Self, MuxError> {
450        if width == 0 || height == 0 {
451            return Err(MuxError::InvalidTrackSpec {
452                spec: "rawvideo".to_string(),
453                message: "rawvideo `size` must declare non-zero width and height".to_string(),
454            });
455        }
456        if fps_num == 0 || fps_den == 0 {
457            return Err(MuxError::InvalidTrackSpec {
458                spec: "rawvideo".to_string(),
459                message: "rawvideo `fps` must declare non-zero numerator and denominator"
460                    .to_string(),
461            });
462        }
463        Ok(Self {
464            width,
465            height,
466            pixel_format,
467            fps_num,
468            fps_den,
469        })
470    }
471
472    /// Returns the declared frame width in pixels.
473    pub const fn width(&self) -> u32 {
474        self.width
475    }
476
477    /// Returns the declared frame height in pixels.
478    pub const fn height(&self) -> u32 {
479        self.height
480    }
481
482    /// Returns the declared pixel format.
483    pub const fn pixel_format(&self) -> MuxRawVideoPixelFormat {
484        self.pixel_format
485    }
486
487    /// Returns the declared frame-rate numerator.
488    pub const fn fps_num(&self) -> u32 {
489        self.fps_num
490    }
491
492    /// Returns the declared frame-rate denominator.
493    pub const fn fps_den(&self) -> u32 {
494        self.fps_den
495    }
496
497    fn format_suffix(&self) -> String {
498        format!(
499            "rawvideo:size={}x{},spfmt={},fps={}/{}",
500            self.width,
501            self.height,
502            self.pixel_format.canonical_name(),
503            self.fps_num,
504            self.fps_den
505        )
506    }
507}
508
509#[derive(Clone, Debug, PartialEq, Eq, Hash)]
510pub enum MuxTrackSpec {
511    /// Import one input path, optionally selecting one track when the source is containerized.
512    Path {
513        /// The filesystem path to import.
514        path: PathBuf,
515        /// The optional public selector to resolve inside that source.
516        selector: Option<MuxMp4TrackSelector>,
517    },
518    /// Import one bare raw-video input using explicit out-of-band geometry and frame-rate data.
519    RawVideo {
520        /// The filesystem path to import.
521        path: PathBuf,
522        /// The explicit raw-video parameters.
523        params: MuxRawVideoParams,
524    },
525}
526
527impl MuxTrackSpec {
528    /// Creates one path-first track specification from `path`.
529    pub fn path(path: impl Into<PathBuf>) -> Self {
530        Self::Path {
531            path: path.into(),
532            selector: None,
533        }
534    }
535
536    /// Creates one path-first track specification from `path` and `selector`.
537    pub fn selected(path: impl Into<PathBuf>, selector: MuxMp4TrackSelector) -> Self {
538        Self::Path {
539            path: path.into(),
540            selector: Some(selector),
541        }
542    }
543
544    /// Creates one compatibility selected track specification from `path` and `selector`.
545    pub fn mp4(path: impl Into<PathBuf>, selector: MuxMp4TrackSelector) -> Self {
546        Self::selected(path, selector)
547    }
548
549    /// Creates one explicit bare raw-video track specification from `path` and `params`.
550    pub fn raw_video(path: impl Into<PathBuf>, params: MuxRawVideoParams) -> Self {
551        Self::RawVideo {
552            path: path.into(),
553            params,
554        }
555    }
556
557    /// Returns the filesystem path referenced by this track specification.
558    pub fn input_path(&self) -> &Path {
559        match self {
560            Self::Path { path, .. } => path.as_path(),
561            Self::RawVideo { path, .. } => path.as_path(),
562        }
563    }
564}
565
566impl FromStr for MuxTrackSpec {
567    type Err = MuxError;
568
569    fn from_str(value: &str) -> Result<Self, Self::Err> {
570        if value.is_empty() {
571            return Err(MuxError::InvalidTrackSpec {
572                spec: value.to_string(),
573                message: "missing input path".to_string(),
574            });
575        }
576
577        if let Some((path, selector_text)) = value.rsplit_once('#') {
578            if path.is_empty() {
579                return Err(MuxError::InvalidTrackSpec {
580                    spec: value.to_string(),
581                    message: "missing input path before `#`".to_string(),
582                });
583            }
584            if let Some(rawvideo_text) = selector_text.strip_prefix("rawvideo:") {
585                let params = parse_raw_video_params(value, rawvideo_text)?;
586                return Ok(Self::RawVideo {
587                    path: PathBuf::from(path),
588                    params,
589                });
590            }
591            let selector = parse_mp4_track_selector(value, selector_text)?;
592            return Ok(Self::Path {
593                path: PathBuf::from(path),
594                selector: Some(selector),
595            });
596        }
597
598        Ok(Self::path(value))
599    }
600}
601
602fn parse_mp4_track_selector(spec: &str, selector: &str) -> Result<MuxMp4TrackSelector, MuxError> {
603    if selector.is_empty() {
604        return Err(MuxError::InvalidTrackSpec {
605            spec: spec.to_string(),
606            message:
607                "expected one selector after `#`, such as `video`, `audio`, `text`, or `track:ID`"
608                    .to_string(),
609        });
610    }
611    if selector.contains('=') || selector.contains(',') {
612        return Err(MuxError::InvalidTrackSpec {
613            spec: spec.to_string(),
614            message: "public mux track specs only allow selector suffixes such as `#video`, `#audio`, `#text`, or `#track:ID`; raw `#name=value` parameters are no longer accepted".to_string(),
615        });
616    }
617    if selector == "video" {
618        return Ok(MuxMp4TrackSelector::Video);
619    }
620    if selector == "audio" {
621        return Ok(MuxMp4TrackSelector::Audio { occurrence: 1 });
622    }
623    if selector == "text" {
624        return Ok(MuxMp4TrackSelector::Text { occurrence: 1 });
625    }
626    if let Some(index) = selector.strip_prefix("audio:") {
627        let occurrence = index
628            .parse::<u32>()
629            .map_err(|_| MuxError::InvalidTrackSpec {
630                spec: spec.to_string(),
631                message: format!("invalid audio occurrence `{index}`"),
632            })?;
633        if occurrence == 0 {
634            return Err(MuxError::InvalidTrackSpec {
635                spec: spec.to_string(),
636                message: "audio occurrences are one-based; `audio:0` is invalid".to_string(),
637            });
638        }
639        return Ok(MuxMp4TrackSelector::Audio { occurrence });
640    }
641    if let Some(index) = selector.strip_prefix("text:") {
642        let occurrence = index
643            .parse::<u32>()
644            .map_err(|_| MuxError::InvalidTrackSpec {
645                spec: spec.to_string(),
646                message: format!("invalid text occurrence `{index}`"),
647            })?;
648        if occurrence == 0 {
649            return Err(MuxError::InvalidTrackSpec {
650                spec: spec.to_string(),
651                message: "text occurrences are one-based; `text:0` is invalid".to_string(),
652            });
653        }
654        return Ok(MuxMp4TrackSelector::Text { occurrence });
655    }
656    if let Some(track_id) = selector.strip_prefix("track:") {
657        let track_id = track_id
658            .parse::<u32>()
659            .map_err(|_| MuxError::InvalidTrackSpec {
660                spec: spec.to_string(),
661                message: format!("invalid track id `{track_id}`"),
662            })?;
663        if track_id == 0 {
664            return Err(MuxError::InvalidTrackSpec {
665                spec: spec.to_string(),
666                message: "track ids are one-based; `track:0` is invalid".to_string(),
667            });
668        }
669        return Ok(MuxMp4TrackSelector::TrackId { track_id });
670    }
671
672    Err(MuxError::InvalidTrackSpec {
673        spec: spec.to_string(),
674        message: format!(
675            "unsupported MP4 track selector `{selector}`; expected `video`, `audio`, `audio:N`, `text`, `text:N`, or `track:ID`"
676        ),
677    })
678}
679
680fn parse_raw_video_params(spec: &str, rawvideo_text: &str) -> Result<MuxRawVideoParams, MuxError> {
681    if rawvideo_text.is_empty() {
682        return Err(MuxError::InvalidTrackSpec {
683            spec: spec.to_string(),
684            message:
685                "expected rawvideo parameters after `#rawvideo:`, such as `size=1920x1080,spfmt=yuv420,fps=25/1`"
686                    .to_string(),
687        });
688    }
689
690    let mut width = None::<u32>;
691    let mut height = None::<u32>;
692    let mut pixel_format = None::<MuxRawVideoPixelFormat>;
693    let mut fps_num = None::<u32>;
694    let mut fps_den = None::<u32>;
695
696    for token in rawvideo_text.split(',') {
697        let (name, value) = token.split_once('=').ok_or_else(|| MuxError::InvalidTrackSpec {
698            spec: spec.to_string(),
699            message: format!(
700                "invalid rawvideo parameter `{token}`; expected `name=value` pairs separated by commas"
701            ),
702        })?;
703        match name {
704            "size" => {
705                let (parsed_width, parsed_height) =
706                    value
707                        .split_once('x')
708                        .ok_or_else(|| MuxError::InvalidTrackSpec {
709                            spec: spec.to_string(),
710                            message: "rawvideo `size` must use `WIDTHxHEIGHT`".to_string(),
711                        })?;
712                width =
713                    Some(
714                        parsed_width
715                            .parse::<u32>()
716                            .map_err(|_| MuxError::InvalidTrackSpec {
717                                spec: spec.to_string(),
718                                message: format!("invalid rawvideo width `{parsed_width}`"),
719                            })?,
720                    );
721                height =
722                    Some(
723                        parsed_height
724                            .parse::<u32>()
725                            .map_err(|_| MuxError::InvalidTrackSpec {
726                                spec: spec.to_string(),
727                                message: format!("invalid rawvideo height `{parsed_height}`"),
728                            })?,
729                    );
730            }
731            "spfmt" => {
732                pixel_format = Some(MuxRawVideoPixelFormat::parse(spec, value)?);
733            }
734            "fps" => {
735                let (parsed_num, parsed_den) =
736                    value
737                        .split_once('/')
738                        .ok_or_else(|| MuxError::InvalidTrackSpec {
739                            spec: spec.to_string(),
740                            message: "rawvideo `fps` must use `NUM/DEN`".to_string(),
741                        })?;
742                fps_num =
743                    Some(
744                        parsed_num
745                            .parse::<u32>()
746                            .map_err(|_| MuxError::InvalidTrackSpec {
747                                spec: spec.to_string(),
748                                message: format!(
749                                    "invalid rawvideo frame-rate numerator `{parsed_num}`"
750                                ),
751                            })?,
752                    );
753                fps_den =
754                    Some(
755                        parsed_den
756                            .parse::<u32>()
757                            .map_err(|_| MuxError::InvalidTrackSpec {
758                                spec: spec.to_string(),
759                                message: format!(
760                                    "invalid rawvideo frame-rate denominator `{parsed_den}`"
761                                ),
762                            })?,
763                    );
764            }
765            _ => {
766                return Err(MuxError::InvalidTrackSpec {
767                    spec: spec.to_string(),
768                    message: format!(
769                        "unsupported rawvideo parameter `{name}`; expected `size`, `spfmt`, or `fps`"
770                    ),
771                });
772            }
773        }
774    }
775
776    let width = width.ok_or_else(|| MuxError::InvalidTrackSpec {
777        spec: spec.to_string(),
778        message: "rawvideo track specs must declare `size=WIDTHxHEIGHT`".to_string(),
779    })?;
780    let height = height.ok_or_else(|| MuxError::InvalidTrackSpec {
781        spec: spec.to_string(),
782        message: "rawvideo track specs must declare `size=WIDTHxHEIGHT`".to_string(),
783    })?;
784    let pixel_format = pixel_format.ok_or_else(|| MuxError::InvalidTrackSpec {
785        spec: spec.to_string(),
786        message: "rawvideo track specs must declare `spfmt=PIXFMT`".to_string(),
787    })?;
788    let fps_num = fps_num.ok_or_else(|| MuxError::InvalidTrackSpec {
789        spec: spec.to_string(),
790        message: "rawvideo track specs must declare `fps=NUM/DEN`".to_string(),
791    })?;
792    let fps_den = fps_den.ok_or_else(|| MuxError::InvalidTrackSpec {
793        spec: spec.to_string(),
794        message: "rawvideo track specs must declare `fps=NUM/DEN`".to_string(),
795    })?;
796    MuxRawVideoParams::new(width, height, pixel_format, fps_num, fps_den).map_err(|error| {
797        match error {
798            MuxError::InvalidTrackSpec { message, .. } => MuxError::InvalidTrackSpec {
799                spec: spec.to_string(),
800                message,
801            },
802            other => other,
803        }
804    })
805}
806
807/// Duration-boundary mode for the narrowed public mux surface.
808///
809/// The current `mp4forge` mux follow-on keeps the public duration surface intentionally narrow:
810/// callers may request exactly one boundary mode for fragmented output when the current one-file
811/// MP4 output can model it correctly.
812#[derive(Clone, Copy, Debug, PartialEq)]
813pub enum MuxDurationMode {
814    /// Coordinate track chunks around one target segment duration in seconds.
815    Segment { seconds: f64 },
816    /// Coordinate track chunks around one target fragment duration in seconds.
817    Fragment { seconds: f64 },
818}
819
820impl MuxDurationMode {
821    /// Returns the public mode label used by diagnostics and CLI help.
822    pub const fn label(&self) -> &'static str {
823        match self {
824            Self::Segment { .. } => "segment_duration",
825            Self::Fragment { .. } => "fragment_duration",
826        }
827    }
828
829    /// Returns the requested duration in seconds.
830    pub const fn seconds(&self) -> f64 {
831        match self {
832            Self::Segment { seconds } | Self::Fragment { seconds } => *seconds,
833        }
834    }
835}
836
837/// Container layout used by the public mux request surface.
838///
839/// The default `mp4forge` mux behavior remains one flat `ftyp + moov + mdat` file. Fragmented
840/// output is additive and explicit so callers do not accidentally change container structure just
841/// by supplying one duration mode.
842#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
843pub enum MuxOutputLayout {
844    /// Write one flat self-contained MP4 with `ftyp`, `moov`, and `mdat`.
845    #[default]
846    Flat,
847    /// Write one fragmented MP4 with `sidx` plus one or more `moof`/`mdat` pairs.
848    Fragmented,
849}
850
851impl MuxOutputLayout {
852    /// Returns the public layout label used by CLI parsing and diagnostics.
853    pub const fn label(&self) -> &'static str {
854        match self {
855            Self::Flat => "flat",
856            Self::Fragmented => "fragmented",
857        }
858    }
859}
860
861/// Destination mode used by the public mux request surface.
862///
863/// The force-new mode writes one newly created output file to a caller-supplied path. The
864/// destination-path mode follows an update-or-create model: if the destination already exists as
865/// an MP4, its tracks are preserved and additional tracks are imported into it; otherwise the same
866/// path is treated as the newly created output file.
867#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
868pub enum MuxDestinationMode {
869    /// Write one newly created output file supplied separately to the file-backed helpers.
870    #[default]
871    CreateNew,
872    /// Preserve one destination MP4 when it already exists, or create it at the same path.
873    UpdateOrCreateDestination,
874}
875
876impl MuxDestinationMode {
877    /// Returns the public destination-mode label used by CLI parsing and diagnostics.
878    pub const fn label(&self) -> &'static str {
879        match self {
880            Self::CreateNew => "create-new",
881            Self::UpdateOrCreateDestination => "update-or-create-destination",
882        }
883    }
884}
885
886/// Event-message metadata to emit before one fragmented media fragment.
887#[derive(Clone, Debug, PartialEq, Eq)]
888pub struct MuxFragmentEventMessage {
889    fragment_index: u32,
890    version: u8,
891    scheme_id_uri: String,
892    value: String,
893    timescale: u32,
894    presentation_time_delta: u32,
895    presentation_time: u64,
896    event_duration: u32,
897    id: u32,
898    message_data: Vec<u8>,
899}
900
901impl MuxFragmentEventMessage {
902    /// Creates one version-0 event message for the zero-based fragment index.
903    #[allow(clippy::too_many_arguments)]
904    pub fn new_v0<S, V, M>(
905        fragment_index: u32,
906        scheme_id_uri: S,
907        value: V,
908        timescale: u32,
909        presentation_time_delta: u32,
910        event_duration: u32,
911        id: u32,
912        message_data: M,
913    ) -> Self
914    where
915        S: Into<String>,
916        V: Into<String>,
917        M: Into<Vec<u8>>,
918    {
919        Self {
920            fragment_index,
921            version: 0,
922            scheme_id_uri: scheme_id_uri.into(),
923            value: value.into(),
924            timescale,
925            presentation_time_delta,
926            presentation_time: 0,
927            event_duration,
928            id,
929            message_data: message_data.into(),
930        }
931    }
932
933    /// Creates one version-1 event message for the zero-based fragment index.
934    #[allow(clippy::too_many_arguments)]
935    pub fn new_v1<S, V, M>(
936        fragment_index: u32,
937        scheme_id_uri: S,
938        value: V,
939        timescale: u32,
940        presentation_time: u64,
941        event_duration: u32,
942        id: u32,
943        message_data: M,
944    ) -> Self
945    where
946        S: Into<String>,
947        V: Into<String>,
948        M: Into<Vec<u8>>,
949    {
950        Self {
951            fragment_index,
952            version: 1,
953            scheme_id_uri: scheme_id_uri.into(),
954            value: value.into(),
955            timescale,
956            presentation_time_delta: 0,
957            presentation_time,
958            event_duration,
959            id,
960            message_data: message_data.into(),
961        }
962    }
963
964    /// Returns the zero-based output fragment index this message belongs to.
965    pub const fn fragment_index(&self) -> u32 {
966        self.fragment_index
967    }
968
969    /// Returns the encoded `emsg` version.
970    pub const fn version(&self) -> u8 {
971        self.version
972    }
973
974    /// Returns the event scheme identifier.
975    pub fn scheme_id_uri(&self) -> &str {
976        &self.scheme_id_uri
977    }
978
979    /// Returns the event value string.
980    pub fn value(&self) -> &str {
981        &self.value
982    }
983
984    /// Returns the event timescale.
985    pub const fn timescale(&self) -> u32 {
986        self.timescale
987    }
988
989    /// Returns the version-0 presentation time delta.
990    pub const fn presentation_time_delta(&self) -> u32 {
991        self.presentation_time_delta
992    }
993
994    /// Returns the version-1 presentation time.
995    pub const fn presentation_time(&self) -> u64 {
996        self.presentation_time
997    }
998
999    /// Returns the event duration.
1000    pub const fn event_duration(&self) -> u32 {
1001        self.event_duration
1002    }
1003
1004    /// Returns the event identifier.
1005    pub const fn id(&self) -> u32 {
1006        self.id
1007    }
1008
1009    /// Returns the event payload bytes.
1010    pub fn message_data(&self) -> &[u8] {
1011        &self.message_data
1012    }
1013}
1014
1015/// Producer-reference-time metadata to emit before one fragmented media fragment.
1016#[derive(Clone, Debug, PartialEq, Eq)]
1017pub struct MuxProducerReferenceTime {
1018    fragment_index: u32,
1019    version: u8,
1020    flags: u32,
1021    reference_track_id: u32,
1022    ntp_timestamp: u64,
1023    media_time: u64,
1024}
1025
1026impl MuxProducerReferenceTime {
1027    /// Creates one version-1 producer-reference-time entry for the zero-based fragment index.
1028    pub const fn new(
1029        fragment_index: u32,
1030        reference_track_id: u32,
1031        ntp_timestamp: u64,
1032        media_time: u64,
1033    ) -> Self {
1034        Self {
1035            fragment_index,
1036            version: 1,
1037            flags: 0,
1038            reference_track_id,
1039            ntp_timestamp,
1040            media_time,
1041        }
1042    }
1043
1044    /// Returns a copy of this entry with an explicit encoded `prft` version.
1045    pub const fn with_version(mut self, version: u8) -> Self {
1046        self.version = version;
1047        self
1048    }
1049
1050    /// Returns a copy of this entry with explicit `prft` flags.
1051    pub const fn with_flags(mut self, flags: u32) -> Self {
1052        self.flags = flags;
1053        self
1054    }
1055
1056    /// Returns the zero-based output fragment index this entry belongs to.
1057    pub const fn fragment_index(&self) -> u32 {
1058        self.fragment_index
1059    }
1060
1061    /// Returns the encoded `prft` version.
1062    pub const fn version(&self) -> u8 {
1063        self.version
1064    }
1065
1066    /// Returns the encoded `prft` flags.
1067    pub const fn flags(&self) -> u32 {
1068        self.flags
1069    }
1070
1071    /// Returns the referenced track identifier.
1072    pub const fn reference_track_id(&self) -> u32 {
1073        self.reference_track_id
1074    }
1075
1076    /// Returns the NTP timestamp payload.
1077    pub const fn ntp_timestamp(&self) -> u64 {
1078        self.ntp_timestamp
1079    }
1080
1081    /// Returns the media-time payload before version-specific narrowing.
1082    pub const fn media_time(&self) -> u64 {
1083        self.media_time
1084    }
1085}
1086
1087/// One high-level mux request aligned with the public CLI surface.
1088///
1089/// The narrowed public `mux` surface now centers on repeated [`MuxTrackSpec`] values, one
1090/// caller-supplied destination path, one explicit output layout, and at most one
1091/// duration-boundary mode.
1092#[derive(Clone, Debug, Default, PartialEq)]
1093pub struct MuxRequest {
1094    tracks: Vec<MuxTrackSpec>,
1095    output_layout: MuxOutputLayout,
1096    destination_mode: MuxDestinationMode,
1097    duration_mode: Option<MuxDurationMode>,
1098    preserve_flat_authority_layout: bool,
1099    fragment_event_messages: Vec<MuxFragmentEventMessage>,
1100    producer_reference_times: Vec<MuxProducerReferenceTime>,
1101}
1102
1103impl MuxRequest {
1104    /// Creates one mux request from repeated public track specs.
1105    pub fn new(tracks: Vec<MuxTrackSpec>) -> Self {
1106        Self {
1107            tracks,
1108            output_layout: MuxOutputLayout::Flat,
1109            destination_mode: MuxDestinationMode::CreateNew,
1110            duration_mode: None,
1111            preserve_flat_authority_layout: false,
1112            fragment_event_messages: Vec::new(),
1113            producer_reference_times: Vec::new(),
1114        }
1115    }
1116
1117    /// Returns the public track specs carried by this request.
1118    pub fn tracks(&self) -> &[MuxTrackSpec] {
1119        &self.tracks
1120    }
1121
1122    /// Returns the explicit container layout requested by the caller.
1123    pub const fn output_layout(&self) -> MuxOutputLayout {
1124        self.output_layout
1125    }
1126
1127    /// Returns the destination mode requested by the caller.
1128    pub const fn destination_mode(&self) -> MuxDestinationMode {
1129        self.destination_mode
1130    }
1131
1132    /// Returns the configured public duration-boundary mode, if any.
1133    pub const fn duration_mode(&self) -> Option<MuxDurationMode> {
1134        self.duration_mode
1135    }
1136
1137    pub(crate) const fn preserve_flat_authority_layout(&self) -> bool {
1138        self.preserve_flat_authority_layout
1139    }
1140
1141    /// Returns configured event messages for fragmented output.
1142    pub fn fragment_event_messages(&self) -> &[MuxFragmentEventMessage] {
1143        &self.fragment_event_messages
1144    }
1145
1146    /// Returns configured producer-reference-time entries for fragmented output.
1147    pub fn producer_reference_times(&self) -> &[MuxProducerReferenceTime] {
1148        &self.producer_reference_times
1149    }
1150
1151    /// Returns a copy of this request with one explicit container layout configured.
1152    pub const fn with_output_layout(mut self, output_layout: MuxOutputLayout) -> Self {
1153        self.output_layout = output_layout;
1154        self
1155    }
1156
1157    /// Returns a copy of this request with one explicit destination mode configured.
1158    pub const fn with_destination_mode(mut self, destination_mode: MuxDestinationMode) -> Self {
1159        self.destination_mode = destination_mode;
1160        self
1161    }
1162
1163    /// Returns a copy of this request with one public duration-boundary mode configured.
1164    pub const fn with_duration_mode(mut self, duration_mode: MuxDurationMode) -> Self {
1165        self.duration_mode = Some(duration_mode);
1166        self
1167    }
1168
1169    pub(crate) const fn with_preserve_flat_authority_layout(
1170        mut self,
1171        preserve_flat_authority_layout: bool,
1172    ) -> Self {
1173        self.preserve_flat_authority_layout = preserve_flat_authority_layout;
1174        self
1175    }
1176
1177    /// Returns a copy of this request with one appended fragmented event message.
1178    pub fn with_fragment_event_message(mut self, message: MuxFragmentEventMessage) -> Self {
1179        self.fragment_event_messages.push(message);
1180        self
1181    }
1182
1183    /// Returns a copy of this request with one appended fragmented producer-reference-time entry.
1184    pub fn with_producer_reference_time(mut self, entry: MuxProducerReferenceTime) -> Self {
1185        self.producer_reference_times.push(entry);
1186        self
1187    }
1188}
1189
1190/// Interleave policy used when ordering staged media items into one output payload.
1191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
1192pub enum MuxInterleavePolicy {
1193    /// Orders staged items by normalized decode time while keeping ties stable by source and
1194    /// source-offset order.
1195    #[default]
1196    DecodeTime,
1197    /// Orders coordinated chunks by chunk ordinal first, then keeps ties stable by source and
1198    /// source-offset order. This is used only on preserved-authority flat carry paths where the
1199    /// authority layout dictates the interleave window sequence directly.
1200    ChunkOrdinalThenSource,
1201}
1202
1203/// One staged media item that a later mux step can schedule into one output payload.
1204///
1205/// The current foundation expects `decode_time` to already be normalized onto one interleave
1206/// timeline across every staged source involved in the plan. Future phases can widen the staging
1207/// model with richer timeline normalization once full container assembly lands.
1208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1209pub struct MuxStagedMediaItem {
1210    source_index: usize,
1211    track_id: u32,
1212    decode_time: u64,
1213    composition_time_offset: i32,
1214    duration: u32,
1215    data_offset: u64,
1216    data_size: u32,
1217    is_sync_sample: bool,
1218    sample_description_index: u32,
1219}
1220
1221impl MuxStagedMediaItem {
1222    /// Creates one staged media item for a later mux payload plan.
1223    pub const fn new(
1224        source_index: usize,
1225        track_id: u32,
1226        decode_time: u64,
1227        duration: u32,
1228        data_offset: u64,
1229        data_size: u32,
1230    ) -> Self {
1231        Self {
1232            source_index,
1233            track_id,
1234            decode_time,
1235            composition_time_offset: 0,
1236            duration,
1237            data_offset,
1238            data_size,
1239            is_sync_sample: false,
1240            sample_description_index: 1,
1241        }
1242    }
1243
1244    /// Returns the staged source slot this item will read from during payload copy.
1245    pub const fn source_index(&self) -> usize {
1246        self.source_index
1247    }
1248
1249    /// Returns the destination track identifier for this item.
1250    pub const fn track_id(&self) -> u32 {
1251        self.track_id
1252    }
1253
1254    /// Returns the normalized decode time used by the current interleave planner.
1255    pub const fn decode_time(&self) -> u64 {
1256        self.decode_time
1257    }
1258
1259    /// Returns the composition offset carried with this item.
1260    pub const fn composition_time_offset(&self) -> i32 {
1261        self.composition_time_offset
1262    }
1263
1264    /// Returns this item's decode duration on the staged mux timeline.
1265    pub const fn duration(&self) -> u32 {
1266        self.duration
1267    }
1268
1269    /// Returns the source byte offset for this item's sample payload.
1270    pub const fn data_offset(&self) -> u64 {
1271        self.data_offset
1272    }
1273
1274    /// Returns the number of bytes to copy for this item's sample payload.
1275    pub const fn data_size(&self) -> u32 {
1276        self.data_size
1277    }
1278
1279    /// Returns whether the staged item is marked as a sync sample.
1280    pub const fn is_sync_sample(&self) -> bool {
1281        self.is_sync_sample
1282    }
1283
1284    /// Returns the one-based sample-description index used for this staged sample.
1285    pub const fn sample_description_index(&self) -> u32 {
1286        self.sample_description_index
1287    }
1288
1289    /// Returns a copy of this item with a non-zero composition offset.
1290    pub const fn with_composition_time_offset(mut self, composition_time_offset: i32) -> Self {
1291        self.composition_time_offset = composition_time_offset;
1292        self
1293    }
1294
1295    /// Returns a copy of this item with an explicit sync-sample marker.
1296    pub const fn with_sync_sample(mut self, is_sync_sample: bool) -> Self {
1297        self.is_sync_sample = is_sync_sample;
1298        self
1299    }
1300
1301    /// Returns a copy of this item with an explicit one-based sample-description index.
1302    pub const fn with_sample_description_index(mut self, sample_description_index: u32) -> Self {
1303        self.sample_description_index = sample_description_index;
1304        self
1305    }
1306}
1307
1308/// One planned media item with its final output payload placement.
1309///
1310/// This is the current mux-side boundary surface for future higher-level work: one item carries
1311/// the sample order, the source byte range, the decode interval, and the output payload span
1312/// without exposing the crate-private queue internals directly.
1313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1314pub struct MuxPlannedMediaItem {
1315    staged: MuxStagedMediaItem,
1316    output_offset: u64,
1317}
1318
1319impl MuxPlannedMediaItem {
1320    /// Returns the original staged media item.
1321    pub const fn staged(&self) -> &MuxStagedMediaItem {
1322        &self.staged
1323    }
1324
1325    /// Returns the byte offset this item occupies in the final payload order.
1326    pub const fn output_offset(&self) -> u64 {
1327        self.output_offset
1328    }
1329
1330    /// Returns the first byte offset after this item's payload in the final output order.
1331    pub const fn output_end_offset(&self) -> u64 {
1332        self.output_offset + self.staged.data_size as u64
1333    }
1334
1335    /// Returns the decode end time of this item on the planned mux timeline.
1336    pub const fn decode_end_time(&self) -> u64 {
1337        self.staged.decode_time + self.staged.duration as u64
1338    }
1339}
1340
1341/// Aggregate per-track timing and item-count information for a mux plan.
1342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1343pub struct MuxTrackPlan {
1344    track_id: u32,
1345    item_count: u32,
1346    first_decode_time: u64,
1347    end_decode_time: u64,
1348}
1349
1350impl MuxTrackPlan {
1351    /// Returns the track identifier summarized by this plan entry.
1352    pub const fn track_id(&self) -> u32 {
1353        self.track_id
1354    }
1355
1356    /// Returns the number of staged items scheduled for this track.
1357    pub const fn item_count(&self) -> u32 {
1358        self.item_count
1359    }
1360
1361    /// Returns the earliest decode time assigned to this track in the current plan.
1362    pub const fn first_decode_time(&self) -> u64 {
1363        self.first_decode_time
1364    }
1365
1366    /// Returns the decode end time of the last staged item scheduled for this track.
1367    pub const fn end_decode_time(&self) -> u64 {
1368        self.end_decode_time
1369    }
1370}
1371
1372/// Planned mux payload order and per-track timing summaries.
1373///
1374/// The stable task-level plan view intentionally mirrors the internal mux event graph. Callers
1375/// continue to consume planned items and per-track summaries, while the crate-private event graph
1376/// drives the current payload-copy, chunk coordination, and planned sample-reader helpers
1377/// underneath.
1378#[derive(Clone, Debug, PartialEq, Eq)]
1379pub struct MuxPlan {
1380    interleave_policy: MuxInterleavePolicy,
1381    planned_items: Vec<MuxPlannedMediaItem>,
1382    track_plans: Vec<MuxTrackPlan>,
1383    total_payload_size: u64,
1384    coordination: MuxCoordinationPlan,
1385    event_graph: MuxEventGraph,
1386}
1387
1388impl MuxPlan {
1389    /// Returns the interleave policy used when building this plan.
1390    pub const fn interleave_policy(&self) -> MuxInterleavePolicy {
1391        self.interleave_policy
1392    }
1393
1394    /// Returns the staged items in final payload order.
1395    ///
1396    /// This slice is the stable task-level view of the current mux event graph. Callers that need
1397    /// sample-order timing or payload spans should build on these planned items instead of
1398    /// depending on the crate-private event graph directly.
1399    pub fn planned_items(&self) -> &[MuxPlannedMediaItem] {
1400        &self.planned_items
1401    }
1402
1403    /// Returns the per-track summaries collected during planning.
1404    pub fn track_plans(&self) -> &[MuxTrackPlan] {
1405        &self.track_plans
1406    }
1407
1408    /// Returns the total number of bytes the planned payload copy will emit.
1409    pub const fn total_payload_size(&self) -> u64 {
1410        self.total_payload_size
1411    }
1412
1413    pub(crate) fn chunk_sample_counts(&self, track_id: u32) -> Result<&[u32], MuxError> {
1414        self.coordination.chunk_sample_counts(track_id)
1415    }
1416
1417    pub(crate) fn event_graph(&self) -> &MuxEventGraph {
1418        &self.event_graph
1419    }
1420}
1421
1422/// File-level MP4 mux configuration for the real container-writing surface.
1423#[derive(Clone, Debug, PartialEq, Eq)]
1424pub struct MuxFileConfig {
1425    movie_timescale: u32,
1426    major_brand: FourCc,
1427    minor_version: u32,
1428    compatible_brands: Vec<FourCc>,
1429    auto_flat_profile: bool,
1430    allow_audio_only_iods: bool,
1431    keep_flat_free_box: bool,
1432    keep_flat_authority_brands: bool,
1433    preserve_auto_flat_movie_timescale: bool,
1434    emit_default_flat_tool_metadata: bool,
1435    flat_source_encoding_metadata: Option<String>,
1436    flat_source_encoder_metadata: Option<String>,
1437    flat_source_movie_creation_time: Option<u64>,
1438    flat_source_movie_modification_time: Option<u64>,
1439    preserved_flat_prefix_bytes: Vec<u8>,
1440    preserved_flat_iods_bytes: Option<Vec<u8>>,
1441    preserved_flat_udta_bytes: Option<Vec<u8>>,
1442    fragment_event_messages: Vec<MuxFragmentEventMessage>,
1443    producer_reference_times: Vec<MuxProducerReferenceTime>,
1444}
1445
1446impl MuxFileConfig {
1447    /// Creates one MP4 mux configuration with the supplied movie timescale.
1448    ///
1449    /// The default brand layout is `isom` plus `mp42` compatibility.
1450    pub fn new(movie_timescale: u32) -> Self {
1451        Self {
1452            movie_timescale,
1453            major_brand: FourCc::from_bytes(*b"isom"),
1454            minor_version: 0,
1455            compatible_brands: vec![FourCc::from_bytes(*b"isom"), FourCc::from_bytes(*b"mp42")],
1456            auto_flat_profile: false,
1457            allow_audio_only_iods: false,
1458            keep_flat_free_box: false,
1459            keep_flat_authority_brands: false,
1460            preserve_auto_flat_movie_timescale: false,
1461            emit_default_flat_tool_metadata: true,
1462            flat_source_encoding_metadata: None,
1463            flat_source_encoder_metadata: None,
1464            flat_source_movie_creation_time: None,
1465            flat_source_movie_modification_time: None,
1466            preserved_flat_prefix_bytes: Vec::new(),
1467            preserved_flat_iods_bytes: None,
1468            preserved_flat_udta_bytes: None,
1469            fragment_event_messages: Vec::new(),
1470            producer_reference_times: Vec::new(),
1471        }
1472    }
1473
1474    /// Returns the movie timescale used for `mvhd` and `tkhd` durations.
1475    pub const fn movie_timescale(&self) -> u32 {
1476        self.movie_timescale
1477    }
1478
1479    /// Returns the file's major brand.
1480    pub const fn major_brand(&self) -> FourCc {
1481        self.major_brand
1482    }
1483
1484    /// Returns the file's minor version.
1485    pub const fn minor_version(&self) -> u32 {
1486        self.minor_version
1487    }
1488
1489    /// Returns the compatible brands written into `ftyp`.
1490    pub fn compatible_brands(&self) -> &[FourCc] {
1491        &self.compatible_brands
1492    }
1493
1494    /// Returns a copy of this configuration with a different major brand.
1495    pub const fn with_major_brand(mut self, major_brand: FourCc) -> Self {
1496        self.major_brand = major_brand;
1497        self
1498    }
1499
1500    /// Returns a copy of this configuration with a different minor version.
1501    pub const fn with_minor_version(mut self, minor_version: u32) -> Self {
1502        self.minor_version = minor_version;
1503        self
1504    }
1505
1506    /// Adds `brand` to the compatibility list if it is not already present.
1507    pub fn add_compatible_brand(&mut self, brand: FourCc) {
1508        if !self.compatible_brands.contains(&brand) {
1509            self.compatible_brands.push(brand);
1510        }
1511    }
1512
1513    /// Returns a copy of this configuration with one extra compatible brand.
1514    pub fn with_compatible_brand(mut self, brand: FourCc) -> Self {
1515        self.add_compatible_brand(brand);
1516        self
1517    }
1518
1519    pub(crate) fn with_compatible_brands(mut self, compatible_brands: Vec<FourCc>) -> Self {
1520        self.compatible_brands = compatible_brands;
1521        self
1522    }
1523
1524    pub(crate) const fn auto_flat_profile(&self) -> bool {
1525        self.auto_flat_profile
1526    }
1527
1528    pub(crate) const fn with_auto_flat_profile(mut self, auto_flat_profile: bool) -> Self {
1529        self.auto_flat_profile = auto_flat_profile;
1530        self
1531    }
1532
1533    pub(crate) const fn allow_audio_only_iods(&self) -> bool {
1534        self.allow_audio_only_iods
1535    }
1536
1537    pub(crate) const fn with_allow_audio_only_iods(mut self, allow_audio_only_iods: bool) -> Self {
1538        self.allow_audio_only_iods = allow_audio_only_iods;
1539        self
1540    }
1541
1542    pub(crate) const fn keep_flat_free_box(&self) -> bool {
1543        self.keep_flat_free_box
1544    }
1545
1546    pub(crate) const fn with_keep_flat_free_box(mut self, keep_flat_free_box: bool) -> Self {
1547        self.keep_flat_free_box = keep_flat_free_box;
1548        self
1549    }
1550
1551    pub(crate) const fn keep_flat_authority_brands(&self) -> bool {
1552        self.keep_flat_authority_brands
1553    }
1554
1555    pub(crate) const fn with_keep_flat_authority_brands(
1556        mut self,
1557        keep_flat_authority_brands: bool,
1558    ) -> Self {
1559        self.keep_flat_authority_brands = keep_flat_authority_brands;
1560        self
1561    }
1562
1563    pub(crate) const fn preserve_auto_flat_movie_timescale(&self) -> bool {
1564        self.preserve_auto_flat_movie_timescale
1565    }
1566
1567    pub(crate) const fn with_preserve_auto_flat_movie_timescale(
1568        mut self,
1569        preserve_auto_flat_movie_timescale: bool,
1570    ) -> Self {
1571        self.preserve_auto_flat_movie_timescale = preserve_auto_flat_movie_timescale;
1572        self
1573    }
1574
1575    pub(crate) const fn emit_default_flat_tool_metadata(&self) -> bool {
1576        self.emit_default_flat_tool_metadata
1577    }
1578
1579    pub(crate) const fn with_emit_default_flat_tool_metadata(
1580        mut self,
1581        emit_default_flat_tool_metadata: bool,
1582    ) -> Self {
1583        self.emit_default_flat_tool_metadata = emit_default_flat_tool_metadata;
1584        self
1585    }
1586
1587    pub(crate) fn flat_source_encoding_metadata(&self) -> Option<&str> {
1588        self.flat_source_encoding_metadata.as_deref()
1589    }
1590
1591    pub(crate) fn with_flat_source_encoding_metadata(
1592        mut self,
1593        flat_source_encoding_metadata: Option<String>,
1594    ) -> Self {
1595        self.flat_source_encoding_metadata = flat_source_encoding_metadata;
1596        self
1597    }
1598
1599    pub(crate) fn flat_source_encoder_metadata(&self) -> Option<&str> {
1600        self.flat_source_encoder_metadata.as_deref()
1601    }
1602
1603    pub(crate) fn with_flat_source_encoder_metadata(
1604        mut self,
1605        flat_source_encoder_metadata: Option<String>,
1606    ) -> Self {
1607        self.flat_source_encoder_metadata = flat_source_encoder_metadata;
1608        self
1609    }
1610
1611    pub(crate) const fn flat_source_movie_creation_time(&self) -> Option<u64> {
1612        self.flat_source_movie_creation_time
1613    }
1614
1615    pub(crate) const fn with_flat_source_movie_creation_time(
1616        mut self,
1617        flat_source_movie_creation_time: Option<u64>,
1618    ) -> Self {
1619        self.flat_source_movie_creation_time = flat_source_movie_creation_time;
1620        self
1621    }
1622
1623    pub(crate) const fn flat_source_movie_modification_time(&self) -> Option<u64> {
1624        self.flat_source_movie_modification_time
1625    }
1626
1627    pub(crate) const fn with_flat_source_movie_modification_time(
1628        mut self,
1629        flat_source_movie_modification_time: Option<u64>,
1630    ) -> Self {
1631        self.flat_source_movie_modification_time = flat_source_movie_modification_time;
1632        self
1633    }
1634
1635    pub(crate) fn preserved_flat_prefix_bytes(&self) -> &[u8] {
1636        &self.preserved_flat_prefix_bytes
1637    }
1638
1639    pub(crate) fn with_preserved_flat_prefix_bytes(
1640        mut self,
1641        preserved_flat_prefix_bytes: Vec<u8>,
1642    ) -> Self {
1643        self.preserved_flat_prefix_bytes = preserved_flat_prefix_bytes;
1644        self
1645    }
1646
1647    pub(crate) fn preserved_flat_iods_bytes(&self) -> Option<&[u8]> {
1648        self.preserved_flat_iods_bytes.as_deref()
1649    }
1650
1651    pub(crate) fn with_preserved_flat_iods_bytes(
1652        mut self,
1653        preserved_flat_iods_bytes: Option<Vec<u8>>,
1654    ) -> Self {
1655        self.preserved_flat_iods_bytes = preserved_flat_iods_bytes;
1656        self
1657    }
1658
1659    pub(crate) fn preserved_flat_udta_bytes(&self) -> Option<&[u8]> {
1660        self.preserved_flat_udta_bytes.as_deref()
1661    }
1662
1663    pub(crate) fn with_preserved_flat_udta_bytes(
1664        mut self,
1665        preserved_flat_udta_bytes: Option<Vec<u8>>,
1666    ) -> Self {
1667        self.preserved_flat_udta_bytes = preserved_flat_udta_bytes;
1668        self
1669    }
1670
1671    pub(crate) fn fragment_event_messages(&self) -> &[MuxFragmentEventMessage] {
1672        &self.fragment_event_messages
1673    }
1674
1675    pub(crate) fn with_fragment_event_messages(
1676        mut self,
1677        fragment_event_messages: Vec<MuxFragmentEventMessage>,
1678    ) -> Self {
1679        self.fragment_event_messages = fragment_event_messages;
1680        self
1681    }
1682
1683    pub(crate) fn producer_reference_times(&self) -> &[MuxProducerReferenceTime] {
1684        &self.producer_reference_times
1685    }
1686
1687    pub(crate) fn with_producer_reference_times(
1688        mut self,
1689        producer_reference_times: Vec<MuxProducerReferenceTime>,
1690    ) -> Self {
1691        self.producer_reference_times = producer_reference_times;
1692        self
1693    }
1694}
1695
1696/// Track kind used by the real MP4 mux surface.
1697#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1698pub enum MuxTrackKind {
1699    /// Sound track with `smhd`, `soun`, and non-zero default track volume.
1700    Audio,
1701    /// Visual track with `vmhd`, `vide`, width, and height metadata.
1702    Video,
1703    /// Timed text track with `nmhd`, `text`, and zero default track volume.
1704    Text,
1705    /// Timed subtitle track with `sthd`, `subt`, and zero default track volume.
1706    Subtitle,
1707}
1708
1709impl MuxTrackKind {
1710    /// Returns whether this track kind is audio.
1711    pub const fn is_audio(self) -> bool {
1712        matches!(self, Self::Audio)
1713    }
1714
1715    /// Returns whether this track kind is video.
1716    pub const fn is_video(self) -> bool {
1717        matches!(self, Self::Video)
1718    }
1719
1720    /// Returns whether this track kind is one of the timed-text families.
1721    pub const fn is_textual(self) -> bool {
1722        matches!(self, Self::Text | Self::Subtitle)
1723    }
1724}
1725
1726const DEFAULT_TKHD_FLAGS: u32 = 0x0000_0001 | 0x0000_0002 | 0x0000_0004;
1727const DEFAULT_AUDIO_ALTERNATE_GROUP: i16 = 1;
1728const DEFAULT_SUBTITLE_ALTERNATE_GROUP: i16 = 0;
1729const DEFAULT_TKHD_MATRIX: [i32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
1730
1731const fn default_alternate_group_for_kind(kind: MuxTrackKind) -> i16 {
1732    match kind {
1733        MuxTrackKind::Audio => DEFAULT_AUDIO_ALTERNATE_GROUP,
1734        MuxTrackKind::Subtitle => DEFAULT_SUBTITLE_ALTERNATE_GROUP,
1735        MuxTrackKind::Video | MuxTrackKind::Text => 0,
1736    }
1737}
1738
1739/// Per-track configuration for the real MP4 mux surface.
1740///
1741/// The muxer accepts a primary encoded sample-entry box and can retain additional entries for
1742/// imported tracks that switch sample descriptions over time.
1743#[derive(Clone, Debug, PartialEq, Eq)]
1744pub struct MuxTrackConfig {
1745    track_id: u32,
1746    kind: MuxTrackKind,
1747    timescale: u32,
1748    language: [u8; 3],
1749    handler_name: String,
1750    track_width: u16,
1751    track_height: u16,
1752    track_width_fixed_16_16: Option<u32>,
1753    track_height_fixed_16_16: Option<u32>,
1754    tkhd_flags: u32,
1755    alternate_group: i16,
1756    volume: i16,
1757    matrix: [i32; 9],
1758    edit_media_time: Option<u64>,
1759    sample_roll_distance: Option<i16>,
1760    emit_roll_sbgp: bool,
1761    sample_entry_box: Vec<u8>,
1762    sample_entry_boxes: Vec<Vec<u8>>,
1763    sync_sample_table_mode: SyncSampleTableMode,
1764    stts_run_encoding_mode: SttsRunEncodingMode,
1765    stsc_run_encoding_mode: StscRunEncodingMode,
1766    flat_timing_override: Option<FlatTimingOverride>,
1767    flat_audio_profile_level_indication: Option<u8>,
1768    fragmented_decode_time_offset: Option<u64>,
1769    fragmented_reference_group_fragment_counts: Option<Vec<u32>>,
1770    flat_source_track_creation_time: Option<u64>,
1771    flat_source_track_modification_time: Option<u64>,
1772    flat_source_media_creation_time: Option<u64>,
1773    flat_source_media_modification_time: Option<u64>,
1774    omit_flat_iods: bool,
1775    flat_stsc_override: Option<crate::boxes::iso14496_12::Stsc>,
1776    preserved_flat_stbl_boxes: Vec<Vec<u8>>,
1777    preserved_flat_trak_boxes: Vec<Vec<u8>>,
1778}
1779
1780#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1781pub(crate) enum SyncSampleTableMode {
1782    Auto,
1783    ForceEmpty,
1784    ForceFirstOnly,
1785}
1786
1787#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1788pub(crate) enum StscRunEncodingMode {
1789    CollapseIdentical,
1790    PreserveTerminalBoundary,
1791}
1792
1793#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1794pub(crate) enum SttsRunEncodingMode {
1795    CollapseIdentical,
1796    PreservePerSample,
1797}
1798
1799#[derive(Clone, Debug, PartialEq, Eq)]
1800pub(crate) struct FlatTimingOverride {
1801    pub(crate) sample_durations: Vec<u32>,
1802    pub(crate) composition_offsets: Vec<i32>,
1803    pub(crate) media_duration: u64,
1804    pub(crate) presentation_duration: u64,
1805}
1806
1807impl MuxTrackConfig {
1808    /// Creates one audio-track configuration with a full encoded sample-entry box.
1809    pub fn new_audio(track_id: u32, timescale: u32, sample_entry_box: Vec<u8>) -> Self {
1810        Self {
1811            track_id,
1812            kind: MuxTrackKind::Audio,
1813            timescale,
1814            language: *b"und",
1815            handler_name: "SoundHandler".to_string(),
1816            track_width: 0,
1817            track_height: 0,
1818            track_width_fixed_16_16: None,
1819            track_height_fixed_16_16: None,
1820            tkhd_flags: DEFAULT_TKHD_FLAGS,
1821            alternate_group: default_alternate_group_for_kind(MuxTrackKind::Audio),
1822            volume: 0x0100,
1823            matrix: DEFAULT_TKHD_MATRIX,
1824            edit_media_time: None,
1825            sample_roll_distance: None,
1826            emit_roll_sbgp: true,
1827            sample_entry_box: sample_entry_box.clone(),
1828            sample_entry_boxes: vec![sample_entry_box],
1829            sync_sample_table_mode: SyncSampleTableMode::Auto,
1830            stts_run_encoding_mode: SttsRunEncodingMode::CollapseIdentical,
1831            stsc_run_encoding_mode: StscRunEncodingMode::CollapseIdentical,
1832            flat_timing_override: None,
1833            flat_audio_profile_level_indication: None,
1834            fragmented_decode_time_offset: None,
1835            fragmented_reference_group_fragment_counts: None,
1836            flat_source_track_creation_time: None,
1837            flat_source_track_modification_time: None,
1838            flat_source_media_creation_time: None,
1839            flat_source_media_modification_time: None,
1840            omit_flat_iods: false,
1841            flat_stsc_override: None,
1842            preserved_flat_stbl_boxes: Vec::new(),
1843            preserved_flat_trak_boxes: Vec::new(),
1844        }
1845    }
1846
1847    /// Creates one video-track configuration with a full encoded sample-entry box.
1848    pub fn new_video(
1849        track_id: u32,
1850        timescale: u32,
1851        width: u16,
1852        height: u16,
1853        sample_entry_box: Vec<u8>,
1854    ) -> Self {
1855        Self {
1856            track_id,
1857            kind: MuxTrackKind::Video,
1858            timescale,
1859            language: *b"und",
1860            handler_name: "VideoHandler".to_string(),
1861            track_width: width,
1862            track_height: height,
1863            track_width_fixed_16_16: None,
1864            track_height_fixed_16_16: None,
1865            tkhd_flags: DEFAULT_TKHD_FLAGS,
1866            alternate_group: default_alternate_group_for_kind(MuxTrackKind::Video),
1867            volume: 0,
1868            matrix: DEFAULT_TKHD_MATRIX,
1869            edit_media_time: None,
1870            sample_roll_distance: None,
1871            emit_roll_sbgp: true,
1872            sample_entry_box: sample_entry_box.clone(),
1873            sample_entry_boxes: vec![sample_entry_box],
1874            sync_sample_table_mode: SyncSampleTableMode::Auto,
1875            stts_run_encoding_mode: SttsRunEncodingMode::CollapseIdentical,
1876            stsc_run_encoding_mode: StscRunEncodingMode::CollapseIdentical,
1877            flat_timing_override: None,
1878            flat_audio_profile_level_indication: None,
1879            fragmented_decode_time_offset: None,
1880            fragmented_reference_group_fragment_counts: None,
1881            flat_source_track_creation_time: None,
1882            flat_source_track_modification_time: None,
1883            flat_source_media_creation_time: None,
1884            flat_source_media_modification_time: None,
1885            omit_flat_iods: false,
1886            flat_stsc_override: None,
1887            preserved_flat_stbl_boxes: Vec::new(),
1888            preserved_flat_trak_boxes: Vec::new(),
1889        }
1890    }
1891
1892    /// Creates one timed-text track configuration with a full encoded sample-entry box.
1893    pub fn new_text(
1894        track_id: u32,
1895        timescale: u32,
1896        width: u16,
1897        height: u16,
1898        sample_entry_box: Vec<u8>,
1899    ) -> Self {
1900        Self {
1901            track_id,
1902            kind: MuxTrackKind::Text,
1903            timescale,
1904            language: *b"und",
1905            handler_name: "TextHandler".to_string(),
1906            track_width: width,
1907            track_height: height,
1908            track_width_fixed_16_16: None,
1909            track_height_fixed_16_16: None,
1910            tkhd_flags: DEFAULT_TKHD_FLAGS,
1911            alternate_group: default_alternate_group_for_kind(MuxTrackKind::Text),
1912            volume: 0,
1913            matrix: DEFAULT_TKHD_MATRIX,
1914            edit_media_time: None,
1915            sample_roll_distance: None,
1916            emit_roll_sbgp: true,
1917            sample_entry_box: sample_entry_box.clone(),
1918            sample_entry_boxes: vec![sample_entry_box],
1919            sync_sample_table_mode: SyncSampleTableMode::Auto,
1920            stts_run_encoding_mode: SttsRunEncodingMode::CollapseIdentical,
1921            stsc_run_encoding_mode: StscRunEncodingMode::CollapseIdentical,
1922            flat_timing_override: None,
1923            flat_audio_profile_level_indication: None,
1924            fragmented_decode_time_offset: None,
1925            fragmented_reference_group_fragment_counts: None,
1926            flat_source_track_creation_time: None,
1927            flat_source_track_modification_time: None,
1928            flat_source_media_creation_time: None,
1929            flat_source_media_modification_time: None,
1930            omit_flat_iods: false,
1931            flat_stsc_override: None,
1932            preserved_flat_stbl_boxes: Vec::new(),
1933            preserved_flat_trak_boxes: Vec::new(),
1934        }
1935    }
1936
1937    /// Creates one timed-subtitle track configuration with a full encoded sample-entry box.
1938    pub fn new_subtitle(
1939        track_id: u32,
1940        timescale: u32,
1941        width: u16,
1942        height: u16,
1943        sample_entry_box: Vec<u8>,
1944    ) -> Self {
1945        Self {
1946            track_id,
1947            kind: MuxTrackKind::Subtitle,
1948            timescale,
1949            language: *b"und",
1950            handler_name: "SubtitleHandler".to_string(),
1951            track_width: width,
1952            track_height: height,
1953            track_width_fixed_16_16: None,
1954            track_height_fixed_16_16: None,
1955            tkhd_flags: DEFAULT_TKHD_FLAGS,
1956            alternate_group: default_alternate_group_for_kind(MuxTrackKind::Subtitle),
1957            volume: 0,
1958            matrix: DEFAULT_TKHD_MATRIX,
1959            edit_media_time: None,
1960            sample_roll_distance: None,
1961            emit_roll_sbgp: true,
1962            sample_entry_box: sample_entry_box.clone(),
1963            sample_entry_boxes: vec![sample_entry_box],
1964            sync_sample_table_mode: SyncSampleTableMode::Auto,
1965            stts_run_encoding_mode: SttsRunEncodingMode::CollapseIdentical,
1966            stsc_run_encoding_mode: StscRunEncodingMode::CollapseIdentical,
1967            flat_timing_override: None,
1968            flat_audio_profile_level_indication: None,
1969            fragmented_decode_time_offset: None,
1970            fragmented_reference_group_fragment_counts: None,
1971            flat_source_track_creation_time: None,
1972            flat_source_track_modification_time: None,
1973            flat_source_media_creation_time: None,
1974            flat_source_media_modification_time: None,
1975            omit_flat_iods: false,
1976            flat_stsc_override: None,
1977            preserved_flat_stbl_boxes: Vec::new(),
1978            preserved_flat_trak_boxes: Vec::new(),
1979        }
1980    }
1981
1982    /// Returns the track identifier.
1983    pub const fn track_id(&self) -> u32 {
1984        self.track_id
1985    }
1986
1987    /// Returns the configured track kind.
1988    pub const fn kind(&self) -> MuxTrackKind {
1989        self.kind
1990    }
1991
1992    /// Returns the media timescale used by this track's `mdhd` and sample tables.
1993    pub const fn timescale(&self) -> u32 {
1994        self.timescale
1995    }
1996
1997    /// Returns the three-letter ISO-639-2 language code carried by this track.
1998    pub const fn language(&self) -> [u8; 3] {
1999        self.language
2000    }
2001
2002    /// Returns the handler name written into `hdlr`.
2003    pub fn handler_name(&self) -> &str {
2004        &self.handler_name
2005    }
2006
2007    /// Returns the width recorded in `tkhd` for this track.
2008    pub const fn track_width(&self) -> u16 {
2009        self.track_width
2010    }
2011
2012    /// Returns the height recorded in `tkhd` for this track.
2013    pub const fn track_height(&self) -> u16 {
2014        self.track_height
2015    }
2016
2017    pub(crate) const fn track_width_fixed_16_16(&self) -> Option<u32> {
2018        self.track_width_fixed_16_16
2019    }
2020
2021    pub(crate) const fn track_height_fixed_16_16(&self) -> Option<u32> {
2022        self.track_height_fixed_16_16
2023    }
2024
2025    pub(crate) const fn tkhd_flags(&self) -> u32 {
2026        self.tkhd_flags
2027    }
2028
2029    pub(crate) const fn flat_source_track_creation_time(&self) -> Option<u64> {
2030        self.flat_source_track_creation_time
2031    }
2032
2033    pub(crate) const fn flat_source_track_modification_time(&self) -> Option<u64> {
2034        self.flat_source_track_modification_time
2035    }
2036
2037    pub(crate) const fn flat_source_media_creation_time(&self) -> Option<u64> {
2038        self.flat_source_media_creation_time
2039    }
2040
2041    pub(crate) const fn flat_source_media_modification_time(&self) -> Option<u64> {
2042        self.flat_source_media_modification_time
2043    }
2044
2045    pub(crate) const fn omit_flat_iods(&self) -> bool {
2046        self.omit_flat_iods
2047    }
2048
2049    pub(crate) const fn alternate_group(&self) -> i16 {
2050        self.alternate_group
2051    }
2052
2053    /// Returns the fixed-point 8.8 track volume written into `tkhd`.
2054    pub const fn volume(&self) -> i16 {
2055        self.volume
2056    }
2057
2058    pub(crate) const fn matrix(&self) -> [i32; 9] {
2059        self.matrix
2060    }
2061
2062    /// Returns the optional media-time trim that should be written into one edit list.
2063    pub const fn edit_media_time(&self) -> Option<u64> {
2064        self.edit_media_time
2065    }
2066
2067    pub(crate) const fn sample_roll_distance(&self) -> Option<i16> {
2068        self.sample_roll_distance
2069    }
2070
2071    pub(crate) const fn emit_roll_sbgp(&self) -> bool {
2072        self.emit_roll_sbgp
2073    }
2074
2075    /// Returns the primary full encoded sample-entry box written under `stsd`.
2076    pub fn sample_entry_box(&self) -> &[u8] {
2077        &self.sample_entry_box
2078    }
2079
2080    /// Returns every encoded sample-entry box written under `stsd`.
2081    pub fn sample_entry_boxes(&self) -> &[Vec<u8>] {
2082        &self.sample_entry_boxes
2083    }
2084
2085    /// Returns a copy of this configuration with a different language code.
2086    pub const fn with_language(mut self, language: [u8; 3]) -> Self {
2087        self.language = language;
2088        self
2089    }
2090
2091    /// Returns a copy of this configuration with a different `hdlr` name.
2092    pub fn with_handler_name(mut self, handler_name: impl Into<String>) -> Self {
2093        self.handler_name = handler_name.into();
2094        self
2095    }
2096
2097    pub(crate) const fn with_tkhd_flags(mut self, tkhd_flags: u32) -> Self {
2098        self.tkhd_flags = tkhd_flags;
2099        self
2100    }
2101
2102    pub(crate) const fn with_flat_source_track_creation_time(
2103        mut self,
2104        flat_source_track_creation_time: Option<u64>,
2105    ) -> Self {
2106        self.flat_source_track_creation_time = flat_source_track_creation_time;
2107        self
2108    }
2109
2110    pub(crate) const fn with_flat_source_track_modification_time(
2111        mut self,
2112        flat_source_track_modification_time: Option<u64>,
2113    ) -> Self {
2114        self.flat_source_track_modification_time = flat_source_track_modification_time;
2115        self
2116    }
2117
2118    pub(crate) const fn with_flat_source_media_creation_time(
2119        mut self,
2120        flat_source_media_creation_time: Option<u64>,
2121    ) -> Self {
2122        self.flat_source_media_creation_time = flat_source_media_creation_time;
2123        self
2124    }
2125
2126    pub(crate) const fn with_flat_source_media_modification_time(
2127        mut self,
2128        flat_source_media_modification_time: Option<u64>,
2129    ) -> Self {
2130        self.flat_source_media_modification_time = flat_source_media_modification_time;
2131        self
2132    }
2133
2134    pub(crate) const fn with_omit_flat_iods(mut self, omit_flat_iods: bool) -> Self {
2135        self.omit_flat_iods = omit_flat_iods;
2136        self
2137    }
2138
2139    pub(crate) const fn with_alternate_group(mut self, alternate_group: i16) -> Self {
2140        self.alternate_group = alternate_group;
2141        self
2142    }
2143
2144    /// Returns a copy of this configuration with a different fixed-point 8.8 track volume.
2145    pub const fn with_volume(mut self, volume: i16) -> Self {
2146        self.volume = volume;
2147        self
2148    }
2149
2150    pub(crate) const fn with_matrix(mut self, matrix: [i32; 9]) -> Self {
2151        self.matrix = matrix;
2152        self
2153    }
2154
2155    pub(crate) const fn with_tkhd_dimensions_fixed_16_16(
2156        mut self,
2157        track_width_fixed_16_16: u32,
2158        track_height_fixed_16_16: u32,
2159    ) -> Self {
2160        self.track_width_fixed_16_16 = Some(track_width_fixed_16_16);
2161        self.track_height_fixed_16_16 = Some(track_height_fixed_16_16);
2162        self
2163    }
2164
2165    /// Returns a copy of this configuration with one edit-list media-time trim.
2166    pub const fn with_edit_media_time(mut self, edit_media_time: u64) -> Self {
2167        self.edit_media_time = Some(edit_media_time);
2168        self
2169    }
2170
2171    pub(crate) const fn with_sample_roll_distance(mut self, sample_roll_distance: i16) -> Self {
2172        self.sample_roll_distance = Some(sample_roll_distance);
2173        self
2174    }
2175
2176    pub(crate) const fn with_emit_roll_sbgp(mut self, emit_roll_sbgp: bool) -> Self {
2177        self.emit_roll_sbgp = emit_roll_sbgp;
2178        self
2179    }
2180
2181    pub(crate) fn with_sample_entry_boxes(mut self, sample_entry_boxes: Vec<Vec<u8>>) -> Self {
2182        if let Some(first) = sample_entry_boxes.first() {
2183            self.sample_entry_box = first.clone();
2184        }
2185        self.sample_entry_boxes = sample_entry_boxes;
2186        self
2187    }
2188
2189    pub(crate) const fn with_sync_sample_table_mode(
2190        mut self,
2191        sync_sample_table_mode: SyncSampleTableMode,
2192    ) -> Self {
2193        self.sync_sample_table_mode = sync_sample_table_mode;
2194        self
2195    }
2196
2197    pub(crate) const fn stts_run_encoding_mode(&self) -> SttsRunEncodingMode {
2198        self.stts_run_encoding_mode
2199    }
2200
2201    pub(crate) const fn with_stts_run_encoding_mode(
2202        mut self,
2203        stts_run_encoding_mode: SttsRunEncodingMode,
2204    ) -> Self {
2205        self.stts_run_encoding_mode = stts_run_encoding_mode;
2206        self
2207    }
2208
2209    pub(crate) const fn stsc_run_encoding_mode(&self) -> StscRunEncodingMode {
2210        self.stsc_run_encoding_mode
2211    }
2212
2213    pub(crate) const fn with_stsc_run_encoding_mode(
2214        mut self,
2215        stsc_run_encoding_mode: StscRunEncodingMode,
2216    ) -> Self {
2217        self.stsc_run_encoding_mode = stsc_run_encoding_mode;
2218        self
2219    }
2220
2221    pub(crate) fn flat_timing_override(&self) -> Option<&FlatTimingOverride> {
2222        self.flat_timing_override.as_ref()
2223    }
2224
2225    pub(crate) fn with_flat_timing_override(
2226        mut self,
2227        flat_timing_override: FlatTimingOverride,
2228    ) -> Self {
2229        self.flat_timing_override = Some(flat_timing_override);
2230        self
2231    }
2232
2233    pub(crate) const fn flat_audio_profile_level_indication(&self) -> Option<u8> {
2234        self.flat_audio_profile_level_indication
2235    }
2236
2237    pub(crate) const fn with_flat_audio_profile_level_indication(
2238        mut self,
2239        flat_audio_profile_level_indication: u8,
2240    ) -> Self {
2241        self.flat_audio_profile_level_indication = Some(flat_audio_profile_level_indication);
2242        self
2243    }
2244
2245    pub(crate) const fn fragmented_decode_time_offset(&self) -> Option<u64> {
2246        self.fragmented_decode_time_offset
2247    }
2248
2249    pub(crate) const fn with_fragmented_decode_time_offset(
2250        mut self,
2251        fragmented_decode_time_offset: u64,
2252    ) -> Self {
2253        self.fragmented_decode_time_offset = Some(fragmented_decode_time_offset);
2254        self
2255    }
2256
2257    pub(crate) fn fragmented_reference_group_fragment_counts(&self) -> Option<&[u32]> {
2258        self.fragmented_reference_group_fragment_counts.as_deref()
2259    }
2260
2261    pub(crate) fn with_fragmented_reference_group_fragment_counts(
2262        mut self,
2263        fragmented_reference_group_fragment_counts: Vec<u32>,
2264    ) -> Self {
2265        self.fragmented_reference_group_fragment_counts =
2266            Some(fragmented_reference_group_fragment_counts);
2267        self
2268    }
2269
2270    pub(crate) fn flat_stsc_override(&self) -> Option<&crate::boxes::iso14496_12::Stsc> {
2271        self.flat_stsc_override.as_ref()
2272    }
2273
2274    pub(crate) fn with_flat_stsc_override(
2275        mut self,
2276        flat_stsc_override: crate::boxes::iso14496_12::Stsc,
2277    ) -> Self {
2278        self.flat_stsc_override = Some(flat_stsc_override);
2279        self
2280    }
2281
2282    pub(crate) fn preserved_flat_stbl_boxes(&self) -> &[Vec<u8>] {
2283        &self.preserved_flat_stbl_boxes
2284    }
2285
2286    pub(crate) fn with_preserved_flat_stbl_boxes(
2287        mut self,
2288        preserved_flat_stbl_boxes: Vec<Vec<u8>>,
2289    ) -> Self {
2290        self.preserved_flat_stbl_boxes = preserved_flat_stbl_boxes;
2291        self
2292    }
2293
2294    pub(crate) fn preserved_flat_trak_boxes(&self) -> &[Vec<u8>] {
2295        &self.preserved_flat_trak_boxes
2296    }
2297
2298    pub(crate) fn with_preserved_flat_trak_boxes(
2299        mut self,
2300        preserved_flat_trak_boxes: Vec<Vec<u8>>,
2301    ) -> Self {
2302        self.preserved_flat_trak_boxes = preserved_flat_trak_boxes;
2303        self
2304    }
2305}
2306
2307/// Errors returned by the additive mux foundation helpers.
2308#[derive(Debug)]
2309pub enum MuxError {
2310    /// One public mux track spec did not match the fixed supported grammar.
2311    InvalidTrackSpec { spec: String, message: String },
2312    /// The current fragmented mux request selected more than one video track for one output.
2313    MultipleVideoTracks { count: usize },
2314    /// The current mux request did not carry any tracks.
2315    MissingTrackSpecs,
2316    /// One requested MP4 track selector did not resolve to a matching track.
2317    MissingTrackSelection { spec: String },
2318    /// One track import was recognized but is not supported by the current mux follow-on.
2319    UnsupportedTrackImport { spec: String, message: String },
2320    /// One duration-boundary mode conflicts with the current request shape or requested value.
2321    InvalidDurationMode { mode: &'static str, message: String },
2322    /// One explicit mux output layout conflicts with the current request shape.
2323    InvalidOutputLayout {
2324        layout: &'static str,
2325        message: String,
2326    },
2327    /// One explicit destination mode conflicts with the current request shape.
2328    InvalidDestinationMode { mode: &'static str, message: String },
2329    /// The output path conflicts with one of the supplied input paths.
2330    OutputPathConflict { output: PathBuf, input: PathBuf },
2331    /// One track timeline could not be normalized onto the selected movie timescale exactly.
2332    IncompatibleTrackTiming {
2333        track_id: u32,
2334        track_timescale: u32,
2335        movie_timescale: u32,
2336        value: i64,
2337    },
2338    /// One chunk or segment coordination plan was internally inconsistent.
2339    InvalidChunkPlan { track_id: u32, message: String },
2340    /// The planned payload would overflow a 64-bit output offset or size.
2341    PayloadSizeOverflow,
2342    /// One planned item referenced a staged source index that was not provided by the caller.
2343    MissingSourceIndex {
2344        source_index: usize,
2345        source_count: usize,
2346    },
2347    /// A progressive source would need to seek backward to satisfy the staged plan.
2348    NonMonotonicSourceOffset {
2349        source_index: usize,
2350        previous_offset: u64,
2351        next_offset: u64,
2352    },
2353    /// A progressive source ended before it reached the requested staged offset.
2354    IncompleteAdvance {
2355        source_index: usize,
2356        expected_offset: u64,
2357        actual_offset: u64,
2358    },
2359    /// A source did not produce the number of bytes described by the plan.
2360    IncompleteCopy {
2361        source_index: usize,
2362        expected_size: u64,
2363        actual_size: u64,
2364    },
2365    /// The real mux surface requires a non-zero movie timescale.
2366    InvalidMovieTimescale,
2367    /// One real mux track configuration used a zero or otherwise incompatible media timescale.
2368    InvalidTrackTimescale { track_id: u32 },
2369    /// One real mux track language code was not a valid three-letter ISO-639-2 code.
2370    InvalidTrackLanguage { track_id: u32, language: String },
2371    /// More than one track configuration used the same track identifier.
2372    DuplicateTrackId { track_id: u32 },
2373    /// The plan referenced a track that was not configured for the real mux surface.
2374    MissingTrackId { track_id: u32 },
2375    /// One configured track had no planned samples.
2376    TrackHasNoSamples { track_id: u32 },
2377    /// One track regressed in decode ordering inside the mux event graph.
2378    NonMonotonicTrackDecodeTime {
2379        track_id: u32,
2380        previous_decode_time: u64,
2381        next_decode_time: u64,
2382    },
2383    /// One configured sample-entry box was not a single valid encoded box.
2384    InvalidSampleEntryBox { track_id: u32, message: String },
2385    /// The real mux layout overflowed one container field.
2386    LayoutOverflow(&'static str),
2387    /// A typed box payload could not be encoded.
2388    Codec(CodecError),
2389    /// A container box could not be written or finalized.
2390    Writer(WriterError),
2391    /// A box header could not be parsed or encoded.
2392    Header(HeaderError),
2393    /// One typed extract helper failed while importing a track.
2394    Extract(crate::extract::ExtractError),
2395    /// One typed probe helper failed while importing a track.
2396    Probe(crate::probe::ProbeError),
2397    /// An I/O error occurred while reading staged payloads or writing output bytes.
2398    Io(io::Error),
2399}
2400
2401impl fmt::Display for MuxError {
2402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2403        match self {
2404            Self::InvalidTrackSpec { spec, message } => {
2405                write!(f, "invalid mux track spec `{spec}`: {message}")
2406            }
2407            Self::MultipleVideoTracks { count } => write!(
2408                f,
2409                "fragmented output supports at most one video track per mux output, but {count} were requested"
2410            ),
2411            Self::MissingTrackSpecs => {
2412                write!(
2413                    f,
2414                    "the current mux surface requires at least one `--track` input"
2415                )
2416            }
2417            Self::MissingTrackSelection { spec } => {
2418                write!(
2419                    f,
2420                    "mux track spec `{spec}` did not resolve to a matching input track"
2421                )
2422            }
2423            Self::UnsupportedTrackImport { spec, message } => {
2424                write!(f, "mux track spec `{spec}` is not supported: {message}")
2425            }
2426            Self::InvalidDurationMode { mode, message } => {
2427                write!(f, "invalid mux {mode}: {message}")
2428            }
2429            Self::InvalidOutputLayout { layout, message } => {
2430                write!(f, "invalid mux layout `{layout}`: {message}")
2431            }
2432            Self::InvalidDestinationMode { mode, message } => {
2433                write!(f, "invalid mux destination mode `{mode}`: {message}")
2434            }
2435            Self::OutputPathConflict { output, input } => write!(
2436                f,
2437                "output path `{}` conflicts with input `{}`",
2438                output.display(),
2439                input.display()
2440            ),
2441            Self::IncompatibleTrackTiming {
2442                track_id,
2443                track_timescale,
2444                movie_timescale,
2445                value,
2446            } => write!(
2447                f,
2448                "track {track_id} timing value {value} from timescale {track_timescale} cannot be normalized exactly onto movie timescale {movie_timescale}"
2449            ),
2450            Self::InvalidChunkPlan { track_id, message } => {
2451                write!(
2452                    f,
2453                    "track {track_id} produced an invalid chunk plan: {message}"
2454                )
2455            }
2456            Self::PayloadSizeOverflow => {
2457                write!(f, "planned mux payload size overflowed the supported range")
2458            }
2459            Self::MissingSourceIndex {
2460                source_index,
2461                source_count,
2462            } => write!(
2463                f,
2464                "mux plan referenced source index {source_index}, but only {source_count} sources were provided"
2465            ),
2466            Self::NonMonotonicSourceOffset {
2467                source_index,
2468                previous_offset,
2469                next_offset,
2470            } => write!(
2471                f,
2472                "source index {source_index} would need to move backward from offset {previous_offset} to {next_offset}"
2473            ),
2474            Self::IncompleteAdvance {
2475                source_index,
2476                expected_offset,
2477                actual_offset,
2478            } => write!(
2479                f,
2480                "source index {source_index} ended while advancing to offset {expected_offset}; only reached {actual_offset}"
2481            ),
2482            Self::IncompleteCopy {
2483                source_index,
2484                expected_size,
2485                actual_size,
2486            } => write!(
2487                f,
2488                "source index {source_index} produced {actual_size} bytes, expected {expected_size}"
2489            ),
2490            Self::InvalidMovieTimescale => {
2491                write!(f, "real mux output requires a non-zero movie timescale")
2492            }
2493            Self::InvalidTrackTimescale { track_id } => {
2494                write!(
2495                    f,
2496                    "track {track_id} uses an invalid or incompatible media timescale for the planned mux timeline"
2497                )
2498            }
2499            Self::InvalidTrackLanguage { track_id, language } => write!(
2500                f,
2501                "track {track_id} uses invalid language code `{language}`; expected three ASCII letters"
2502            ),
2503            Self::DuplicateTrackId { track_id } => {
2504                write!(f, "duplicate mux track id {track_id}")
2505            }
2506            Self::MissingTrackId { track_id } => {
2507                write!(
2508                    f,
2509                    "mux plan referenced track id {track_id}, but no matching track configuration was provided"
2510                )
2511            }
2512            Self::TrackHasNoSamples { track_id } => {
2513                write!(f, "mux track {track_id} has no planned samples")
2514            }
2515            Self::NonMonotonicTrackDecodeTime {
2516                track_id,
2517                previous_decode_time,
2518                next_decode_time,
2519            } => write!(
2520                f,
2521                "track {track_id} regressed in decode order from {previous_decode_time} to {next_decode_time}"
2522            ),
2523            Self::InvalidSampleEntryBox { track_id, message } => write!(
2524                f,
2525                "track {track_id} provided an invalid sample-entry box: {message}"
2526            ),
2527            Self::LayoutOverflow(field) => write!(
2528                f,
2529                "real mux layout overflowed the supported range while building {field}"
2530            ),
2531            Self::Codec(error) => error.fmt(f),
2532            Self::Writer(error) => error.fmt(f),
2533            Self::Header(error) => error.fmt(f),
2534            Self::Extract(error) => error.fmt(f),
2535            Self::Probe(error) => error.fmt(f),
2536            Self::Io(error) => write!(f, "{error}"),
2537        }
2538    }
2539}
2540
2541impl MuxError {
2542    /// Stable coarse category label for additive mux diagnostics.
2543    pub fn category(&self) -> &'static str {
2544        match self {
2545            Self::InvalidTrackSpec { .. }
2546            | Self::MultipleVideoTracks { .. }
2547            | Self::MissingTrackSpecs
2548            | Self::MissingTrackSelection { .. }
2549            | Self::InvalidDurationMode { .. }
2550            | Self::InvalidOutputLayout { .. }
2551            | Self::InvalidDestinationMode { .. }
2552            | Self::OutputPathConflict { .. }
2553            | Self::InvalidMovieTimescale
2554            | Self::InvalidTrackTimescale { .. }
2555            | Self::InvalidTrackLanguage { .. } => "input",
2556            Self::UnsupportedTrackImport { .. } => "unsupported",
2557            Self::IncompatibleTrackTiming { .. } | Self::NonMonotonicTrackDecodeTime { .. } => {
2558                "timing"
2559            }
2560            Self::InvalidChunkPlan { .. }
2561            | Self::PayloadSizeOverflow
2562            | Self::MissingSourceIndex { .. }
2563            | Self::NonMonotonicSourceOffset { .. }
2564            | Self::IncompleteAdvance { .. }
2565            | Self::IncompleteCopy { .. }
2566            | Self::DuplicateTrackId { .. }
2567            | Self::MissingTrackId { .. }
2568            | Self::TrackHasNoSamples { .. }
2569            | Self::InvalidSampleEntryBox { .. }
2570            | Self::LayoutOverflow(_) => "layout",
2571            Self::Codec(_) | Self::Writer(_) | Self::Header(_) => "writer",
2572            Self::Extract(_) | Self::Probe(_) => "input",
2573            Self::Io(_) => "io",
2574        }
2575    }
2576
2577    /// Stable coarse stage label for additive mux diagnostics.
2578    pub fn stage(&self) -> &'static str {
2579        match self {
2580            Self::InvalidTrackSpec { .. }
2581            | Self::MultipleVideoTracks { .. }
2582            | Self::MissingTrackSpecs
2583            | Self::InvalidDurationMode { .. }
2584            | Self::InvalidOutputLayout { .. }
2585            | Self::InvalidDestinationMode { .. }
2586            | Self::OutputPathConflict { .. } => "request",
2587            Self::MissingTrackSelection { .. }
2588            | Self::UnsupportedTrackImport { .. }
2589            | Self::Extract(_)
2590            | Self::Probe(_) => "import",
2591            Self::IncompatibleTrackTiming { .. }
2592            | Self::InvalidChunkPlan { .. }
2593            | Self::PayloadSizeOverflow
2594            | Self::MissingSourceIndex { .. }
2595            | Self::InvalidMovieTimescale
2596            | Self::InvalidTrackTimescale { .. }
2597            | Self::InvalidTrackLanguage { .. }
2598            | Self::DuplicateTrackId { .. }
2599            | Self::MissingTrackId { .. }
2600            | Self::TrackHasNoSamples { .. }
2601            | Self::NonMonotonicTrackDecodeTime { .. }
2602            | Self::InvalidSampleEntryBox { .. }
2603            | Self::LayoutOverflow(_) => "plan",
2604            Self::NonMonotonicSourceOffset { .. }
2605            | Self::IncompleteAdvance { .. }
2606            | Self::IncompleteCopy { .. }
2607            | Self::Io(_) => "payload",
2608            Self::Codec(_) | Self::Writer(_) | Self::Header(_) => "write",
2609        }
2610    }
2611}
2612
2613impl Error for MuxError {
2614    fn source(&self) -> Option<&(dyn Error + 'static)> {
2615        match self {
2616            Self::Codec(error) => Some(error),
2617            Self::Writer(error) => Some(error),
2618            Self::Header(error) => Some(error),
2619            Self::Extract(error) => Some(error),
2620            Self::Probe(error) => Some(error),
2621            Self::Io(error) => Some(error),
2622            _ => None,
2623        }
2624    }
2625}
2626
2627impl From<io::Error> for MuxError {
2628    fn from(error: io::Error) -> Self {
2629        Self::Io(error)
2630    }
2631}
2632
2633impl From<CodecError> for MuxError {
2634    fn from(error: CodecError) -> Self {
2635        Self::Codec(error)
2636    }
2637}
2638
2639impl From<WriterError> for MuxError {
2640    fn from(error: WriterError) -> Self {
2641        Self::Writer(error)
2642    }
2643}
2644
2645impl From<HeaderError> for MuxError {
2646    fn from(error: HeaderError) -> Self {
2647        Self::Header(error)
2648    }
2649}
2650
2651impl From<crate::extract::ExtractError> for MuxError {
2652    fn from(error: crate::extract::ExtractError) -> Self {
2653        Self::Extract(error)
2654    }
2655}
2656
2657impl From<crate::probe::ProbeError> for MuxError {
2658    fn from(error: crate::probe::ProbeError) -> Self {
2659        Self::Probe(error)
2660    }
2661}
2662
2663/// Plans one output payload order from staged media items using the selected interleave policy.
2664pub fn plan_staged_media_items(
2665    items: Vec<MuxStagedMediaItem>,
2666    interleave_policy: MuxInterleavePolicy,
2667) -> Result<MuxPlan, MuxError> {
2668    plan_staged_media_items_with_coordination(items, interleave_policy, Vec::new())
2669}
2670
2671/// Plans one output payload order with explicit per-track chunk sample counts.
2672///
2673/// The chunk counts are required by fragmented low-level writers because each media fragment is
2674/// built from one chunk ordinal across the participating tracks. The counts for each track must
2675/// cover that track's staged samples exactly.
2676pub fn plan_staged_media_items_with_chunk_sample_counts<I>(
2677    items: Vec<MuxStagedMediaItem>,
2678    interleave_policy: MuxInterleavePolicy,
2679    chunk_sample_counts_by_track: I,
2680) -> Result<MuxPlan, MuxError>
2681where
2682    I: IntoIterator<Item = (u32, Vec<u32>)>,
2683{
2684    let coordination = chunk_sample_counts_by_track
2685        .into_iter()
2686        .map(|(track_id, chunk_sample_counts)| {
2687            TrackCoordinationDirective::new(track_id, chunk_sample_counts)
2688        })
2689        .collect();
2690    plan_staged_media_items_with_coordination(items, interleave_policy, coordination)
2691}
2692
2693pub(crate) fn plan_staged_media_items_with_coordination(
2694    items: Vec<MuxStagedMediaItem>,
2695    interleave_policy: MuxInterleavePolicy,
2696    coordination_directives: Vec<TrackCoordinationDirective>,
2697) -> Result<MuxPlan, MuxError> {
2698    let mut queue_items = items
2699        .into_iter()
2700        .map(MuxQueueItem::from_staged)
2701        .collect::<Vec<_>>();
2702
2703    match interleave_policy {
2704        MuxInterleavePolicy::DecodeTime | MuxInterleavePolicy::ChunkOrdinalThenSource => {
2705            // Keep equal decode-time items stable by source and byte offset before the queue
2706            // layer applies the decode-time ordering key. This preserves path-first merge order
2707            // even when a carried track keeps a large external track identifier such as a TS PID.
2708            queue_items.sort_by_key(|item| {
2709                (
2710                    item.staged.source_index,
2711                    item.staged.data_offset,
2712                    item.staged.track_id,
2713                )
2714            });
2715        }
2716    }
2717
2718    let queue = OrderedWorkQueue::new(queue_items);
2719    let mut items_by_track = BTreeMap::<u32, Vec<MuxStagedMediaItem>>::new();
2720    let mut track_state = BTreeMap::<u32, MuxTrackPlanState>::new();
2721
2722    for item in queue.iter() {
2723        let end_decode_time = item
2724            .staged
2725            .decode_time
2726            .checked_add(u64::from(item.staged.duration))
2727            .ok_or(MuxError::PayloadSizeOverflow)?;
2728        items_by_track
2729            .entry(item.staged.track_id)
2730            .or_default()
2731            .push(item.staged);
2732        track_state
2733            .entry(item.staged.track_id)
2734            .and_modify(|state| {
2735                state.item_count += 1;
2736                state.end_decode_time = state.end_decode_time.max(end_decode_time);
2737                state.first_decode_time = state.first_decode_time.min(item.staged.decode_time);
2738            })
2739            .or_insert(MuxTrackPlanState {
2740                item_count: 1,
2741                first_decode_time: item.staged.decode_time,
2742                end_decode_time,
2743            });
2744    }
2745
2746    let track_plans = track_state
2747        .into_iter()
2748        .map(|(track_id, state)| MuxTrackPlan {
2749            track_id,
2750            item_count: state.item_count,
2751            first_decode_time: state.first_decode_time,
2752            end_decode_time: state.end_decode_time,
2753        })
2754        .collect::<Vec<_>>();
2755
2756    let coordination =
2757        MuxCoordinationPlan::from_track_plans(&track_plans, coordination_directives)?;
2758    let (planned_items, total_payload_size) =
2759        build_planned_items_from_tracks(&items_by_track, &coordination, interleave_policy)?;
2760    let event_graph = MuxEventGraph::from_plan(
2761        &planned_items,
2762        &track_plans,
2763        total_payload_size,
2764        &coordination,
2765    );
2766
2767    Ok(MuxPlan {
2768        interleave_policy,
2769        planned_items,
2770        track_plans,
2771        total_payload_size,
2772        coordination,
2773        event_graph,
2774    })
2775}
2776
2777/// Writes one real MP4 file to `writer` from staged seekable `sources`, `plan`, and track
2778/// metadata.
2779///
2780/// This higher-level mux surface assembles `ftyp`, `moov`, and `mdat` around the staged sample
2781/// order produced by [`plan_staged_media_items`]. The lower-level payload-copy helpers remain
2782/// available for callers that only need interleaved raw payload output.
2783pub fn write_mp4_mux<R, W>(
2784    sources: &mut [R],
2785    writer: &mut W,
2786    file_config: &MuxFileConfig,
2787    track_configs: &[MuxTrackConfig],
2788    plan: &MuxPlan,
2789) -> Result<(), MuxError>
2790where
2791    R: Read + Seek,
2792    W: Write,
2793{
2794    mp4::write_mp4_mux(sources, writer, file_config, track_configs, plan)
2795}
2796
2797/// Opens staged source files and writes one real MP4 file to `output_path`.
2798pub fn write_mp4_mux_to_path<P, Q>(
2799    source_paths: &[P],
2800    output_path: Q,
2801    file_config: &MuxFileConfig,
2802    track_configs: &[MuxTrackConfig],
2803    plan: &MuxPlan,
2804) -> Result<(), MuxError>
2805where
2806    P: AsRef<Path>,
2807    Q: AsRef<Path>,
2808{
2809    mp4::write_mp4_mux_to_path(source_paths, output_path, file_config, track_configs, plan)
2810}
2811
2812/// Writes one fragmented MP4 to `writer` from staged seekable `sources`, `plan`, and track
2813/// metadata.
2814///
2815/// The emitted byte stream is one initialization section, one top-level index when present, and
2816/// one or more media fragments in the same order as the high-level fragmented mux request path.
2817pub fn write_fragmented_mp4_mux<R, W>(
2818    sources: &mut [R],
2819    writer: &mut W,
2820    file_config: &MuxFileConfig,
2821    track_configs: &[MuxTrackConfig],
2822    single_sidx_reference: bool,
2823    plan: &MuxPlan,
2824) -> Result<(), MuxError>
2825where
2826    R: Read + Seek,
2827    W: Write,
2828{
2829    mp4::write_fragmented_mp4_mux(
2830        sources,
2831        writer,
2832        file_config,
2833        track_configs,
2834        single_sidx_reference,
2835        plan,
2836    )
2837}
2838
2839/// Opens staged source files and writes one fragmented MP4 file to `output_path`.
2840pub fn write_fragmented_mp4_mux_to_path<P, Q>(
2841    source_paths: &[P],
2842    output_path: Q,
2843    file_config: &MuxFileConfig,
2844    track_configs: &[MuxTrackConfig],
2845    single_sidx_reference: bool,
2846    plan: &MuxPlan,
2847) -> Result<(), MuxError>
2848where
2849    P: AsRef<Path>,
2850    Q: AsRef<Path>,
2851{
2852    let mut sources = source_paths
2853        .iter()
2854        .map(File::open)
2855        .collect::<Result<Vec<_>, _>>()?;
2856    let mut writer = BufWriter::new(File::create(output_path)?);
2857    write_fragmented_mp4_mux(
2858        &mut sources,
2859        &mut writer,
2860        file_config,
2861        track_configs,
2862        single_sidx_reference,
2863        plan,
2864    )
2865}
2866
2867/// Writes fragmented initialization bytes and media-fragment bytes to separate writers.
2868///
2869/// Concatenating the init writer output followed by the media writer output yields the same byte
2870/// stream as [`write_fragmented_mp4_mux`] except for volatile creation-time fields.
2871pub fn write_fragmented_mp4_mux_split<R, I, M>(
2872    sources: &mut [R],
2873    init_writer: &mut I,
2874    media_writer: &mut M,
2875    file_config: &MuxFileConfig,
2876    track_configs: &[MuxTrackConfig],
2877    single_sidx_reference: bool,
2878    plan: &MuxPlan,
2879) -> Result<(), MuxError>
2880where
2881    R: Read + Seek,
2882    I: Write,
2883    M: Write,
2884{
2885    mp4::write_fragmented_mp4_mux_split(
2886        sources,
2887        init_writer,
2888        media_writer,
2889        file_config,
2890        track_configs,
2891        single_sidx_reference,
2892        plan,
2893    )
2894}
2895
2896/// Writes fragmented initialization bytes and standalone media-segment bytes to separate writers.
2897///
2898/// The media writer receives concatenated standalone media-segment units. Each unit starts with a
2899/// segment type box, then a local segment index, then the media fragment bytes.
2900pub fn write_fragmented_mp4_mux_segmented<R, I, M>(
2901    sources: &mut [R],
2902    init_writer: &mut I,
2903    media_writer: &mut M,
2904    file_config: &MuxFileConfig,
2905    track_configs: &[MuxTrackConfig],
2906    plan: &MuxPlan,
2907) -> Result<(), MuxError>
2908where
2909    R: Read + Seek,
2910    I: Write,
2911    M: Write,
2912{
2913    mp4::write_fragmented_mp4_mux_segmented(
2914        sources,
2915        init_writer,
2916        media_writer,
2917        file_config,
2918        track_configs,
2919        plan,
2920    )
2921}
2922
2923/// Opens staged source files and writes fragmented init/media outputs to separate paths.
2924pub fn write_fragmented_mp4_mux_split_to_paths<P, I, M>(
2925    source_paths: &[P],
2926    init_path: I,
2927    media_path: M,
2928    file_config: &MuxFileConfig,
2929    track_configs: &[MuxTrackConfig],
2930    single_sidx_reference: bool,
2931    plan: &MuxPlan,
2932) -> Result<(), MuxError>
2933where
2934    P: AsRef<Path>,
2935    I: AsRef<Path>,
2936    M: AsRef<Path>,
2937{
2938    let mut sources = source_paths
2939        .iter()
2940        .map(File::open)
2941        .collect::<Result<Vec<_>, _>>()?;
2942    let mut init_writer = BufWriter::new(File::create(init_path)?);
2943    let mut media_writer = BufWriter::new(File::create(media_path)?);
2944    write_fragmented_mp4_mux_split(
2945        &mut sources,
2946        &mut init_writer,
2947        &mut media_writer,
2948        file_config,
2949        track_configs,
2950        single_sidx_reference,
2951        plan,
2952    )
2953}
2954
2955/// Writes one fragmented MP4 and flushes the writer after the top-level index and after each
2956/// media fragment.
2957///
2958/// This preserves the same final bytes as [`write_fragmented_mp4_mux`] while exposing deterministic
2959/// flush points for callers that send the stream incrementally.
2960pub fn write_fragmented_mp4_mux_chunked<R, W>(
2961    sources: &mut [R],
2962    writer: &mut W,
2963    file_config: &MuxFileConfig,
2964    track_configs: &[MuxTrackConfig],
2965    single_sidx_reference: bool,
2966    plan: &MuxPlan,
2967) -> Result<(), MuxError>
2968where
2969    R: Read + Seek,
2970    W: Write,
2971{
2972    mp4::write_fragmented_mp4_mux_chunked(
2973        sources,
2974        writer,
2975        file_config,
2976        track_configs,
2977        single_sidx_reference,
2978        plan,
2979    )
2980}
2981
2982/// Writes one real MP4 file through the additive Tokio-based async mux surface.
2983#[cfg(feature = "async")]
2984#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
2985pub async fn write_mp4_mux_async<R, W>(
2986    sources: &mut [R],
2987    writer: &mut W,
2988    file_config: &MuxFileConfig,
2989    track_configs: &[MuxTrackConfig],
2990    plan: &MuxPlan,
2991) -> Result<(), MuxError>
2992where
2993    R: AsyncReadSeek,
2994    W: AsyncWrite + Unpin,
2995{
2996    mp4::write_mp4_mux_async(sources, writer, file_config, track_configs, plan).await
2997}
2998
2999/// Opens staged source files asynchronously and writes one real MP4 file to `output_path`.
3000#[cfg(feature = "async")]
3001#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3002pub async fn write_mp4_mux_to_path_async<P, Q>(
3003    source_paths: &[P],
3004    output_path: Q,
3005    file_config: &MuxFileConfig,
3006    track_configs: &[MuxTrackConfig],
3007    plan: &MuxPlan,
3008) -> Result<(), MuxError>
3009where
3010    P: AsRef<Path>,
3011    Q: AsRef<Path>,
3012{
3013    mp4::write_mp4_mux_to_path_async(source_paths, output_path, file_config, track_configs, plan)
3014        .await
3015}
3016
3017/// Writes one fragmented MP4 through the additive Tokio-based async mux surface.
3018#[cfg(feature = "async")]
3019#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3020pub async fn write_fragmented_mp4_mux_async<R, W>(
3021    sources: &mut [R],
3022    writer: &mut W,
3023    file_config: &MuxFileConfig,
3024    track_configs: &[MuxTrackConfig],
3025    single_sidx_reference: bool,
3026    plan: &MuxPlan,
3027) -> Result<(), MuxError>
3028where
3029    R: AsyncReadSeek,
3030    W: AsyncWrite + Unpin,
3031{
3032    mp4::write_fragmented_mp4_mux_async(
3033        sources,
3034        writer,
3035        file_config,
3036        track_configs,
3037        single_sidx_reference,
3038        plan,
3039    )
3040    .await
3041}
3042
3043/// Opens staged source files asynchronously and writes one fragmented MP4 to `output_path`.
3044#[cfg(feature = "async")]
3045#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3046pub async fn write_fragmented_mp4_mux_to_path_async<P, Q>(
3047    source_paths: &[P],
3048    output_path: Q,
3049    file_config: &MuxFileConfig,
3050    track_configs: &[MuxTrackConfig],
3051    single_sidx_reference: bool,
3052    plan: &MuxPlan,
3053) -> Result<(), MuxError>
3054where
3055    P: AsRef<Path>,
3056    Q: AsRef<Path>,
3057{
3058    let mut sources = Vec::with_capacity(source_paths.len());
3059    for path in source_paths {
3060        sources.push(TokioFile::open(path).await?);
3061    }
3062    let output = TokioFile::create(output_path).await?;
3063    let mut writer = tokio::io::BufWriter::new(output);
3064    write_fragmented_mp4_mux_async(
3065        &mut sources,
3066        &mut writer,
3067        file_config,
3068        track_configs,
3069        single_sidx_reference,
3070        plan,
3071    )
3072    .await
3073}
3074
3075/// Writes fragmented initialization bytes and media-fragment bytes to separate async writers.
3076#[cfg(feature = "async")]
3077#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3078pub async fn write_fragmented_mp4_mux_split_async<R, I, M>(
3079    sources: &mut [R],
3080    init_writer: &mut I,
3081    media_writer: &mut M,
3082    file_config: &MuxFileConfig,
3083    track_configs: &[MuxTrackConfig],
3084    single_sidx_reference: bool,
3085    plan: &MuxPlan,
3086) -> Result<(), MuxError>
3087where
3088    R: AsyncReadSeek,
3089    I: AsyncWrite + Unpin,
3090    M: AsyncWrite + Unpin,
3091{
3092    mp4::write_fragmented_mp4_mux_split_async(
3093        sources,
3094        init_writer,
3095        media_writer,
3096        file_config,
3097        track_configs,
3098        single_sidx_reference,
3099        plan,
3100    )
3101    .await
3102}
3103
3104/// Writes fragmented initialization bytes and standalone media-segment bytes to separate async
3105/// writers.
3106#[cfg(feature = "async")]
3107#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3108pub async fn write_fragmented_mp4_mux_segmented_async<R, I, M>(
3109    sources: &mut [R],
3110    init_writer: &mut I,
3111    media_writer: &mut M,
3112    file_config: &MuxFileConfig,
3113    track_configs: &[MuxTrackConfig],
3114    plan: &MuxPlan,
3115) -> Result<(), MuxError>
3116where
3117    R: AsyncReadSeek,
3118    I: AsyncWrite + Unpin,
3119    M: AsyncWrite + Unpin,
3120{
3121    mp4::write_fragmented_mp4_mux_segmented_async(
3122        sources,
3123        init_writer,
3124        media_writer,
3125        file_config,
3126        track_configs,
3127        plan,
3128    )
3129    .await
3130}
3131
3132/// Writes one fragmented MP4 asynchronously and flushes after the top-level index and each media
3133/// fragment.
3134#[cfg(feature = "async")]
3135#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3136pub async fn write_fragmented_mp4_mux_chunked_async<R, W>(
3137    sources: &mut [R],
3138    writer: &mut W,
3139    file_config: &MuxFileConfig,
3140    track_configs: &[MuxTrackConfig],
3141    single_sidx_reference: bool,
3142    plan: &MuxPlan,
3143) -> Result<(), MuxError>
3144where
3145    R: AsyncReadSeek,
3146    W: AsyncWrite + Unpin,
3147{
3148    mp4::write_fragmented_mp4_mux_chunked_async(
3149        sources,
3150        writer,
3151        file_config,
3152        track_configs,
3153        single_sidx_reference,
3154        plan,
3155    )
3156    .await
3157}
3158
3159/// Copies the payload bytes described by `plan` from the staged seekable `sources` into
3160/// `writer`.
3161pub fn copy_planned_payloads<R, W>(
3162    sources: &mut [R],
3163    writer: &mut W,
3164    plan: &MuxPlan,
3165) -> Result<(), MuxError>
3166where
3167    R: Read + Seek,
3168    W: Write,
3169{
3170    let mut cursor = plan.event_graph.cursor();
3171    while let Some(sample) = cursor.next_sample() {
3172        let staged = sample.planned_item().staged();
3173        let Some(source) = sources.get_mut(staged.source_index()) else {
3174            return Err(MuxError::MissingSourceIndex {
3175                source_index: staged.source_index(),
3176                source_count: sources.len(),
3177            });
3178        };
3179
3180        source.seek(SeekFrom::Start(staged.data_offset()))?;
3181        let mut limited = source.take(u64::from(staged.data_size()));
3182        let copied = io::copy(&mut limited, writer)?;
3183        if copied != u64::from(staged.data_size()) {
3184            return Err(MuxError::IncompleteCopy {
3185                source_index: staged.source_index(),
3186                expected_size: u64::from(staged.data_size()),
3187                actual_size: copied,
3188            });
3189        }
3190    }
3191
3192    Ok(())
3193}
3194
3195/// Copies the payload bytes described by `plan` from staged non-seekable `sources` into `writer`.
3196///
3197/// This progressive path keeps one forward-only read cursor per source. It supports plans whose
3198/// staged items consume each source in monotonic byte-offset order, and it reports a structured
3199/// error when a caller asks it to seek backward implicitly.
3200pub fn copy_planned_payloads_progressive<R, W>(
3201    sources: &mut [R],
3202    writer: &mut W,
3203    plan: &MuxPlan,
3204) -> Result<(), MuxError>
3205where
3206    R: Read,
3207    W: Write,
3208{
3209    let mut source_offsets = vec![0_u64; sources.len()];
3210    let mut cursor = plan.event_graph.cursor();
3211    while let Some(sample) = cursor.next_sample() {
3212        let staged = sample.planned_item().staged();
3213        let Some(source) = sources.get_mut(staged.source_index()) else {
3214            return Err(MuxError::MissingSourceIndex {
3215                source_index: staged.source_index(),
3216                source_count: sources.len(),
3217            });
3218        };
3219
3220        let source_offset = source_offsets.get_mut(staged.source_index()).unwrap();
3221        advance_progressive_source(
3222            source,
3223            staged.source_index(),
3224            source_offset,
3225            staged.data_offset(),
3226        )?;
3227        copy_progressive_payload(
3228            source,
3229            writer,
3230            staged.source_index(),
3231            source_offset,
3232            u64::from(staged.data_size()),
3233        )?;
3234    }
3235
3236    Ok(())
3237}
3238
3239/// Opens staged source files and copies the payload bytes described by `plan` into `output_path`.
3240pub fn copy_planned_payloads_to_path<P, Q>(
3241    source_paths: &[P],
3242    output_path: Q,
3243    plan: &MuxPlan,
3244) -> Result<(), MuxError>
3245where
3246    P: AsRef<Path>,
3247    Q: AsRef<Path>,
3248{
3249    let mut sources = source_paths
3250        .iter()
3251        .map(File::open)
3252        .collect::<Result<Vec<_>, _>>()?;
3253    let mut writer = BufWriter::new(File::create(output_path)?);
3254    copy_planned_payloads(&mut sources, &mut writer, plan)
3255}
3256
3257/// Copies the payload bytes described by `plan` from the staged seekable async `sources` into
3258/// `writer`.
3259#[cfg(feature = "async")]
3260#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3261pub async fn copy_planned_payloads_async<R, W>(
3262    sources: &mut [R],
3263    writer: &mut W,
3264    plan: &MuxPlan,
3265) -> Result<(), MuxError>
3266where
3267    R: AsyncReadSeek,
3268    W: AsyncWrite + Unpin,
3269{
3270    let mut buffer = vec![0_u8; 16 * 1024];
3271    let mut cursor = plan.event_graph.cursor();
3272    while let Some(sample) = cursor.next_sample() {
3273        let staged = sample.planned_item().staged();
3274        let Some(source) = sources.get_mut(staged.source_index()) else {
3275            return Err(MuxError::MissingSourceIndex {
3276                source_index: staged.source_index(),
3277                source_count: sources.len(),
3278            });
3279        };
3280
3281        source.seek(SeekFrom::Start(staged.data_offset())).await?;
3282        let mut remaining = u64::from(staged.data_size());
3283        let mut copied = 0_u64;
3284        while remaining > 0 {
3285            let chunk_len = remaining.min(buffer.len() as u64) as usize;
3286            let read = source.read(&mut buffer[..chunk_len]).await?;
3287            if read == 0 {
3288                break;
3289            }
3290            writer.write_all(&buffer[..read]).await?;
3291            copied += read as u64;
3292            remaining -= read as u64;
3293        }
3294
3295        if copied != u64::from(staged.data_size()) {
3296            return Err(MuxError::IncompleteCopy {
3297                source_index: staged.source_index(),
3298                expected_size: u64::from(staged.data_size()),
3299                actual_size: copied,
3300            });
3301        }
3302    }
3303
3304    writer.flush().await?;
3305    Ok(())
3306}
3307
3308/// Copies the payload bytes described by `plan` from staged non-seekable async `sources` into
3309/// `writer`.
3310///
3311/// Like [`copy_planned_payloads_progressive`], this path supports only plans whose staged items
3312/// consume each source in monotonic byte-offset order.
3313#[cfg(feature = "async")]
3314#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3315pub async fn copy_planned_payloads_async_progressive<R, W>(
3316    sources: &mut [R],
3317    writer: &mut W,
3318    plan: &MuxPlan,
3319) -> Result<(), MuxError>
3320where
3321    R: AsyncReadForward,
3322    W: AsyncWriteForward,
3323{
3324    let mut source_offsets = vec![0_u64; sources.len()];
3325    let mut buffer = vec![0_u8; 16 * 1024];
3326    let mut cursor = plan.event_graph.cursor();
3327    while let Some(sample) = cursor.next_sample() {
3328        let staged = sample.planned_item().staged();
3329        let Some(source) = sources.get_mut(staged.source_index()) else {
3330            return Err(MuxError::MissingSourceIndex {
3331                source_index: staged.source_index(),
3332                source_count: sources.len(),
3333            });
3334        };
3335
3336        let source_offset = source_offsets.get_mut(staged.source_index()).unwrap();
3337        advance_progressive_source_async(
3338            source,
3339            staged.source_index(),
3340            source_offset,
3341            staged.data_offset(),
3342            &mut buffer,
3343        )
3344        .await?;
3345        copy_progressive_payload_async(
3346            source,
3347            writer,
3348            staged.source_index(),
3349            source_offset,
3350            u64::from(staged.data_size()),
3351            &mut buffer,
3352        )
3353        .await?;
3354    }
3355
3356    writer.flush().await?;
3357    Ok(())
3358}
3359
3360/// Opens staged source files asynchronously and copies the payload bytes described by `plan` into
3361/// `output_path`.
3362#[cfg(feature = "async")]
3363#[cfg_attr(docsrs, doc(cfg(all(feature = "mux", feature = "async"))))]
3364pub async fn copy_planned_payloads_to_path_async<P, Q>(
3365    source_paths: &[P],
3366    output_path: Q,
3367    plan: &MuxPlan,
3368) -> Result<(), MuxError>
3369where
3370    P: AsRef<Path>,
3371    Q: AsRef<Path>,
3372{
3373    let mut sources = Vec::with_capacity(source_paths.len());
3374    for path in source_paths {
3375        sources.push(TokioFile::open(path).await?);
3376    }
3377    let mut writer = TokioFile::create(output_path).await?;
3378    copy_planned_payloads_async(&mut sources, &mut writer, plan).await
3379}
3380
3381struct MuxQueueItem {
3382    staged: MuxStagedMediaItem,
3383}
3384
3385impl MuxQueueItem {
3386    fn from_staged(staged: MuxStagedMediaItem) -> Self {
3387        Self { staged }
3388    }
3389}
3390
3391impl QueueWorkItem for MuxQueueItem {
3392    fn queue_order_key(&self) -> u64 {
3393        self.staged.decode_time
3394    }
3395}
3396
3397struct MuxTrackPlanState {
3398    item_count: u32,
3399    first_decode_time: u64,
3400    end_decode_time: u64,
3401}
3402
3403#[derive(Clone, Copy)]
3404struct PlannedChunk {
3405    chunk_index: usize,
3406    order_key: PlannedChunkOrderKey,
3407    track_id: u32,
3408    start_index: usize,
3409    end_index: usize,
3410}
3411
3412#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3413struct PlannedChunkOrderKey {
3414    decode_time: u64,
3415    source_index: usize,
3416    data_offset: u64,
3417    track_id: u32,
3418}
3419
3420fn build_planned_items_from_tracks(
3421    items_by_track: &BTreeMap<u32, Vec<MuxStagedMediaItem>>,
3422    coordination: &MuxCoordinationPlan,
3423    interleave_policy: MuxInterleavePolicy,
3424) -> Result<(Vec<MuxPlannedMediaItem>, u64), MuxError> {
3425    let mut chunks = Vec::new();
3426    let total_sample_count = items_by_track.values().map(Vec::len).sum();
3427    for (&track_id, items) in items_by_track {
3428        let chunk_sample_counts = coordination.chunk_sample_counts(track_id)?;
3429        let mut start_index = 0_usize;
3430        for (chunk_index, &samples_per_chunk) in chunk_sample_counts.iter().enumerate() {
3431            let chunk_len = usize::try_from(samples_per_chunk)
3432                .map_err(|_| MuxError::LayoutOverflow("chunk sample-count conversion"))?;
3433            let end_index = start_index
3434                .checked_add(chunk_len)
3435                .ok_or(MuxError::LayoutOverflow("chunk sample indexing"))?;
3436            let first_sample =
3437                items
3438                    .get(start_index)
3439                    .ok_or_else(|| MuxError::InvalidChunkPlan {
3440                        track_id,
3441                        message: "chunk boundaries ran past the staged sample count".to_string(),
3442                    })?;
3443            chunks.push(PlannedChunk {
3444                chunk_index,
3445                order_key: PlannedChunkOrderKey {
3446                    decode_time: first_sample.decode_time(),
3447                    source_index: first_sample.source_index(),
3448                    data_offset: first_sample.data_offset(),
3449                    track_id,
3450                },
3451                track_id,
3452                start_index,
3453                end_index,
3454            });
3455            start_index = end_index;
3456        }
3457        if start_index != items.len() {
3458            return Err(MuxError::InvalidChunkPlan {
3459                track_id,
3460                message: "chunk boundaries did not cover every staged sample".to_string(),
3461            });
3462        }
3463    }
3464
3465    match interleave_policy {
3466        MuxInterleavePolicy::DecodeTime => {
3467            chunks.sort_by_key(|chunk| chunk.order_key);
3468        }
3469        MuxInterleavePolicy::ChunkOrdinalThenSource => {
3470            chunks.sort_by_key(|chunk| {
3471                (
3472                    chunk.chunk_index,
3473                    chunk.order_key.source_index,
3474                    chunk.order_key.data_offset,
3475                    chunk.order_key.track_id,
3476                    chunk.order_key.decode_time,
3477                )
3478            });
3479        }
3480    }
3481
3482    let mut planned_items = Vec::with_capacity(total_sample_count);
3483    let mut total_payload_size = 0_u64;
3484    for chunk in chunks {
3485        let items = items_by_track
3486            .get(&chunk.track_id)
3487            .ok_or(MuxError::MissingTrackId {
3488                track_id: chunk.track_id,
3489            })?;
3490        for staged in &items[chunk.start_index..chunk.end_index] {
3491            planned_items.push(MuxPlannedMediaItem {
3492                staged: *staged,
3493                output_offset: total_payload_size,
3494            });
3495            total_payload_size = total_payload_size
3496                .checked_add(u64::from(staged.data_size()))
3497                .ok_or(MuxError::PayloadSizeOverflow)?;
3498        }
3499    }
3500
3501    Ok((planned_items, total_payload_size))
3502}
3503
3504fn advance_progressive_source<R>(
3505    source: &mut R,
3506    source_index: usize,
3507    current_offset: &mut u64,
3508    target_offset: u64,
3509) -> Result<(), MuxError>
3510where
3511    R: Read,
3512{
3513    if target_offset < *current_offset {
3514        return Err(MuxError::NonMonotonicSourceOffset {
3515            source_index,
3516            previous_offset: *current_offset,
3517            next_offset: target_offset,
3518        });
3519    }
3520
3521    let mut remaining = target_offset - *current_offset;
3522    let mut buffer = [0_u8; 16 * 1024];
3523    while remaining > 0 {
3524        let chunk_len = remaining.min(buffer.len() as u64) as usize;
3525        let read = source.read(&mut buffer[..chunk_len])?;
3526        if read == 0 {
3527            return Err(MuxError::IncompleteAdvance {
3528                source_index,
3529                expected_offset: target_offset,
3530                actual_offset: *current_offset,
3531            });
3532        }
3533        *current_offset += read as u64;
3534        remaining -= read as u64;
3535    }
3536
3537    Ok(())
3538}
3539
3540fn copy_progressive_payload<R, W>(
3541    source: &mut R,
3542    writer: &mut W,
3543    source_index: usize,
3544    current_offset: &mut u64,
3545    size: u64,
3546) -> Result<(), MuxError>
3547where
3548    R: Read,
3549    W: Write,
3550{
3551    let mut remaining = size;
3552    let mut copied = 0_u64;
3553    let mut buffer = [0_u8; 16 * 1024];
3554    while remaining > 0 {
3555        let chunk_len = remaining.min(buffer.len() as u64) as usize;
3556        let read = source.read(&mut buffer[..chunk_len])?;
3557        if read == 0 {
3558            return Err(MuxError::IncompleteCopy {
3559                source_index,
3560                expected_size: size,
3561                actual_size: copied,
3562            });
3563        }
3564        writer.write_all(&buffer[..read])?;
3565        *current_offset += read as u64;
3566        copied += read as u64;
3567        remaining -= read as u64;
3568    }
3569
3570    Ok(())
3571}
3572
3573#[cfg(feature = "async")]
3574async fn advance_progressive_source_async<R>(
3575    source: &mut R,
3576    source_index: usize,
3577    current_offset: &mut u64,
3578    target_offset: u64,
3579    buffer: &mut [u8],
3580) -> Result<(), MuxError>
3581where
3582    R: AsyncReadForward,
3583{
3584    if target_offset < *current_offset {
3585        return Err(MuxError::NonMonotonicSourceOffset {
3586            source_index,
3587            previous_offset: *current_offset,
3588            next_offset: target_offset,
3589        });
3590    }
3591
3592    let mut remaining = target_offset - *current_offset;
3593    while remaining > 0 {
3594        let chunk_len = remaining.min(buffer.len() as u64) as usize;
3595        let read = source.read(&mut buffer[..chunk_len]).await?;
3596        if read == 0 {
3597            return Err(MuxError::IncompleteAdvance {
3598                source_index,
3599                expected_offset: target_offset,
3600                actual_offset: *current_offset,
3601            });
3602        }
3603        *current_offset += read as u64;
3604        remaining -= read as u64;
3605    }
3606
3607    Ok(())
3608}
3609
3610#[cfg(feature = "async")]
3611async fn copy_progressive_payload_async<R, W>(
3612    source: &mut R,
3613    writer: &mut W,
3614    source_index: usize,
3615    current_offset: &mut u64,
3616    size: u64,
3617    buffer: &mut [u8],
3618) -> Result<(), MuxError>
3619where
3620    R: AsyncReadForward,
3621    W: AsyncWriteForward,
3622{
3623    let mut remaining = size;
3624    let mut copied = 0_u64;
3625    while remaining > 0 {
3626        let chunk_len = remaining.min(buffer.len() as u64) as usize;
3627        let read = source.read(&mut buffer[..chunk_len]).await?;
3628        if read == 0 {
3629            return Err(MuxError::IncompleteCopy {
3630                source_index,
3631                expected_size: size,
3632                actual_size: copied,
3633            });
3634        }
3635        writer.write_all(&buffer[..read]).await?;
3636        *current_offset += read as u64;
3637        copied += read as u64;
3638        remaining -= read as u64;
3639    }
3640
3641    Ok(())
3642}
3643
3644#[cfg(test)]
3645mod tests {
3646    use super::*;
3647
3648    #[test]
3649    fn coordinated_chunk_plans_keep_multi_sample_chunks_contiguous_in_output_order() {
3650        let plan = plan_staged_media_items_with_coordination(
3651            vec![
3652                MuxStagedMediaItem::new(0, 1, 0, 10, 0, 4),
3653                MuxStagedMediaItem::new(0, 1, 10, 10, 4, 4),
3654                MuxStagedMediaItem::new(1, 2, 0, 10, 0, 3),
3655                MuxStagedMediaItem::new(1, 2, 10, 10, 3, 3),
3656            ],
3657            MuxInterleavePolicy::DecodeTime,
3658            vec![
3659                TrackCoordinationDirective::new(1, vec![2]),
3660                TrackCoordinationDirective::new(2, vec![2]),
3661            ],
3662        )
3663        .unwrap();
3664
3665        let planned = plan.planned_items();
3666        assert_eq!(planned.len(), 4);
3667        assert_eq!(planned[0].staged().track_id(), 1);
3668        assert_eq!(planned[1].staged().track_id(), 1);
3669        assert_eq!(planned[2].staged().track_id(), 2);
3670        assert_eq!(planned[3].staged().track_id(), 2);
3671        assert_eq!(planned[0].output_offset(), 0);
3672        assert_eq!(planned[1].output_offset(), 4);
3673        assert_eq!(planned[2].output_offset(), 8);
3674        assert_eq!(planned[3].output_offset(), 11);
3675    }
3676
3677    #[test]
3678    fn chunk_ordinal_interleave_keeps_aligned_chunks_in_source_pair_order() {
3679        let plan = plan_staged_media_items_with_coordination(
3680            vec![
3681                MuxStagedMediaItem::new(0, 1, 0, 10, 0, 4),
3682                MuxStagedMediaItem::new(0, 1, 10, 10, 4, 4),
3683                MuxStagedMediaItem::new(0, 1, 20, 10, 8, 4),
3684                MuxStagedMediaItem::new(0, 1, 30, 10, 12, 4),
3685                MuxStagedMediaItem::new(1, 2, 0, 10, 0, 3),
3686                MuxStagedMediaItem::new(1, 2, 15, 10, 3, 3),
3687                MuxStagedMediaItem::new(1, 2, 31, 10, 6, 3),
3688            ],
3689            MuxInterleavePolicy::ChunkOrdinalThenSource,
3690            vec![
3691                TrackCoordinationDirective::new(1, vec![1, 1, 1, 1]),
3692                TrackCoordinationDirective::new(2, vec![1, 1, 1]),
3693            ],
3694        )
3695        .unwrap();
3696
3697        let track_order = plan
3698            .planned_items()
3699            .iter()
3700            .map(|item| item.staged().track_id())
3701            .collect::<Vec<_>>();
3702        assert_eq!(track_order, vec![1, 2, 1, 2, 1, 2, 1]);
3703    }
3704}