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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +94 -4
- mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
- mapillary_tools/camm/camm_parser.py +561 -0
- mapillary_tools/commands/__init__.py +0 -1
- mapillary_tools/commands/__main__.py +0 -6
- mapillary_tools/commands/process.py +0 -50
- mapillary_tools/commands/upload.py +1 -26
- mapillary_tools/constants.py +2 -2
- mapillary_tools/exiftool_read_video.py +13 -11
- mapillary_tools/ffmpeg.py +2 -2
- mapillary_tools/geo.py +0 -54
- mapillary_tools/geotag/blackvue_parser.py +4 -4
- mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
- mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
- mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
- mapillary_tools/geotag/gpmf_parser.py +346 -83
- mapillary_tools/mp4/__init__.py +0 -0
- mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
- mapillary_tools/mp4/mp4_sample_parser.py +322 -0
- mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
- mapillary_tools/process_geotag_properties.py +25 -19
- mapillary_tools/process_sequence_properties.py +6 -6
- mapillary_tools/sample_video.py +17 -16
- mapillary_tools/telemetry.py +71 -0
- mapillary_tools/types.py +18 -0
- mapillary_tools/upload.py +74 -233
- mapillary_tools/upload_api_v4.py +8 -9
- mapillary_tools/utils.py +9 -16
- mapillary_tools/video_data_extraction/cli_options.py +0 -1
- mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
- mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
- mapillary_tools-0.13.1.dist-info/RECORD +75 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
- mapillary_tools/commands/upload_blackvue.py +0 -33
- mapillary_tools/commands/upload_camm.py +0 -33
- mapillary_tools/commands/upload_zip.py +0 -33
- mapillary_tools/geotag/camm_parser.py +0 -306
- mapillary_tools/geotag/mp4_sample_parser.py +0 -426
- mapillary_tools/process_import_meta_properties.py +0 -76
- mapillary_tools-0.12.1.dist-info/RECORD +0 -77
- /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
- /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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.
|
|
247
|
-
|
|
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
|
-
|
|
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.
|
|
267
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
moov_children = _MOOVChildrenParserConstruct.parse_boxlist(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
io.BytesIO(
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
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(
|
|
370
|
-
sample_offset =
|
|
371
|
-
|
|
372
|
-
|
|
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(
|
|
376
|
-
|
|
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(
|
|
380
|
-
for box in _filter_trak_boxes(
|
|
381
|
-
sample_offset =
|
|
382
|
-
|
|
383
|
-
assert len(
|
|
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
|
|
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
|
-
|
|
260
|
-
)
|
|
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"]
|
|
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
|
|
214
|
-
)
|
|
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
|
-
)
|
|
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
|
)
|