Skip to content

Commit 878ce18

Browse files
committed
Handle negative ctts sample_offset values in mp4 parsing
Summary: Some encodings like H.264 and H.265 support negative offsets. We cannot rely on the version field since some encoders incorrectly set ctts version to 0 instead of 1 even when using signed offsets. Leigitimate positive values are relatively small so we can assume the value is signed. The unsigned to signed conversion could have been gated based on the encoding. However I chose not to since large positive values exceeding 2^31 is unrealistic. For reference the stsd and ctts boxes from a video: stsd box: Container: size = 233 type = b'stsd' (total 4) data = Container: version = 0 flags = 0 entries = ListContainer: Container: format = b'hvc1' (total 4) ctts box Container: size = 128480 type = b'ctts' (total 4) data = Container: version = 0 flags = 0 entries = ListContainer: Container: sample_count = 1 sample_offset = 0 Container: sample_count = 1 sample_offset = 60 Container: sample_count = 1 sample_offset = 0 Container: sample_count = 1 sample_offset = 4294967256 Container: sample_count = 1 sample_offset = 4294967276
1 parent ceca21d commit 878ce18

File tree

3 files changed

+28
-1
lines changed

3 files changed

+28
-1
lines changed

mapillary_tools/mp4/mp4_sample_parser.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from . import construct_mp4_parser as cparser, simple_mp4_parser as sparser
88

99

10+
def _convert_to_signed_int32(unsigned_int32: int) -> int:
11+
"""Interpret an unsigned 32-bit value as negative if high bit is set."""
12+
if (unsigned_int32 & (1 << 31)) == 0:
13+
return unsigned_int32
14+
else:
15+
return unsigned_int32 - (1 << 32)
16+
17+
1018
class RawSample(T.NamedTuple):
1119
# 1-based index
1220
description_idx: int
@@ -192,7 +200,13 @@ def extract_raw_samples_from_stbl_data(
192200
composition_offsets = []
193201
for entry in data["entries"]:
194202
for _ in range(entry["sample_count"]):
195-
composition_offsets.append(entry["sample_offset"])
203+
# Some encodings like H.264 and H.265 support negative offsets.
204+
# We cannot rely on the version field since some encoders incorrectly set
205+
# ctts version to 0 instead of 1 even when using signed offsets.
206+
# Leigitimate positive values are relatively small so we can assume the value is signed.
207+
composition_offsets.append(
208+
_convert_to_signed_int32(entry["sample_offset"])
209+
)
196210
elif box["type"] == b"stss":
197211
syncs = set(data["entries"])
198212

3.83 MB
Binary file not shown.

tests/unit/test_mp4_sample_parser.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,16 @@ def test_movie_box_parser():
5252
assert sample.raw_sample.offset == raw_sample.offset
5353
assert sample.raw_sample.is_sync == raw_sample.is_sync
5454
assert sample.raw_sample.size == raw_sample.size
55+
56+
57+
def test_movie_box_parser_negative_composition_offset():
58+
moov_parser = mp4_sample_parser.MovieBoxParser.parse_file(
59+
Path("tests/data/videos/sample-5s_h265.mp4")
60+
)
61+
assert 2 == len(list(moov_parser.extract_tracks()))
62+
video_track = moov_parser.extract_track_at(0)
63+
assert video_track.is_video_track()
64+
raw_samples = list(video_track.extract_raw_samples())
65+
assert 146 == len(raw_samples)
66+
# Make sure the parser can parse negative composition offsets
67+
assert 0 < len([s for s in raw_samples if s.composition_offset < 0])

0 commit comments

Comments
 (0)