Skip to content

Commit 33fb535

Browse files
committed
[video_stream] Remove the advance parameter
It was always set to `True` and made it really confusing when implementing certain functionality. This also reduces the internal state that some implementations need to keep.
1 parent 7a21089 commit 33fb535

File tree

7 files changed

+78
-209
lines changed

7 files changed

+78
-209
lines changed

scenedetect/backends/moviepy.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,9 @@ def aspect_ratio(self) -> float:
131131
def position(self) -> FrameTimecode:
132132
"""Current position within stream as FrameTimecode.
133133
134-
This can be interpreted as presentation time stamp of the last frame which was
135-
decoded by calling `read` with advance=True.
136-
137-
This method will always return 0 (e.g. be equal to `base_timecode`) if no frames
138-
have been `read`."""
134+
This can be interpreted as presentation time stamp of the last frame which was decoded by
135+
calling `read`. This will always return 0 (e.g. be equal to `base_timecode`) if no frames
136+
have been `read` yet."""
139137
frame_number = max(self._frame_number - 1, 0)
140138
return FrameTimecode(frame_number, self.frame_rate)
141139

@@ -151,10 +149,8 @@ def position_ms(self) -> float:
151149
def frame_number(self) -> int:
152150
"""Current position within stream in frames as an int.
153151
154-
1 indicates the first frame was just decoded by the last call to `read` with advance=True,
155-
whereas 0 indicates that no frames have been `read`.
156-
157-
This method will always return 0 if no frames have been `read`."""
152+
0 indicates that no frames have been `read`, 1 indicates the first frame was just read.
153+
"""
158154
return self._frame_number
159155

160156
def seek(self, target: ty.Union[FrameTimecode, float, int]):
@@ -209,24 +205,7 @@ def reset(self, print_infos=False):
209205
self._eof = False
210206
self._reader = FFMPEG_VideoReader(self._path, print_infos=print_infos)
211207

212-
def read(self, decode: bool = True, advance: bool = True) -> ty.Union[np.ndarray, bool]:
213-
"""Read and decode the next frame as a np.ndarray. Returns False when video ends.
214-
215-
Arguments:
216-
decode: Decode and return the frame.
217-
advance: Seek to the next frame. If False, will return the current (last) frame.
218-
219-
Returns:
220-
If decode = True, the decoded frame (np.ndarray), or False (bool) if end of video.
221-
If decode = False, a bool indicating if advancing to the the next frame succeeded.
222-
"""
223-
if not advance:
224-
last_frame_valid = self._last_frame is not None and self._last_frame is not False
225-
if not last_frame_valid:
226-
return False
227-
if self._last_frame_rgb is None:
228-
self._last_frame_rgb = cv2.cvtColor(self._last_frame, cv2.COLOR_BGR2RGB)
229-
return self._last_frame_rgb
208+
def read(self, decode: bool = True) -> ty.Union[np.ndarray, bool]:
230209
if not hasattr(self._reader, "lastread") or self._eof:
231210
return False
232211
has_last_read = hasattr(self._reader, "last_read")

scenedetect/backends/opencv.py

Lines changed: 38 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,11 @@ def capture(self) -> cv2.VideoCapture:
141141

142142
@property
143143
def frame_rate(self) -> float:
144-
"""Framerate in frames/sec."""
145144
assert self._frame_rate
146145
return self._frame_rate
147146

148147
@property
149148
def path(self) -> ty.Union[bytes, str]:
150-
"""Video or device path."""
151149
if self._is_device:
152150
assert isinstance(self._path_or_device, (int))
153151
return "Device %d" % self._path_or_device
@@ -156,7 +154,6 @@ def path(self) -> ty.Union[bytes, str]:
156154

157155
@property
158156
def name(self) -> str:
159-
"""Name of the video, without extension, or device."""
160157
if self._is_device:
161158
return self.path
162159
file_name: str = get_file_name(self.path, include_extension=False)
@@ -205,13 +202,6 @@ def timecode(self) -> Timecode:
205202

206203
@property
207204
def position(self) -> FrameTimecode:
208-
"""Current position within stream as FrameTimecode.
209-
210-
This can be interpreted as presentation time stamp of the last frame which was
211-
decoded by calling `read` with advance=True.
212-
213-
This method will always return 0 (e.g. be equal to `base_timecode`) if no frames
214-
have been `read`."""
215205
if _USE_PTS_IN_DEVELOPMENT:
216206
return FrameTimecode(timecode=self.timecode, fps=self.frame_rate)
217207
if self.frame_number < 1:
@@ -220,41 +210,13 @@ def position(self) -> FrameTimecode:
220210

221211
@property
222212
def position_ms(self) -> float:
223-
"""Current position within stream as a float of the presentation time in milliseconds.
224-
The first frame has a time of 0.0 ms.
225-
226-
This method will always return 0.0 if no frames have been `read`."""
227213
return self._cap.get(cv2.CAP_PROP_POS_MSEC)
228214

229215
@property
230216
def frame_number(self) -> int:
231-
"""Current position within stream in frames as an int.
232-
233-
1 indicates the first frame was just decoded by the last call to `read` with advance=True,
234-
whereas 0 indicates that no frames have been `read`.
235-
236-
This method will always return 0 if no frames have been `read`."""
237217
return math.trunc(self._cap.get(cv2.CAP_PROP_POS_FRAMES))
238218

239219
def seek(self, target: ty.Union[FrameTimecode, float, int]):
240-
"""Seek to the given timecode. If given as a frame number, represents the current seek
241-
pointer (e.g. if seeking to 0, the next frame decoded will be the first frame of the video).
242-
243-
For 1-based indices (first frame is frame #1), the target frame number needs to be converted
244-
to 0-based by subtracting one. For example, if we want to seek to the first frame, we call
245-
seek(0) followed by read(). If we want to seek to the 5th frame, we call seek(4) followed
246-
by read(), at which point frame_number will be 5.
247-
248-
Not supported if the VideoStream is a device/camera. Untested with web streams.
249-
250-
Arguments:
251-
target: Target position in video stream to seek to.
252-
If float, interpreted as time in seconds.
253-
If int, interpreted as frame number.
254-
Raises:
255-
SeekError: An error occurs while seeking, or seeking is not supported.
256-
ValueError: `target` is not a valid value (i.e. it is negative).
257-
"""
258220
if self._is_device:
259221
raise SeekError("Cannot seek if input is a device!")
260222
if target < 0:
@@ -282,40 +244,27 @@ def reset(self):
282244
self._cap.release()
283245
self._open_capture(self._frame_rate)
284246

285-
def read(self, decode: bool = True, advance: bool = True) -> ty.Union[np.ndarray, bool]:
286-
"""Read and decode the next frame as a np.ndarray. Returns False when video ends,
287-
or the maximum number of decode attempts has passed.
288-
289-
Arguments:
290-
decode: Decode and return the frame.
291-
advance: Seek to the next frame. If False, will return the current (last) frame.
292-
293-
Returns:
294-
If decode = True, the decoded frame (np.ndarray), or False (bool) if end of video.
295-
If decode = False, a bool indicating if advancing to the the next frame succeeded.
296-
"""
247+
def read(self, decode: bool = True) -> ty.Union[np.ndarray, bool]:
297248
if not self._cap.isOpened():
298249
return False
299-
# Grab the next frame if possible.
300-
if advance:
301-
has_grabbed = self._cap.grab()
302-
# If we failed to grab the frame, retry a few times if required.
303-
if not has_grabbed:
304-
if self.duration > 0 and self.position < (self.duration - 1):
305-
for _ in range(self._max_decode_attempts):
306-
has_grabbed = self._cap.grab()
307-
if has_grabbed:
308-
break
309-
# Report previous failure in debug mode.
310-
if has_grabbed:
311-
self._decode_failures += 1
312-
logger.debug("Frame failed to decode.")
313-
if not self._warning_displayed and self._decode_failures > 1:
314-
logger.warning("Failed to decode some frames, results may be inaccurate.")
315-
# We didn't manage to grab a frame even after retrying, so just return.
316-
if not has_grabbed:
317-
return False
318-
self._has_grabbed = True
250+
has_grabbed = self._cap.grab()
251+
# If we failed to grab the frame, retry a few times if required.
252+
if not has_grabbed:
253+
if self.duration > 0 and self.position < (self.duration - 1):
254+
for _ in range(self._max_decode_attempts):
255+
has_grabbed = self._cap.grab()
256+
if has_grabbed:
257+
break
258+
# Report previous failure in debug mode.
259+
if has_grabbed:
260+
self._decode_failures += 1
261+
logger.debug("Frame failed to decode.")
262+
if not self._warning_displayed and self._decode_failures > 1:
263+
logger.warning("Failed to decode some frames, results may be inaccurate.")
264+
# We didn't manage to grab a frame even after retrying, so just return.
265+
if not has_grabbed:
266+
return False
267+
self._has_grabbed = True
319268
# Need to make sure we actually grabbed a frame before calling retrieve.
320269
if decode and self._has_grabbed:
321270
_, frame = self._cap.retrieve()
@@ -490,35 +439,18 @@ def aspect_ratio(self) -> float:
490439

491440
@property
492441
def position(self) -> FrameTimecode:
493-
"""Current position within stream as FrameTimecode. Use the :meth:`position_ms`
494-
if an accurate duration of elapsed time is required, as `position` is currently
495-
based off of the number of frames, and may not be accurate for devicesor live streams.
496-
497-
This method will always return 0 (e.g. be equal to `base_timecode`) if no frames
498-
have been `read`."""
499-
500442
if self.frame_number < 1:
501443
return self.base_timecode
502444
return self.base_timecode + (self.frame_number - 1)
503445

504446
@property
505447
def position_ms(self) -> float:
506-
"""Current position within stream as a float of the presentation time in milliseconds.
507-
The first frame has a time of 0.0 ms.
508-
509-
This method will always return 0.0 if no frames have been `read`."""
510448
if self._num_frames == 0:
511449
return 0.0
512450
return self._cap.get(cv2.CAP_PROP_POS_MSEC) - self._time_base
513451

514452
@property
515453
def frame_number(self) -> int:
516-
"""Current position within stream in frames as an int.
517-
518-
1 indicates the first frame was just decoded by the last call to `read` with advance=True,
519-
whereas 0 indicates that no frames have been `read`.
520-
521-
This method will always return 0 if no frames have been `read`."""
522454
return self._num_frames
523455

524456
def seek(self, target: ty.Union[FrameTimecode, float, int]):
@@ -529,41 +461,28 @@ def reset(self):
529461
"""Not supported."""
530462
raise NotImplementedError("Reset is not supported.")
531463

532-
def read(self, decode: bool = True, advance: bool = True) -> ty.Union[np.ndarray, bool]:
533-
"""Read and decode the next frame as a np.ndarray. Returns False when video ends,
534-
or the maximum number of decode attempts has passed.
535-
536-
Arguments:
537-
decode: Decode and return the frame.
538-
advance: Seek to the next frame. If False, will return the current (last) frame.
539-
540-
Returns:
541-
If decode = True, the decoded frame (np.ndarray), or False (bool) if end of video.
542-
If decode = False, a bool indicating if advancing to the the next frame succeeded.
543-
"""
464+
def read(self, decode: bool = True) -> ty.Union[np.ndarray, bool]:
544465
if not self._cap.isOpened():
545466
return False
546-
# Grab the next frame if possible.
547-
if advance:
548-
has_grabbed = self._cap.grab()
549-
# If we failed to grab the frame, retry a few times if required.
550-
if not has_grabbed:
551-
for _ in range(self._max_read_attempts):
552-
has_grabbed = self._cap.grab()
553-
if has_grabbed:
554-
break
555-
# Report previous failure in debug mode.
467+
has_grabbed = self._cap.grab()
468+
# If we failed to grab the frame, retry a few times if required.
469+
if not has_grabbed:
470+
for _ in range(self._max_read_attempts):
471+
has_grabbed = self._cap.grab()
556472
if has_grabbed:
557-
self._decode_failures += 1
558-
logger.debug("Frame failed to decode.")
559-
if not self._warning_displayed and self._decode_failures > 1:
560-
logger.warning("Failed to decode some frames, results may be inaccurate.")
561-
# We didn't manage to grab a frame even after retrying, so just return.
562-
if not has_grabbed:
563-
return False
564-
if self._num_frames == 0:
565-
self._time_base = self._cap.get(cv2.CAP_PROP_POS_MSEC)
566-
self._num_frames += 1
473+
break
474+
# Report previous failure in debug mode.
475+
if has_grabbed:
476+
self._decode_failures += 1
477+
logger.debug("Frame failed to decode.")
478+
if not self._warning_displayed and self._decode_failures > 1:
479+
logger.warning("Failed to decode some frames, results may be inaccurate.")
480+
# We didn't manage to grab a frame even after retrying, so just return.
481+
if not has_grabbed:
482+
return False
483+
if self._num_frames == 0:
484+
self._time_base = self._cap.get(cv2.CAP_PROP_POS_MSEC)
485+
self._num_frames += 1
567486
# Need to make sure we actually grabbed a frame before calling retrieve.
568487
if decode and self._num_frames > 0:
569488
_, frame = self._cap.retrieve()

scenedetect/backends/pyav.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,9 @@ def seek(self, target: ty.Union[FrameTimecode, float, int]) -> None:
257257
self._frame = None
258258
self._container.seek(target_pts, stream=self._video_stream)
259259
if not beginning:
260-
self.read(decode=False, advance=True)
260+
self.read(decode=False)
261261
while self.position < target:
262-
if self.read(decode=False, advance=True) is False:
262+
if self.read(decode=False) is False:
263263
break
264264

265265
def reset(self):
@@ -271,33 +271,18 @@ def reset(self):
271271
except Exception as ex:
272272
raise VideoOpenFailure() from ex
273273

274-
def read(self, decode: bool = True, advance: bool = True) -> ty.Union[np.ndarray, bool]:
275-
"""Read and decode the next frame as a np.ndarray. Returns False when video ends.
276-
277-
Arguments:
278-
decode: Decode and return the frame.
279-
advance: Seek to the next frame. If False, will return the current (last) frame.
280-
281-
Returns:
282-
If decode = True, the decoded frame (np.ndarray), or False (bool) if end of video.
283-
If decode = False, a bool indicating if advancing to the the next frame succeeded.
284-
"""
285-
has_advanced = False
286-
if advance:
287-
try:
288-
last_frame = self._frame
289-
self._frame = next(self._container.decode(video=0))
290-
except av.error.EOFError:
291-
self._frame = last_frame
292-
if self._handle_eof():
293-
return self.read(decode, advance=True)
294-
return False
295-
except StopIteration:
296-
return False
297-
has_advanced = True
298-
if decode:
299-
return self._frame.to_ndarray(format="bgr24")
300-
return has_advanced
274+
def read(self, decode: bool = True) -> ty.Union[np.ndarray, bool]:
275+
try:
276+
last_frame = self._frame
277+
self._frame = next(self._container.decode(video=0))
278+
except av.error.EOFError:
279+
self._frame = last_frame
280+
if self._handle_eof():
281+
return self.read(decode)
282+
return False
283+
except StopIteration:
284+
return False
285+
return self._frame.to_ndarray(format="bgr24") if decode else True
301286

302287
#
303288
# Private Methods/Properties

0 commit comments

Comments
 (0)