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}