|
31 | 31 | ("00:00:03.921", "00:00:09.676"), |
32 | 32 | ] |
33 | 33 |
|
| 34 | +# Expected scene cuts for `goldeneye-vfr-drop3.mp4` — a synthetic VFR clip created from the first |
| 35 | +# 10s of goldeneye.mp4 by dropping every 3rd frame (frames 2,5,8,...). PTS durations alternate |
| 36 | +# between 1001 and 2002 (time_base=1/24000), nominal fps=24000/1001, avg fps≈16. The last scene |
| 37 | +# ends at the clip boundary and may vary slightly between backends. |
| 38 | +EXPECTED_SCENES_VFR_DROP3: ty.List[ty.Tuple[str, str]] = [ |
| 39 | + ("00:00:00.000", "00:00:03.754"), |
| 40 | + ("00:00:03.754", "00:00:08.759"), |
| 41 | +] |
| 42 | + |
34 | 43 |
|
35 | 44 | class TestVFR: |
36 | 45 | """Test VFR video handling.""" |
@@ -142,6 +151,46 @@ def test_vfr_csv_output(self, test_vfr_video: str, tmp_path): |
142 | 151 | rows = list(reader) |
143 | 152 | assert len(rows) >= 3 # 2 header rows + data |
144 | 153 |
|
| 154 | + @pytest.mark.parametrize("backend", ["pyav", "opencv"]) |
| 155 | + def test_vfr_drop3_scene_detection(self, test_vfr_drop3_video: str, backend: str): |
| 156 | + """Synthetic VFR video (drop every 3rd frame, alternating 1x/2x durations) should produce |
| 157 | + timecodes matching known ground truth with both backends.""" |
| 158 | + video = open_video(test_vfr_drop3_video, backend=backend) |
| 159 | + sm = SceneManager() |
| 160 | + sm.add_detector(ContentDetector()) |
| 161 | + sm.detect_scenes(video=video, show_progress=False) |
| 162 | + scene_list = sm.get_scene_list() |
| 163 | + |
| 164 | + assert len(scene_list) >= len(EXPECTED_SCENES_VFR_DROP3), ( |
| 165 | + f"[{backend}] Expected at least {len(EXPECTED_SCENES_VFR_DROP3)} scenes, got {len(scene_list)}" |
| 166 | + ) |
| 167 | + for i, ((start, end), (exp_start_tc, exp_end_tc)) in enumerate( |
| 168 | + zip(scene_list, EXPECTED_SCENES_VFR_DROP3, strict=False) |
| 169 | + ): |
| 170 | + assert start.get_timecode() == exp_start_tc, ( |
| 171 | + f"[{backend}] Scene {i + 1} start: expected {exp_start_tc!r}, got {start.get_timecode()!r}" |
| 172 | + ) |
| 173 | + assert end.get_timecode() == exp_end_tc, ( |
| 174 | + f"[{backend}] Scene {i + 1} end: expected {exp_end_tc!r}, got {end.get_timecode()!r}" |
| 175 | + ) |
| 176 | + |
| 177 | + @pytest.mark.parametrize("backend", ["pyav", "opencv"]) |
| 178 | + def test_vfr_drop3_position_monotonic(self, test_vfr_drop3_video: str, backend: str): |
| 179 | + """PTS-based position should be monotonically non-decreasing on synthetic VFR video.""" |
| 180 | + video = open_video(test_vfr_drop3_video, backend=backend) |
| 181 | + last_seconds = -1.0 |
| 182 | + frame_count = 0 |
| 183 | + while True: |
| 184 | + if video.read() is False: |
| 185 | + break |
| 186 | + current = video.position.seconds |
| 187 | + assert current >= last_seconds, ( |
| 188 | + f"[{backend}] Position decreased at frame {frame_count}: {current} < {last_seconds}" |
| 189 | + ) |
| 190 | + last_seconds = current |
| 191 | + frame_count += 1 |
| 192 | + assert frame_count == 160 # 2/3 of original 240 frames in 10s at 24000/1001 |
| 193 | + |
145 | 194 | def test_cfr_position_is_timecode(self, test_movie_clip: str): |
146 | 195 | """CFR video positions should also be Timecode-backed with PTS support.""" |
147 | 196 | video = open_video(test_movie_clip, backend="pyav") |
|
0 commit comments