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}