Skip to main content

rff_core/
media.rs

1//! Core media enumerations: media types, codec identifiers, and raw sample
2//! layouts (pixel formats for video, sample formats for audio).
3//!
4//! These deliberately cover only a small, growing subset of what FFmpeg
5//! supports. Add variants as new codecs land — every variant is `#[non_exhaustive]`
6//! friendly so downstream `match`es must keep a wildcard arm.
7
8use std::fmt;
9
10/// The broad category of an elementary stream.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum MediaType {
13    Video,
14    Audio,
15    Subtitle,
16    Data,
17    Attachment,
18}
19
20impl fmt::Display for MediaType {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        let s = match self {
23            MediaType::Video => "video",
24            MediaType::Audio => "audio",
25            MediaType::Subtitle => "subtitle",
26            MediaType::Data => "data",
27            MediaType::Attachment => "attachment",
28        };
29        f.write_str(s)
30    }
31}
32
33/// Identifier for a specific codec (the *what*, independent of any particular
34/// encoder/decoder *implementation*).
35///
36/// Marked `#[non_exhaustive]`: we add codecs over time, and external matchers
37/// must handle the open-ended set.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
39#[non_exhaustive]
40pub enum CodecId {
41    /// Sentinel for "no/unknown codec".
42    #[default]
43    None,
44    /// H.264 / AVC video.
45    H264,
46    /// H.265 / HEVC video. Decode only.
47    Hevc,
48    /// Opus audio.
49    Opus,
50    /// AVIF still image (an AV1 intra frame in a HEIF box).
51    Avif,
52    /// PNG still image (DEFLATE-compressed RGB/RGBA).
53    Png,
54    /// JPEG still image (a.k.a. MJPEG).
55    Jpeg,
56    /// GIF image (first frame; palette-based).
57    Gif,
58    /// WebP image (VP8 / VP8L).
59    Webp,
60    /// Linear PCM audio (uncompressed; layout in the stream's sample format).
61    Pcm,
62    /// Vorbis audio (Ogg Vorbis).
63    Vorbis,
64    /// FLAC lossless audio.
65    Flac,
66    /// JPEG XL image.
67    Jxl,
68    /// AAC audio (Advanced Audio Coding).
69    Aac,
70    /// VP9 video.
71    Vp9,
72    /// AV2 video (AOMedia's successor to AV1). Decode only.
73    Av2,
74    /// MP3 audio (MPEG-1/2 Audio Layer III).
75    Mp3,
76    /// Raw uncompressed video (planar/packed pixels; layout in the stream's
77    /// pixel format). The video analog of [`CodecId::Pcm`].
78    RawVideo,
79    /// SubRip subtitle (`.srt`): plain-text cues with `HH:MM:SS,mmm` timing.
80    Subrip,
81    /// WebVTT subtitle (`.vtt`): plain-text cues with `HH:MM:SS.mmm` timing.
82    WebVtt,
83}
84
85impl CodecId {
86    /// Short canonical name, matching the token used on the `ffmpeg` command
87    /// line (`-c:v h264`, `-c:a opus`, ...).
88    pub fn name(self) -> &'static str {
89        match self {
90            CodecId::None => "none",
91            CodecId::H264 => "h264",
92            CodecId::Hevc => "hevc",
93            CodecId::Opus => "opus",
94            CodecId::Avif => "avif",
95            CodecId::Png => "png",
96            CodecId::Jpeg => "mjpeg",
97            CodecId::Gif => "gif",
98            CodecId::Webp => "webp",
99            CodecId::Pcm => "pcm",
100            CodecId::Vorbis => "vorbis",
101            CodecId::Flac => "flac",
102            CodecId::Jxl => "jpegxl",
103            CodecId::Aac => "aac",
104            CodecId::Vp9 => "vp9",
105            CodecId::Av2 => "av2",
106            CodecId::Mp3 => "mp3",
107            CodecId::RawVideo => "rawvideo",
108            CodecId::Subrip => "subrip",
109            CodecId::WebVtt => "webvtt",
110        }
111    }
112
113    /// The media category this codec produces.
114    pub fn media_type(self) -> MediaType {
115        match self {
116            CodecId::None => MediaType::Data,
117            CodecId::H264 | CodecId::Hevc => MediaType::Video,
118            CodecId::Opus => MediaType::Audio,
119            // Image codecs carry pixel data; we model them as (single-frame) video.
120            CodecId::Avif => MediaType::Video,
121            CodecId::Png => MediaType::Video,
122            CodecId::Jpeg => MediaType::Video,
123            CodecId::Gif => MediaType::Video,
124            CodecId::Webp => MediaType::Video,
125            CodecId::Pcm => MediaType::Audio,
126            CodecId::Vorbis => MediaType::Audio,
127            CodecId::Flac => MediaType::Audio,
128            CodecId::Jxl => MediaType::Video,
129            CodecId::Aac => MediaType::Audio,
130            CodecId::Vp9 => MediaType::Video,
131            CodecId::Av2 => MediaType::Video,
132            CodecId::Mp3 => MediaType::Audio,
133            CodecId::RawVideo => MediaType::Video,
134            CodecId::Subrip => MediaType::Subtitle,
135            CodecId::WebVtt => MediaType::Subtitle,
136        }
137    }
138
139    /// The sample format a codec NAME pins, when the name carries one.
140    ///
141    /// `CodecId` is deliberately format-agnostic — `pcm_s16le` and `pcm_f32le`
142    /// are both [`CodecId::Pcm`] — but for raw PCM the name IS the format, and
143    /// dropping it means `-c:a pcm_s16le` silently writes whatever the decoder
144    /// happened to emit. Compressed codecs return `None`: their sample format is
145    /// an internal detail, not something the CLI name selects.
146    pub fn sample_format_from_name(name: &str) -> Option<SampleFormat> {
147        match name {
148            "pcm_s16le" => Some(SampleFormat::S16),
149            "pcm_f32le" => Some(SampleFormat::F32),
150            _ => None,
151        }
152    }
153
154    /// Look up a codec id from its canonical CLI name.
155    pub fn from_name(name: &str) -> Option<CodecId> {
156        match name {
157            "h264" | "avc" | "libx264" => Some(CodecId::H264),
158            "hevc" | "h265" | "x265" | "libx265" => Some(CodecId::Hevc),
159            "opus" | "libopus" => Some(CodecId::Opus),
160            "avif" => Some(CodecId::Avif),
161            "png" => Some(CodecId::Png),
162            "mjpeg" | "jpeg" | "jpg" => Some(CodecId::Jpeg),
163            "gif" => Some(CodecId::Gif),
164            "webp" => Some(CodecId::Webp),
165            "pcm" | "pcm_s16le" | "pcm_f32le" => Some(CodecId::Pcm),
166            "vorbis" => Some(CodecId::Vorbis),
167            "flac" => Some(CodecId::Flac),
168            "jpegxl" | "jxl" => Some(CodecId::Jxl),
169            "aac" => Some(CodecId::Aac),
170            "vp9" | "libvpx-vp9" => Some(CodecId::Vp9),
171            "av2" | "libavm" => Some(CodecId::Av2),
172            "mp3" | "mp3float" | "libmp3lame" => Some(CodecId::Mp3),
173            "rawvideo" | "yuv4mpegpipe" => Some(CodecId::RawVideo),
174            "srt" | "subrip" => Some(CodecId::Subrip),
175            "vtt" | "webvtt" => Some(CodecId::WebVtt),
176            _ => None,
177        }
178    }
179}
180
181impl fmt::Display for CodecId {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.write_str(self.name())
184    }
185}
186
187/// Luma/chroma value range of a Y'CbCr stream.
188///
189/// This is metadata, not a conversion: it records how the samples in a plane
190/// should be interpreted, so containers can label them and consumers can avoid
191/// re-scaling data that is already in the range they want.
192///
193/// Getting it wrong is expensive and silent. Writing full-range samples into a
194/// y4m without an `XCOLORRANGE=FULL` tag makes every reader treat them as
195/// limited-range; on real content that fabricated **12 dB** of error between two
196/// byte-identical payloads (41.3 dB compared raw, 29.0 dB compared through the
197/// mislabelled container).
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
199pub enum ColorRange {
200    /// Not known. Consumers apply their own default — for Y'CbCr that is
201    /// conventionally limited range, which is why leaving this unset on
202    /// full-range data is a bug rather than a neutral choice.
203    #[default]
204    Unspecified,
205    /// "TV" / MPEG range: Y 16–235, Cb/Cr 16–240.
206    Limited,
207    /// "PC" / JPEG range: all components 0–255. JPEG and PNG are always this.
208    Full,
209}
210
211impl ColorRange {
212    /// The y4m `XCOLORRANGE` tag value, or `None` when unspecified.
213    pub fn y4m_tag(self) -> Option<&'static str> {
214        match self {
215            ColorRange::Unspecified => None,
216            ColorRange::Limited => Some("LIMITED"),
217            ColorRange::Full => Some("FULL"),
218        }
219    }
220
221    /// Parse a y4m `XCOLORRANGE` value.
222    pub fn from_y4m_tag(tag: &str) -> ColorRange {
223        match tag.trim().to_ascii_uppercase().as_str() {
224            "FULL" => ColorRange::Full,
225            "LIMITED" => ColorRange::Limited,
226            _ => ColorRange::Unspecified,
227        }
228    }
229}
230
231/// Raw pixel layout of a decoded video frame. A small starter subset.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
233#[non_exhaustive]
234pub enum PixelFormat {
235    /// Planar Y'CbCr 4:2:0, 8-bit (the workhorse for H.264).
236    Yuv420p,
237    /// Planar Y'CbCr 4:2:2, 8-bit.
238    Yuv422p,
239    /// Planar Y'CbCr 4:4:4, 8-bit.
240    Yuv444p,
241    /// Planar Y'CbCr 4:2:0, 10-bit (each sample stored as little-endian u16).
242    Yuv420p10,
243    /// Planar Y'CbCr 4:2:2, 10-bit (little-endian u16 samples).
244    Yuv422p10,
245    /// Planar Y'CbCr 4:4:4, 10-bit (little-endian u16 samples).
246    Yuv444p10,
247    /// Planar Y'CbCr 4:2:0, 12-bit (little-endian u16 samples).
248    Yuv420p12,
249    /// Planar Y'CbCr 4:2:2, 12-bit (little-endian u16 samples).
250    Yuv422p12,
251    /// Planar Y'CbCr 4:4:4, 12-bit (little-endian u16 samples).
252    Yuv444p12,
253    /// Packed RGB, 8 bits per channel.
254    Rgb24,
255    /// Packed RGBA, 8 bits per channel.
256    Rgba,
257}
258
259impl PixelFormat {
260    pub fn name(self) -> &'static str {
261        match self {
262            PixelFormat::Yuv420p => "yuv420p",
263            PixelFormat::Yuv422p => "yuv422p",
264            PixelFormat::Yuv444p => "yuv444p",
265            PixelFormat::Yuv420p10 => "yuv420p10le",
266            PixelFormat::Yuv422p10 => "yuv422p10le",
267            PixelFormat::Yuv444p10 => "yuv444p10le",
268            PixelFormat::Yuv420p12 => "yuv420p12le",
269            PixelFormat::Yuv422p12 => "yuv422p12le",
270            PixelFormat::Yuv444p12 => "yuv444p12le",
271            PixelFormat::Rgb24 => "rgb24",
272            PixelFormat::Rgba => "rgba",
273        }
274    }
275
276    /// Bits per component sample (8 for the 8-bit formats, 10 for the 10-bit
277    /// planar ones).
278    pub fn bit_depth(self) -> u32 {
279        match self {
280            PixelFormat::Yuv420p10 | PixelFormat::Yuv422p10 | PixelFormat::Yuv444p10 => 10,
281            PixelFormat::Yuv420p12 | PixelFormat::Yuv422p12 | PixelFormat::Yuv444p12 => 12,
282            _ => 8,
283        }
284    }
285
286    /// Bytes used to store one component sample: 1 for 8-bit, 2 for the 10-bit
287    /// formats (whose samples live in the low bits of a little-endian `u16`).
288    pub fn bytes_per_sample(self) -> usize {
289        if self.bit_depth() > 8 {
290            2
291        } else {
292            1
293        }
294    }
295}
296
297/// Raw audio sample layout. A small starter subset.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299#[non_exhaustive]
300pub enum SampleFormat {
301    /// Signed 16-bit, interleaved.
302    S16,
303    /// 32-bit float, interleaved.
304    F32,
305    /// 32-bit float, planar (one buffer per channel).
306    F32Planar,
307}
308
309impl SampleFormat {
310    pub fn name(self) -> &'static str {
311        match self {
312            SampleFormat::S16 => "s16",
313            SampleFormat::F32 => "flt",
314            SampleFormat::F32Planar => "fltp",
315        }
316    }
317
318    /// Bytes occupied by one sample of one channel.
319    pub fn bytes_per_sample(self) -> usize {
320        match self {
321            SampleFormat::S16 => 2,
322            SampleFormat::F32 | SampleFormat::F32Planar => 4,
323        }
324    }
325}