Skip to main content

mediadecode_ffmpeg/
container.rs

1//! [`ContainerFormat`] — what libavformat decided the bytes are wrapped
2//! in, surfaced as the demuxer's own word.
3//!
4//! Read once from `AVFormatContext.iformat` while the session is opened
5//! and kept for its life: libavformat picks the demuxer during
6//! `avformat_open_input` and never changes it, so re-reading could only
7//! answer the same thing more expensively.
8
9use smol_bytes::Utf8Bytes;
10
11/// Upper bound on the NUL search over an `AVInputFormat`'s strings.
12///
13/// The longest name in FFmpeg's demuxer table is a comma list a few
14/// dozen bytes long and the longest description a couple of hundred;
15/// the cap is generous for both and exists only so that a
16/// version-skewed table cannot turn the walk into an unbounded read.
17const FORMAT_TEXT_MAX_BYTES: usize = 1024;
18
19/// The container identification libavformat made for a session.
20///
21/// # This is the DEMUXER's identity, not a filename's
22///
23/// It is `AVInputFormat`'s own words — what libavformat concluded from
24/// the *bytes*, having probed them. That is precisely what a
25/// content-addressed row wants and what an extension cannot give: the
26/// same bytes at `movie.mov` and `movie.mp4` are one content, and this
27/// answers identically for both because it never looked at the path.
28///
29/// # One demuxer, several words — and [`name`](Self::name) is the list
30///
31/// FFmpeg registers one demuxer per *family*, so its name is a
32/// comma-separated list of the short names that family handles:
33/// `"mov,mp4,m4a,3gp,3g2,mj2"` for the ISOBMFF demuxer, `"matroska,webm"`
34/// for Matroska. [`name`](Self::name) is that string verbatim and
35/// [`names`](Self::names) walks it.
36///
37/// **The list does not narrow to one word, and this type does not
38/// pretend it does.** libavformat identified the *demuxer*; which brand
39/// inside that family a file is — an `.mp4` against an `.m4a` — is a
40/// question it did not answer and one nothing here can answer for it.
41/// A door that returned a single word would have had to pick, and a
42/// pick is the guess this whole seat exists to avoid.
43///
44/// # Crossing into a typed vocabulary
45///
46/// The words are FFmpeg's own slugs, which is what makes them
47/// crossable: a consumer that wants
48/// [`mediaframe::container::Format`](https://docs.rs/mediaframe) tries
49/// [`names`](Self::names) against its `FromStr` and takes what it
50/// recognises, deciding for itself what to do when a family offers
51/// several. This crate stays out of that decision — the fold belongs to
52/// whoever owns the vocabulary, and doing it here would put a second
53/// one somewhere a caller cannot see it.
54///
55/// # An open vocabulary
56///
57/// Never an enum. A container FFmpeg learns to demux in its next
58/// release names itself here with nothing to change on this side, which
59/// is the whole reason the identity is carried as text.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct ContainerFormat {
62  name: Utf8Bytes,
63  long_name: Option<Utf8Bytes>,
64}
65
66impl ContainerFormat {
67  /// Constructs a `ContainerFormat` from a demuxer's short-name list and
68  /// its optional description.
69  ///
70  /// Public so a consumer can build the value in a test or a fake; a
71  /// session builds its own from `AVFormatContext.iformat`.
72  #[inline]
73  pub const fn new(name: Utf8Bytes, long_name: Option<Utf8Bytes>) -> Self {
74    Self { name, long_name }
75  }
76
77  /// The demuxer's short-name list, verbatim — `"mov,mp4,m4a,3gp,3g2,mj2"`.
78  ///
79  /// See [`names`](Self::names) for the words in it.
80  #[inline]
81  pub fn name(&self) -> &str {
82    self.name.as_str()
83  }
84
85  /// The demuxer's human description — `"QuickTime / MOV"` — or `None`
86  /// where the table carries none.
87  ///
88  /// FFmpeg's prose, for display. It is not stable across releases and
89  /// nothing should key on it.
90  #[inline]
91  pub fn long_name(&self) -> Option<&str> {
92    self.long_name.as_deref()
93  }
94
95  /// The short names in [`name`](Self::name), in the order the demuxer
96  /// lists them.
97  ///
98  /// Every FFmpeg name is a non-empty word or a comma list of them, so
99  /// this yields at least one item for any format a session opened
100  /// with.
101  pub fn names(&self) -> impl Iterator<Item = &str> {
102    self.name.split(',').filter(|word| !word.is_empty())
103  }
104
105  /// Reads the format out of a live `AVFormatContext`, or `None` where
106  /// it carries no `iformat` or the table's name is not readable text.
107  ///
108  /// The name is the identity: a format that cannot be named is no
109  /// answer at all, so it is `None` rather than a value with an empty
110  /// word. A missing *description* is not the same thing and keeps the
111  /// value.
112  ///
113  /// # Safety
114  ///
115  /// `context` must be a live `*const AVFormatContext`.
116  pub(crate) unsafe fn from_context(
117    context: *const ffmpeg_next::ffi::AVFormatContext,
118  ) -> Option<Self> {
119    if context.is_null() {
120      return None;
121    }
122    // SAFETY: `context` is live per the contract; `iformat` is a public
123    // field holding a pointer into libavformat's own demuxer table (or
124    // null before a successful open).
125    let iformat = unsafe { (*context).iformat };
126    if iformat.is_null() {
127      return None;
128    }
129    // SAFETY: a non-null `iformat` points at a `static const`
130    // `AVInputFormat` compiled into libavformat — live for the process
131    // and never written — so both string fields may be read, and each
132    // is independently nullable.
133    let (name, long_name) = unsafe { ((*iformat).name, (*iformat).long_name) };
134    // SAFETY: both are null or NUL-terminated string literals in that
135    // same static table; the reader answers `None` for null.
136    // `from_static` rather than `from`: the reader borrows out of
137    // libavformat's static table, and this stores that borrow instead
138    // of copying it. FFmpeg has long names past the inline window — the
139    // SER demuxer's is sixty-five bytes — so the copy was a real
140    // allocation on an ordinary open, for a string the process already
141    // owns.
142    let name =
143      Utf8Bytes::from_static(unsafe { crate::ffi::table_text(name, FORMAT_TEXT_MAX_BYTES) }?);
144    let long_name = unsafe { crate::ffi::table_text(long_name, FORMAT_TEXT_MAX_BYTES) }
145      .map(Utf8Bytes::from_static);
146    Some(Self::new(name, long_name))
147  }
148}
149
150#[cfg(test)]
151mod tests;