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}
56
57impl TrackInfo {
58 /// Get value for `tag`. Different variants of `TrackInfoTag` may have
59 /// different value types, please refer to [`TrackInfoTag`].
60 pub fn get(&self, tag: TrackInfoTag) -> Option<&EntryValue> {
61 self.entries.get(&tag)
62 }
63
64 /// Parsed GPS info, if `GpsIso6709` was present in the source. Mirrors
65 /// [`Exif::gps_info`](crate::Exif::gps_info).
66 pub fn gps_info(&self) -> Option<&GPSInfo> {
67 self.gps_info.as_ref()
68 }
69
70 /// Iterate over `(tag, value)` pairs. The tag is yielded by value
71 /// because [`TrackInfoTag`] is `Copy`. The parsed `GPSInfo` is **not**
72 /// included here — get it via [`TrackInfo::gps_info`].
73 pub fn iter(&self) -> impl Iterator<Item = (TrackInfoTag, &EntryValue)> {
74 self.entries.iter().map(|(k, v)| (*k, v))
75 }
76
77 /// Deprecated: 3.0.0 reserved this for "track source also embeds a
78 /// secondary track" cases (e.g. `.mka` audio container that also
79 /// carries video) but the detection was never wired up — the method
80 /// always returned `false`. v3.1 drops the symmetric counterpart on
81 /// the image side ([`Exif::has_embedded_track`](crate::Exif::has_embedded_track))
82 /// to a content-detected flag, but the track-source variant stays
83 /// without a real use case, so this remains a no-op for source
84 /// compatibility only. May be re-introduced if a concrete use case
85 /// emerges.
86 #[deprecated(
87 since = "3.1.0",
88 note = "no concrete use case in v3.x; always returned false in 3.0.0. Kept as a no-op for source-compat; will be removed if no use case emerges by v4."
89 )]
90 pub fn has_embedded_media(&self) -> bool {
91 false
92 }
93
94 pub(crate) fn put(&mut self, tag: TrackInfoTag, value: EntryValue) {
95 self.entries.insert(tag, value);
96 }
97}
98
99/// Parse video/audio info from `reader`. The file format will be detected
100/// automatically by parser, if the format is not supported, an `Err` will be
101/// returned.
102///
103/// Currently supported file formats are:
104///
105/// - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc.
106/// - Matroska based file format: *.webm, *.mkv, *.mka, etc.
107///
108/// ## Explanation of the generic parameters of this function:
109///
110/// - In order to improve parsing efficiency, the parser will internally skip
111/// some useless bytes during parsing the byte stream, which is called
112/// `Skip` internally.
113///
114/// - In order to support both `Read` and `Read` + `Seek` types, the interface
115/// of input parameters is defined as `Read`.
116///
117/// - Since Rust does not support specialization, the parser cannot internally
118/// distinguish between `Read` and `Seek` and provide different `Skip`
119/// implementations for them.
120///
121/// Therefore, We chose to let the user specify how `Skip` works:
122///
123/// - `parse_track_info::<SkipSeek, _>(reader)` means the `reader` supports
124/// `Seek`, so `Skip` will use the `Seek` trait to implement efficient skip
125/// operations.
126///
127/// - `parse_track_info::<SkipRead, _>(reader)` means the `reader` dosn't
128/// support `Seek`, so `Skip` will fall back to using `Read` to implement the
129/// skip operations.
130///
131/// ## Performance impact
132///
133/// If your `reader` only supports `Read`, it may cause performance loss when
134/// processing certain large files. For example, *.mov files place metadata at
135/// the end of the file, therefore, when parsing such files, locating metadata
136/// will be slightly slower.
137///
138/// ## Examples
139///
140/// ```rust
141/// use nom_exif::*;
142/// use std::fs::File;
143/// use chrono::DateTime;
144///
145/// let ms = MediaSource::open("./testdata/meta.mov").unwrap();
146/// assert_eq!(ms.kind(), MediaKind::Track);
147/// let mut parser = MediaParser::new();
148/// let info: TrackInfo = parser.parse_track(ms).unwrap();
149///
150/// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into()));
151/// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into()));
152/// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into()));
153/// assert_eq!(info.gps_info().unwrap().latitude_ref, LatRef::North);
154/// assert_eq!(
155/// info.gps_info().unwrap().latitude,
156/// LatLng::new(URational::new(27, 1), URational::new(7, 1), URational::new(4116, 100)),
157/// );
158/// ```
159#[tracing::instrument(skip(input))]
160pub(crate) fn parse_track_info(
161 input: &[u8],
162 mime_video: MediaMimeTrack,
163) -> Result<TrackInfo, ParsingError> {
164 let mut info: TrackInfo = match mime_video {
165 crate::file::MediaMimeTrack::QuickTime
166 | crate::file::MediaMimeTrack::_3gpp
167 | crate::file::MediaMimeTrack::Mp4 => {
168 let range = extract_moov_body_from_buf(input)?;
169 let moov_body = &input[range];
170 parse_isobmff(moov_body)?
171 }
172 crate::file::MediaMimeTrack::Webm | crate::file::MediaMimeTrack::Matroska => {
173 parse_webm(input)?.into()
174 }
175 };
176
177 if let Some(gps) = info.get(TrackInfoTag::GpsIso6709) {
178 info.gps_info = gps.as_str().and_then(|s| s.parse().ok());
179 }
180
181 Ok(info)
182}
183
184impl TrackInfoTag {
185 /// Stable, programmatic name of this tag (matches the `Display` output).
186 pub const fn name(self) -> &'static str {
187 match self {
188 TrackInfoTag::Make => "Make",
189 TrackInfoTag::Model => "Model",
190 TrackInfoTag::Software => "Software",
191 TrackInfoTag::CreateDate => "CreateDate",
192 TrackInfoTag::DurationMs => "DurationMs",
193 TrackInfoTag::Width => "Width",
194 TrackInfoTag::Height => "Height",
195 TrackInfoTag::GpsIso6709 => "GpsIso6709",
196 TrackInfoTag::Author => "Author",
197 }
198 }
199}
200
201impl std::fmt::Display for TrackInfoTag {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 f.write_str(self.name())
204 }
205}
206
207impl std::str::FromStr for TrackInfoTag {
208 type Err = crate::ConvertError;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
211 Ok(match s {
212 "Make" => TrackInfoTag::Make,
213 "Model" => TrackInfoTag::Model,
214 "Software" => TrackInfoTag::Software,
215 "CreateDate" => TrackInfoTag::CreateDate,
216 "DurationMs" => TrackInfoTag::DurationMs,
217 "Width" => TrackInfoTag::Width,
218 "Height" => TrackInfoTag::Height,
219 "GpsIso6709" => TrackInfoTag::GpsIso6709,
220 "Author" => TrackInfoTag::Author,
221 other => return Err(crate::ConvertError::UnknownTagName(other.to_owned())),
222 })
223 }
224}
225
226#[cfg(test)]
227mod p6_baseline {
228 use crate::{MediaParser, MediaSource, TrackInfoTag};
229
230 #[test]
231 fn p6_baseline_meta_mov_dump_snapshot() {
232 // Lock down the post-refactor invariant: parsing testdata/meta.mov
233 // through the public API yields the same set of (tag, value) pairs
234 // before and after every P6 task. Captured as a sorted formatted
235 // string so the assertion is a single Vec compare.
236 let mut parser = MediaParser::new();
237 let ms = MediaSource::open("testdata/meta.mov").unwrap();
238 let info = parser.parse_track(ms).unwrap();
239
240 // Probe the well-known tags (Make/Model/GpsIso6709/DurationMs).
241 // The rest is exercised indirectly by other tests.
242 let mut entries: Vec<String> = [
243 TrackInfoTag::Make,
244 TrackInfoTag::Model,
245 TrackInfoTag::GpsIso6709,
246 TrackInfoTag::DurationMs,
247 TrackInfoTag::Width,
248 TrackInfoTag::Height,
249 ]
250 .into_iter()
251 .filter_map(|t| info.get(t).map(|v| format!("{t:?}={v}")))
252 .collect();
253 entries.sort();
254 assert!(
255 entries.len() >= 4,
256 "expected >=4 well-known tags, got {entries:?}"
257 );
258 assert!(
259 entries.iter().any(|s| s.starts_with("Make=")),
260 "expected Make tag in snapshot, got {entries:?}"
261 );
262 }
263
264 #[test]
265 fn track_info_tag_name_is_const_str() {
266 const _: &str = TrackInfoTag::Make.name();
267 assert_eq!(TrackInfoTag::Make.name(), "Make");
268 assert_eq!(TrackInfoTag::GpsIso6709.name(), "GpsIso6709");
269 assert_eq!(TrackInfoTag::DurationMs.name(), "DurationMs");
270 }
271
272 #[test]
273 fn track_info_tag_from_str_round_trip() {
274 use std::str::FromStr;
275 for t in [
276 TrackInfoTag::Make,
277 TrackInfoTag::Model,
278 TrackInfoTag::Software,
279 TrackInfoTag::CreateDate,
280 TrackInfoTag::DurationMs,
281 TrackInfoTag::Width,
282 TrackInfoTag::Height,
283 TrackInfoTag::GpsIso6709,
284 TrackInfoTag::Author,
285 ] {
286 assert_eq!(TrackInfoTag::from_str(t.name()).unwrap(), t);
287 }
288 }
289
290 #[test]
291 fn track_info_tag_from_str_unknown_returns_convert_error() {
292 use crate::ConvertError;
293 use std::str::FromStr;
294 let err = TrackInfoTag::from_str("Bogus").unwrap_err();
295 assert!(matches!(err, ConvertError::UnknownTagName(s) if s == "Bogus"));
296 }
297
298 #[test]
299 #[allow(deprecated)]
300 fn track_info_deprecated_has_embedded_media_returns_false() {
301 // 3.0.0 reserved this method for a "track source carries another
302 // embedded track" detection that never materialized. v3.1 leaves
303 // it as a deprecated no-op until a real use case shows up.
304 let mut parser = crate::MediaParser::new();
305 let info = parser
306 .parse_track(crate::MediaSource::open("testdata/meta.mov").unwrap())
307 .unwrap();
308 assert!(!info.has_embedded_media());
309 }
310}