mapillary-tools 0.12.1__py3-none-any.whl → 0.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +94 -4
  3. mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
  4. mapillary_tools/camm/camm_parser.py +561 -0
  5. mapillary_tools/commands/__init__.py +0 -1
  6. mapillary_tools/commands/__main__.py +0 -6
  7. mapillary_tools/commands/process.py +0 -50
  8. mapillary_tools/commands/upload.py +1 -26
  9. mapillary_tools/constants.py +2 -2
  10. mapillary_tools/exiftool_read_video.py +13 -11
  11. mapillary_tools/ffmpeg.py +2 -2
  12. mapillary_tools/geo.py +0 -54
  13. mapillary_tools/geotag/blackvue_parser.py +4 -4
  14. mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
  15. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
  16. mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
  17. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
  18. mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
  19. mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
  20. mapillary_tools/geotag/gpmf_parser.py +346 -83
  21. mapillary_tools/mp4/__init__.py +0 -0
  22. mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
  23. mapillary_tools/mp4/mp4_sample_parser.py +322 -0
  24. mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
  25. mapillary_tools/process_geotag_properties.py +25 -19
  26. mapillary_tools/process_sequence_properties.py +6 -6
  27. mapillary_tools/sample_video.py +17 -16
  28. mapillary_tools/telemetry.py +71 -0
  29. mapillary_tools/types.py +18 -0
  30. mapillary_tools/upload.py +74 -233
  31. mapillary_tools/upload_api_v4.py +8 -9
  32. mapillary_tools/utils.py +9 -16
  33. mapillary_tools/video_data_extraction/cli_options.py +0 -1
  34. mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
  35. mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
  36. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
  37. mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
  38. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
  39. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
  40. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
  41. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
  42. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
  43. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
  44. mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
  45. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
  46. mapillary_tools-0.13.1.dist-info/RECORD +75 -0
  47. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
  48. mapillary_tools/commands/upload_blackvue.py +0 -33
  49. mapillary_tools/commands/upload_camm.py +0 -33
  50. mapillary_tools/commands/upload_zip.py +0 -33
  51. mapillary_tools/geotag/camm_parser.py +0 -306
  52. mapillary_tools/geotag/mp4_sample_parser.py +0 -426
  53. mapillary_tools/process_import_meta_properties.py +0 -76
  54. mapillary_tools-0.12.1.dist-info/RECORD +0 -77
  55. /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
  56. /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
  57. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
  58. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
  59. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,322 @@
1
+ import datetime
2
+ import typing as T
3
+ from pathlib import Path
4
+
5
+ from . import construct_mp4_parser as cparser, simple_mp4_parser as sparser
6
+
7
+
8
+ class RawSample(T.NamedTuple):
9
+ # 1-based index
10
+ description_idx: int
11
+
12
+ # sample offset (offset from the beginning of the file)
13
+ offset: int
14
+
15
+ # sample size (in bytes)
16
+ size: int
17
+
18
+ # sample_delta read from stts entries that decides when to decode the sample,
19
+ # i.e. STTS(n) in the forumula DT(n+1) = DT(n) + STTS(n)
20
+ # NOTE: timescale is not applied yet (hence int)
21
+ timedelta: int
22
+
23
+ # sample composition offset that decides when to present the sample,
24
+ # i.e. CTTS(n) in the forumula CT(n) = DT(n) + CTTS(n).
25
+ # NOTE: timescale is not applied yet (hence int)
26
+ composition_offset: int
27
+
28
+ # if it is a sync sample
29
+ is_sync: bool
30
+
31
+
32
+ class Sample(T.NamedTuple):
33
+ raw_sample: RawSample
34
+
35
+ # accumulated timedelta in seconds, i.e. DT(n) / timescale
36
+ exact_time: float
37
+
38
+ # accumulated composition timedelta in seconds, i.e. CT(n) / timescale
39
+ exact_composition_time: float
40
+
41
+ # exact timedelta in seconds, i.e. STTS(n) / timescale
42
+ exact_timedelta: float
43
+
44
+ # reference to the sample description
45
+ description: T.Dict
46
+
47
+
48
+ def _extract_raw_samples(
49
+ sizes: T.Sequence[int],
50
+ chunk_entries: T.Sequence[T.Dict],
51
+ chunk_offsets: T.Sequence[int],
52
+ timedeltas: T.Sequence[int],
53
+ composition_offsets: T.Optional[T.Sequence[int]],
54
+ syncs: T.Optional[T.Set[int]],
55
+ ) -> T.Generator[RawSample, None, None]:
56
+ if not sizes:
57
+ return
58
+
59
+ if not chunk_entries:
60
+ return
61
+
62
+ assert len(sizes) <= len(timedeltas), (
63
+ f"got less ({len(timedeltas)}) sample time deltas (stts) than expected ({len(sizes)})"
64
+ )
65
+
66
+ sample_idx = 0
67
+ chunk_idx = 0
68
+
69
+ # iterate compressed chunks
70
+ for entry_idx, entry in enumerate(chunk_entries):
71
+ if entry_idx + 1 < len(chunk_entries):
72
+ nbr_chunks = (
73
+ chunk_entries[entry_idx + 1]["first_chunk"] - entry["first_chunk"]
74
+ )
75
+ else:
76
+ nbr_chunks = 1
77
+
78
+ # iterate chunks
79
+ for _ in range(nbr_chunks):
80
+ sample_offset = chunk_offsets[chunk_idx]
81
+ # iterate samples in this chunk
82
+ for _ in range(entry["samples_per_chunk"]):
83
+ is_sync = syncs is None or (sample_idx + 1) in syncs
84
+ composition_offset = (
85
+ composition_offsets[sample_idx]
86
+ if composition_offsets is not None
87
+ else 0
88
+ )
89
+ yield RawSample(
90
+ description_idx=entry["sample_description_index"],
91
+ offset=sample_offset,
92
+ size=sizes[sample_idx],
93
+ timedelta=timedeltas[sample_idx],
94
+ composition_offset=composition_offset,
95
+ is_sync=is_sync,
96
+ )
97
+ sample_offset += sizes[sample_idx]
98
+ sample_idx += 1
99
+ chunk_idx += 1
100
+
101
+ # below handles the single-entry case:
102
+ # If all the chunks have the same number of samples per chunk
103
+ # and use the same sample description, this table has one entry.
104
+
105
+ # iterate chunks
106
+ while sample_idx < len(sizes):
107
+ sample_offset = chunk_offsets[chunk_idx]
108
+ # iterate samples in this chunk
109
+ for _ in range(chunk_entries[-1]["samples_per_chunk"]):
110
+ is_sync = syncs is None or (sample_idx + 1) in syncs
111
+ composition_offset = (
112
+ composition_offsets[sample_idx]
113
+ if composition_offsets is not None
114
+ else 0
115
+ )
116
+ yield RawSample(
117
+ description_idx=chunk_entries[-1]["sample_description_index"],
118
+ offset=sample_offset,
119
+ size=sizes[sample_idx],
120
+ timedelta=timedeltas[sample_idx],
121
+ composition_offset=composition_offset,
122
+ is_sync=is_sync,
123
+ )
124
+ sample_offset += sizes[sample_idx]
125
+ sample_idx += 1
126
+ chunk_idx += 1
127
+
128
+
129
+ def _extract_samples(
130
+ raw_samples: T.Iterator[RawSample],
131
+ descriptions: T.List,
132
+ timescale: int,
133
+ ) -> T.Generator[Sample, None, None]:
134
+ acc_delta = 0
135
+ for raw_sample in raw_samples:
136
+ yield Sample(
137
+ raw_sample=raw_sample,
138
+ description=descriptions[raw_sample.description_idx - 1],
139
+ exact_time=acc_delta / timescale,
140
+ exact_timedelta=raw_sample.timedelta / timescale,
141
+ # CT(n) = DT(n) + CTTS(n)
142
+ exact_composition_time=(acc_delta + raw_sample.composition_offset)
143
+ / timescale,
144
+ )
145
+ acc_delta += raw_sample.timedelta
146
+
147
+
148
+ STBLBoxlistConstruct = cparser.Box64ConstructBuilder(
149
+ T.cast(cparser.SwitchMapType, cparser.CMAP[b"stbl"])
150
+ ).BoxList
151
+
152
+
153
+ def extract_raw_samples_from_stbl_data(
154
+ stbl: bytes,
155
+ ) -> T.Tuple[T.List[T.Dict], T.Generator[RawSample, None, None]]:
156
+ descriptions = []
157
+ sizes = []
158
+ chunk_offsets = []
159
+ chunk_entries = []
160
+ timedeltas: T.List[int] = []
161
+ composition_offsets: T.Optional[T.List[int]] = None
162
+ syncs: T.Optional[T.Set[int]] = None
163
+
164
+ stbl_children = T.cast(
165
+ T.Sequence[cparser.BoxDict], STBLBoxlistConstruct.parse(stbl)
166
+ )
167
+
168
+ for box in stbl_children:
169
+ data: T.Dict = T.cast(T.Dict, box["data"])
170
+
171
+ if box["type"] == b"stsd":
172
+ descriptions = list(data["entries"])
173
+ elif box["type"] == b"stsz":
174
+ if data["sample_size"] == 0:
175
+ sizes = list(data["entries"])
176
+ else:
177
+ sizes = [data["sample_size"] for _ in range(data["sample_count"])]
178
+ elif box["type"] == b"stco":
179
+ chunk_offsets = list(data["entries"])
180
+ elif box["type"] == b"co64":
181
+ chunk_offsets = list(data["entries"])
182
+ elif box["type"] == b"stsc":
183
+ chunk_entries = list(data["entries"])
184
+ elif box["type"] == b"stts":
185
+ timedeltas = []
186
+ for entry in data["entries"]:
187
+ for _ in range(entry["sample_count"]):
188
+ timedeltas.append(entry["sample_delta"])
189
+ elif box["type"] == b"ctts":
190
+ composition_offsets = []
191
+ for entry in data["entries"]:
192
+ for _ in range(entry["sample_count"]):
193
+ composition_offsets.append(entry["sample_offset"])
194
+ elif box["type"] == b"stss":
195
+ syncs = set(data["entries"])
196
+
197
+ # some stbl have less timedeltas than the sample count i.e. len(sizes),
198
+ # in this case append 0's to timedeltas
199
+ while len(timedeltas) < len(sizes):
200
+ timedeltas.append(0)
201
+ if composition_offsets is not None:
202
+ while len(composition_offsets) < len(sizes):
203
+ composition_offsets.append(0)
204
+
205
+ raw_samples = _extract_raw_samples(
206
+ sizes, chunk_entries, chunk_offsets, timedeltas, composition_offsets, syncs
207
+ )
208
+ return descriptions, raw_samples
209
+
210
+
211
+ _STSDBoxListConstruct = cparser.Box64ConstructBuilder(
212
+ # pyre-ignore[6]: pyre does not support recursive type SwitchMapType
213
+ {b"stsd": cparser.CMAP[b"stsd"]}
214
+ ).BoxList
215
+
216
+
217
+ class TrackBoxParser:
218
+ trak_children: T.Sequence[cparser.BoxDict]
219
+ stbl_data: bytes
220
+
221
+ def __init__(self, trak_children: T.Sequence[cparser.BoxDict]):
222
+ self.trak_children = trak_children
223
+ stbl = cparser.find_box_at_pathx(
224
+ self.trak_children, [b"mdia", b"minf", b"stbl"]
225
+ )
226
+ self.stbl_data = T.cast(bytes, stbl["data"])
227
+
228
+ def extract_tkhd_boxdata(self) -> T.Dict:
229
+ return T.cast(
230
+ T.Dict, cparser.find_box_at_pathx(self.trak_children, [b"tkhd"])["data"]
231
+ )
232
+
233
+ def is_video_track(self) -> bool:
234
+ hdlr = cparser.find_box_at_pathx(self.trak_children, [b"mdia", b"hdlr"])
235
+ return T.cast(T.Dict[str, T.Any], hdlr["data"])["handler_type"] == b"vide"
236
+
237
+ def extract_sample_descriptions(self) -> T.List[T.Dict]:
238
+ # TODO: return [] if parsing fail
239
+ boxes = _STSDBoxListConstruct.parse(self.stbl_data)
240
+ stsd = cparser.find_box_at_pathx(
241
+ T.cast(T.Sequence[cparser.BoxDict], boxes), [b"stsd"]
242
+ )
243
+ return T.cast(T.List[T.Dict], T.cast(T.Dict, stsd["data"])["entries"])
244
+
245
+ def extract_elst_boxdata(self) -> T.Optional[T.Dict]:
246
+ box = cparser.find_box_at_path(self.trak_children, [b"edts", b"elst"])
247
+ if box is None:
248
+ return None
249
+ return T.cast(T.Dict, box["data"])
250
+
251
+ def extract_mdhd_boxdata(self) -> T.Dict:
252
+ box = cparser.find_box_at_pathx(self.trak_children, [b"mdia", b"mdhd"])
253
+ return T.cast(T.Dict, box["data"])
254
+
255
+ def extract_raw_samples(self) -> T.Generator[RawSample, None, None]:
256
+ _, raw_samples = extract_raw_samples_from_stbl_data(self.stbl_data)
257
+ yield from raw_samples
258
+
259
+ def extract_samples(self) -> T.Generator[Sample, None, None]:
260
+ descriptions, raw_samples = extract_raw_samples_from_stbl_data(self.stbl_data)
261
+ mdhd = T.cast(
262
+ T.Dict,
263
+ cparser.find_box_at_pathx(self.trak_children, [b"mdia", b"mdhd"])["data"],
264
+ )
265
+ yield from _extract_samples(raw_samples, descriptions, mdhd["timescale"])
266
+
267
+
268
+ class MovieBoxParser:
269
+ moov_children: T.Sequence[cparser.BoxDict]
270
+
271
+ def __init__(self, moov_data: bytes):
272
+ self.moov_children = T.cast(
273
+ T.Sequence[cparser.BoxDict],
274
+ cparser.MOOVWithoutSTBLBuilderConstruct.BoxList.parse(moov_data),
275
+ )
276
+
277
+ @classmethod
278
+ def parse_file(cls, video_path: Path) -> "MovieBoxParser":
279
+ with video_path.open("rb") as fp:
280
+ moov = sparser.parse_box_data_firstx(fp, [b"moov"])
281
+ return MovieBoxParser(moov)
282
+
283
+ @classmethod
284
+ def parse_stream(cls, stream: T.BinaryIO) -> "MovieBoxParser":
285
+ moov = sparser.parse_box_data_firstx(stream, [b"moov"])
286
+ return MovieBoxParser(moov)
287
+
288
+ def extract_mvhd_boxdata(self) -> T.Dict:
289
+ mvhd = cparser.find_box_at_pathx(self.moov_children, [b"mvhd"])
290
+ return T.cast(T.Dict, mvhd["data"])
291
+
292
+ def extract_tracks(self) -> T.Generator[TrackBoxParser, None, None]:
293
+ for box in self.moov_children:
294
+ if box["type"] == b"trak":
295
+ yield TrackBoxParser(T.cast(T.Sequence[cparser.BoxDict], box["data"]))
296
+
297
+ def extract_track_at(self, stream_idx: int) -> TrackBoxParser:
298
+ """
299
+ stream_idx should be the stream_index specifier. See http://ffmpeg.org/ffmpeg.html#Stream-specifiers-1
300
+ > Stream numbering is based on the order of the streams as detected by libavformat
301
+ """
302
+ trak_boxes = [box for box in self.moov_children if box["type"] == b"trak"]
303
+ if not (0 <= stream_idx < len(trak_boxes)):
304
+ raise IndexError(
305
+ "unable to read stream at %d from the track list (length %d)",
306
+ stream_idx,
307
+ len(trak_boxes),
308
+ )
309
+ trak_children = T.cast(
310
+ T.Sequence[cparser.BoxDict], trak_boxes[stream_idx]["data"]
311
+ )
312
+ return TrackBoxParser(trak_children)
313
+
314
+
315
+ _DT_1904 = datetime.datetime.utcfromtimestamp(0).replace(year=1904)
316
+
317
+
318
+ def to_datetime(seconds_since_1904: int) -> datetime.datetime:
319
+ """
320
+ Convert seconds since midnight, Jan. 1, 1904, in UTC time
321
+ """
322
+ return _DT_1904 + datetime.timedelta(seconds=seconds_since_1904)
@@ -6,11 +6,22 @@ from . import (
6
6
  construct_mp4_parser as cparser,
7
7
  io_utils,
8
8
  mp4_sample_parser as sample_parser,
9
- simple_mp4_parser as parser,
9
+ simple_mp4_parser as sparser,
10
10
  )
11
11
  from .construct_mp4_parser import BoxDict
12
12
  from .mp4_sample_parser import RawSample
13
13
 
14
+ """
15
+ Variable naming conventions:
16
+
17
+ - *_box: a BoxDict
18
+ - *_children: a list of child BoxDicts under the parent box
19
+ - *_boxdata: BoxDict["data"]
20
+ - *_data: the data in bytes of a box (without the header (type and size))
21
+ - *_typed_data: the data in bytes of a box (with the header (type and size))
22
+ """
23
+
24
+
14
25
  UINT32_MAX = 2**32 - 1
15
26
  UINT64_MAX = 2**64 - 1
16
27
 
@@ -128,6 +139,7 @@ def _build_stts(sample_deltas: T.Iterable[int]) -> BoxDict:
128
139
  class _CompressedSampleCompositionOffset:
129
140
  __slots__ = ("sample_count", "sample_offset")
130
141
  # make sure dataclasses.asdict() produce the result as CompositionTimeToSampleBox expects
142
+ # SO DO NOT RENAME THE PROPERTIES BELOW
131
143
  sample_count: int
132
144
  sample_offset: int
133
145
 
@@ -225,7 +237,7 @@ _STBLChildrenBuilderConstruct = cparser.Box32ConstructBuilder(
225
237
  )
226
238
 
227
239
 
228
- def _update_sbtl(trak: BoxDict, sample_offset: int) -> int:
240
+ def _update_sbtl_sample_offsets(trak: BoxDict, sample_offset: int) -> int:
229
241
  assert trak["type"] == b"trak"
230
242
 
231
243
  # new samples with offsets updated
@@ -243,14 +255,13 @@ def _update_sbtl(trak: BoxDict, sample_offset: int) -> int:
243
255
  )
244
256
  sample_offset += sample.size
245
257
  stbl_box = cparser.find_box_at_pathx(trak, [b"trak", b"mdia", b"minf", b"stbl"])
246
- descriptions, _ = sample_parser.parse_raw_samples_from_stbl(
247
- io.BytesIO(T.cast(bytes, stbl_box["data"]))
258
+ descriptions, _ = sample_parser.extract_raw_samples_from_stbl_data(
259
+ T.cast(bytes, stbl_box["data"])
248
260
  )
249
261
  stbl_children_boxes = build_stbl_from_raw_samples(
250
262
  descriptions, repositioned_samples
251
263
  )
252
- new_stbl_bytes = _STBLChildrenBuilderConstruct.build_boxlist(stbl_children_boxes)
253
- stbl_box["data"] = new_stbl_bytes
264
+ stbl_box["data"] = _STBLChildrenBuilderConstruct.build_boxlist(stbl_children_boxes)
254
265
 
255
266
  return sample_offset
256
267
 
@@ -263,13 +274,13 @@ def iterate_samples(
263
274
  stbl_box = cparser.find_box_at_pathx(
264
275
  box, [b"trak", b"mdia", b"minf", b"stbl"]
265
276
  )
266
- _, raw_samples_iter = sample_parser.parse_raw_samples_from_stbl(
267
- io.BytesIO(T.cast(bytes, stbl_box["data"]))
277
+ _, raw_samples_iter = sample_parser.extract_raw_samples_from_stbl_data(
278
+ T.cast(bytes, stbl_box["data"])
268
279
  )
269
280
  yield from raw_samples_iter
270
281
 
271
282
 
272
- def _build_mdat_header_bytes(mdat_size: int) -> bytes:
283
+ def _build_mdat_header_data(mdat_size: int) -> bytes:
273
284
  if UINT32_MAX < mdat_size + 8:
274
285
  return cparser.BoxHeader64.build(
275
286
  {
@@ -302,7 +313,7 @@ def find_movie_timescale(moov_children: T.Sequence[BoxDict]) -> int:
302
313
  return T.cast(T.Dict, mvhd["data"])["timescale"]
303
314
 
304
315
 
305
- def _build_moov_bytes(moov_children: T.Sequence[BoxDict]) -> bytes:
316
+ def _build_moov_typed_data(moov_children: T.Sequence[BoxDict]) -> bytes:
306
317
  return cparser.MP4WithoutSTBLBuilderConstruct.build_box(
307
318
  {
308
319
  "type": b"moov",
@@ -324,62 +335,77 @@ def transform_mp4(
324
335
  ) -> io_utils.ChainedIO:
325
336
  # extract ftyp
326
337
  src_fp.seek(0)
327
- source_ftyp_box_data = parser.parse_mp4_data_firstx(src_fp, [b"ftyp"])
328
- source_ftyp_data = cparser.MP4WithoutSTBLBuilderConstruct.build_box(
329
- {"type": b"ftyp", "data": source_ftyp_box_data}
330
- )
338
+ ftyp_data = sparser.parse_mp4_data_firstx(src_fp, [b"ftyp"])
331
339
 
332
340
  # extract moov
333
341
  src_fp.seek(0)
334
- src_moov_data = parser.parse_mp4_data_firstx(src_fp, [b"moov"])
335
- moov_children = _MOOVChildrenParserConstruct.parse_boxlist(src_moov_data)
342
+ moov_data = sparser.parse_mp4_data_firstx(src_fp, [b"moov"])
343
+ moov_children = _MOOVChildrenParserConstruct.parse_boxlist(moov_data)
336
344
 
337
345
  # filter tracks in moov
338
346
  moov_children = list(_filter_moov_children_boxes(moov_children))
339
347
 
340
348
  # extract video samples
341
349
  source_samples = list(iterate_samples(moov_children))
342
- movie_sample_readers = [
350
+ sample_readers: T.List[io.IOBase] = [
343
351
  io_utils.SlicedIO(src_fp, sample.offset, sample.size)
344
352
  for sample in source_samples
345
353
  ]
346
354
  if sample_generator is not None:
347
- sample_readers = list(sample_generator(src_fp, moov_children))
348
- else:
349
- sample_readers = []
355
+ sample_readers.extend(sample_generator(src_fp, moov_children))
350
356
 
351
357
  _update_all_trak_tkhd(moov_children)
352
358
 
353
- # moov_boxes should be immutable since here
359
+ return build_mp4(ftyp_data, moov_children, sample_readers)
360
+
361
+
362
+ def build_mp4(
363
+ ftyp_data: bytes,
364
+ moov_children: T.Sequence[BoxDict],
365
+ sample_readers: T.Iterable[io.IOBase],
366
+ ) -> io_utils.ChainedIO:
367
+ ftyp_typed_data = cparser.MP4WithoutSTBLBuilderConstruct.build_box(
368
+ {"type": b"ftyp", "data": ftyp_data}
369
+ )
354
370
  mdat_body_size = sum(sample.size for sample in iterate_samples(moov_children))
371
+ # moov_children should be immutable since here
372
+ new_moov_typed_data = _rewrite_and_build_moov_typed_data(
373
+ len(ftyp_typed_data), moov_children
374
+ )
355
375
  return io_utils.ChainedIO(
356
376
  [
357
- io.BytesIO(source_ftyp_data),
358
- io.BytesIO(_rewrite_moov(len(source_ftyp_data), moov_children)),
359
- io.BytesIO(_build_mdat_header_bytes(mdat_body_size)),
360
- *movie_sample_readers,
377
+ # ftyp
378
+ io.BytesIO(ftyp_typed_data),
379
+ # moov
380
+ io.BytesIO(new_moov_typed_data),
381
+ # mdat
382
+ io.BytesIO(_build_mdat_header_data(mdat_body_size)),
361
383
  *sample_readers,
362
384
  ]
363
385
  )
364
386
 
365
387
 
366
- def _rewrite_moov(moov_offset: int, moov_boxes: T.Sequence[BoxDict]) -> bytes:
388
+ def _rewrite_and_build_moov_typed_data(
389
+ moov_offset: int, moov_children: T.Sequence[BoxDict]
390
+ ) -> bytes:
367
391
  # build moov for calculating moov size
368
392
  sample_offset = 0
369
- for box in _filter_trak_boxes(moov_boxes):
370
- sample_offset = _update_sbtl(box, sample_offset)
371
- moov_data = _build_moov_bytes(moov_boxes)
372
- moov_data_size = len(moov_data)
393
+ for box in _filter_trak_boxes(moov_children):
394
+ sample_offset = _update_sbtl_sample_offsets(box, sample_offset)
395
+ moov_typed_data = _build_moov_typed_data(moov_children)
396
+ moov_typed_data_size = len(moov_typed_data)
373
397
 
374
398
  # mdat header size
375
- mdat_body_size = sum(sample.size for sample in iterate_samples(moov_boxes))
376
- mdat_header = _build_mdat_header_bytes(mdat_body_size)
399
+ mdat_body_size = sum(sample.size for sample in iterate_samples(moov_children))
400
+ mdat_header_data = _build_mdat_header_data(mdat_body_size)
377
401
 
378
402
  # build moov for real
379
- sample_offset = moov_offset + len(moov_data) + len(mdat_header)
380
- for box in _filter_trak_boxes(moov_boxes):
381
- sample_offset = _update_sbtl(box, sample_offset)
382
- moov_data = _build_moov_bytes(moov_boxes)
383
- assert len(moov_data) == moov_data_size, f"{len(moov_data)} != {moov_data_size}"
403
+ sample_offset = moov_offset + len(moov_typed_data) + len(mdat_header_data)
404
+ for box in _filter_trak_boxes(moov_children):
405
+ sample_offset = _update_sbtl_sample_offsets(box, sample_offset)
406
+ moov_typed_data = _build_moov_typed_data(moov_children)
407
+ assert len(moov_typed_data) == moov_typed_data_size, (
408
+ f"{len(moov_typed_data)} != {moov_typed_data_size}"
409
+ )
384
410
 
385
- return moov_data
411
+ return moov_typed_data
@@ -170,7 +170,7 @@ def _process_videos(
170
170
 
171
171
 
172
172
  def _normalize_import_paths(
173
- import_path: T.Union[Path, T.Sequence[Path]]
173
+ import_path: T.Union[Path, T.Sequence[Path]],
174
174
  ) -> T.Sequence[Path]:
175
175
  import_paths: T.Sequence[Path]
176
176
  if isinstance(import_path, Path):
@@ -206,16 +206,8 @@ def process_geotag_properties(
206
206
 
207
207
  metadatas: T.List[types.MetadataOrError] = []
208
208
 
209
- # if more than one filetypes speficied, check filename suffixes,
210
- # i.e. files not ended with .jpg or .mp4 will be ignored
211
- check_file_suffix = len(filetypes) > 1
212
-
213
209
  if FileType.IMAGE in filetypes:
214
- image_paths = utils.find_images(
215
- import_paths,
216
- skip_subfolders=skip_subfolders,
217
- check_file_suffix=check_file_suffix,
218
- )
210
+ image_paths = utils.find_images(import_paths, skip_subfolders=skip_subfolders)
219
211
  if image_paths:
220
212
  image_metadatas = _process_images(
221
213
  image_paths,
@@ -240,9 +232,7 @@ def process_geotag_properties(
240
232
  or FileType.VIDEO in filetypes
241
233
  ):
242
234
  video_paths = utils.find_videos(
243
- import_paths,
244
- skip_subfolders=skip_subfolders,
245
- check_file_suffix=check_file_suffix,
235
+ import_paths, skip_subfolders=skip_subfolders
246
236
  )
247
237
  if video_paths:
248
238
  video_metadata = _process_videos(
@@ -255,9 +245,9 @@ def process_geotag_properties(
255
245
  metadatas.extend(video_metadata)
256
246
 
257
247
  # filenames should be deduplicated in utils.find_images/utils.find_videos
258
- assert len(metadatas) == len(
259
- set(metadata.filename for metadata in metadatas)
260
- ), "duplicate filenames found"
248
+ assert len(metadatas) == len(set(metadata.filename for metadata in metadatas)), (
249
+ "duplicate filenames found"
250
+ )
261
251
 
262
252
  return metadatas
263
253
 
@@ -283,13 +273,12 @@ def _process_videos_beta(vars_args: T.Dict):
283
273
 
284
274
  options: CliOptions = {
285
275
  "paths": vars_args["import_path"],
286
- "recursive": vars_args["skip_subfolders"] == False,
276
+ "recursive": vars_args["skip_subfolders"] is False,
287
277
  "geotag_sources_options": geotag_sources_opts,
288
278
  "geotag_source_path": vars_args["geotag_source_path"],
289
279
  "num_processes": vars_args["num_processes"],
290
280
  "device_make": vars_args["device_make"],
291
281
  "device_model": vars_args["device_model"],
292
- "check_file_suffix": len(vars_args["filetypes"]) > 1,
293
282
  }
294
283
  extractor = VideoDataExtractor(options)
295
284
  return extractor.process()
@@ -424,19 +413,22 @@ def _show_stats_per_filetype(
424
413
  skipped_process_errors: T.Set[T.Type[Exception]],
425
414
  ):
426
415
  good_metadatas: T.List[T.Union[types.VideoMetadata, types.ImageMetadata]] = []
416
+ filesize_to_upload = 0
427
417
  error_metadatas: T.List[types.ErrorMetadata] = []
428
418
  for metadata in metadatas:
429
419
  if isinstance(metadata, types.ErrorMetadata):
430
420
  error_metadatas.append(metadata)
431
421
  else:
432
422
  good_metadatas.append(metadata)
423
+ filesize_to_upload += metadata.filesize or 0
433
424
 
434
425
  LOG.info("%8d %s(s) read in total", len(metadatas), filetype.value)
435
426
  if good_metadatas:
436
427
  LOG.info(
437
- "\t %8d %s(s) are ready to be uploaded",
428
+ "\t %8d %s(s) (%s MB) are ready to be uploaded",
438
429
  len(good_metadatas),
439
430
  filetype.value,
431
+ round(filesize_to_upload / 1024 / 1024, 1),
440
432
  )
441
433
 
442
434
  error_counter = collections.Counter(
@@ -565,6 +557,8 @@ def process_finalize(
565
557
  import_path: T.Union[T.Sequence[Path], Path],
566
558
  metadatas: T.List[types.MetadataOrError],
567
559
  skip_process_errors: bool = False,
560
+ device_make: T.Optional[str] = None,
561
+ device_model: T.Optional[str] = None,
568
562
  overwrite_all_EXIF_tags: bool = False,
569
563
  overwrite_EXIF_time_tag: bool = False,
570
564
  overwrite_EXIF_gps_tag: bool = False,
@@ -575,6 +569,18 @@ def process_finalize(
575
569
  desc_path: T.Optional[str] = None,
576
570
  num_processes: T.Optional[int] = None,
577
571
  ) -> T.List[types.MetadataOrError]:
572
+ for metadata in metadatas:
573
+ if isinstance(metadata, types.VideoMetadata):
574
+ if device_make is not None:
575
+ metadata.make = device_make
576
+ if device_model is not None:
577
+ metadata.model = device_model
578
+ elif isinstance(metadata, types.ImageMetadata):
579
+ if device_make is not None:
580
+ metadata.MAPDeviceMake = device_make
581
+ if device_model is not None:
582
+ metadata.MAPDeviceModel = device_model
583
+
578
584
  # modified in place
579
585
  _apply_offsets(
580
586
  [
@@ -209,9 +209,9 @@ def _interpolate_subsecs_for_sorting(sequence: PointSequence) -> None:
209
209
  gidx = gidx + len(group)
210
210
 
211
211
  for cur, nxt in geo.pairwise(sequence):
212
- assert (
213
- cur.time <= nxt.time
214
- ), f"sequence must be sorted but got {cur.time} > {nxt.time}"
212
+ assert cur.time <= nxt.time, (
213
+ f"sequence must be sorted but got {cur.time} > {nxt.time}"
214
+ )
215
215
 
216
216
 
217
217
  def _parse_filesize_in_bytes(filesize_str: str) -> int:
@@ -335,9 +335,9 @@ def process_sequence_properties(
335
335
 
336
336
  results = error_metadatas + image_metadatas + video_metadatas
337
337
 
338
- assert len(metadatas) == len(
339
- results
340
- ), f"expected {len(metadatas)} results but got {len(results)}"
338
+ assert len(metadatas) == len(results), (
339
+ f"expected {len(metadatas)} results but got {len(results)}"
340
+ )
341
341
  assert sequence_idx == len(
342
342
  set(metadata.MAPSequenceUUID for metadata in image_metadatas)
343
343
  )