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