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