Skip to main content

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}