Skip to content

Commit 20cc890

Browse files
authored
Add test showing blocks are seek_mode=approximate + get_frame_played_at() (#1654)
1 parent 3be0df9 commit 20cc890

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

src/torchcodec/decoders/_blocks/_demuxer.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616

1717
from ._frame import Packet
1818

19-
# TODO_API_BREAKDOWN CORRECTNESS P1: Need to understand the seeking we do: it's
20-
# not completley approximate and it's not completely exact either. Understand
21-
# it, document it (?), test it. Maybe we acutally do want to replicate
22-
# approximate and exact. Might not actually be difficult.
23-
2419
# TODO_API_BREAKDOWN FEAT PERF Do we want / need to support 'batch-like' APIs
2520
# were containers are pre-allocated for perf? Like if a user wants to decode
2621
# specific timestamps for sampling?
@@ -54,6 +49,10 @@ def next_packet(self) -> Packet | None:
5449
handle, is_eof = _blocks_demuxer_next_packet(self._handle)
5550
return None if is_eof else Packet(handle)
5651

52+
# TODO_API_BREAKDOWN FEAT P1: this is VideoDecoder's "approximate"
53+
# seek mode (with get_frame_played_at() only). Do we want to offer an
54+
# "exact" one? It needs a presentation-order keyframe index, i.e. a scan of
55+
# the whole file, which a user would have to opt into (a Demuxer.scan()?).
5756
def seek(self, seconds: float) -> None:
5857
_blocks_demuxer_seek(self._handle, float(seconds))
5958

test/test_decoders.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4294,6 +4294,52 @@ def test_seek_to_non_keyframe_can_land_past_target(self, seconds, device):
42944294
exact = VideoDecoder(H265_VIDEO.path, seek_mode="exact", device=device)
42954295
assert exact.get_frame_played_at(seconds).pts_seconds == seconds
42964296

4297+
@pytest.mark.parametrize("video", (NASA_VIDEO, H265_VIDEO, TEST_SRC_2_720P_MPEG4))
4298+
@pytest.mark.parametrize("device", _block_devices())
4299+
def test_seek_matches_video_decoder_approximate_get_frame_played_at(
4300+
self, video, device
4301+
):
4302+
# Our seek is VideoDecoder's approximate one, frame for frame.
4303+
#
4304+
# It holds against get_frame_played_at() and against nothing else, and
4305+
# that scoping is the point. get_frame_played_at() is the only
4306+
# VideoDecoder API that seeks straight to the timestamp it was given:
4307+
# it turns `seconds` into a pts and hands that to FFmpeg, which is the
4308+
# same two steps Demuxer.seek() takes, so the match is structural
4309+
# rather than a property of these files. Every other API goes through
4310+
# a frame index, which approximate mode derives from the header's
4311+
# average fps and converts back into a pts - a round trip nothing in
4312+
# the blocks performs, and one that doesn't come back where it started
4313+
# unless the file is constant-frame-rate.
4314+
video_decoder = VideoDecoder(video.path, device=device)
4315+
num_frames = video_decoder.metadata.num_frames
4316+
4317+
for index in range(0, num_frames, max(1, num_frames // 10)):
4318+
frame = video_decoder.get_frame_at(index)
4319+
# Aim at the middle of a frame, so that no target lands on a frame
4320+
# boundary where the two sides could round differently.
4321+
seconds = frame.pts_seconds + frame.duration_seconds / 2
4322+
4323+
# A fresh VideoDecoder per target: it skips the seek when the
4324+
# target is just ahead of the last frame it decoded, which would
4325+
# hide the very behaviour we're comparing against.
4326+
expected = VideoDecoder(
4327+
video.path, seek_mode="approximate", device=device
4328+
).get_frame_played_at(seconds)
4329+
4330+
blocks = self._make_blocks(video.path, device)
4331+
got = next(
4332+
frame
4333+
for frame in self._frames_after_seek(blocks, seconds)
4334+
# VideoDecoder's own criterion: the first frame that hasn't
4335+
# finished playing by then. Not `pts_seconds >= seconds`, which
4336+
# would skip the target frame whenever we land right on it.
4337+
if frame.pts_seconds + frame.duration_seconds > seconds
4338+
)
4339+
4340+
assert got.pts_seconds == expected.pts_seconds
4341+
assert_frames_equal(got.data, expected.data)
4342+
42974343
@pytest.mark.parametrize("device", _block_devices())
42984344
def test_seek_without_reset_yields_stale_frames(self, device):
42994345
# What goes wrong if you skip the reset: a decoder always holds a few

0 commit comments

Comments
 (0)