nom_exif/video.rs
1use std::collections::BTreeMap;
2
3use crate::{
4 ebml::webm::parse_webm,
5 error::ParsingError,
6 file::MediaMimeTrack,
7 mov::{extract_moov_body_from_buf, parse_isobmff},
8 EntryValue, GPSInfo,
9};
10
11/// Try to keep the tag name consistent with [`crate::ExifTag`], and add some
12/// unique to video/audio, such as `DurationMs`.
13///
14/// Different variants of `TrackInfoTag` may have different value types, please
15/// refer to the documentation of each variant.
16#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash)]
17#[non_exhaustive]
18pub enum TrackInfoTag {
19 /// Its value is an `EntryValue::Text`.
20 Make,
21
22 /// Its value is an `EntryValue::Text`.
23 Model,
24
25 /// Its value is an `EntryValue::Text`.
26 Software,
27
28 /// Its value is an [`EntryValue::DateTime`].
29 CreateDate,
30
31 /// Duration in millisecond, its value is an `EntryValue::U64`.
32 DurationMs,
33
34 /// Its value is an `EntryValue::U32`.
35 Width,
36
37 /// Its value is an `EntryValue::U32`.
38 Height,
39
40 /// Its value is an `EntryValue::Text`, location presented in ISO6709.
41 ///
42 /// If you need a parsed [`GPSInfo`] which provides more detailed GPS info,
43 /// please use [`TrackInfo::gps_info`].
44 GpsIso6709,
45
46 /// Its value is an `EntryValue::Text`.
47 Author,
48}
49
50/// Represents parsed track info.
51#[derive(Debug, Clone, Default)]
52pub struct TrackInfo {
53 entries: BTreeMap<TrackInfoTag, EntryValue>,
54 gps_info: Option<GPSInfo>,
55 has_embedded_media: bool,
56}
57
58impl TrackInfo {
59 /// Get value for `tag`. Different variants of `TrackInfoTag` may have
60 /// different value types, please refer to [`TrackInfoTag`].
61 pub fn get(&self, tag: TrackInfoTag) -> Option<&EntryValue> {
62 self.entries.get(&tag)
63 }
64
65 /// Parsed GPS info, if `GpsIso6709` was present in the source. Mirrors
66 /// [`Exif::gps_info`](crate::Exif::gps_info).
67 pub fn gps_info(&self) -> Option<&GPSInfo> {
68 self.gps_info.as_ref()
69 }
70
71 /// Iterate over `(tag, value)` pairs. The tag is yielded by value
72 /// because [`TrackInfoTag`] is `Copy`. The parsed `GPSInfo` is **not**
73 /// included here — get it via [`TrackInfo::gps_info`].
74 pub fn iter(&self) -> impl Iterator<Item = (TrackInfoTag, &EntryValue)> {
75 self.entries.iter().map(|(k, v)| (*k, v))
76 }
77
78 /// Whether the source container is known to embed additional media
79 /// streams that this `parse_track` call did *not* surface (e.g. an
80 /// .mka container holding both audio and video, or an .mp4 that also
81 /// embeds a still-image track). Symmetric with
82 /// [`Exif::has_embedded_media`](crate::Exif::has_embedded_media).
83 ///
84 /// **v3.0.0 note:** detection on the track side is not yet
85 /// implemented; this currently always returns `false`. The accessor
86 /// exists so future versions can flip the flag without a breaking
87 /// API change. See spec §8.6 for the design rationale.
88 pub fn has_embedded_media(&self) -> bool {
89 self.has_embedded_media
90 }
91
92 #[allow(dead_code)] // staged: real callers land in v3.x
93 pub(crate) fn set_has_embedded_media(&mut self, v: bool) {
94 self.has_embedded_media = v;
95 }
96
97 pub(crate) fn put(&mut self, tag: TrackInfoTag, value: EntryValue) {
98 self.entries.insert(tag, value);
99 }
100}
101
102/// Parse video/audio info from `reader`. The file format will be detected
103/// automatically by parser, if the format is not supported, an `Err` will be
104/// returned.
105///
106/// Currently supported file formats are:
107///
108/// - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc.
109/// - Matroska based file format: *.webm, *.mkv, *.mka, etc.
110///
111/// ## Explanation of the generic parameters of this function:
112///
113/// - In order to improve parsing efficiency, the parser will internally skip
114/// some useless bytes during parsing the byte stream, which is called
115/// `Skip` internally.
116///
117/// - In order to support both `Read` and `Read` + `Seek` types, the interface
118/// of input parameters is defined as `Read`.
119///
120/// - Since Rust does not support specialization, the parser cannot internally
121/// distinguish between `Read` and `Seek` and provide different `Skip`
122/// implementations for them.
123///
124/// Therefore, We chose to let the user specify how `Skip` works:
125///
126/// - `parse_track_info::<SkipSeek, _>(reader)` means the `reader` supports
127/// `Seek`, so `Skip` will use the `Seek` trait to implement efficient skip
128/// operations.
129///
130/// - `parse_track_info::<SkipRead, _>(reader)` means the `reader` dosn't
131/// support `Seek`, so `Skip` will fall back to using `Read` to implement the
132/// skip operations.
133///
134/// ## Performance impact
135///
136/// If your `reader` only supports `Read`, it may cause performance loss when
137/// processing certain large files. For example, *.mov files place metadata at
138/// the end of the file, therefore, when parsing such files, locating metadata
139/// will be slightly slower.
140///
141/// ## Examples
142///
143/// ```rust
144/// use nom_exif::*;
145/// use std::fs::File;
146/// use chrono::DateTime;
147///
148/// let ms = MediaSource::open("./testdata/meta.mov").unwrap();
149/// assert_eq!(ms.kind(), MediaKind::Track);
150/// let mut parser = MediaParser::new();
151/// let info: TrackInfo = parser.parse_track(ms).unwrap();
152///
153/// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into()));
154/// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into()));
155/// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into()));
156/// assert_eq!(info.gps_info().unwrap().latitude_ref, LatRef::North);
157/// assert_eq!(
158/// info.gps_info().unwrap().latitude,
159/// LatLng::new(URational::new(27, 1), URational::new(7, 1), URational::new(4116, 100)),
160/// );
161/// ```
162#[tracing::instrument(skip(input))]
163pub(crate) fn parse_track_info(
164 input: &[u8],
165 mime_video: MediaMimeTrack,
166) -> Result<TrackInfo, ParsingError> {
167 let mut info: TrackInfo = match mime_video {
168 crate::file::MediaMimeTrack::QuickTime
169 | crate::file::MediaMimeTrack::_3gpp
170 | crate::file::MediaMimeTrack::Mp4 => {
171 let range = extract_moov_body_from_buf(input)?;
172 let moov_body = &input[range];
173 parse_isobmff(moov_body)?
174 }
175 crate::file::MediaMimeTrack::Webm | crate::file::MediaMimeTrack::Matroska => {
176 parse_webm(input)?.into()
177 }
178 };
179
180 if let Some(gps) = info.get(TrackInfoTag::GpsIso6709) {
181 info.gps_info = gps.as_str().and_then(|s| s.parse().ok());
182 }
183
184 Ok(info)
185}
186
187impl TrackInfoTag {
188 /// Stable, programmatic name of this tag (matches the `Display` output).
189 pub const fn name(self) -> &'static str {
190 match self {
191 TrackInfoTag::Make => "Make",
192 TrackInfoTag::Model => "Model",
193 TrackInfoTag::Software => "Software",
194 TrackInfoTag::CreateDate => "CreateDate",
195 TrackInfoTag::DurationMs => "DurationMs",
196 TrackInfoTag::Width => "Width",
197 TrackInfoTag::Height => "Height",
198 TrackInfoTag::GpsIso6709 => "GpsIso6709",
199 TrackInfoTag::Author => "Author",
200 }
201 }
202}
203
204impl std::fmt::Display for TrackInfoTag {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 f.write_str(self.name())
207 }
208}
209
210impl std::str::FromStr for TrackInfoTag {
211 type Err = crate::ConvertError;
212
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 Ok(match s {
215 "Make" => TrackInfoTag::Make,
216 "Model" => TrackInfoTag::Model,
217 "Software" => TrackInfoTag::Software,
218 "CreateDate" => TrackInfoTag::CreateDate,
219 "DurationMs" => TrackInfoTag::DurationMs,
220 "Width" => TrackInfoTag::Width,
221 "Height" => TrackInfoTag::Height,
222 "GpsIso6709" => TrackInfoTag::GpsIso6709,
223 "Author" => TrackInfoTag::Author,
224 other => return Err(crate::ConvertError::UnknownTagName(other.to_owned())),
225 })
226 }
227}
228
229#[cfg(test)]
230mod p6_baseline {
231 use crate::{MediaParser, MediaSource, TrackInfoTag};
232
233 #[test]
234 fn p6_baseline_meta_mov_dump_snapshot() {
235 // Lock down the post-refactor invariant: parsing testdata/meta.mov
236 // through the public API yields the same set of (tag, value) pairs
237 // before and after every P6 task. Captured as a sorted formatted
238 // string so the assertion is a single Vec compare.
239 let mut parser = MediaParser::new();
240 let ms = MediaSource::open("testdata/meta.mov").unwrap();
241 let info = parser.parse_track(ms).unwrap();
242
243 // Probe the well-known tags (Make/Model/GpsIso6709/DurationMs).
244 // The rest is exercised indirectly by other tests.
245 let mut entries: Vec<String> = [
246 TrackInfoTag::Make,
247 TrackInfoTag::Model,
248 TrackInfoTag::GpsIso6709,
249 TrackInfoTag::DurationMs,
250 TrackInfoTag::Width,
251 TrackInfoTag::Height,
252 ]
253 .into_iter()
254 .filter_map(|t| info.get(t).map(|v| format!("{t:?}={v}")))
255 .collect();
256 entries.sort();
257 assert!(
258 entries.len() >= 4,
259 "expected >=4 well-known tags, got {entries:?}"
260 );
261 assert!(
262 entries.iter().any(|s| s.starts_with("Make=")),
263 "expected Make tag in snapshot, got {entries:?}"
264 );
265 }
266
267 #[test]
268 fn track_info_tag_name_is_const_str() {
269 const _: &str = TrackInfoTag::Make.name();
270 assert_eq!(TrackInfoTag::Make.name(), "Make");
271 assert_eq!(TrackInfoTag::GpsIso6709.name(), "GpsIso6709");
272 assert_eq!(TrackInfoTag::DurationMs.name(), "DurationMs");
273 }
274
275 #[test]
276 fn track_info_tag_from_str_round_trip() {
277 use std::str::FromStr;
278 for t in [
279 TrackInfoTag::Make,
280 TrackInfoTag::Model,
281 TrackInfoTag::Software,
282 TrackInfoTag::CreateDate,
283 TrackInfoTag::DurationMs,
284 TrackInfoTag::Width,
285 TrackInfoTag::Height,
286 TrackInfoTag::GpsIso6709,
287 TrackInfoTag::Author,
288 ] {
289 assert_eq!(TrackInfoTag::from_str(t.name()).unwrap(), t);
290 }
291 }
292
293 #[test]
294 fn track_info_tag_from_str_unknown_returns_convert_error() {
295 use crate::ConvertError;
296 use std::str::FromStr;
297 let err = TrackInfoTag::from_str("Bogus").unwrap_err();
298 assert!(matches!(err, ConvertError::UnknownTagName(s) if s == "Bogus"));
299 }
300
301 #[test]
302 fn track_info_has_embedded_media_default_false() {
303 // v3.0.0 ships the API contract; detection is a v3.x deliverable.
304 // This test pins the day-one behavior so accidentally flipping it
305 // requires explicit ack.
306 let mut parser = crate::MediaParser::new();
307 let ms = crate::MediaSource::open("testdata/meta.mov").unwrap();
308 let info = parser.parse_track(ms).unwrap();
309 assert!(!info.has_embedded_media());
310 }
311}