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    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}