Skip to content

Commit b28460a

Browse files
committed
[backends] Add retries for OSError in VideoStreamMoviePy #496
1 parent 71ce5a8 commit b28460a

File tree

2 files changed

+38
-3
lines changed

2 files changed

+38
-3
lines changed

scenedetect/backends/moviepy.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
image sequences or AviSynth scripts are supported as inputs.
1717
"""
1818

19+
import time
1920
import typing as ty
2021
from fractions import Fraction
2122
from logging import getLogger
@@ -31,6 +32,33 @@
3132

3233
logger = getLogger("pyscenedetect")
3334

35+
# MoviePy spawns ffmpeg as a subprocess and reads frame bytes over stdout. Under
36+
# load the parent can read before the child has flushed its first write, which
37+
# surfaces as OSError (see #496). A short retry clears nearly all such flakes.
38+
_FFMPEG_RETRY_COUNT = 2
39+
_FFMPEG_RETRY_BACKOFF_SECS = 0.5
40+
41+
42+
def _retry_on_oserror(op_name: str, fn: ty.Callable):
43+
"""Run ``fn``, retrying up to ``_FFMPEG_RETRY_COUNT`` times on ``OSError``."""
44+
last_exc: ty.Optional[OSError] = None
45+
for attempt in range(_FFMPEG_RETRY_COUNT + 1):
46+
try:
47+
return fn()
48+
except OSError as ex:
49+
last_exc = ex
50+
if attempt < _FFMPEG_RETRY_COUNT:
51+
logger.warning(
52+
"ffmpeg %s failed (attempt %d/%d), retrying: %s",
53+
op_name,
54+
attempt + 1,
55+
_FFMPEG_RETRY_COUNT + 1,
56+
ex,
57+
)
58+
time.sleep(_FFMPEG_RETRY_BACKOFF_SECS)
59+
assert last_exc is not None
60+
raise last_exc
61+
3462

3563
class VideoStreamMoviePy(VideoStream):
3664
"""MoviePy `FFMPEG_VideoReader` backend."""
@@ -63,7 +91,9 @@ def __init__(
6391
# cases return IOErrors (e.g. could not read duration/video resolution). These
6492
# should be mapped to specific errors, e.g. write a function to map MoviePy
6593
# exceptions to a new set of equivalents.
66-
self._reader = FFMPEG_VideoReader(path, print_infos=print_infos)
94+
self._reader = _retry_on_oserror(
95+
"open", lambda: FFMPEG_VideoReader(path, print_infos=print_infos)
96+
)
6797
# This will always be one behind self._reader.lastread when we finally call read()
6898
# as MoviePy caches the first frame when opening the video. Thus self._last_frame
6999
# will always be the current frame, and self._reader.lastread will be the next.
@@ -184,7 +214,9 @@ def seek(self, target: ty.Union[FrameTimecode, float, int]):
184214
if not isinstance(target, FrameTimecode):
185215
target = FrameTimecode(target, self.frame_rate)
186216
try:
187-
self._last_frame = self._reader.get_frame(target.seconds)
217+
self._last_frame = _retry_on_oserror(
218+
"seek", lambda: self._reader.get_frame(target.seconds)
219+
)
188220
if hasattr(self._reader, "last_read") and target >= self.duration:
189221
raise SeekError("MoviePy > 2.0 does not have proper EOF semantics (#461).")
190222
self._frame_number = min(
@@ -212,7 +244,9 @@ def reset(self, print_infos=False):
212244
self._last_frame_rgb = None
213245
self._frame_number = 0
214246
self._eof = False
215-
self._reader = FFMPEG_VideoReader(self._path, print_infos=print_infos)
247+
self._reader = _retry_on_oserror(
248+
"reset", lambda: FFMPEG_VideoReader(self._path, print_infos=print_infos)
249+
)
216250

217251
def read(self, decode: bool = True) -> ty.Union[np.ndarray, bool]:
218252
if not hasattr(self._reader, "lastread") or self._eof:

website/pages/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ Although there have been minimal changes to most API examples, there are several
678678
- [feature] VFR videos are handled correctly by the OpenCV and PyAV backends, and should work correctly with default parameters
679679
- [feature] New `save-xml` command supports saving scenes in Final Cut Pro formats [#156](https://github.com/Breakthrough/PySceneDetect/issues/156)
680680
- [bugfix] Fix floating-point precision error in `save-otio` output where frame values near integer boundaries (e.g. `90.00000000000001`) were serialized with spurious precision
681+
- [bugfix] Add mitigation for transient `OSError` in the MoviePy backend as it is susceptible to subprocess pipe races on slow or heavily loaded systems [#496](https://github.com/Breakthrough/PySceneDetect/issues/496)
681682
- [refactor] Remove deprecated `-d`/`--min-delta-hsv` option from `detect-adaptive` command
682683

683684
### API Changes

0 commit comments

Comments
 (0)