nom_exif/
video.rs

1use std::{
2    collections::{btree_map::IntoIter, BTreeMap},
3    fmt::Display,
4};
5
6use thiserror::Error;
7
8use crate::{
9    ebml::webm::parse_webm,
10    error::ParsingError,
11    file::MimeVideo,
12    mov::{extract_moov_body_from_buf, parse_mp4, parse_qt},
13    EntryValue, GPSInfo,
14};
15
16/// Try to keep the tag name consistent with [`crate::ExifTag`], and add some
17/// unique to video/audio, such as `DurationMs`.
18///
19/// Different variants of `TrackInfoTag` may have different value types, please
20/// refer to the documentation of each variant.
21#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash)]
22#[non_exhaustive]
23pub enum TrackInfoTag {
24    /// Its value is an `EntryValue::Text`.
25    Make,
26
27    /// Its value is an `EntryValue::Text`.
28    Model,
29
30    /// Its value is an `EntryValue::Text`.
31    Software,
32
33    /// Its value is an [`EntryValue::Time`].
34    CreateDate,
35
36    /// Duration in millisecond, its value is an `EntryValue::U64`.
37    DurationMs,
38
39    /// Its value is an `EntryValue::U32`.
40    ImageWidth,
41
42    /// Its value is an `EntryValue::U32`.
43    ImageHeight,
44
45    /// Its value is an `EntryValue::Text`, location presented in ISO6709.
46    ///
47    /// If you need a parsed [`GPSInfo`] which provides more detailed GPS info,
48    /// please use [`TrackInfo::get_gps_info`].
49    GpsIso6709,
50
51    /// Its value is an `EntryValue::Text`.
52    Author,
53}
54
55/// Represents parsed track info.
56#[derive(Debug, Clone, Default)]
57pub struct TrackInfo {
58    entries: BTreeMap<TrackInfoTag, EntryValue>,
59    gps_info: Option<GPSInfo>,
60}
61
62impl TrackInfo {
63    /// Get value for `tag`. Different variants of `TrackInfoTag` may have
64    /// different value types, please refer to [`TrackInfoTag`].
65    pub fn get(&self, tag: TrackInfoTag) -> Option<&EntryValue> {
66        self.entries.get(&tag)
67    }
68
69    /// Get parsed `GPSInfo`.
70    pub fn get_gps_info(&self) -> Option<&GPSInfo> {
71        self.gps_info.as_ref()
72    }
73
74    /// Get an iterator for `(&TrackInfoTag, &EntryValue)`. The parsed
75    /// `GPSInfo` is not included.
76    pub fn iter(&self) -> impl Iterator<Item = (&TrackInfoTag, &EntryValue)> {
77        self.entries.iter()
78    }
79
80    pub(crate) fn put(&mut self, tag: TrackInfoTag, value: EntryValue) {
81        self.entries.insert(tag, value);
82    }
83}
84
85/// Parse video/audio info from `reader`. The file format will be detected
86/// automatically by parser, if the format is not supported, an `Err` will be
87/// returned.
88///
89/// Currently supported file formats are:
90///
91/// - ISO base media file format (ISOBMFF): *.mp4, *.mov, *.3gp, etc.
92/// - Matroska based file format: *.webm, *.mkv, *.mka, etc.
93///
94/// ## Explanation of the generic parameters of this function:
95///
96/// - In order to improve parsing efficiency, the parser will internally skip
97///   some useless bytes during parsing the byte stream, which is called
98///   [`Skip`] internally.
99///
100/// - In order to support both `Read` and `Read` + `Seek` types, the interface
101///   of input parameters is defined as `Read`.
102///   
103/// - Since Rust does not support specialization, the parser cannot internally
104///   distinguish between `Read` and `Seek` and provide different `Skip`
105///   implementations for them.
106///   
107/// Therefore, We chose to let the user specify how `Skip` works:
108///
109/// - `parse_track_info::<SkipSeek, _>(reader)` means the `reader` supports
110///   `Seek`, so `Skip` will use the `Seek` trait to implement efficient skip
111///   operations.
112///   
113/// - `parse_track_info::<SkipRead, _>(reader)` means the `reader` dosn't
114///   support `Seek`, so `Skip` will fall back to using `Read` to implement the
115///   skip operations.
116///
117/// ## Performance impact
118///
119/// If your `reader` only supports `Read`, it may cause performance loss when
120/// processing certain large files. For example, *.mov files place metadata at
121/// the end of the file, therefore, when parsing such files, locating metadata
122/// will be slightly slower.
123///
124/// ## Examples
125///
126/// ```rust
127/// use nom_exif::*;
128/// use std::fs::File;
129/// use chrono::DateTime;
130///
131/// let ms = MediaSource::file_path("./testdata/meta.mov").unwrap();
132/// let mut parser = MediaParser::new();
133/// let info: TrackInfo = parser.parse(ms).unwrap();
134///
135/// assert_eq!(info.get(TrackInfoTag::Make), Some(&"Apple".into()));
136/// assert_eq!(info.get(TrackInfoTag::Model), Some(&"iPhone X".into()));
137/// assert_eq!(info.get(TrackInfoTag::GpsIso6709), Some(&"+27.1281+100.2508+000.000/".into()));
138/// assert_eq!(info.get_gps_info().unwrap().latitude_ref, 'N');
139/// assert_eq!(
140///     info.get_gps_info().unwrap().latitude,
141///     [(27, 1), (7, 1), (68, 100)].into(),
142/// );
143/// ```
144#[tracing::instrument(skip(input))]
145pub(crate) fn parse_track_info(
146    input: &[u8],
147    mime_video: MimeVideo,
148) -> Result<TrackInfo, ParsingError> {
149    let mut info: TrackInfo = match mime_video {
150        crate::file::MimeVideo::QuickTime
151        | crate::file::MimeVideo::_3gpp
152        | crate::file::MimeVideo::Mp4 => {
153            let range = extract_moov_body_from_buf(input)?;
154            let moov_body = &input[range];
155
156            match mime_video {
157                MimeVideo::QuickTime => parse_qt(moov_body)?.into(),
158
159                MimeVideo::Mp4 | MimeVideo::_3gpp => parse_mp4(moov_body)?.into(),
160
161                _ => unreachable!(),
162            }
163        }
164        crate::file::MimeVideo::Webm | crate::file::MimeVideo::Matroska => {
165            parse_webm(input)?.into()
166        }
167    };
168
169    if let Some(gps) = info.get(TrackInfoTag::GpsIso6709) {
170        info.gps_info = gps.as_str().and_then(|s| s.parse().ok());
171    }
172
173    Ok(info)
174}
175
176impl IntoIterator for TrackInfo {
177    type Item = (TrackInfoTag, EntryValue);
178    type IntoIter = IntoIter<TrackInfoTag, EntryValue>;
179
180    fn into_iter(self) -> Self::IntoIter {
181        self.entries.into_iter()
182    }
183}
184
185impl From<BTreeMap<TrackInfoTag, EntryValue>> for TrackInfo {
186    fn from(entries: BTreeMap<TrackInfoTag, EntryValue>) -> Self {
187        Self {
188            entries,
189            gps_info: None,
190        }
191    }
192}
193
194impl Display for TrackInfoTag {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        let s: &str = (*self).into();
197        s.fmt(f)
198    }
199}
200
201impl From<TrackInfoTag> for &str {
202    fn from(value: TrackInfoTag) -> Self {
203        match value {
204            TrackInfoTag::Make => "Make",
205            TrackInfoTag::Model => "Model",
206            TrackInfoTag::Software => "Software",
207            TrackInfoTag::CreateDate => "CreateDate",
208            TrackInfoTag::DurationMs => "DurationMs",
209            TrackInfoTag::ImageWidth => "ImageWidth",
210            TrackInfoTag::ImageHeight => "ImageHeight",
211            TrackInfoTag::GpsIso6709 => "GpsIso6709",
212            TrackInfoTag::Author => "Author",
213        }
214    }
215}
216
217#[derive(Debug, Error)]
218#[error("unknown TrackInfoTag: {0}")]
219pub struct UnknownTrackInfoTag(pub String);
220
221impl TryFrom<&str> for TrackInfoTag {
222    type Error = UnknownTrackInfoTag;
223    fn try_from(value: &str) -> Result<Self, Self::Error> {
224        let tag = match value {
225            "Make" => TrackInfoTag::Make,
226            "Model" => TrackInfoTag::Model,
227            "Software" => TrackInfoTag::Software,
228            "CreateDate" => TrackInfoTag::CreateDate,
229            "DurationMs" => TrackInfoTag::DurationMs,
230            "ImageWidth" => TrackInfoTag::ImageWidth,
231            "ImageHeight" => TrackInfoTag::ImageHeight,
232            "GpsIso6709" => TrackInfoTag::GpsIso6709,
233            "Author" => TrackInfoTag::Author,
234            x => return Err(UnknownTrackInfoTag(x.to_owned())),
235        };
236
237        Ok(tag)
238    }
239}