Skip to main content

mediadecode_ffmpeg/
codec_id.rs

1//! `CodecId` newtype wrapping FFmpeg's `AVCodecID` discriminant.
2//!
3//! Constructed from hardcoded `AVCodecID` enum variants in our build's
4//! bindgen-generated bindings, so we never cast an arbitrary `i32` into
5//! the bindgen enum (that cast is UB when the value isn't in the enum's
6//! discriminant set — the same hazard `crate::pix_fmt` documents). The
7//! raw `i32` stored inside is what ends up passed to FFmpeg's C API
8//! (which declares the codec id as `c_int`), so the boundary is sound.
9
10use core::fmt;
11
12use ffmpeg_next::ffi::AVCodecID;
13use smol_bytes::Utf8Bytes;
14
15/// Upper bound on the NUL search in [`CodecId::name`] and
16/// [`CodecId::long_name`].
17///
18/// FFmpeg's longest codec name is a couple of dozen bytes and its
19/// longest description a couple of hundred; the cap is generous for
20/// both and exists only so that a version-skewed descriptor table
21/// cannot turn the walk into an unbounded read — the discipline the
22/// rest of this crate's FFI text handling follows.
23const DESCRIPTOR_TEXT_MAX_BYTES: usize = 1024;
24
25/// Codec identifier. Wraps the integer value of an `AVCodecID` enum
26/// variant; comparisons and storage work without ever transmuting back
27/// into the bindgen enum.
28///
29/// # The number is the identity; the name is a rendering of it
30///
31/// [`raw`](Self::raw) is what this type *is* — what FFmpeg passed, what
32/// a store keys on, what equality compares. [`name`](Self::name) and
33/// [`long_name`](Self::long_name) read libavcodec's descriptor table for
34/// the word beside that number, so a consumer can cross into a typed
35/// codec vocabulary (`mediaframe`'s, say) or write a human row without
36/// this crate owning a second table that could come to disagree with
37/// FFmpeg's. Neither is a key: two builds of FFmpeg agree on the number
38/// long before they agree on every string.
39#[repr(transparent)]
40#[derive(Copy, Clone, Eq, PartialEq, Hash)]
41pub struct CodecId(i32);
42
43impl CodecId {
44  /// Constructs a `CodecId` from the raw integer FFmpeg uses for
45  /// `AVCodecContext::codec_id` etc. Use this only when you have a
46  /// value that came from FFmpeg or that you know maps to a real
47  /// codec; arbitrary integers are still legal but `Debug` will fall
48  /// back to printing the raw value.
49  #[inline]
50  pub const fn from_raw(raw: i32) -> Self {
51    Self(raw)
52  }
53
54  /// Returns the underlying integer.
55  #[inline]
56  pub const fn raw(self) -> i32 {
57    self.0
58  }
59
60  // --- The descriptor's words ------------------------------------------
61
62  /// FFmpeg's own short name for this codec — `"h264"`, `"aac"`,
63  /// `"subrip"` — or `None` where this build's libavcodec has no
64  /// descriptor for the id.
65  ///
66  /// **An open vocabulary, deliberately.** The answer is whatever the
67  /// linked libavcodec's `codec_descriptors[]` says, not a set this
68  /// crate enumerates: a codec FFmpeg learns in its next release names
69  /// itself here with no change on this side, which an `enum` of codecs
70  /// could never do. The constants above are the ids this crate has
71  /// reason to *compare* against; they are not the roster of what this
72  /// method can answer.
73  ///
74  /// **`None` is a real answer**, and it is why the descriptor table is
75  /// read rather than `avcodec_get_name`: that function never fails, and
76  /// for an id it cannot place it hands back the string
77  /// `"unknown_codec"` — a sentinel a consumer would store as if it were
78  /// a codec's word. Here an id with no descriptor has no name, said
79  /// plainly.
80  ///
81  /// The word is the one FFmpeg's CLI and its containers use, which is
82  /// what makes it the right thing to cross a typed vocabulary with;
83  /// [`raw`](Self::raw) stays the identity.
84  pub fn name(self) -> Option<Utf8Bytes> {
85    let descriptor = self.descriptor();
86    if descriptor.is_null() {
87      return None;
88    }
89    // SAFETY: `descriptor` is non-null and points into libavcodec's
90    // `static const codec_descriptors[]`, so the field read is in
91    // bounds; `addr_of!` reaches it **without forming a reference** —
92    // see [`Self::descriptor`] for why that matters here. The pointer
93    // it yields is null or a NUL-terminated string literal in that same
94    // table, which is the reader's contract.
95    let name = unsafe { core::ptr::addr_of!((*descriptor).name).read() };
96    // SAFETY: as above — the name is a static, NUL-terminated literal.
97    // `from_static` stores the borrow rather than copying it; see
98    // [`crate::ffi::table_text`].
99    unsafe { crate::ffi::table_text(name, DESCRIPTOR_TEXT_MAX_BYTES) }.map(Utf8Bytes::from_static)
100  }
101
102  /// FFmpeg's human description for this codec — `"H.264 / AVC / MPEG-4
103  /// AVC / MPEG-4 part 10"` — or `None` where the build has no
104  /// descriptor, or the descriptor carries no description.
105  ///
106  /// A display string and nothing more: it is FFmpeg's prose, it is not
107  /// stable across releases, and nothing should key on it. Use
108  /// [`name`](Self::name) to cross vocabularies and [`raw`](Self::raw)
109  /// to identify.
110  pub fn long_name(self) -> Option<Utf8Bytes> {
111    let descriptor = self.descriptor();
112    if descriptor.is_null() {
113      return None;
114    }
115    // SAFETY: as [`Self::name`], for the same table and the same
116    // reason. `long_name` is independently nullable — a descriptor may
117    // carry a name and no description — which the reader answers with
118    // `None`.
119    let long_name = unsafe { core::ptr::addr_of!((*descriptor).long_name).read() };
120    // SAFETY: as above.
121    unsafe { crate::ffi::table_text(long_name, DESCRIPTOR_TEXT_MAX_BYTES) }
122      .map(Utf8Bytes::from_static)
123  }
124
125  /// libavcodec's descriptor for this id, or null where this build
126  /// names no codec for it.
127  ///
128  /// # Two enum hazards, and both are kept out
129  ///
130  /// **Going in**, the id crosses as the `c_int` it is — through the
131  /// crate's own `avcodec_descriptor_get` redeclaration in
132  /// `decoder::c_shims` — so no `AVCodecID` is ever constructed from a
133  /// value that may not be in this build's discriminant set. libavcodec
134  /// bounds-checks the id itself and answers null for anything outside
135  /// its table, so every `i32` this type can hold is a defined call.
136  ///
137  /// **Coming back**, the pointer is left raw and the callers read
138  /// their one field through `addr_of!`. `AVCodecDescriptor` embeds an
139  /// `AVCodecID` and an `AVMediaType`, and forming a
140  /// `&AVCodecDescriptor` would assert every field valid — including
141  /// those two, which come from the *linked* library and need not lie
142  /// inside the discriminant set the *bindings* were generated from.
143  /// That is the same skew `crate::ffi`'s module note is about, met on
144  /// the way out of C rather than on the way in.
145  fn descriptor(self) -> *const ffmpeg_next::ffi::AVCodecDescriptor {
146    // SAFETY: the redeclared shim takes a plain `c_int`, so no bindgen
147    // enum is formed from `self.0`, and libavcodec answers null for an
148    // id it cannot place.
149    unsafe { crate::decoder::c_shims::avcodec_descriptor_get(self.0) }
150  }
151
152  // --- Sentinels -------------------------------------------------------
153
154  /// `AV_CODEC_ID_NONE` — sentinel for "no codec."
155  pub const NONE: Self = Self(AVCodecID::AV_CODEC_ID_NONE as i32);
156
157  // --- Video codecs ----------------------------------------------------
158
159  /// H.264 / AVC (ITU-T H.264 / ISO/IEC 14496-10).
160  pub const H264: Self = Self(AVCodecID::AV_CODEC_ID_H264 as i32);
161  /// H.265 / HEVC (ITU-T H.265 / ISO/IEC 23008-2).
162  pub const HEVC: Self = Self(AVCodecID::AV_CODEC_ID_HEVC as i32);
163  /// AV1 (Alliance for Open Media).
164  pub const AV1: Self = Self(AVCodecID::AV_CODEC_ID_AV1 as i32);
165  /// VP9 (Google).
166  pub const VP9: Self = Self(AVCodecID::AV_CODEC_ID_VP9 as i32);
167  /// VP8 (Google).
168  pub const VP8: Self = Self(AVCodecID::AV_CODEC_ID_VP8 as i32);
169  /// MPEG-2 Video (ITU-T H.262 / ISO/IEC 13818-2).
170  pub const MPEG2VIDEO: Self = Self(AVCodecID::AV_CODEC_ID_MPEG2VIDEO as i32);
171  /// MPEG-4 Part 2 Visual (ISO/IEC 14496-2).
172  pub const MPEG4: Self = Self(AVCodecID::AV_CODEC_ID_MPEG4 as i32);
173  /// Apple ProRes.
174  pub const PRORES: Self = Self(AVCodecID::AV_CODEC_ID_PRORES as i32);
175  /// Avid DNxHD / DNxHR (SMPTE VC-3).
176  pub const DNXHD: Self = Self(AVCodecID::AV_CODEC_ID_DNXHD as i32);
177  /// FFV1 — lossless intra-frame.
178  pub const FFV1: Self = Self(AVCodecID::AV_CODEC_ID_FFV1 as i32);
179  /// JPEG 2000.
180  pub const JPEG2000: Self = Self(AVCodecID::AV_CODEC_ID_JPEG2000 as i32);
181  /// MJPEG.
182  pub const MJPEG: Self = Self(AVCodecID::AV_CODEC_ID_MJPEG as i32);
183  /// VC-1 (SMPTE 421M, Microsoft Windows Media Video 9).
184  pub const VC1: Self = Self(AVCodecID::AV_CODEC_ID_VC1 as i32);
185  /// VVC / H.266 (ITU-T H.266).
186  pub const VVC: Self = Self(AVCodecID::AV_CODEC_ID_VVC as i32);
187
188  // --- Audio codecs ----------------------------------------------------
189
190  /// AAC (ISO/IEC 14496-3).
191  pub const AAC: Self = Self(AVCodecID::AV_CODEC_ID_AAC as i32);
192  /// MP3 (MPEG-1/2 Audio Layer III).
193  pub const MP3: Self = Self(AVCodecID::AV_CODEC_ID_MP3 as i32);
194  /// Opus (RFC 6716).
195  pub const OPUS: Self = Self(AVCodecID::AV_CODEC_ID_OPUS as i32);
196  /// FLAC — Free Lossless Audio Codec.
197  pub const FLAC: Self = Self(AVCodecID::AV_CODEC_ID_FLAC as i32);
198  /// AC-3 (ATSC A/52, Dolby Digital).
199  pub const AC3: Self = Self(AVCodecID::AV_CODEC_ID_AC3 as i32);
200  /// E-AC-3 (Dolby Digital Plus).
201  pub const EAC3: Self = Self(AVCodecID::AV_CODEC_ID_EAC3 as i32);
202  /// Apple Lossless Audio Codec.
203  pub const ALAC: Self = Self(AVCodecID::AV_CODEC_ID_ALAC as i32);
204  /// DTS / DTS-HD.
205  pub const DTS: Self = Self(AVCodecID::AV_CODEC_ID_DTS as i32);
206  /// Vorbis.
207  pub const VORBIS: Self = Self(AVCodecID::AV_CODEC_ID_VORBIS as i32);
208  /// PCM signed 16-bit little-endian.
209  pub const PCM_S16LE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_S16LE as i32);
210  /// PCM signed 16-bit big-endian.
211  pub const PCM_S16BE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_S16BE as i32);
212  /// PCM signed 24-bit little-endian.
213  pub const PCM_S24LE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_S24LE as i32);
214  /// PCM signed 32-bit little-endian.
215  pub const PCM_S32LE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_S32LE as i32);
216  /// PCM 32-bit float little-endian.
217  pub const PCM_F32LE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_F32LE as i32);
218  /// PCM 64-bit float little-endian.
219  pub const PCM_F64LE: Self = Self(AVCodecID::AV_CODEC_ID_PCM_F64LE as i32);
220
221  // --- Subtitle codecs -------------------------------------------------
222
223  /// SubRip (.srt).
224  pub const SUBRIP: Self = Self(AVCodecID::AV_CODEC_ID_SUBRIP as i32);
225  /// Advanced SubStation Alpha (.ass / .ssa).
226  pub const ASS: Self = Self(AVCodecID::AV_CODEC_ID_ASS as i32);
227  /// WebVTT (.vtt).
228  pub const WEBVTT: Self = Self(AVCodecID::AV_CODEC_ID_WEBVTT as i32);
229  /// 3GPP Timed Text / MOV text track.
230  pub const MOV_TEXT: Self = Self(AVCodecID::AV_CODEC_ID_MOV_TEXT as i32);
231  /// DVB subtitle (bitmap).
232  pub const DVB_SUBTITLE: Self = Self(AVCodecID::AV_CODEC_ID_DVB_SUBTITLE as i32);
233  /// HDMV / Blu-ray PGS subtitle (bitmap).
234  pub const HDMV_PGS_SUBTITLE: Self = Self(AVCodecID::AV_CODEC_ID_HDMV_PGS_SUBTITLE as i32);
235  /// DVD VOBSUB subtitle (bitmap).
236  pub const DVD_SUBTITLE: Self = Self(AVCodecID::AV_CODEC_ID_DVD_SUBTITLE as i32);
237}
238
239impl fmt::Debug for CodecId {
240  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241    let name = match *self {
242      Self::NONE => "NONE",
243      Self::H264 => "H264",
244      Self::HEVC => "HEVC",
245      Self::AV1 => "AV1",
246      Self::VP9 => "VP9",
247      Self::VP8 => "VP8",
248      Self::MPEG2VIDEO => "MPEG2VIDEO",
249      Self::MPEG4 => "MPEG4",
250      Self::PRORES => "PRORES",
251      Self::DNXHD => "DNXHD",
252      Self::FFV1 => "FFV1",
253      Self::JPEG2000 => "JPEG2000",
254      Self::MJPEG => "MJPEG",
255      Self::VC1 => "VC1",
256      Self::VVC => "VVC",
257      Self::AAC => "AAC",
258      Self::MP3 => "MP3",
259      Self::OPUS => "OPUS",
260      Self::FLAC => "FLAC",
261      Self::AC3 => "AC3",
262      Self::EAC3 => "EAC3",
263      Self::ALAC => "ALAC",
264      Self::DTS => "DTS",
265      Self::VORBIS => "VORBIS",
266      Self::PCM_S16LE => "PCM_S16LE",
267      Self::PCM_S16BE => "PCM_S16BE",
268      Self::PCM_S24LE => "PCM_S24LE",
269      Self::PCM_S32LE => "PCM_S32LE",
270      Self::PCM_F32LE => "PCM_F32LE",
271      Self::PCM_F64LE => "PCM_F64LE",
272      Self::SUBRIP => "SUBRIP",
273      Self::ASS => "ASS",
274      Self::WEBVTT => "WEBVTT",
275      Self::MOV_TEXT => "MOV_TEXT",
276      Self::DVB_SUBTITLE => "DVB_SUBTITLE",
277      Self::HDMV_PGS_SUBTITLE => "HDMV_PGS_SUBTITLE",
278      Self::DVD_SUBTITLE => "DVD_SUBTITLE",
279      _ => return write!(f, "CodecId({})", self.0),
280    };
281    write!(f, "CodecId::{name}")
282  }
283}
284
285#[cfg(test)]
286mod tests {
287  use super::*;
288
289  #[test]
290  fn from_raw_round_trips() {
291    let id = CodecId::from_raw(27);
292    assert_eq!(id.raw(), 27);
293  }
294
295  #[test]
296  fn known_constants_match_av_values() {
297    assert_eq!(CodecId::H264.raw(), AVCodecID::AV_CODEC_ID_H264 as i32);
298    assert_eq!(CodecId::AAC.raw(), AVCodecID::AV_CODEC_ID_AAC as i32);
299    assert_eq!(CodecId::SUBRIP.raw(), AVCodecID::AV_CODEC_ID_SUBRIP as i32);
300  }
301
302  #[test]
303  fn debug_uses_name_for_known_codecs() {
304    assert_eq!(format!("{:?}", CodecId::H264), "CodecId::H264");
305    assert_eq!(format!("{:?}", CodecId::AAC), "CodecId::AAC");
306  }
307
308  #[test]
309  fn debug_falls_back_to_raw_for_unknown() {
310    let unknown = CodecId::from_raw(-99_999);
311    assert_eq!(format!("{:?}", unknown), "CodecId(-99999)");
312  }
313
314  #[test]
315  fn equality_is_value_based() {
316    assert_eq!(CodecId::H264, CodecId::from_raw(CodecId::H264.raw()));
317    assert_ne!(CodecId::H264, CodecId::HEVC);
318  }
319
320  /// **The name producer, against the linked libavcodec** — the whole of
321  /// issue #43's ask: a codec id two crates deep now renders as the word
322  /// FFmpeg itself spells it with.
323  ///
324  /// The three arms span the media kinds, because the descriptor table
325  /// is one table and a consumer crossing into a typed vocabulary
326  /// crosses on all three.
327  #[test]
328  fn known_codecs_name_themselves() {
329    assert_eq!(CodecId::H264.name().as_deref(), Some("h264"));
330    assert_eq!(CodecId::AAC.name().as_deref(), Some("aac"));
331    assert_eq!(CodecId::SUBRIP.name().as_deref(), Some("subrip"));
332  }
333
334  /// The long name is prose and is only asked to *be* there — pinning
335  /// FFmpeg's wording would pin a string its next release may reword,
336  /// which is exactly why nothing should key on it.
337  #[test]
338  fn a_known_codec_carries_a_description_too() {
339    let long = CodecId::H264.long_name().expect("h264 has a description");
340    assert!(
341      long.contains("H.264"),
342      "libavcodec's description for h264 should name the standard, got {long:?}",
343    );
344  }
345
346  /// **An id with no descriptor has no name**, which is the reason the
347  /// descriptor table is read rather than `avcodec_get_name` — that
348  /// function answers `"unknown_codec"` here, a sentinel a consumer
349  /// would store as though it were a codec's word.
350  ///
351  /// Two shapes, both reachable through the public `from_raw` door: a
352  /// negative id, and one far past the end of any table.
353  #[test]
354  fn an_id_with_no_descriptor_has_no_name() {
355    for unknown in [CodecId::from_raw(-99_999), CodecId::from_raw(i32::MAX)] {
356      assert_eq!(unknown.name(), None, "{unknown:?} named itself");
357      assert_eq!(unknown.long_name(), None, "{unknown:?} described itself");
358    }
359  }
360
361  /// `AV_CODEC_ID_NONE` is a **sentinel, not a codec**, and libavcodec
362  /// keeps no descriptor for it — so the "no codec" row reads as absent
363  /// rather than as a track coded with something called `none`.
364  #[test]
365  fn the_none_sentinel_names_no_codec() {
366    assert_eq!(CodecId::NONE.name(), None);
367  }
368
369  /// The number stays the key. A name is read *off* the id and never
370  /// back into it: two ids that name differently are two ids, and the
371  /// identity a store writes is [`CodecId::raw`].
372  #[test]
373  fn the_name_is_a_rendering_and_the_number_is_the_identity() {
374    let from_number = CodecId::from_raw(CodecId::H264.raw());
375    assert_eq!(from_number, CodecId::H264);
376    assert_eq!(from_number.name(), CodecId::H264.name());
377    assert_ne!(CodecId::H264.name(), CodecId::HEVC.name());
378  }
379}