1# -*- coding: utf-8 -*-
2#
3#         PySceneDetect: Python-Based Video Scene Detector
4#   ---------------------------------------------------------------
5#     [  Site: http://www.bcastell.com/projects/PySceneDetect/   ]
6#     [  Github: https://github.com/Breakthrough/PySceneDetect/  ]
7#     [  Documentation: http://pyscenedetect.readthedocs.org/    ]
8#
9# Copyright (C) 2014-2020 Brandon Castellano <http://www.bcastell.com>.
10#
11# PySceneDetect is licensed under the BSD 3-Clause License; see the included
12# LICENSE file, or visit one of the following pages for details:
13#  - https://github.com/Breakthrough/PySceneDetect/
14#  - http://www.bcastell.com/projects/PySceneDetect/
15#
16# This software uses Numpy, OpenCV, click, tqdm, simpletable, and pytest.
17# See the included LICENSE files or one of the above URLs for more information.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
22# AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
23# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25#
26
27""" ``scenedetect.video_manager`` Module
28
29This module contains the :py:class:`VideoManager` class, which provides a consistent
30interface to reading videos, specific exceptions raised upon certain error
31conditions, and some global helper functions to open/close multiple videos,
32as well as validate their parameters.
33
34The :py:class:`VideoManager` can be constructed with a path to a video (or sequence of
35videos) and a start and end time/duration, then passed to a `SceneManager`
36object for performing scene detection analysis.  If the start time is modified,
37then it also needs to be reflected in the `SceneManager`.
38
39The :py:class:`VideoManager` class attempts to emulate some methods of the OpenCV
40cv2.VideoCapture object, and can be used interchangably with one with
41respect to a SceneManager object.
42"""
43
44# There also used to be an asynchronous implementation in addition to the
45# synchronous VideoManager, but the performance was poor. In the future, I may
46# consider rewriting an asynchronous frame grabber in C++ and write a C-API to
47# interface with the Python ctypes module. - B.C.
48
49
50# Standard Library Imports
51from __future__ import print_function
52import os
53import math
54
55# Third-Party Library Imports
56import cv2
57
58# PySceneDetect Library Imports
59from scenedetect.platform import STRING_TYPE
60from scenedetect.frame_timecode import FrameTimecode, MINIMUM_FRAMES_PER_SECOND_FLOAT
61
62
63##
64## VideoManager Exceptions
65##
66
67class VideoOpenFailure(Exception):
68    """ VideoOpenFailure: Raised when an OpenCV VideoCapture object fails to open (i.e. calling
69    the isOpened() method returns a non True value). """
70    def __init__(self, file_list=None, message=
71                 "OpenCV VideoCapture object failed to return True when calling isOpened()."):
72        # type: (Iterable[(str, str)], str)
73        # Pass message string to base Exception class.
74        super(VideoOpenFailure, self).__init__(message)
75        # list of (filename: str, filepath: str)
76        self.file_list = file_list
77
78
79class VideoFramerateUnavailable(Exception):
80    """ VideoFramerateUnavailable: Raised when the framerate cannot be determined from the video,
81    and the framerate has not been overriden/forced in the VideoManager. """
82    def __init__(self, file_name=None, file_path=None, message=
83                 "OpenCV VideoCapture object failed to return framerate when calling "
84                 "get(cv2.CAP_PROP_FPS)."):
85        # type: (str, str, str)
86        # Pass message string to base Exception class.
87        super(VideoFramerateUnavailable, self).__init__(message)
88        # Set other exception properties.
89        self.file_name = file_name
90        self.file_path = file_path
91
92
93class VideoParameterMismatch(Exception):
94    """ VideoParameterMismatch: Raised when opening multiple videos with a VideoManager, and some
95    of the video parameters (frame height, frame width, and framerate/FPS) do not match. """
96    def __init__(self, file_list=None, message=
97                 "OpenCV VideoCapture object parameters do not match."):
98        # type: (Iterable[Tuple[int, float, float, str, str]], str)
99        # Pass message string to base Exception class.
100        super(VideoParameterMismatch, self).__init__(message)
101        # list of (param_mismatch_type: int, parameter value, expected value,
102        #          filename: str, filepath: str)
103        # where param_mismatch_type is an OpenCV CAP_PROP (e.g. CAP_PROP_FPS).
104        self.file_list = file_list
105
106
107class VideoDecodingInProgress(RuntimeError):
108    """ VideoDecodingInProgress: Raised when attempting to call certain VideoManager methods that
109    must be called *before* start() has been called. """
110    # pylint: disable=unnecessary-pass
111    pass
112
113
114class VideoDecoderNotStarted(RuntimeError):
115    """ VideoDecodingInProgress: Raised when attempting to call certain VideoManager methods that
116    must be called *after* start() has been called. """
117    # pylint: disable=unnecessary-pass
118    pass
119
120
121class InvalidDownscaleFactor(ValueError):
122    """ InvalidDownscaleFactor: Raised when trying to set invalid downscale factor,
123    i.e. the supplied downscale factor was not a positive integer greater than zero. """
124    # pylint: disable=unnecessary-pass
125    pass
126
127
128##
129## VideoManager Constants & Helper Functions
130##
131
132DEFAULT_DOWNSCALE_FACTORS = {
133    3200: 12,   # ~4k
134    2100:  8,   # ~2k
135    1700:  6,   # ~1080p
136    1200:  5,
137    900:   4,   # ~720p
138    600:   3,
139    400:   2    # ~480p
140}
141"""Dict[int, int]: The default downscale factor for a video of size W x H,
142which enforces the constraint that W >= 200 to ensure an adequate amount
143of pixels for scene detection while providing a speedup in processing. """
144
145
146
147def compute_downscale_factor(frame_width):
148    # type: (int) -> int
149    """ Compute Downscale Factor: Returns the optimal default downscale factor based on
150    a video's resolution (specifically, the width parameter).
151
152    Returns:
153        int: The defalt downscale factor to use with a video of frame_height x frame_width.
154    """
155    for width in sorted(DEFAULT_DOWNSCALE_FACTORS, reverse=True):
156        if frame_width >= width:
157            return DEFAULT_DOWNSCALE_FACTORS[width]
158    return 1
159
160
161def get_video_name(video_file):
162    # type: (str) -> Tuple[str, str]
163    """ Get Video Name: Returns a string representing the video file/device name.
164
165    Returns:
166        str: Video file name or device ID. In the case of a video, only the file
167            name is returned, not the whole path. For a device, the string format
168            is 'Device 123', where 123 is the integer ID of the capture device.
169    """
170    if isinstance(video_file, int):
171        return ('Device %d' % video_file, video_file)
172    return (os.path.split(video_file)[1], video_file)
173
174
175def get_num_frames(cap_list):
176    # type: (List[cv2.VideoCapture]) -> int
177    """ Get Number of Frames: Returns total number of frames in the cap_list.
178
179    Calls get(CAP_PROP_FRAME_COUNT) and returns the sum for all VideoCaptures.
180    """
181    return sum([math.trunc(cap.get(cv2.CAP_PROP_FRAME_COUNT)) for cap in cap_list])
182
183
184def open_captures(video_files, framerate=None, validate_parameters=True):
185    # type: (Iterable[str], float, bool) -> Tuple[List[VideoCapture], float, Tuple[int, int]]
186    """ Open Captures - helper function to open all capture objects, set the framerate,
187    and ensure that all open captures have been opened and the framerates match on a list
188    of video file paths, or a list containing a single device ID.
189
190    Arguments:
191        video_files (list of str(s)/int): A list of one or more paths (str), or a list
192            of a single integer device ID, to open as an OpenCV VideoCapture object.
193            A ValueError will be raised if the list does not conform to the above.
194        framerate (float, optional): Framerate to assume when opening the video_files.
195            If not set, the first open video is used for deducing the framerate of
196            all videos in the sequence.
197        validate_parameters (bool, optional): If true, will ensure that the frame sizes
198            (width, height) and frame rate (FPS) of all passed videos is the same.
199            A VideoParameterMismatch is raised if the framerates do not match.
200
201    Returns:
202        A tuple of form (cap_list, framerate, framesize) where cap_list is a list of open
203        OpenCV VideoCapture objects in the same order as the video_files list, framerate
204        is a float of the video(s) framerate(s), and framesize is a tuple of (width, height)
205        where width and height are integers representing the frame size in pixels.
206
207    Raises:
208        ValueError: No video file(s) specified, or invalid/multiple device IDs specified.
209        TypeError: `framerate` must be type `float`.
210        IOError: Video file(s) not found.
211        VideoFramerateUnavailable: Video framerate could not be obtained and `framerate`
212            was not set manually.
213        VideoParameterMismatch: All videos in `video_files` do not have equal parameters.
214            Set `validate_parameters=False` to skip this check.
215        VideoOpenFailure: Video(s) could not be opened.
216    """
217    is_device = False
218    if not video_files:
219        raise ValueError("Expected at least 1 video file or device ID.")
220    if isinstance(video_files[0], int):
221        if len(video_files) > 1:
222            raise ValueError("If device ID is specified, no video sources may be appended.")
223        elif video_files[0] < 0:
224            raise ValueError("Invalid/negative device ID specified.")
225        is_device = True
226    elif not all([isinstance(video_file, (str, STRING_TYPE)) for video_file in video_files]):
227        raise ValueError("Unexpected element type in video_files list (expected str(s)/int).")
228    elif framerate is not None and not isinstance(framerate, float):
229        raise TypeError("Expected type float for parameter framerate.")
230    # Check if files exist.
231    if not is_device and any([not os.path.exists(video_file) for video_file in video_files]):
232        raise IOError("Video file(s) not found.")
233    cap_list = []
234
235    try:
236        cap_list = [cv2.VideoCapture(video_file) for video_file in video_files]
237        video_names = [get_video_name(video_file) for video_file in video_files]
238        closed_caps = [video_names[i] for i, cap in
239                       enumerate(cap_list) if not cap.isOpened()]
240        if closed_caps:
241            raise VideoOpenFailure(closed_caps)
242
243        cap_framerates = [cap.get(cv2.CAP_PROP_FPS) for cap in cap_list]
244        cap_framerate, check_framerate = validate_capture_framerate(
245            video_names, cap_framerates, framerate)
246        # Store frame sizes as integers (VideoCapture.get() returns float).
247        cap_frame_sizes = [(math.trunc(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
248                            math.trunc(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
249                           for cap in cap_list]
250        cap_frame_size = cap_frame_sizes[0]
251
252        # If we need to validate the parameters, we check that the FPS and width/height
253        # of all open captures is identical (or almost identical in the case of FPS).
254        if validate_parameters:
255            validate_capture_parameters(
256                video_names=video_names, cap_frame_sizes=cap_frame_sizes,
257                check_framerate=check_framerate, cap_framerates=cap_framerates)
258
259    except:
260        release_captures(cap_list)
261        raise
262
263    return (cap_list, cap_framerate, cap_frame_size)
264
265
266def release_captures(cap_list):
267    # type: (Iterable[VideoCapture]) -> None
268    """ Close Captures:  Calls release() on every capture in cap_list. """
269    for cap in cap_list:
270        cap.release()
271
272
273def close_captures(cap_list):
274    # type: (Iterable[VideoCapture]) -> None
275    """ Close Captures:  Calls close() on every capture in cap_list. """
276    for cap in cap_list:
277        cap.close()
278
279
280def validate_capture_framerate(video_names, cap_framerates, framerate=None):
281    # type: (List[Tuple[str, str]], List[float], Optional[float]) -> Tuple[float, bool]
282    """ Validate Capture Framerate: Ensures that the passed capture framerates are valid and equal.
283
284    Raises:
285        ValueError: Invalid framerate (must be positive non-zero value).
286        TypeError: Framerate must be of type float.
287        VideoFramerateUnavailable: Framerate for video could not be obtained,
288            and `framerate` was not set.
289    """
290    check_framerate = True
291    cap_framerate = cap_framerates[0]
292    if framerate is not None:
293        if isinstance(framerate, float):
294            if framerate < MINIMUM_FRAMES_PER_SECOND_FLOAT:
295                raise ValueError("Invalid framerate (must be a positive non-zero value).")
296            cap_framerate = framerate
297            check_framerate = False
298        else:
299            raise TypeError("Expected float for framerate, got %s." % type(framerate).__name__)
300    else:
301        unavailable_framerates = [(video_names[i][0], video_names[i][1])
302                                  for i, fps in enumerate(cap_framerates)
303                                  if fps < MINIMUM_FRAMES_PER_SECOND_FLOAT]
304        if unavailable_framerates:
305            raise VideoFramerateUnavailable(unavailable_framerates)
306    return (cap_framerate, check_framerate)
307
308
309def validate_capture_parameters(video_names, cap_frame_sizes, check_framerate=False,
310                                cap_framerates=None):
311    # type: (List[Tuple[str, str]], List[Tuple[int, int]], Optional[bool],
312    #        Optional[List[float]]) -> None
313    """ Validate Capture Parameters: Ensures that all passed capture frame sizes and (optionally)
314    framerates are equal.  Raises VideoParameterMismatch if there is a mismatch.
315
316    Raises:
317        VideoParameterMismatch
318    """
319    bad_params = []
320    max_framerate_delta = MINIMUM_FRAMES_PER_SECOND_FLOAT
321    # Check heights/widths match.
322    bad_params += [(cv2.CAP_PROP_FRAME_WIDTH, frame_size[0],
323                    cap_frame_sizes[0][0], video_names[i][0], video_names[i][1]) for
324                   i, frame_size in enumerate(cap_frame_sizes)
325                   if abs(frame_size[0] - cap_frame_sizes[0][0]) > 0]
326    bad_params += [(cv2.CAP_PROP_FRAME_HEIGHT, frame_size[1],
327                    cap_frame_sizes[0][1], video_names[i][0], video_names[i][1]) for
328                   i, frame_size in enumerate(cap_frame_sizes)
329                   if abs(frame_size[1] - cap_frame_sizes[0][1]) > 0]
330    # Check framerates if required.
331    if check_framerate:
332        bad_params += [(cv2.CAP_PROP_FPS, fps, cap_framerates[0], video_names[i][0],
333                        video_names[i][1]) for i, fps in enumerate(cap_framerates)
334                       if math.fabs(fps - cap_framerates[0]) > max_framerate_delta]
335
336    if bad_params:
337        raise VideoParameterMismatch(bad_params)
338
339
340##
341## VideoManager Class Implementation
342##
343
344class VideoManager(object):
345    """ Provides a cv2.VideoCapture-like interface to a set of one or more video files,
346    or a single device ID. Supports seeking and setting end time/duration. """
347
348    def __init__(self, video_files, framerate=None, logger=None):
349        # type: (List[str], Optional[float])
350        """ VideoManager Constructor Method (__init__)
351
352        Arguments:
353            video_files (list of str(s)/int): A list of one or more paths (str), or a list
354                of a single integer device ID, to open as an OpenCV VideoCapture object.
355            framerate (float, optional): Framerate to assume when storing FrameTimecodes.
356                If not set (i.e. is None), it will be deduced from the first open capture
357                in video_files, else raises a VideoFramerateUnavailable exception.
358
359        Raises:
360            ValueError: No video file(s) specified, or invalid/multiple device IDs specified.
361            TypeError: `framerate` must be type `float`.
362            IOError: Video file(s) not found.
363            VideoFramerateUnavailable: Video framerate could not be obtained and `framerate`
364                was not set manually.
365            VideoParameterMismatch: All videos in `video_files` do not have equal parameters.
366                Set `validate_parameters=False` to skip this check.
367            VideoOpenFailure: Video(s) could not be opened.
368        """
369        if not video_files:
370            raise ValueError("At least one string/integer must be passed in the video_files list.")
371        # These VideoCaptures are only open in this process.
372        self._cap_list, self._cap_framerate, self._cap_framesize = open_captures(
373            video_files=video_files, framerate=framerate)
374        self._end_of_video = False
375        self._start_time = self.get_base_timecode()
376        self._end_time = None
377        self._curr_time = self.get_base_timecode()
378        self._last_frame = None
379        self._curr_cap, self._curr_cap_idx = None, None
380        self._video_file_paths = video_files
381        self._logger = logger
382        if self._logger is not None:
383            self._logger.info(
384                'Loaded %d video%s, framerate: %.2f FPS, resolution: %d x %d',
385                len(self._cap_list), 's' if len(self._cap_list) > 1 else '',
386                self.get_framerate(), *self.get_framesize())
387        self._started = False
388        self._downscale_factor = 1
389        self._frame_length = get_num_frames(self._cap_list)
390
391
392    def set_downscale_factor(self, downscale_factor=None):
393        # type: (Optional[int]) -> None
394        """ Set Downscale Factor - sets the downscale/subsample factor of returned frames.
395
396        If N is the downscale_factor, the size of the frames returned becomes
397        frame_width/N x frame_height/N via subsampling.
398
399        If downscale_factor is None, the downscale factor is computed automatically
400        based on the current video's resolution.  A downscale_factor of 1 indicates
401        no downscaling.
402        """
403        if downscale_factor is None:
404            self._downscale_factor = compute_downscale_factor(self.get_framesize()[0])
405        else:
406            if not downscale_factor > 0:
407                raise InvalidDownscaleFactor()
408            self._downscale_factor = downscale_factor
409        if self._logger is not None:
410            effective_framesize = self.get_framesize_effective()
411            self._logger.info(
412                'Downscale factor set to %d, effective resolution: %d x %d',
413                self._downscale_factor, effective_framesize[0], effective_framesize[1])
414
415
416    def get_num_videos(self):
417        # type: () -> int
418        """ Get Number of Videos - returns the length of the internal capture list,
419        representing the number of videos the VideoManager was constructed with.
420
421        Returns:
422            int: Number of videos, equal to length of capture list.
423        """
424        return len(self._cap_list)
425
426
427    def get_video_paths(self):
428        # type: () -> List[str]
429        """ Get Video Paths - returns list of strings containing paths to the open video(s).
430
431        Returns:
432            List[str]: List of paths to the video files opened by the VideoManager.
433        """
434        return list(self._video_file_paths)
435
436
437    def get_framerate(self):
438        # type: () -> float
439        """ Get Framerate - returns the framerate the VideoManager is assuming for all
440        open VideoCaptures.  Obtained from either the capture itself, or the passed
441        framerate parameter when the VideoManager object was constructed.
442
443        Returns:
444            float: Framerate, in frames/sec.
445        """
446        return self._cap_framerate
447
448
449    def get_base_timecode(self):
450        # type: () -> FrameTimecode
451        """ Get Base Timecode - returns a FrameTimecode object at frame 0 / time 00:00:00.
452
453        The timecode returned by this method can be used to perform arithmetic (e.g.
454        addition), passing the resulting values back to the VideoManager (e.g. for the
455        :py:meth:`set_duration()` method), as the framerate of the returned FrameTimecode
456        object matches that of the VideoManager.
457
458        As such, this method is equivalent to creating a FrameTimecode at frame 0 with
459        the VideoManager framerate, for example, given a VideoManager called obj,
460        the following expression will evaluate as True:
461
462            obj.get_base_timecode() == FrameTimecode(0, obj.get_framerate())
463
464        Furthermore, the base timecode object returned by a particular VideoManager
465        should not be passed to another one, unless you first verify that their
466        framerates are the same.
467
468        Returns:
469            FrameTimecode object set to frame 0/time 00:00:00 with the video(s) framerate.
470        """
471        return FrameTimecode(timecode=0, fps=self._cap_framerate)
472
473
474    def get_current_timecode(self):
475        # type: () -> FrameTimecode
476        """ Get Current Timecode - returns a FrameTimecode object at current VideoManager position.
477
478        Returns:
479            FrameTimecode: Timecode at the current VideoManager position.
480        """
481        return self._curr_time
482
483
484    def get_framesize(self):
485        # type: () -> Tuple[int, int]
486        """ Get Frame Size - returns the frame size of the video(s) open in the
487        VideoManager's capture objects.
488
489        Returns:
490            Tuple[int, int]: Video frame size in the form (width, height) where width
491            and height represent the size of the video frame in pixels.
492        """
493        return self._cap_framesize
494
495
496    def get_framesize_effective(self):
497        # type: () -> Tuple[int, int]
498        """ Get Frame Size - returns the frame size of the video(s) open in the
499        VideoManager's capture objects, divided by the current downscale factor.
500
501        Returns:
502            Tuple[int, int]: Video frame size in the form (width, height) where width
503            and height represent the size of the video frame in pixels.
504        """
505        return [num_pixels / self._downscale_factor for num_pixels in self._cap_framesize]
506
507
508    def set_duration(self, duration=None, start_time=None, end_time=None):
509        # type: (Optional[FrameTimecode], Optional[FrameTimecode], Optional[FrameTimecode]) -> None
510        """ Set Duration - sets the duration/length of the video(s) to decode, as well as
511        the start/end times.  Must be called before :py:meth:`start()` is called, otherwise
512        a VideoDecodingInProgress exception will be thrown.  May be called after
513        :py:meth:`reset()` as well.
514
515        Arguments:
516            duration (Optional[FrameTimecode]): The (maximum) duration in time to
517                decode from the opened video(s). Mutually exclusive with end_time
518                (i.e. if duration is set, end_time must be None).
519            start_time (Optional[FrameTimecode]): The time/first frame at which to
520                start decoding frames from. If set, the input video(s) will be
521                seeked to when start() is called, at which point the frame at
522                start_time can be obtained by calling retrieve().
523            end_time (Optional[FrameTimecode]): The time at which to stop decoding
524                frames from the opened video(s). Mutually exclusive with duration
525                (i.e. if end_time is set, duration must be None).
526
527        Raises:
528            VideoDecodingInProgress: Must call before start().
529        """
530        if self._started:
531            raise VideoDecodingInProgress()
532
533        # Ensure any passed timecodes have the proper framerate.
534        if ((duration is not None and not duration.equal_framerate(self._cap_framerate)) or
535                (start_time is not None and not start_time.equal_framerate(self._cap_framerate)) or
536                (end_time is not None and not end_time.equal_framerate(self._cap_framerate))):
537            raise ValueError("FrameTimecode framerate does not match.")
538
539        if duration is not None and end_time is not None:
540            raise TypeError("Only one of duration and end_time may be specified, not both.")
541
542        if start_time is not None:
543            self._start_time = start_time
544
545        if end_time is not None:
546            if end_time < start_time:
547                raise ValueError("end_time is before start_time in time.")
548            self._end_time = end_time
549        elif duration is not None:
550            self._end_time = self._start_time + duration
551
552        if self._end_time is not None:
553            self._frame_length = min(self._frame_length, self._end_time.get_frames() + 1)
554        self._frame_length -= self._start_time.get_frames()
555
556        if self._logger is not None:
557            self._logger.info(
558                'Duration set, start: %s, duration: %s, end: %s.',
559                start_time.get_timecode() if start_time is not None else start_time,
560                duration.get_timecode() if duration is not None else duration,
561                end_time.get_timecode() if end_time is not None else end_time)
562
563
564    def get_duration(self):
565        # type: () -> FrameTimecode
566        """ Get Duration - gets the duration/length of the video(s) to decode,
567        as well as the start/end times.
568
569        If the end time was not set by :py:meth:`set_duration()`, the end timecode
570        is calculated as the start timecode + total duration.
571
572        Returns:
573            Tuple[FrameTimecode, FrameTimecode, FrameTimecode]: The current video(s)
574                total duration, start timecode, and end timecode.
575        """
576        frame_length = self.get_base_timecode() + self._frame_length
577        end_time = self._end_time
578        if end_time is None:
579            end_time = self.get_base_timecode() + frame_length
580        return (frame_length, self._start_time, end_time)
581
582
583    def start(self):
584        # type: () -> None
585        """ Start - starts video decoding and seeks to start time.  Raises
586        exception VideoDecodingInProgress if the method is called after the
587        decoder process has already been started.
588
589        Raises:
590            VideoDecodingInProgress: Must call :py:meth:`stop()` before this
591                method if :py:meth:`start()` has already been called after
592                initial construction.
593        """
594        if self._started:
595            raise VideoDecodingInProgress()
596
597        self._started = True
598        self._get_next_cap()
599        self.seek(self._start_time)
600
601
602    def seek(self, timecode):
603        # type: (FrameTimecode) -> bool
604        """ Seek - seeks forwards to the passed timecode.
605
606        Only supports seeking forwards (i.e. timecode must be greater than the
607        current position).  Can only be used after the :py:meth:`start()`
608        method has been called.
609
610        Arguments:
611            timecode (FrameTimecode): Time in video to seek forwards to.
612
613        Returns:
614            bool: True if seeking succeeded, False if no more frames / end of video.
615
616        Raises:
617            VideoDecoderNotStarted: Must call :py:meth:`start()` before this method.
618        """
619        if not self._started:
620            raise VideoDecoderNotStarted()
621
622        if isinstance(self._curr_cap, cv2.VideoCapture):
623            if self._curr_cap is not None and self._end_of_video is not True:
624                self._curr_cap.set(cv2.CAP_PROP_POS_FRAMES, timecode.get_frames() - 1)
625                self._curr_time = timecode - 1
626
627        while self._curr_time < timecode:
628            if not self.grab():  # raises VideoDecoderNotStarted if start() was not called
629                return False
630        return True
631
632
633    def release(self):
634        # type: () -> None
635        """ Release (cv2.VideoCapture method), releases all open capture(s). """
636        release_captures(self._cap_list)
637        self._cap_list = []
638        self._started = False
639
640
641    def reset(self):
642        # type: () -> None
643        """ Reset - Reopens captures passed to the constructor of the VideoManager.
644
645        Can only be called after the :py:meth:`release()` method has been called.
646
647        Raises:
648            VideoDecodingInProgress: Must call :py:meth:`release()` before this method.
649        """
650        if self._started:
651            raise VideoDecodingInProgress()
652
653        self._started = False
654        self._end_of_video = False
655        self._curr_time = self.get_base_timecode()
656        self._cap_list, self._cap_framerate, self._cap_framesize = open_captures(
657            video_files=self._video_file_paths, framerate=self._curr_time.get_framerate())
658        self._curr_cap, self._curr_cap_idx = None, None
659
660
661    def get(self, capture_prop, index=None):
662        # type: (int, Optional[int]) -> Union[float, int]
663        """ Get (cv2.VideoCapture method) - obtains capture properties from the current
664        VideoCapture object in use.  Index represents the same index as the original
665        video_files list passed to the constructor.  Getting/setting the position (POS)
666        properties has no effect; seeking is implemented using VideoDecoder methods.
667
668        Note that getting the property CAP_PROP_FRAME_COUNT will return the integer sum of
669        the frame count for all VideoCapture objects if index is not specified (or is None),
670        otherwise the frame count for the given VideoCapture index is returned instead.
671
672        Arguments:
673            capture_prop: OpenCV VideoCapture property to get (i.e. CAP_PROP_FPS).
674            index (int, optional): Index in file_list of capture to get property from (default
675                is zero). Index is not checked and will raise exception if out of bounds.
676
677        Returns:
678            float: Return value from calling get(property) on the VideoCapture object.
679        """
680        if capture_prop == cv2.CAP_PROP_FRAME_COUNT and index is None:
681            return self._frame_length
682        elif capture_prop == cv2.CAP_PROP_POS_FRAMES:
683            return self._curr_time
684        elif index is None:
685            index = 0
686        return self._cap_list[index].get(capture_prop)
687
688
689    def grab(self):
690        # type: () -> bool
691        """ Grab (cv2.VideoCapture method) - retrieves a frame but does not return it.
692
693        Returns:
694            bool: True if a frame was grabbed, False otherwise.
695
696        Raises:
697            VideoDecoderNotStarted: Must call :py:meth:`start()` before this method.
698        """
699        if not self._started:
700            raise VideoDecoderNotStarted()
701
702        grabbed = False
703        if self._curr_cap is not None and not self._end_of_video:
704            while not grabbed:
705                grabbed = self._curr_cap.grab()
706                if not grabbed and not self._get_next_cap():
707                    break
708                else:
709                    self._curr_time += 1
710        if self._end_time is not None and self._curr_time > self._end_time:
711            grabbed = False
712            self._last_frame = None
713        return grabbed
714
715
716    def retrieve(self):
717        # type: () -> Tuple[bool, Union[None, numpy.ndarray]]
718        """ Retrieve (cv2.VideoCapture method) - retrieves and returns a frame.
719
720        Frame returned corresponds to last call to :py:meth:`grab()`.
721
722        Returns:
723            Tuple[bool, Union[None, numpy.ndarray]]: Returns tuple of
724            (True, frame_image) if a frame was grabbed during the last call
725            to grab(), and where frame_image is a numpy ndarray of the
726            decoded frame, otherwise returns (False, None).
727
728        Raises:
729            VideoDecoderNotStarted: Must call :py:meth:`start()` before this method.
730        """
731        if not self._started:
732            raise VideoDecoderNotStarted()
733
734        retrieved = False
735        if self._curr_cap is not None and not self._end_of_video:
736            while not retrieved:
737                retrieved, self._last_frame = self._curr_cap.retrieve()
738                if not retrieved and not self._get_next_cap():
739                    break
740                if self._downscale_factor > 1:
741                    self._last_frame = self._last_frame[
742                        ::self._downscale_factor, ::self._downscale_factor, :]
743        if self._end_time is not None and self._curr_time > self._end_time:
744            retrieved = False
745            self._last_frame = None
746        return (retrieved, self._last_frame)
747
748
749    def read(self):
750        # type: () -> Tuple[bool, Union[None, numpy.ndarray]]
751        """ Read (cv2.VideoCapture method) - retrieves and returns a frame.
752
753        Returns:
754            Tuple[bool, Union[None, numpy.ndarray]]: Returns tuple of
755            (True, frame_image) if a frame was grabbed, where frame_image
756            is a numpy ndarray of the decoded frame, otherwise (False, None).
757
758        Raises:
759            VideoDecoderNotStarted: Must call :py:meth:`start()` before this method.
760        """
761        if not self._started:
762            raise VideoDecoderNotStarted()
763
764        read_frame = False
765        if self._curr_cap is not None and not self._end_of_video:
766            read_frame, self._last_frame = self._curr_cap.read()
767
768            # Switch to the next capture when the current one is over
769            if not read_frame and self._get_next_cap():
770                read_frame, self._last_frame = self._curr_cap.read()
771
772            # Downscale frame if there was any
773            if read_frame and self._downscale_factor > 1:
774                self._last_frame = self._last_frame[
775                    ::self._downscale_factor, ::self._downscale_factor, :]
776
777        if self._end_time is not None and self._curr_time > self._end_time:
778            read_frame = False
779            self._last_frame = None
780        if read_frame:
781            self._curr_time += 1
782        return (read_frame, self._last_frame)
783
784
785    def _get_next_cap(self):
786        # type: () -> bool
787        self._curr_cap = None
788        if self._curr_cap_idx is None:
789            self._curr_cap_idx = 0
790            self._curr_cap = self._cap_list[0]
791            return True
792        else:
793            if not (self._curr_cap_idx + 1) < len(self._cap_list):
794                self._end_of_video = True
795                return False
796            self._curr_cap_idx += 1
797            self._curr_cap = self._cap_list[self._curr_cap_idx]
798            return True
799