mapillary-tools 0.12.1__tar.gz → 0.13.1a1__tar.gz

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 (92) hide show
  1. {mapillary_tools-0.12.1/mapillary_tools.egg-info → mapillary_tools-0.13.1a1}/PKG-INFO +10 -3
  2. mapillary_tools-0.13.1a1/mapillary_tools/__init__.py +1 -0
  3. mapillary_tools-0.13.1a1/mapillary_tools/api_v4.py +151 -0
  4. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/camm}/camm_builder.py +122 -62
  5. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/camm}/camm_parser.py +120 -84
  6. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/__init__.py +0 -1
  7. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/__main__.py +0 -6
  8. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/process.py +0 -50
  9. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/upload.py +1 -26
  10. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/constants.py +2 -2
  11. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/exiftool_read_video.py +13 -11
  12. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/ffmpeg.py +2 -2
  13. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geo.py +0 -54
  14. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/blackvue_parser.py +4 -4
  15. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
  16. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
  17. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
  18. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
  19. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
  20. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/gpmf_parser.py +346 -83
  21. mapillary_tools-0.13.1a1/mapillary_tools/mp4/__init__.py +0 -0
  22. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/mp4}/construct_mp4_parser.py +32 -16
  23. mapillary_tools-0.13.1a1/mapillary_tools/mp4/mp4_sample_parser.py +322 -0
  24. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/mp4}/simple_mp4_builder.py +64 -38
  25. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/process_geotag_properties.py +25 -19
  26. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/process_sequence_properties.py +6 -6
  27. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/sample_video.py +17 -16
  28. mapillary_tools-0.13.1a1/mapillary_tools/telemetry.py +59 -0
  29. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/types.py +18 -0
  30. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/upload.py +75 -233
  31. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/upload_api_v4.py +8 -9
  32. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/utils.py +9 -16
  33. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/cli_options.py +0 -1
  34. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
  35. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
  36. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
  37. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/camm_parser.py +7 -6
  38. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
  39. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
  40. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
  41. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
  42. mapillary_tools-0.13.1a1/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +71 -0
  43. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
  44. mapillary_tools-0.13.1a1/mapillary_tools/video_data_extraction/video_data_parser_factory.py +39 -0
  45. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1/mapillary_tools.egg-info}/PKG-INFO +10 -3
  46. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools.egg-info/SOURCES.txt +9 -11
  47. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools.egg-info/requires.txt +0 -3
  48. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/requirements.txt +0 -1
  49. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/schema/image_description_schema.json +14 -0
  50. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/setup.py +2 -0
  51. mapillary_tools-0.12.1/mapillary_tools/__init__.py +0 -1
  52. mapillary_tools-0.12.1/mapillary_tools/api_v4.py +0 -61
  53. mapillary_tools-0.12.1/mapillary_tools/commands/upload_blackvue.py +0 -33
  54. mapillary_tools-0.12.1/mapillary_tools/commands/upload_camm.py +0 -33
  55. mapillary_tools-0.12.1/mapillary_tools/commands/upload_zip.py +0 -33
  56. mapillary_tools-0.12.1/mapillary_tools/geotag/mp4_sample_parser.py +0 -426
  57. mapillary_tools-0.12.1/mapillary_tools/process_import_meta_properties.py +0 -76
  58. mapillary_tools-0.12.1/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -29
  59. mapillary_tools-0.12.1/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -46
  60. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/LICENSE +0 -0
  61. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/MANIFEST.in +0 -0
  62. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/README.md +0 -0
  63. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/authenticate.py +0 -0
  64. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/authenticate.py +0 -0
  65. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/process_and_upload.py +0 -0
  66. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/sample_video.py +0 -0
  67. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/video_process.py +0 -0
  68. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/video_process_and_upload.py +0 -0
  69. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/commands/zip.py +0 -0
  70. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/config.py +0 -0
  71. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/exceptions.py +0 -0
  72. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/exif_read.py +0 -0
  73. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/exif_write.py +0 -0
  74. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/exiftool_read.py +0 -0
  75. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/__init__.py +0 -0
  76. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_from_generic.py +0 -0
  77. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
  78. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
  79. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
  80. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
  81. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
  82. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/gps_filter.py +0 -0
  83. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/geotag/utils.py +0 -0
  84. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/history.py +0 -0
  85. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/ipc.py +0 -0
  86. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/mp4}/io_utils.py +0 -0
  87. {mapillary_tools-0.12.1/mapillary_tools/geotag → mapillary_tools-0.13.1a1/mapillary_tools/mp4}/simple_mp4_parser.py +0 -0
  88. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools/uploader.py +0 -0
  89. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools.egg-info/dependency_links.txt +0 -0
  90. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools.egg-info/entry_points.txt +0 -0
  91. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/mapillary_tools.egg-info/top_level.txt +0 -0
  92. {mapillary_tools-0.12.1 → mapillary_tools-0.13.1a1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: mapillary_tools
3
- Version: 0.12.1
3
+ Version: 0.13.1a1
4
4
  Summary: Mapillary Image/Video Import Pipeline
5
5
  Home-page: https://github.com/mapillary/mapillary_tools
6
6
  Author: Mapillary
@@ -18,7 +18,14 @@ Requires-Dist: requests[socks]<3.0.0,>=2.20.0
18
18
  Requires-Dist: tqdm<5.0,>=4.0
19
19
  Requires-Dist: typing_extensions
20
20
  Requires-Dist: jsonschema~=4.17.3
21
- Requires-Dist: dataclasses; python_version <= "3.6"
21
+ Dynamic: author
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
22
29
 
23
30
  <p align="center">
24
31
  <a href="https://github.com/mapillary/mapillary_tools/">
@@ -0,0 +1 @@
1
+ VERSION = "0.13.1a1"
@@ -0,0 +1,151 @@
1
+ import logging
2
+ import os
3
+ import ssl
4
+ import typing as T
5
+
6
+ import requests
7
+ from requests.adapters import HTTPAdapter
8
+
9
+ LOG = logging.getLogger(__name__)
10
+ MAPILLARY_CLIENT_TOKEN = os.getenv(
11
+ "MAPILLARY_CLIENT_TOKEN", "MLY|5675152195860640|6b02c72e6e3c801e5603ab0495623282"
12
+ )
13
+ MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
14
+ "MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
15
+ )
16
+ REQUESTS_TIMEOUT = 60 # 1 minutes
17
+ USE_SYSTEM_CERTS: bool = False
18
+
19
+
20
+ class HTTPSystemCertsAdapter(HTTPAdapter):
21
+ """
22
+ This adapter uses the system's certificate store instead of the certifi module.
23
+
24
+ The implementation is based on the project https://pypi.org/project/pip-system-certs/,
25
+ which has a system-wide effect.
26
+ """
27
+
28
+ def init_poolmanager(self, *args, **kwargs):
29
+ ssl_context = ssl.create_default_context()
30
+ ssl_context.load_default_certs()
31
+ kwargs["ssl_context"] = ssl_context
32
+
33
+ super().init_poolmanager(*args, **kwargs)
34
+
35
+ def cert_verify(self, *args, **kwargs):
36
+ super().cert_verify(*args, **kwargs)
37
+
38
+ # By default Python requests uses the ca_certs from the certifi module
39
+ # But we want to use the certificate store instead.
40
+ # By clearing the ca_certs variable we force it to fall back on that behaviour (handled in urllib3)
41
+ if "conn" in kwargs:
42
+ conn = kwargs["conn"]
43
+ else:
44
+ conn = args[0]
45
+
46
+ conn.ca_certs = None
47
+
48
+
49
+ def request_post(
50
+ url: str,
51
+ data: T.Optional[T.Any] = None,
52
+ json: T.Optional[dict] = None,
53
+ **kwargs,
54
+ ) -> requests.Response:
55
+ global USE_SYSTEM_CERTS
56
+
57
+ if USE_SYSTEM_CERTS:
58
+ with requests.Session() as session:
59
+ session.mount("https://", HTTPSystemCertsAdapter())
60
+ return session.post(url, data=data, json=json, **kwargs)
61
+
62
+ else:
63
+ try:
64
+ return requests.post(url, data=data, json=json, **kwargs)
65
+ except requests.exceptions.SSLError as ex:
66
+ if "SSLCertVerificationError" not in str(ex):
67
+ raise ex
68
+ USE_SYSTEM_CERTS = True
69
+ # HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
70
+ LOG.warning(
71
+ "SSL error occurred, falling back to system SSL certificates: %s", ex
72
+ )
73
+ with requests.Session() as session:
74
+ session.mount("https://", HTTPSystemCertsAdapter())
75
+ return session.post(url, data=data, json=json, **kwargs)
76
+
77
+
78
+ def request_get(
79
+ url: str,
80
+ params: T.Optional[dict] = None,
81
+ **kwargs,
82
+ ) -> requests.Response:
83
+ global USE_SYSTEM_CERTS
84
+
85
+ if USE_SYSTEM_CERTS:
86
+ with requests.Session() as session:
87
+ session.mount("https://", HTTPSystemCertsAdapter())
88
+ return session.get(url, params=params, **kwargs)
89
+ else:
90
+ try:
91
+ return requests.get(url, params=params, **kwargs)
92
+ except requests.exceptions.SSLError as ex:
93
+ if "SSLCertVerificationError" not in str(ex):
94
+ raise ex
95
+ USE_SYSTEM_CERTS = True
96
+ # HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
97
+ LOG.warning(
98
+ "SSL error occurred, falling back to system SSL certificates: %s", ex
99
+ )
100
+ with requests.Session() as session:
101
+ session.mount("https://", HTTPSystemCertsAdapter())
102
+ return session.get(url, params=params, **kwargs)
103
+
104
+
105
+ def get_upload_token(email: str, password: str) -> requests.Response:
106
+ resp = request_post(
107
+ f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
108
+ params={"access_token": MAPILLARY_CLIENT_TOKEN},
109
+ json={"email": email, "password": password, "locale": "en_US"},
110
+ timeout=REQUESTS_TIMEOUT,
111
+ )
112
+ resp.raise_for_status()
113
+ return resp
114
+
115
+
116
+ def fetch_organization(
117
+ user_access_token: str, organization_id: T.Union[int, str]
118
+ ) -> requests.Response:
119
+ resp = request_get(
120
+ f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
121
+ params={
122
+ "fields": ",".join(["slug", "description", "name"]),
123
+ },
124
+ headers={
125
+ "Authorization": f"OAuth {user_access_token}",
126
+ },
127
+ timeout=REQUESTS_TIMEOUT,
128
+ )
129
+ resp.raise_for_status()
130
+ return resp
131
+
132
+
133
+ ActionType = T.Literal[
134
+ "upload_started_upload", "upload_finished_upload", "upload_failed_upload"
135
+ ]
136
+
137
+
138
+ def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
139
+ resp = request_post(
140
+ f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
141
+ json={
142
+ "action_type": action_type,
143
+ "properties": properties,
144
+ },
145
+ headers={
146
+ "Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
147
+ },
148
+ timeout=REQUESTS_TIMEOUT,
149
+ )
150
+ resp.raise_for_status()
151
+ return resp
@@ -1,61 +1,98 @@
1
1
  import io
2
2
  import typing as T
3
3
 
4
- from .. import geo, types
5
-
6
- from . import (
7
- camm_parser,
4
+ from .. import geo, telemetry, types
5
+ from ..mp4 import (
8
6
  construct_mp4_parser as cparser,
9
7
  mp4_sample_parser as sample_parser,
10
8
  simple_mp4_builder as builder,
11
9
  )
12
- from .simple_mp4_builder import BoxDict
13
10
 
11
+ from . import camm_parser
12
+
13
+
14
+ TelemetryMeasurement = T.Union[
15
+ geo.Point,
16
+ telemetry.TelemetryMeasurement,
17
+ ]
14
18
 
15
- def build_camm_sample(point: geo.Point) -> bytes:
16
- return camm_parser.CAMMSampleData.build(
17
- {
18
- "type": camm_parser.CAMMType.MIN_GPS.value,
19
- "data": [
20
- point.lat,
21
- point.lon,
22
- -1.0 if point.alt is None else point.alt,
23
- ],
24
- }
25
- )
26
19
 
20
+ def _build_camm_sample(measurement: TelemetryMeasurement) -> bytes:
21
+ if isinstance(measurement, geo.Point):
22
+ return camm_parser.CAMMSampleData.build(
23
+ {
24
+ "type": camm_parser.CAMMType.MIN_GPS.value,
25
+ "data": [
26
+ measurement.lat,
27
+ measurement.lon,
28
+ -1.0 if measurement.alt is None else measurement.alt,
29
+ ],
30
+ }
31
+ )
32
+ elif isinstance(measurement, telemetry.AccelerationData):
33
+ # Accelerometer reading in meters/second^2 along XYZ axes of the camera.
34
+ return camm_parser.CAMMSampleData.build(
35
+ {
36
+ "type": camm_parser.CAMMType.ACCELERATION.value,
37
+ "data": [
38
+ measurement.x,
39
+ measurement.y,
40
+ measurement.z,
41
+ ],
42
+ }
43
+ )
44
+ elif isinstance(measurement, telemetry.GyroscopeData):
45
+ # Gyroscope signal in radians/seconds around XYZ axes of the camera. Rotation is positive in the counterclockwise direction.
46
+ return camm_parser.CAMMSampleData.build(
47
+ {
48
+ "type": camm_parser.CAMMType.GYRO.value,
49
+ "data": [
50
+ measurement.x,
51
+ measurement.y,
52
+ measurement.z,
53
+ ],
54
+ }
55
+ )
56
+ elif isinstance(measurement, telemetry.MagnetometerData):
57
+ # Ambient magnetic field.
58
+ return camm_parser.CAMMSampleData.build(
59
+ {
60
+ "type": camm_parser.CAMMType.MAGNETIC_FIELD.value,
61
+ "data": [
62
+ measurement.x,
63
+ measurement.y,
64
+ measurement.z,
65
+ ],
66
+ }
67
+ )
68
+ else:
69
+ raise ValueError(f"unexpected measurement type {type(measurement)}")
27
70
 
28
- def _create_edit_list(
71
+
72
+ def _create_edit_list_from_points(
29
73
  point_segments: T.Sequence[T.Sequence[geo.Point]],
30
74
  movie_timescale: int,
31
75
  media_timescale: int,
32
- ) -> BoxDict:
76
+ ) -> builder.BoxDict:
33
77
  entries: T.List[T.Dict] = []
34
78
 
35
- for idx, points in enumerate(point_segments):
36
- if not points:
37
- entries = [
38
- {
39
- "media_time": 0,
40
- "segment_duration": 0,
41
- "media_rate_integer": 1,
42
- "media_rate_fraction": 0,
43
- }
44
- ]
45
- break
79
+ non_empty_point_segments = [points for points in point_segments if points]
46
80
 
47
- assert (
48
- 0 <= points[0].time
49
- ), f"expect non-negative point time but got {points[0]}"
50
- assert (
51
- points[0].time <= points[-1].time
52
- ), f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
81
+ for idx, points in enumerate(non_empty_point_segments):
82
+ assert 0 <= points[0].time, (
83
+ f"expect non-negative point time but got {points[0]}"
84
+ )
85
+ assert points[0].time <= points[-1].time, (
86
+ f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
87
+ )
53
88
 
54
89
  if idx == 0:
55
90
  if 0 < points[0].time:
56
91
  segment_duration = int(points[0].time * movie_timescale)
92
+ # put an empty edit list entry to skip the initial gap
57
93
  entries.append(
58
94
  {
95
+ # If this field is set to –1, it is an empty edit
59
96
  "media_time": -1,
60
97
  "segment_duration": segment_duration,
61
98
  "media_rate_integer": 1,
@@ -63,7 +100,6 @@ def _create_edit_list(
63
100
  }
64
101
  )
65
102
  else:
66
- assert point_segments[-1][-1].time <= points[0].time
67
103
  media_time = int(points[0].time * media_timescale)
68
104
  segment_duration = int((points[-1].time - points[0].time) * movie_timescale)
69
105
  entries.append(
@@ -83,19 +119,31 @@ def _create_edit_list(
83
119
  }
84
120
 
85
121
 
86
- def convert_points_to_raw_samples(
87
- points: T.Sequence[geo.Point], timescale: int
122
+ def _multiplex(
123
+ points: T.Sequence[geo.Point],
124
+ measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
125
+ ) -> T.List[TelemetryMeasurement]:
126
+ mutiplexed: T.List[TelemetryMeasurement] = [*points, *(measurements or [])]
127
+ mutiplexed.sort(key=lambda m: m.time)
128
+
129
+ return mutiplexed
130
+
131
+
132
+ def convert_telemetry_to_raw_samples(
133
+ measurements: T.Sequence[TelemetryMeasurement],
134
+ timescale: int,
88
135
  ) -> T.Generator[sample_parser.RawSample, None, None]:
89
- for idx, point in enumerate(points):
90
- camm_sample_data = build_camm_sample(point)
136
+ for idx, measurement in enumerate(measurements):
137
+ camm_sample_data = _build_camm_sample(measurement)
91
138
 
92
- if idx + 1 < len(points):
93
- timedelta = int((points[idx + 1].time - point.time) * timescale)
139
+ if idx + 1 < len(measurements):
140
+ timedelta = int((measurements[idx + 1].time - measurement.time) * timescale)
94
141
  else:
95
142
  timedelta = 0
96
- assert (
97
- 0 <= timedelta <= builder.UINT32_MAX
98
- ), f"expected timedelta {timedelta} between {points[idx]} and {points[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
143
+
144
+ assert 0 <= timedelta <= builder.UINT32_MAX, (
145
+ f"expected timedelta {timedelta} between {measurements[idx]} and {measurements[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
146
+ )
99
147
 
100
148
  yield sample_parser.RawSample(
101
149
  # will update later
@@ -114,7 +162,9 @@ _STBLChildrenBuilderConstruct = cparser.Box32ConstructBuilder(
114
162
  )
115
163
 
116
164
 
117
- def _create_camm_stbl(raw_samples: T.Iterable[sample_parser.RawSample]) -> BoxDict:
165
+ def _create_camm_stbl(
166
+ raw_samples: T.Iterable[sample_parser.RawSample],
167
+ ) -> builder.BoxDict:
118
168
  descriptions = [
119
169
  {
120
170
  "format": b"camm",
@@ -135,10 +185,10 @@ def _create_camm_stbl(raw_samples: T.Iterable[sample_parser.RawSample]) -> BoxDi
135
185
  def create_camm_trak(
136
186
  raw_samples: T.Sequence[sample_parser.RawSample],
137
187
  media_timescale: int,
138
- ) -> BoxDict:
188
+ ) -> builder.BoxDict:
139
189
  stbl = _create_camm_stbl(raw_samples)
140
190
 
141
- hdlr: BoxDict = {
191
+ hdlr: builder.BoxDict = {
142
192
  "type": b"hdlr",
143
193
  "data": {
144
194
  "handler_type": b"camm",
@@ -150,7 +200,7 @@ def create_camm_trak(
150
200
  assert media_timescale <= builder.UINT64_MAX
151
201
 
152
202
  # Media Header Box
153
- mdhd: BoxDict = {
203
+ mdhd: builder.BoxDict = {
154
204
  "type": b"mdhd",
155
205
  "data": {
156
206
  # use 64-bit version
@@ -166,7 +216,7 @@ def create_camm_trak(
166
216
  },
167
217
  }
168
218
 
169
- dinf: BoxDict = {
219
+ dinf: builder.BoxDict = {
170
220
  "type": b"dinf",
171
221
  "data": [
172
222
  # self reference dref box
@@ -187,7 +237,7 @@ def create_camm_trak(
187
237
  ],
188
238
  }
189
239
 
190
- minf: BoxDict = {
240
+ minf: builder.BoxDict = {
191
241
  "type": b"minf",
192
242
  "data": [
193
243
  dinf,
@@ -195,7 +245,7 @@ def create_camm_trak(
195
245
  ],
196
246
  }
197
247
 
198
- tkhd: BoxDict = {
248
+ tkhd: builder.BoxDict = {
199
249
  "type": b"tkhd",
200
250
  "data": {
201
251
  # use 32-bit version of the box
@@ -213,7 +263,7 @@ def create_camm_trak(
213
263
  },
214
264
  }
215
265
 
216
- mdia: BoxDict = {
266
+ mdia: builder.BoxDict = {
217
267
  "type": b"mdia",
218
268
  "data": [
219
269
  mdhd,
@@ -231,23 +281,31 @@ def create_camm_trak(
231
281
  }
232
282
 
233
283
 
234
- def camm_sample_generator2(video_metadata: types.VideoMetadata):
284
+ def camm_sample_generator2(
285
+ video_metadata: types.VideoMetadata,
286
+ telemetry_measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
287
+ ):
235
288
  def _f(
236
289
  fp: T.BinaryIO,
237
- moov_children: T.List[BoxDict],
290
+ moov_children: T.List[builder.BoxDict],
238
291
  ) -> T.Generator[io.IOBase, None, None]:
239
292
  movie_timescale = builder.find_movie_timescale(moov_children)
240
293
  # make sure the precision of timedeltas not lower than 0.001 (1ms)
241
294
  media_timescale = max(1000, movie_timescale)
295
+
296
+ # points with negative time are skipped
297
+ # TODO: interpolate first point at time == 0
298
+ # TODO: measurements with negative times should be skipped too
299
+ points = [point for point in video_metadata.points if point.time >= 0]
300
+
301
+ measurements = _multiplex(points, telemetry_measurements)
242
302
  camm_samples = list(
243
- convert_points_to_raw_samples(video_metadata.points, media_timescale)
303
+ convert_telemetry_to_raw_samples(measurements, media_timescale)
244
304
  )
245
305
  camm_trak = create_camm_trak(camm_samples, media_timescale)
246
- elst = _create_edit_list(
247
- [video_metadata.points], movie_timescale, media_timescale
248
- )
306
+ elst = _create_edit_list_from_points([points], movie_timescale, media_timescale)
249
307
  if T.cast(T.Dict, elst["data"])["entries"]:
250
- T.cast(T.List[BoxDict], camm_trak["data"]).append(
308
+ T.cast(T.List[builder.BoxDict], camm_trak["data"]).append(
251
309
  {
252
310
  "type": b"edts",
253
311
  "data": [elst],
@@ -255,7 +313,7 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
255
313
  )
256
314
  moov_children.append(camm_trak)
257
315
 
258
- udta_data: T.List[BoxDict] = []
316
+ udta_data: T.List[builder.BoxDict] = []
259
317
  if video_metadata.make:
260
318
  udta_data.append(
261
319
  {
@@ -279,6 +337,8 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
279
337
  )
280
338
 
281
339
  # if yield, the moov_children will not be modified
282
- return (io.BytesIO(build_camm_sample(point)) for point in video_metadata.points)
340
+ return (
341
+ io.BytesIO(_build_camm_sample(measurement)) for measurement in measurements
342
+ )
283
343
 
284
344
  return _f