Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions mapillary_tools/camm/camm_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def _create_camm_stbl(
def create_camm_trak(
raw_samples: T.Sequence[sample_parser.RawSample],
media_timescale: int,
creation_time: int = 0,
modification_time: int = 0,
) -> builder.BoxDict:
stbl = _create_camm_stbl(raw_samples)

Expand All @@ -152,11 +154,8 @@ def create_camm_trak(
"data": {
# use 64-bit version
"version": 1,
# TODO: find timestamps from mvhd?
# do not set dynamic timestamps (e.g. time.time()) here because we'd like to
# make sure the md5 of the new mp4 file unchanged
"creation_time": 0,
"modification_time": 0,
"creation_time": creation_time,
"modification_time": modification_time,
"timescale": media_timescale,
"duration": media_duration,
"language": 21956,
Expand Down Expand Up @@ -197,11 +196,8 @@ def create_camm_trak(
"data": {
# use 32-bit version of the box
"version": 0,
# TODO: find timestamps from mvhd?
# do not set dynamic timestamps (e.g. time.time()) here because we'd like to
# make sure the md5 of the new mp4 file unchanged
"creation_time": 0,
"modification_time": 0,
"creation_time": creation_time,
"modification_time": modification_time,
# will update the track ID later
"track_ID": 0,
# If the duration of this track cannot be determined then duration is set to all 1s (32-bit maxint).
Expand Down Expand Up @@ -237,6 +233,12 @@ def _f(
# Make sure the precision of timedeltas not lower than 0.001 (1ms)
media_timescale = max(1000, movie_timescale)

# Carry creation/modification times from the source video's mvhd
mvhd = cparser.find_box_at_pathx(moov_children, [b"mvhd"])
mvhd_data = T.cast(T.Dict[str, T.Any], mvhd["data"])
creation_time = mvhd_data.get("creation_time", 0)
modification_time = mvhd_data.get("modification_time", 0)

# Multiplex points for creating elst
track: list[geo.Point] = [
*(camm_info.gps or []),
Expand Down Expand Up @@ -264,7 +266,9 @@ def _f(
convert_telemetry_to_raw_samples(measurements, media_timescale)
)

camm_trak = create_camm_trak(camm_samples, media_timescale)
camm_trak = create_camm_trak(
camm_samples, media_timescale, creation_time, modification_time
)

if T.cast(T.Dict, elst["data"])["entries"]:
T.cast(T.List[builder.BoxDict], camm_trak["data"]).append(
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/test_camm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,62 @@ def test_build_and_parse3():
)
x = encode_decode_empty_camm_mp4(metadata)
assert [] == x.points


def test_camm_trak_carries_mvhd_timestamps():
"""Verify that creation_time and modification_time from the source video's
mvhd are carried into the CAMM track's tkhd and mdhd boxes."""
from mapillary_tools.mp4 import mp4_sample_parser as sample_parser

movie_timescale = 1_000_000
src_creation_time = 3_692_845_200 # 2021-01-01 in MP4 epoch
src_modification_time = 3_692_845_300

mvhd: cparser.BoxDict = {
"type": b"mvhd",
"data": {
"creation_time": src_creation_time,
"modification_time": src_modification_time,
"timescale": movie_timescale,
"duration": int(36000 * movie_timescale),
},
}

empty_mp4: T.List[cparser.BoxDict] = [
{"type": b"ftyp", "data": b"test"},
{"type": b"moov", "data": [mvhd]},
]
src = cparser.MP4WithoutSTBLBuilderConstruct.build_boxlist(empty_mp4)

points = [
geo.Point(time=0.1, lat=0.01, lon=0.2, alt=None, angle=None),
geo.Point(time=0.2, lat=0.02, lon=0.3, alt=None, angle=None),
]
metadata = types.VideoMetadata(
Path(""),
filetype=types.FileType.CAMM,
points=points,
)
input_camm_info = uploader.VideoUploader.prepare_camm_info(metadata)
target_fp = simple_mp4_builder.transform_mp4(
io.BytesIO(src), camm_builder.camm_sample_generator2(input_camm_info)
)

# Parse the output MP4 and find the CAMM track
movie = sample_parser.MovieBoxParser.parse_stream(T.cast(T.BinaryIO, target_fp))
camm_track = None
for track in movie.extract_tracks():
descs = track.extract_sample_descriptions()
if any(d.get("format") == b"camm" for d in descs):
camm_track = track
break

assert camm_track is not None, "CAMM track not found in output MP4"

tkhd = camm_track.extract_tkhd_boxdata()
assert tkhd["creation_time"] == src_creation_time
assert tkhd["modification_time"] == src_modification_time

mdhd = camm_track.extract_mdhd_boxdata()
assert mdhd["creation_time"] == src_creation_time
assert mdhd["modification_time"] == src_modification_time