1use std::{
2 collections::{btree_map, BTreeMap},
3 io::{Read, Seek},
4 ops::Range,
5};
6
7use chrono::DateTime;
8use nom::{bytes::streaming, IResult};
9
10#[allow(deprecated)]
11use crate::{
12 bbox::{
13 find_box, parse_video_tkhd_in_moov, travel_header, IlstBox, KeysBox, MvhdBox, ParseBox,
14 },
15 error::ParsingError,
16 loader::{BufLoader, Load},
17 partial_vec::PartialVec,
18 skip::Seekable,
19 video::TrackInfoTag,
20 EntryValue, FileFormat,
21};
22
23#[deprecated(since = "2.0.0")]
60#[tracing::instrument(skip_all)]
61#[allow(deprecated)]
62pub fn parse_metadata<R: Read + Seek>(reader: R) -> crate::Result<Vec<(String, EntryValue)>> {
63 let mut loader = BufLoader::<Seekable, _>::new(reader);
64 let ff = FileFormat::try_from_load(&mut loader)?;
65 match ff {
66 FileFormat::Jpeg | FileFormat::Heif => {
67 return Err(crate::error::Error::ParseFailed(
68 "can not parse metadata from an image".into(),
69 ));
70 }
71 FileFormat::QuickTime | FileFormat::MP4 => (),
72 FileFormat::Ebml => {
73 return Err(crate::error::Error::ParseFailed(
74 "please use MediaParser to parse *.webm, *.mkv files".into(),
75 ))
76 }
77 };
78
79 let moov_body = extract_moov_body(loader)?;
80
81 let (_, mut entries) = match parse_moov_body(&moov_body) {
82 Ok((remain, Some(entries))) => (remain, entries),
83 Ok((remain, None)) => (remain, Vec::new()),
84 Err(_) => {
85 return Err("invalid moov body".into());
86 }
87 };
88
89 let map: BTreeMap<TrackInfoTag, EntryValue> = map_qt_tag_to_video_tag(entries.clone());
90 let mut extras = parse_mvhd_tkhd(&moov_body);
91
92 const CREATIONDATE_KEY: &str = "com.apple.quicktime.creationdate";
93 if map.contains_key(&TrackInfoTag::CreateDate) {
94 extras.remove(&TrackInfoTag::CreateDate);
95 let date = map.get(&TrackInfoTag::CreateDate);
96 if let Some(pos) = entries.iter().position(|x| x.0 == CREATIONDATE_KEY) {
97 if let Some(date) = date {
98 entries[pos] = (CREATIONDATE_KEY.to_string(), date.clone());
99 } else {
100 entries.remove(pos);
101 }
102 }
103 }
104
105 entries.extend(extras.into_iter().map(|(k, v)| match k {
106 TrackInfoTag::ImageWidth => ("width".to_string(), v),
107 TrackInfoTag::ImageHeight => ("height".to_string(), v),
108 TrackInfoTag::DurationMs => (
109 "duration".to_string(),
110 EntryValue::U32(v.as_u64().unwrap() as u32),
112 ),
113 TrackInfoTag::CreateDate => (CREATIONDATE_KEY.to_string(), v),
114 _ => unreachable!(),
115 }));
116
117 if !map.contains_key(&TrackInfoTag::GpsIso6709) {
121 if let Some(gps) = parse_mp4_gps(&moov_body) {
122 const LOCATION_KEY: &str = "com.apple.quicktime.location.ISO6709";
123 entries.push((LOCATION_KEY.to_string(), gps.into()));
124 }
125 }
126
127 Ok(entries)
128}
129
130#[tracing::instrument(skip_all)]
131pub(crate) fn parse_qt(
132 moov_body: &[u8],
133) -> Result<BTreeMap<TrackInfoTag, EntryValue>, ParsingError> {
134 let (_, entries) = match parse_moov_body(moov_body) {
135 Ok((remain, Some(entries))) => (remain, entries),
136 Ok((remain, None)) => (remain, Vec::new()),
137 Err(_) => {
138 return Err("invalid moov body".into());
139 }
140 };
141
142 let mut entries: BTreeMap<TrackInfoTag, EntryValue> = map_qt_tag_to_video_tag(entries);
143 let extras = parse_mvhd_tkhd(moov_body);
144 if entries.contains_key(&TrackInfoTag::CreateDate) {
145 entries.remove(&TrackInfoTag::CreateDate);
146 }
147 entries.extend(extras);
148
149 Ok(entries)
150}
151
152#[tracing::instrument(skip_all)]
153pub(crate) fn parse_mp4(
154 moov_body: &[u8],
155) -> Result<BTreeMap<TrackInfoTag, EntryValue>, ParsingError> {
156 let (_, entries) = match parse_moov_body(moov_body) {
157 Ok((remain, Some(entries))) => (remain, entries),
158 Ok((remain, None)) => (remain, Vec::new()),
159 Err(_) => {
160 return Err("invalid moov body".into());
161 }
162 };
163
164 let mut entries: BTreeMap<TrackInfoTag, EntryValue> = map_qt_tag_to_video_tag(entries);
165 let extras = parse_mvhd_tkhd(moov_body);
166 entries.extend(extras);
167
168 if let btree_map::Entry::Vacant(e) = entries.entry(TrackInfoTag::GpsIso6709) {
172 if let Some(gps) = parse_mp4_gps(moov_body) {
173 e.insert(gps.into());
174 }
175 }
176
177 Ok(entries)
178}
179
180fn parse_mvhd_tkhd(moov_body: &[u8]) -> BTreeMap<TrackInfoTag, EntryValue> {
181 let mut entries = BTreeMap::new();
182 if let Ok((_, Some(bbox))) = find_box(moov_body, "mvhd") {
183 if let Ok((_, mvhd)) = MvhdBox::parse_box(bbox.data) {
184 entries.insert(TrackInfoTag::DurationMs, mvhd.duration_ms().into());
185
186 entries.insert(
187 TrackInfoTag::CreateDate,
188 EntryValue::Time(mvhd.creation_time()),
189 );
190 }
191 }
192
193 if let Ok(Some(tkhd)) = parse_video_tkhd_in_moov(moov_body) {
194 entries.insert(TrackInfoTag::ImageWidth, tkhd.width.into());
195 entries.insert(TrackInfoTag::ImageHeight, tkhd.height.into());
196 }
197
198 entries
199}
200
201fn map_qt_tag_to_video_tag(
202 entries: Vec<(String, EntryValue)>,
203) -> BTreeMap<TrackInfoTag, EntryValue> {
204 entries
205 .into_iter()
206 .filter_map(|(k, v)| {
207 if k == "com.apple.quicktime.creationdate" {
208 v.as_str()
209 .and_then(|s| DateTime::parse_from_str(s, "%+").ok())
210 .map(|t| (TrackInfoTag::CreateDate, EntryValue::Time(t)))
211 } else if k == "com.apple.quicktime.make" {
212 Some((TrackInfoTag::Make, v))
213 } else if k == "com.apple.quicktime.model" {
214 Some((TrackInfoTag::Model, v))
215 } else if k == "com.apple.quicktime.software" {
216 Some((TrackInfoTag::Software, v))
217 } else if k == "com.apple.quicktime.location.ISO6709" {
218 Some((TrackInfoTag::GpsIso6709, v))
219 } else {
220 None
221 }
222 })
223 .collect()
224}
225
226fn parse_mp4_gps(moov_body: &[u8]) -> Option<String> {
229 let bbox = match find_box(moov_body, "udta/©xyz") {
230 Ok((_, b)) => b,
231 Err(_) => None,
232 };
233 if let Some(bbox) = bbox {
234 if bbox.body_data().len() <= 4 {
235 tracing::warn!("moov/udta/©xyz body is too small");
236 } else {
237 let location = bbox.body_data()[4..] .iter()
239 .map(|b| *b as char)
240 .collect::<String>();
241 return Some(location);
242 }
243 }
244 None
245}
246
247#[deprecated(since = "2.0.0")]
284pub fn parse_mov_metadata<R: Read + Seek>(reader: R) -> crate::Result<Vec<(String, EntryValue)>> {
285 #[allow(deprecated)]
286 parse_metadata(reader)
287}
288
289#[tracing::instrument(skip_all)]
290fn extract_moov_body<L: Load>(mut loader: L) -> Result<PartialVec, crate::Error> {
291 let moov_body_range = loader.load_and_parse(extract_moov_body_from_buf)?;
292
293 tracing::debug!(?moov_body_range);
294 Ok(PartialVec::from_vec_range(
295 loader.into_vec(),
296 moov_body_range,
297 ))
298}
299
300#[tracing::instrument(skip_all)]
305pub(crate) fn extract_moov_body_from_buf(input: &[u8]) -> Result<Range<usize>, ParsingError> {
306 let remain = input;
308
309 let convert_error = |e: nom::Err<_>, msg: &str| match e {
310 nom::Err::Incomplete(needed) => match needed {
311 nom::Needed::Unknown => ParsingError::Need(1),
312 nom::Needed::Size(n) => ParsingError::Need(n.get()),
313 },
314 nom::Err::Failure(_) | nom::Err::Error(_) => ParsingError::Failed(msg.to_string()),
315 };
316
317 let mut to_skip = 0;
318 let mut skipped = 0;
319 let (remain, header) = travel_header(remain, |h, remain| {
320 tracing::debug!(?h.box_type, ?h.box_size, "Got");
321 if h.box_type == "moov" {
322 skipped += h.header_size;
324 false
325 } else if (remain.len() as u64) < h.body_size() {
326 to_skip = h.body_size() as usize - remain.len();
328 false
329 } else {
330 skipped += h.box_size as usize;
332 true
333 }
334 })
335 .map_err(|e| convert_error(e, "search atom moov failed"))?;
336
337 if to_skip > 0 {
338 return Err(ParsingError::ClearAndSkip(to_skip + input.len()));
339 }
340
341 let size: usize = header.body_size().try_into().expect("must fit");
342 let (_, body) =
343 streaming::take(size)(remain).map_err(|e| convert_error(e, "moov is too small"))?;
344
345 Ok(skipped..skipped + body.len())
346}
347
348type EntriesResult<'a> = IResult<&'a [u8], Option<Vec<(String, EntryValue)>>>;
349
350fn parse_moov_body(input: &[u8]) -> EntriesResult {
351 let (remain, Some(meta)) = find_box(input, "meta")? else {
352 return Ok((input, None));
353 };
354
355 let (_, Some(keys)) = find_box(meta.body_data(), "keys")? else {
356 return Ok((remain, None));
357 };
358
359 let (_, Some(ilst)) = find_box(meta.body_data(), "ilst")? else {
360 return Ok((remain, None));
361 };
362
363 let (_, keys) = KeysBox::parse_box(keys.data)?;
364 let (_, ilst) = IlstBox::parse_box(ilst.data)?;
365
366 let entries = keys
367 .entries
368 .into_iter()
369 .map(|k| k.key)
370 .zip(ilst.items.into_iter().map(|v| v.value))
371 .collect::<Vec<_>>();
372
373 Ok((input, Some(entries)))
374}
375
376#[allow(dead_code)]
381fn tz_iso_8601_to_rfc3339(s: String) -> String {
382 use regex::Regex;
383
384 let ss = s.trim();
385 let re = Regex::new(r"([+-][0-9][0-9])([0-9][0-9])?$").unwrap();
387
388 if let Some((offset, tz)) = re.captures(ss).map(|caps| {
389 (
390 caps.get(1).unwrap().start(),
392 format!(
393 "{}:{}",
394 caps.get(1).map_or("00", |m| m.as_str()),
395 caps.get(2).map_or("00", |m| m.as_str())
396 ),
397 )
398 }) {
399 let s1 = &ss.as_bytes()[..offset]; let s2 = tz.as_bytes();
401 s1.iter().chain(s2.iter()).map(|x| *x as char).collect()
402 } else {
403 s
404 }
405}
406
407#[cfg(test)]
408#[allow(deprecated)]
409mod tests {
410 use super::*;
411 use crate::testkit::*;
412 use test_case::test_case;
413
414 #[test_case("meta.mov")]
415 fn mov_parse(path: &str) {
416 let reader = open_sample(path).unwrap();
417 let entries = parse_metadata(reader).unwrap();
418 assert_eq!(
419 entries
420 .iter()
421 .map(|x| format!("{x:?}"))
422 .collect::<Vec<_>>()
423 .join("\n"),
424 "(\"com.apple.quicktime.make\", Text(\"Apple\"))
425(\"com.apple.quicktime.model\", Text(\"iPhone X\"))
426(\"com.apple.quicktime.software\", Text(\"12.1.2\"))
427(\"com.apple.quicktime.location.ISO6709\", Text(\"+27.1281+100.2508+000.000/\"))
428(\"com.apple.quicktime.creationdate\", Time(2019-02-12T15:27:12+08:00))
429(\"duration\", U32(500))
430(\"width\", U32(720))
431(\"height\", U32(1280))"
432 );
433 }
434
435 #[test_case("meta.mov")]
436 fn mov_extract_mov(path: &str) {
437 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
438
439 let buf = read_sample(path).unwrap();
440 tracing::info!(bytes = buf.len(), "File size.");
441 let range = extract_moov_body_from_buf(&buf).unwrap();
442 let (_, entries) = parse_moov_body(&buf[range]).unwrap();
443 assert_eq!(
444 entries
445 .unwrap()
446 .iter()
447 .map(|x| format!("{x:?}"))
448 .collect::<Vec<_>>()
449 .join("\n"),
450 "(\"com.apple.quicktime.make\", Text(\"Apple\"))
451(\"com.apple.quicktime.model\", Text(\"iPhone X\"))
452(\"com.apple.quicktime.software\", Text(\"12.1.2\"))
453(\"com.apple.quicktime.location.ISO6709\", Text(\"+27.1281+100.2508+000.000/\"))
454(\"com.apple.quicktime.creationdate\", Text(\"2019-02-12T15:27:12+08:00\"))"
455 );
456 }
457
458 #[test_case("meta.mp4")]
459 fn parse_mp4(path: &str) {
460 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
461
462 let entries = parse_metadata(open_sample(path).unwrap()).unwrap();
463 assert_eq!(
464 entries
465 .iter()
466 .map(|x| format!("{x:?}"))
467 .collect::<Vec<_>>()
468 .join("\n"),
469 "(\"com.apple.quicktime.creationdate\", Time(2024-02-03T07:05:38+00:00))
470(\"duration\", U32(1063))
471(\"width\", U32(1920))
472(\"height\", U32(1080))
473(\"com.apple.quicktime.location.ISO6709\", Text(\"+27.2939+112.6932/\"))"
474 );
475 }
476
477 #[test_case("embedded-in-heic.mov")]
478 fn parse_embedded_mov(path: &str) {
479 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
480
481 let entries = parse_mov_metadata(open_sample(path).unwrap()).unwrap();
482 assert_eq!(
483 entries
484 .iter()
485 .map(|x| format!("{x:?}"))
486 .collect::<Vec<_>>()
487 .join("\n"),
488 "(\"com.apple.quicktime.location.accuracy.horizontal\", Text(\"14.235563\"))
489(\"com.apple.quicktime.live-photo.auto\", U8(1))
490(\"com.apple.quicktime.content.identifier\", Text(\"DA1A7EE8-0925-4C9F-9266-DDA3F0BB80F0\"))
491(\"com.apple.quicktime.live-photo.vitality-score\", F32(0.93884003))
492(\"com.apple.quicktime.live-photo.vitality-scoring-version\", I64(4))
493(\"com.apple.quicktime.location.ISO6709\", Text(\"+22.5797+113.9380+028.396/\"))
494(\"com.apple.quicktime.make\", Text(\"Apple\"))
495(\"com.apple.quicktime.model\", Text(\"iPhone 15 Pro\"))
496(\"com.apple.quicktime.software\", Text(\"17.1\"))
497(\"com.apple.quicktime.creationdate\", Time(2023-11-02T19:58:34+08:00))
498(\"duration\", U32(2795))
499(\"width\", U32(1920))
500(\"height\", U32(1440))"
501 );
502 }
503
504 #[test]
505 fn test_iso_8601_tz_to_rfc3339() {
506 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
507
508 let s = "2023-11-02T19:58:34+08".to_string();
509 assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00");
510
511 let s = "2023-11-02T19:58:34+0800".to_string();
512 assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00");
513
514 let s = "2023-11-02T19:58:34+08:00".to_string();
515 assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34+08:00");
516
517 let s = "2023-11-02T19:58:34Z".to_string();
518 assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34Z");
519
520 let s = "2023-11-02T19:58:34".to_string();
521 assert_eq!(tz_iso_8601_to_rfc3339(s), "2023-11-02T19:58:34");
522 }
523}