@@ -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