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;