Skip to main content

ff_encode/shared/
container.rs

1//! Container format definitions.
2
3/// Output container format for encoding.
4///
5/// The container format is usually auto-detected from the file extension,
6/// but can be explicitly specified if needed.
7///
8/// Named `OutputContainer` (rather than `Container`) to avoid confusion with
9/// `ff_format::ContainerInfo`, which describes a container *read* from a probed file.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum OutputContainer {
13    /// MP4 / `QuickTime`
14    Mp4,
15
16    /// Fragmented MP4 — CMAF-compatible streaming container.
17    ///
18    /// Uses the same `mp4` `FFmpeg` muxer as [`OutputContainer::Mp4`] but with
19    /// `movflags=+frag_keyframe+empty_moov+default_base_moof` applied before
20    /// writing the header. Required for HTTP Live Streaming fMP4 segments
21    /// (CMAF) and MPEG-DASH.
22    FMp4,
23
24    /// `WebM`
25    WebM,
26
27    /// Matroska
28    Mkv,
29
30    /// AVI
31    Avi,
32
33    /// MOV
34    Mov,
35
36    /// FLAC (lossless audio container)
37    Flac,
38
39    /// OGG (audio container for Vorbis/Opus)
40    Ogg,
41}
42
43impl OutputContainer {
44    /// Get `FFmpeg` format name.
45    #[must_use]
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::Mp4 | Self::FMp4 => "mp4",
49            Self::WebM => "webm",
50            Self::Mkv => "matroska",
51            Self::Avi => "avi",
52            Self::Mov => "mov",
53            Self::Flac => "flac",
54            Self::Ogg => "ogg",
55        }
56    }
57
58    /// Get default file extension.
59    #[must_use]
60    pub const fn default_extension(self) -> &'static str {
61        match self {
62            Self::Mp4 | Self::FMp4 => "mp4",
63            Self::WebM => "webm",
64            Self::Mkv => "mkv",
65            Self::Avi => "avi",
66            Self::Mov => "mov",
67            Self::Flac => "flac",
68            Self::Ogg => "ogg",
69        }
70    }
71
72    /// Guess the container from a file path's extension.
73    ///
74    /// Mirrors [`default_extension`](Self::default_extension) in reverse and is
75    /// case-insensitive. Returns `None` for an unknown or missing extension. The
76    /// `mp4` extension maps to progressive [`Mp4`](Self::Mp4), never fragmented
77    /// [`FMp4`](Self::FMp4), which must be selected explicitly.
78    #[must_use]
79    pub fn from_path(path: &std::path::Path) -> Option<Self> {
80        let ext = path.extension()?.to_str()?.to_ascii_lowercase();
81        match ext.as_str() {
82            "mp4" | "m4v" => Some(Self::Mp4),
83            "webm" => Some(Self::WebM),
84            "mkv" => Some(Self::Mkv),
85            "avi" => Some(Self::Avi),
86            "mov" => Some(Self::Mov),
87            "flac" => Some(Self::Flac),
88            "ogg" => Some(Self::Ogg),
89            _ => None,
90        }
91    }
92
93    /// Returns `true` if this container is fragmented MP4.
94    ///
95    /// When `true`, the encoder applies
96    /// `movflags=+frag_keyframe+empty_moov+default_base_moof` before writing
97    /// the file header, enabling CMAF-compatible streaming output.
98    #[must_use]
99    pub const fn is_fragmented(self) -> bool {
100        matches!(self, Self::FMp4)
101    }
102
103    /// Returns `true` if this container supports the `+faststart` movflag.
104    ///
105    /// `faststart` (moving the `moov` atom to the front for progressive
106    /// download) is specific to the `mp4`/`mov` muxers, so it applies only to
107    /// [`Mp4`](Self::Mp4) and [`Mov`](Self::Mov). Fragmented MP4
108    /// ([`FMp4`](Self::FMp4)) already streams and uses its own movflags, so it
109    /// is excluded.
110    #[must_use]
111    pub const fn supports_faststart(self) -> bool {
112        matches!(self, Self::Mp4 | Self::Mov)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_container_as_str() {
122        assert_eq!(OutputContainer::Mp4.as_str(), "mp4");
123        assert_eq!(OutputContainer::WebM.as_str(), "webm");
124        assert_eq!(OutputContainer::Mkv.as_str(), "matroska");
125    }
126
127    #[test]
128    fn test_container_extension() {
129        assert_eq!(OutputContainer::Mp4.default_extension(), "mp4");
130        assert_eq!(OutputContainer::WebM.default_extension(), "webm");
131        assert_eq!(OutputContainer::Mkv.default_extension(), "mkv");
132        assert_eq!(OutputContainer::Flac.default_extension(), "flac");
133        assert_eq!(OutputContainer::Ogg.default_extension(), "ogg");
134    }
135
136    #[test]
137    fn flac_as_str_should_return_flac() {
138        assert_eq!(OutputContainer::Flac.as_str(), "flac");
139    }
140
141    #[test]
142    fn ogg_as_str_should_return_ogg() {
143        assert_eq!(OutputContainer::Ogg.as_str(), "ogg");
144    }
145
146    #[test]
147    fn fmp4_as_str_should_return_mp4() {
148        assert_eq!(OutputContainer::FMp4.as_str(), "mp4");
149    }
150
151    #[test]
152    fn fmp4_extension_should_return_mp4() {
153        assert_eq!(OutputContainer::FMp4.default_extension(), "mp4");
154    }
155
156    #[test]
157    fn fmp4_is_fragmented_should_return_true() {
158        assert!(OutputContainer::FMp4.is_fragmented());
159        assert!(!OutputContainer::Mp4.is_fragmented());
160        assert!(!OutputContainer::Mkv.is_fragmented());
161    }
162
163    #[test]
164    fn from_path_should_map_known_extensions_case_insensitively() {
165        use std::path::Path;
166        assert_eq!(
167            OutputContainer::from_path(Path::new("out.mp4")),
168            Some(OutputContainer::Mp4)
169        );
170        // Case-insensitive, and `mp4` is progressive Mp4 (never FMp4).
171        assert_eq!(
172            OutputContainer::from_path(Path::new("OUT.MP4")),
173            Some(OutputContainer::Mp4)
174        );
175        assert_eq!(
176            OutputContainer::from_path(Path::new("clip.mov")),
177            Some(OutputContainer::Mov)
178        );
179        assert_eq!(
180            OutputContainer::from_path(Path::new("clip.mkv")),
181            Some(OutputContainer::Mkv)
182        );
183        assert_eq!(OutputContainer::from_path(Path::new("clip.xyz")), None);
184        assert_eq!(OutputContainer::from_path(Path::new("noext")), None);
185    }
186
187    #[test]
188    fn supports_faststart_should_be_true_for_mp4_and_mov() {
189        assert!(OutputContainer::Mp4.supports_faststart());
190        assert!(OutputContainer::Mov.supports_faststart());
191        // Fragmented MP4 streams already and uses its own movflags.
192        assert!(!OutputContainer::FMp4.supports_faststart());
193        assert!(!OutputContainer::WebM.supports_faststart());
194        assert!(!OutputContainer::Mkv.supports_faststart());
195    }
196}