mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.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 (49) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -262
  3. mapillary_tools/authenticate.py +54 -46
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/commands/__main__.py +15 -16
  6. mapillary_tools/commands/upload.py +33 -4
  7. mapillary_tools/config.py +38 -17
  8. mapillary_tools/constants.py +127 -43
  9. mapillary_tools/exceptions.py +4 -0
  10. mapillary_tools/exif_read.py +2 -1
  11. mapillary_tools/exif_write.py +3 -1
  12. mapillary_tools/exiftool_read_video.py +52 -15
  13. mapillary_tools/exiftool_runner.py +4 -24
  14. mapillary_tools/ffmpeg.py +406 -232
  15. mapillary_tools/geo.py +16 -0
  16. mapillary_tools/geotag/__init__.py +0 -0
  17. mapillary_tools/geotag/base.py +8 -4
  18. mapillary_tools/geotag/factory.py +106 -89
  19. mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
  20. mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
  21. mapillary_tools/geotag/geotag_images_from_video.py +35 -0
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
  24. mapillary_tools/geotag/options.py +25 -3
  25. mapillary_tools/geotag/utils.py +9 -12
  26. mapillary_tools/geotag/video_extractors/base.py +1 -1
  27. mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
  28. mapillary_tools/geotag/video_extractors/gpx.py +61 -70
  29. mapillary_tools/geotag/video_extractors/native.py +34 -31
  30. mapillary_tools/history.py +128 -8
  31. mapillary_tools/http.py +211 -0
  32. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  33. mapillary_tools/process_geotag_properties.py +47 -35
  34. mapillary_tools/process_sequence_properties.py +340 -325
  35. mapillary_tools/sample_video.py +8 -8
  36. mapillary_tools/serializer/description.py +587 -0
  37. mapillary_tools/serializer/gpx.py +132 -0
  38. mapillary_tools/types.py +44 -610
  39. mapillary_tools/upload.py +327 -352
  40. mapillary_tools/upload_api_v4.py +125 -72
  41. mapillary_tools/uploader.py +797 -216
  42. mapillary_tools/utils.py +57 -5
  43. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
  44. mapillary_tools-0.14.1.dist-info/RECORD +76 -0
  45. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
  46. mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
  47. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  48. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  49. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ else:
13
13
 
14
14
  from .. import constants, exceptions, exiftool_read, types
15
15
  from ..exiftool_runner import ExiftoolRunner
16
+ from . import options
16
17
  from .base import GeotagVideosFromGeneric
17
18
  from .utils import index_rdf_description_by_path
18
19
  from .video_extractors.exiftool import VideoExifToolExtractor
@@ -22,18 +23,14 @@ LOG = logging.getLogger(__name__)
22
23
 
23
24
  class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
24
25
  def __init__(
25
- self,
26
- xml_path: Path,
27
- num_processes: int | None = None,
26
+ self, source_path: options.SourcePathOption, num_processes: int | None = None
28
27
  ):
29
28
  super().__init__(num_processes=num_processes)
30
- self.xml_path = xml_path
29
+ self.source_path = source_path
31
30
 
32
31
  @classmethod
33
- def build_image_extractors(
34
- cls,
35
- rdf_by_path: dict[str, ET.Element],
36
- video_paths: T.Iterable[Path],
32
+ def build_video_extractors_from_etree(
33
+ cls, rdf_by_path: dict[str, ET.Element], video_paths: T.Iterable[Path]
37
34
  ) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
38
35
  results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
39
36
 
@@ -57,8 +54,49 @@ class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
57
54
  def _generate_video_extractors(
58
55
  self, video_paths: T.Sequence[Path]
59
56
  ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
60
- rdf_by_path = index_rdf_description_by_path([self.xml_path])
61
- return self.build_image_extractors(rdf_by_path, video_paths)
57
+ rdf_by_path = self.find_rdf_by_path(self.source_path, video_paths)
58
+ return self.build_video_extractors_from_etree(rdf_by_path, video_paths)
59
+
60
+ @classmethod
61
+ def find_rdf_by_path(
62
+ cls, option: options.SourcePathOption, paths: T.Iterable[Path]
63
+ ) -> dict[str, ET.Element]:
64
+ # Find RDF descriptions by path in RDF description
65
+ # Sources are matched based on the paths in "rdf:about" in XML elements
66
+ # {"source_path": "/path/to/exiftool.xml"}
67
+ # {"source_path": "/path/to/exiftool_xmls/"}
68
+ if option.source_path is not None:
69
+ return index_rdf_description_by_path([option.source_path])
70
+
71
+ # Find RDF descriptions by pattern matching
72
+ # i.e. "video.mp4" matches "/path/to/video.xml" regardless of "rdf:about"
73
+ # {"pattern": "/path/to/%g.xml"}
74
+ if option.pattern is not None:
75
+ rdf_by_path = {}
76
+ for path in paths:
77
+ canonical_path = exiftool_read.canonical_path(path)
78
+
79
+ # Skip non-existent resolved source paths to avoid verbose warnings
80
+ resolved_source_path = option.resolve(path)
81
+ if not resolved_source_path.exists():
82
+ continue
83
+
84
+ rdf_by_about = index_rdf_description_by_path([resolved_source_path])
85
+ if not rdf_by_about:
86
+ continue
87
+
88
+ rdf = rdf_by_about.get(canonical_path)
89
+ if rdf is None:
90
+ about, rdf = list(rdf_by_about.items())[0]
91
+ if len(rdf_by_about) > 1:
92
+ LOG.warning(
93
+ f"Found {len(rdf_by_about)} RDFs in the XML source {resolved_source_path}. Using the first RDF (with rdf:about={about}) for {path}"
94
+ )
95
+ rdf_by_path[canonical_path] = rdf
96
+
97
+ return rdf_by_path
98
+
99
+ raise AssertionError("Either source_path or pattern must be provided")
62
100
 
63
101
 
64
102
  class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
@@ -66,7 +104,10 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
66
104
  def _generate_video_extractors(
67
105
  self, video_paths: T.Sequence[Path]
68
106
  ) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
69
- runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
107
+ if constants.EXIFTOOL_PATH is None:
108
+ runner = ExiftoolRunner()
109
+ else:
110
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
70
111
 
71
112
  LOG.debug(
72
113
  "Extracting XML from %d videos with ExifTool command: %s",
@@ -76,7 +117,13 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
76
117
  try:
77
118
  xml = runner.extract_xml(video_paths)
78
119
  except FileNotFoundError as ex:
79
- raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
120
+ exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
121
+ return [
122
+ types.describe_error_metadata(
123
+ exiftool_ex, video_path, filetype=types.FileType.VIDEO
124
+ )
125
+ for video_path in video_paths
126
+ ]
80
127
 
81
128
  try:
82
129
  xml_element = ET.fromstring(xml)
@@ -84,7 +131,7 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
84
131
  LOG.warning(
85
132
  "Failed to parse ExifTool XML: %s",
86
133
  str(ex),
87
- exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
134
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
88
135
  )
89
136
  rdf_by_path = {}
90
137
  else:
@@ -92,6 +139,6 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
92
139
  xml_element
93
140
  )
94
141
 
95
- return GeotagVideosFromExifToolXML.build_image_extractors(
142
+ return GeotagVideosFromExifToolXML.build_video_extractors_from_etree(
96
143
  rdf_by_path, video_paths
97
144
  )
@@ -10,6 +10,7 @@ if sys.version_info >= (3, 12):
10
10
  else:
11
11
  from typing_extensions import override
12
12
 
13
+ from .. import exceptions, types
13
14
  from . import options
14
15
  from .base import GeotagVideosFromGeneric
15
16
  from .video_extractors.gpx import GPXVideoExtractor
@@ -21,19 +22,31 @@ LOG = logging.getLogger(__name__)
21
22
  class GeotagVideosFromGPX(GeotagVideosFromGeneric):
22
23
  def __init__(
23
24
  self,
24
- option: options.SourcePathOption | None = None,
25
+ source_path: options.SourcePathOption | None = None,
25
26
  num_processes: int | None = None,
26
27
  ):
27
28
  super().__init__(num_processes=num_processes)
28
- if option is None:
29
- option = options.SourcePathOption(pattern="%f.gpx")
30
- self.option = option
29
+ if source_path is None:
30
+ source_path = options.SourcePathOption(pattern="%g.gpx")
31
+ self.source_path = source_path
31
32
 
32
33
  @override
33
34
  def _generate_video_extractors(
34
35
  self, video_paths: T.Sequence[Path]
35
- ) -> T.Sequence[GPXVideoExtractor]:
36
- return [
37
- GPXVideoExtractor(video_path, self.option.resolve(video_path))
38
- for video_path in video_paths
39
- ]
36
+ ) -> T.Sequence[GPXVideoExtractor | types.ErrorMetadata]:
37
+ results: list[GPXVideoExtractor | types.ErrorMetadata] = []
38
+ for video_path in video_paths:
39
+ source_path = self.source_path.resolve(video_path)
40
+ if source_path.is_file():
41
+ results.append(GPXVideoExtractor(video_path, source_path))
42
+ else:
43
+ results.append(
44
+ types.describe_error_metadata(
45
+ exceptions.MapillaryVideoGPSNotFoundError(
46
+ "GPX file not found for video"
47
+ ),
48
+ filename=video_path,
49
+ filetype=types.FileType.VIDEO,
50
+ )
51
+ )
52
+ return results
@@ -60,9 +60,13 @@ class SourceOption:
60
60
  elif k == "filetypes":
61
61
  kwargs[k] = {types.FileType(t) for t in v}
62
62
  elif k == "source_path":
63
- kwargs.setdefault("source_path", SourcePathOption()).source_path = v
63
+ kwargs.setdefault(
64
+ "source_path", SourcePathOption(source_path=Path(v))
65
+ ).sourthe_path = Path(v)
64
66
  elif k == "pattern":
65
- kwargs.setdefault("source_path", SourcePathOption()).pattern = v
67
+ kwargs.setdefault(
68
+ "source_path", SourcePathOption(pattern=v)
69
+ ).pattern = v
66
70
  elif k == "interpolation_offset_time":
67
71
  kwargs.setdefault(
68
72
  "interpolation", InterpolationOption()
@@ -85,6 +89,24 @@ class SourcePathOption:
85
89
  raise ValueError("Either pattern or source_path must be provided")
86
90
 
87
91
  def resolve(self, path: Path) -> Path:
92
+ """
93
+ Resolve the source path or pattern against the given path.
94
+
95
+ Examples:
96
+ >>> from pathlib import Path
97
+ >>> opt = SourcePathOption(source_path=Path("/foo/bar.mp4"))
98
+ >>> opt.resolve(Path("/baz/qux.mp4"))
99
+ PosixPath('/foo/bar.mp4')
100
+
101
+ >>> opt = SourcePathOption(pattern="videos/%g_sub%e")
102
+ >>> opt.resolve(Path("/data/video1.mp4"))
103
+ PosixPath('/data/videos/video1_sub.mp4')
104
+
105
+ >>> opt = SourcePathOption(pattern="/abs/path/%f")
106
+ >>> opt.resolve(Path("/tmp/abc.mov"))
107
+ PosixPath('/abs/path/abc.mov')
108
+ """
109
+
88
110
  if self.source_path is not None:
89
111
  return self.source_path
90
112
 
@@ -140,7 +162,7 @@ SourceOptionSchema = {
140
162
  "type": "integer",
141
163
  },
142
164
  "interpolation_offset_time": {
143
- "type": "float",
165
+ "type": "number",
144
166
  },
145
167
  "interpolation_use_gpx_start_time": {
146
168
  "type": "boolean",
@@ -37,26 +37,23 @@ def parse_gpx(gpx_file: Path) -> list[Track]:
37
37
  return tracks
38
38
 
39
39
 
40
- def index_rdf_description_by_path(
41
- xml_paths: T.Sequence[Path],
42
- ) -> dict[str, ET.Element]:
43
- rdf_description_by_path: dict[str, ET.Element] = {}
40
+ def index_rdf_description_by_path(xml_paths: T.Sequence[Path]) -> dict[str, ET.Element]:
41
+ rdf_by_path: dict[str, ET.Element] = {}
44
42
 
45
43
  for xml_path in utils.find_xml_files(xml_paths):
46
44
  try:
47
45
  etree = ET.parse(xml_path)
48
- except ET.ParseError as ex:
49
- verbose = LOG.getEffectiveLevel() <= logging.DEBUG
50
- if verbose:
51
- LOG.warning("Failed to parse %s", xml_path, exc_info=True)
52
- else:
53
- LOG.warning("Failed to parse %s: %s", xml_path, ex)
46
+ except Exception as ex:
47
+ LOG.warning(
48
+ f"Failed to parse {xml_path}: {ex}",
49
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
50
+ )
54
51
  continue
55
52
 
56
- rdf_description_by_path.update(
53
+ rdf_by_path.update(
57
54
  exiftool_read.index_rdf_description_by_path_from_xml_element(
58
55
  etree.getroot()
59
56
  )
60
57
  )
61
58
 
62
- return rdf_description_by_path
59
+ return rdf_by_path
@@ -14,5 +14,5 @@ class BaseVideoExtractor(abc.ABC):
14
14
  def __init__(self, video_path: Path):
15
15
  self.video_path = video_path
16
16
 
17
- def extract(self) -> types.VideoMetadataOrError:
17
+ def extract(self) -> types.VideoMetadata:
18
18
  raise NotImplementedError
@@ -21,7 +21,7 @@ class VideoExifToolExtractor(BaseVideoExtractor):
21
21
  self.element = element
22
22
 
23
23
  @override
24
- def extract(self) -> types.VideoMetadataOrError:
24
+ def extract(self) -> types.VideoMetadata:
25
25
  exif = exiftool_read_video.ExifToolReadVideo(ET.ElementTree(self.element))
26
26
 
27
27
  make = exif.extract_make()
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
- import datetime
4
+ import enum
5
5
  import logging
6
6
  import sys
7
7
  import typing as T
@@ -12,7 +12,7 @@ if sys.version_info >= (3, 12):
12
12
  else:
13
13
  from typing_extensions import override
14
14
 
15
- from ... import geo, telemetry, types
15
+ from ... import exceptions, geo, telemetry, types, utils
16
16
  from ..utils import parse_gpx
17
17
  from .base import BaseVideoExtractor
18
18
  from .native import NativeVideoExtractor
@@ -21,106 +21,97 @@ from .native import NativeVideoExtractor
21
21
  LOG = logging.getLogger(__name__)
22
22
 
23
23
 
24
+ class SyncMode(enum.Enum):
25
+ # Sync by video GPS timestamps if found, otherwise rebase
26
+ SYNC = "sync"
27
+ # Sync by video GPS timestamps, and throw if not found
28
+ STRICT_SYNC = "strict_sync"
29
+ # Rebase all GPX timestamps to start from 0
30
+ REBASE = "rebase"
31
+
32
+
24
33
  class GPXVideoExtractor(BaseVideoExtractor):
25
- def __init__(self, video_path: Path, gpx_path: Path):
34
+ def __init__(
35
+ self, video_path: Path, gpx_path: Path, sync_mode: SyncMode = SyncMode.SYNC
36
+ ):
26
37
  self.video_path = video_path
27
38
  self.gpx_path = gpx_path
39
+ self.sync_mode = sync_mode
28
40
 
29
41
  @override
30
- def extract(self) -> types.VideoMetadataOrError:
31
- try:
32
- gpx_tracks = parse_gpx(self.gpx_path)
33
- except Exception as ex:
34
- raise RuntimeError(
35
- f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
36
- )
42
+ def extract(self) -> types.VideoMetadata:
43
+ gpx_tracks = parse_gpx(self.gpx_path)
37
44
 
38
45
  if 1 < len(gpx_tracks):
39
46
  LOG.warning(
40
- "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
41
- len(gpx_tracks),
42
- self.gpx_path,
47
+ f"Found {len(gpx_tracks)} tracks in the GPX file {self.gpx_path}. Will merge points in all the tracks as a single track for interpolation"
43
48
  )
44
49
 
45
50
  gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
46
51
 
47
52
  native_extractor = NativeVideoExtractor(self.video_path)
48
53
 
49
- video_metadata_or_error = native_extractor.extract()
50
-
51
- if isinstance(video_metadata_or_error, types.ErrorMetadata):
54
+ try:
55
+ native_video_metadata = native_extractor.extract()
56
+ except exceptions.MapillaryVideoGPSNotFoundError as ex:
57
+ if self.sync_mode is SyncMode.STRICT_SYNC:
58
+ raise ex
52
59
  self._rebase_times(gpx_points)
53
60
  return types.VideoMetadata(
54
- filename=video_metadata_or_error.filename,
55
- filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
61
+ filename=self.video_path,
62
+ filesize=utils.get_file_size(self.video_path),
63
+ filetype=types.FileType.VIDEO,
56
64
  points=gpx_points,
57
65
  )
58
66
 
59
- video_metadata = video_metadata_or_error
60
-
61
- offset = self._synx_gpx_by_first_gps_timestamp(
62
- gpx_points, video_metadata.points
63
- )
64
-
65
- self._rebase_times(gpx_points, offset=offset)
67
+ if self.sync_mode is SyncMode.REBASE:
68
+ self._rebase_times(gpx_points)
69
+ else:
70
+ offset = self._gpx_offset(gpx_points, native_video_metadata.points)
71
+ self._rebase_times(gpx_points, offset=offset)
66
72
 
67
- return dataclasses.replace(video_metadata_or_error, points=gpx_points)
73
+ return dataclasses.replace(native_video_metadata, points=gpx_points)
68
74
 
69
- @staticmethod
70
- def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
75
+ @classmethod
76
+ def _rebase_times(cls, points: T.Sequence[geo.Point], offset: float = 0.0) -> None:
71
77
  """
72
- Make point times start from 0
78
+ Rebase point times to start from **offset**
73
79
  """
74
80
  if points:
75
81
  first_timestamp = points[0].time
76
82
  for p in points:
77
83
  p.time = (p.time - first_timestamp) + offset
78
- return points
79
84
 
80
- def _synx_gpx_by_first_gps_timestamp(
81
- self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
85
+ @classmethod
86
+ def _gpx_offset(
87
+ cls, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
82
88
  ) -> float:
89
+ """
90
+ Calculate the offset that needs to be applied to the GPX points to sync with the video GPS points.
91
+
92
+ >>> gpx_points = [geo.Point(time=5, lat=1, lon=1, alt=None, angle=None)]
93
+ >>> GPXVideoExtractor._gpx_offset(gpx_points, gpx_points)
94
+ 0.0
95
+ >>> GPXVideoExtractor._gpx_offset(gpx_points, [])
96
+ 0.0
97
+ >>> GPXVideoExtractor._gpx_offset([], gpx_points)
98
+ 0.0
99
+ """
83
100
  offset: float = 0.0
84
101
 
85
- if not gpx_points:
86
- return offset
87
-
88
- first_gpx_dt = datetime.datetime.fromtimestamp(
89
- gpx_points[0].time, tz=datetime.timezone.utc
90
- )
91
- LOG.info("First GPX timestamp: %s", first_gpx_dt)
92
-
93
- if not video_gps_points:
94
- LOG.warning(
95
- "Skip GPX synchronization because no GPS found in video %s",
96
- self.video_path,
97
- )
102
+ if not gpx_points or not video_gps_points:
98
103
  return offset
99
104
 
100
- first_gps_point = video_gps_points[0]
101
- if isinstance(first_gps_point, telemetry.GPSPoint):
102
- if first_gps_point.epoch_time is not None:
103
- first_gps_dt = datetime.datetime.fromtimestamp(
104
- first_gps_point.epoch_time, tz=datetime.timezone.utc
105
- )
106
- LOG.info("First GPS timestamp: %s", first_gps_dt)
107
- offset = gpx_points[0].time - first_gps_point.epoch_time
108
- if offset:
109
- LOG.warning(
110
- "Found offset between GPX %s and video GPS timestamps %s: %s seconds",
111
- first_gpx_dt,
112
- first_gps_dt,
113
- offset,
114
- )
115
- else:
116
- LOG.info(
117
- "GPX and GPS are perfectly synchronized (all starts from %s)",
118
- first_gpx_dt,
119
- )
120
- else:
121
- LOG.warning(
122
- "Skip GPX synchronization because no GPS epoch time found in video %s",
123
- self.video_path,
124
- )
105
+ gps_epoch_time: float | None = None
106
+ gps_point = video_gps_points[0]
107
+ if isinstance(gps_point, telemetry.GPSPoint):
108
+ if gps_point.epoch_time is not None:
109
+ gps_epoch_time = gps_point.epoch_time
110
+ elif isinstance(gps_point, telemetry.CAMMGPSPoint):
111
+ if gps_point.time_gps_epoch is not None:
112
+ gps_epoch_time = gps_point.time_gps_epoch
113
+
114
+ if gps_epoch_time is not None:
115
+ offset = gpx_points[0].time - gps_epoch_time
125
116
 
126
117
  return offset
@@ -12,12 +12,13 @@ else:
12
12
  from ... import blackvue_parser, exceptions, geo, telemetry, types, utils
13
13
  from ...camm import camm_parser
14
14
  from ...gpmf import gpmf_gps_filter, gpmf_parser
15
+ from ...mp4 import construct_mp4_parser, simple_mp4_parser
15
16
  from .base import BaseVideoExtractor
16
17
 
17
18
 
18
19
  class GoProVideoExtractor(BaseVideoExtractor):
19
20
  @override
20
- def extract(self) -> types.VideoMetadataOrError:
21
+ def extract(self) -> types.VideoMetadata:
21
22
  with self.video_path.open("rb") as fp:
22
23
  gopro_info = gpmf_parser.extract_gopro_info(fp)
23
24
 
@@ -29,23 +30,13 @@ class GoProVideoExtractor(BaseVideoExtractor):
29
30
  gps_points = gopro_info.gps
30
31
  assert gps_points is not None, "must have GPS data extracted"
31
32
  if not gps_points:
32
- # Instead of raising an exception, return error metadata to tell the file type
33
- ex: exceptions.MapillaryDescriptionError = (
34
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
35
- )
36
- return types.describe_error_metadata(
37
- ex, self.video_path, filetype=types.FileType.GOPRO
38
- )
33
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
39
34
 
40
35
  gps_points = T.cast(
41
36
  T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
42
37
  )
43
38
  if not gps_points:
44
- # Instead of raising an exception, return error metadata to tell the file type
45
- ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
46
- return types.describe_error_metadata(
47
- ex, self.video_path, filetype=types.FileType.GOPRO
48
- )
39
+ raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
49
40
 
50
41
  video_metadata = types.VideoMetadata(
51
42
  filename=self.video_path,
@@ -61,7 +52,7 @@ class GoProVideoExtractor(BaseVideoExtractor):
61
52
 
62
53
  class CAMMVideoExtractor(BaseVideoExtractor):
63
54
  @override
64
- def extract(self) -> types.VideoMetadataOrError:
55
+ def extract(self) -> types.VideoMetadata:
65
56
  with self.video_path.open("rb") as fp:
66
57
  camm_info = camm_parser.extract_camm_info(fp)
67
58
 
@@ -71,13 +62,7 @@ class CAMMVideoExtractor(BaseVideoExtractor):
71
62
  )
72
63
 
73
64
  if not camm_info.gps and not camm_info.mini_gps:
74
- # Instead of raising an exception, return error metadata to tell the file type
75
- ex: exceptions.MapillaryDescriptionError = (
76
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
77
- )
78
- return types.describe_error_metadata(
79
- ex, self.video_path, filetype=types.FileType.CAMM
80
- )
65
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
81
66
 
82
67
  return types.VideoMetadata(
83
68
  filename=self.video_path,
@@ -91,7 +76,7 @@ class CAMMVideoExtractor(BaseVideoExtractor):
91
76
 
92
77
  class BlackVueVideoExtractor(BaseVideoExtractor):
93
78
  @override
94
- def extract(self) -> types.VideoMetadataOrError:
79
+ def extract(self) -> types.VideoMetadata:
95
80
  with self.video_path.open("rb") as fp:
96
81
  blackvue_info = blackvue_parser.extract_blackvue_info(fp)
97
82
 
@@ -101,19 +86,13 @@ class BlackVueVideoExtractor(BaseVideoExtractor):
101
86
  )
102
87
 
103
88
  if not blackvue_info.gps:
104
- # Instead of raising an exception, return error metadata to tell the file type
105
- ex: exceptions.MapillaryDescriptionError = (
106
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
107
- )
108
- return types.describe_error_metadata(
109
- ex, self.video_path, filetype=types.FileType.BLACKVUE
110
- )
89
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
111
90
 
112
91
  video_metadata = types.VideoMetadata(
113
92
  filename=self.video_path,
114
93
  filesize=utils.get_file_size(self.video_path),
115
94
  filetype=types.FileType.BLACKVUE,
116
- points=blackvue_info.gps or [],
95
+ points=blackvue_info.gps,
117
96
  make=blackvue_info.make,
118
97
  model=blackvue_info.model,
119
98
  )
@@ -127,7 +106,7 @@ class NativeVideoExtractor(BaseVideoExtractor):
127
106
  self.filetypes = filetypes
128
107
 
129
108
  @override
130
- def extract(self) -> types.VideoMetadataOrError:
109
+ def extract(self) -> types.VideoMetadata:
131
110
  ft = self.filetypes
132
111
  extractor: BaseVideoExtractor
133
112
 
@@ -135,6 +114,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
135
114
  extractor = GoProVideoExtractor(self.video_path)
136
115
  try:
137
116
  return extractor.extract()
117
+ except simple_mp4_parser.BoxNotFoundError as ex:
118
+ raise exceptions.MapillaryInvalidVideoError(
119
+ f"Invalid video: {ex}"
120
+ ) from ex
121
+ except construct_mp4_parser.BoxNotFoundError as ex:
122
+ raise exceptions.MapillaryInvalidVideoError(
123
+ f"Invalid video: {ex}"
124
+ ) from ex
138
125
  except exceptions.MapillaryVideoGPSNotFoundError:
139
126
  pass
140
127
 
@@ -142,6 +129,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
142
129
  extractor = CAMMVideoExtractor(self.video_path)
143
130
  try:
144
131
  return extractor.extract()
132
+ except simple_mp4_parser.BoxNotFoundError as ex:
133
+ raise exceptions.MapillaryInvalidVideoError(
134
+ f"Invalid video: {ex}"
135
+ ) from ex
136
+ except construct_mp4_parser.BoxNotFoundError as ex:
137
+ raise exceptions.MapillaryInvalidVideoError(
138
+ f"Invalid video: {ex}"
139
+ ) from ex
145
140
  except exceptions.MapillaryVideoGPSNotFoundError:
146
141
  pass
147
142
 
@@ -149,6 +144,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
149
144
  extractor = BlackVueVideoExtractor(self.video_path)
150
145
  try:
151
146
  return extractor.extract()
147
+ except simple_mp4_parser.BoxNotFoundError as ex:
148
+ raise exceptions.MapillaryInvalidVideoError(
149
+ f"Invalid video: {ex}"
150
+ ) from ex
151
+ except construct_mp4_parser.BoxNotFoundError as ex:
152
+ raise exceptions.MapillaryInvalidVideoError(
153
+ f"Invalid video: {ex}"
154
+ ) from ex
152
155
  except exceptions.MapillaryVideoGPSNotFoundError:
153
156
  pass
154
157