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}