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}