diff --git a/mapillary_tools/camm/camm_builder.py b/mapillary_tools/camm/camm_builder.py index a9a268cb..f4207fb7 100644 --- a/mapillary_tools/camm/camm_builder.py +++ b/mapillary_tools/camm/camm_builder.py @@ -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) @@ -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, @@ -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). @@ -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 []), @@ -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( diff --git a/tests/unit/test_camm_parser.py b/tests/unit/test_camm_parser.py index 40076aa6..34d2ae9e 100644 --- a/tests/unit/test_camm_parser.py +++ b/tests/unit/test_camm_parser.py @@ -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