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