|
20 | 20 | import math |
21 | 21 | import os.path |
22 | 22 | import typing as ty |
| 23 | +from fractions import Fraction |
23 | 24 | from logging import getLogger |
24 | 25 |
|
25 | 26 | import cv2 |
26 | 27 | import numpy as np |
27 | 28 |
|
28 | 29 | from scenedetect.frame_timecode import MAX_FPS_DELTA, FrameTimecode |
29 | 30 | from scenedetect.platform import get_file_name |
30 | | -from scenedetect.video_stream import FrameRateUnavailable, SeekError, VideoOpenFailure, VideoStream |
| 31 | +from scenedetect.timecode import Timecode |
| 32 | +from scenedetect.video_stream import ( |
| 33 | + FrameRateUnavailable, |
| 34 | + SeekError, |
| 35 | + VideoFrame, |
| 36 | + VideoOpenFailure, |
| 37 | + VideoStream, |
| 38 | +) |
31 | 39 |
|
32 | 40 | logger = getLogger("pyscenedetect") |
33 | 41 |
|
@@ -264,6 +272,30 @@ def reset(self): |
264 | 272 | self._cap.release() |
265 | 273 | self._open_capture(self._frame_rate) |
266 | 274 |
|
| 275 | + def __next__(self): |
| 276 | + # NOTE: POS_FRAMES starts from 0 before any frames are read. |
| 277 | + read, image = self._cap.read() |
| 278 | + if not read: |
| 279 | + raise StopIteration() |
| 280 | + # We can only query CAP_PROP_PTS if this uses the ffmpeg backend, however it doesn't seem |
| 281 | + # to work correctly. Quite frequently consecutive frames return the same PTS. We might need |
| 282 | + # to just abandon using PTS with OpenCV and rely on milliseconds. This will still result |
| 283 | + # in occasional off-by-one errors for VFR videos, but better than the status quo. |
| 284 | + # |
| 285 | + # We should also add a config option so users can specify if OpenCV should use fixed or |
| 286 | + # variable timing (i.e. if we should use CAP_PROP_POS_MSEC or CAP_PROP_POS_FRAMES for |
| 287 | + # timestamp calculation). |
| 288 | + USE_PTS = False |
| 289 | + if USE_PTS: |
| 290 | + pts = self._cap.get(cv2.CAP_PROP_PTS) |
| 291 | + time_base = Fraction.from_float(self._cap.get(cv2.CAP_PROP_FPS)) |
| 292 | + time_base = Fraction(numerator=time_base.denominator, denominator=time_base.numerator) |
| 293 | + else: |
| 294 | + pts = self._cap.get(cv2.CAP_PROP_POS_MSEC) |
| 295 | + time_base = Fraction(1, 1000) |
| 296 | + timecode = Timecode(pts=round(pts), time_base=time_base) |
| 297 | + return VideoFrame(image=image, timecode=timecode) |
| 298 | + |
267 | 299 | def read(self, decode: bool = True, advance: bool = True) -> ty.Union[np.ndarray, bool]: |
268 | 300 | """Read and decode the next frame as a np.ndarray. Returns False when video ends, |
269 | 301 | or the maximum number of decode attempts has passed. |
|
0 commit comments