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""" PySceneDetect scenedetect.video_manager Tests
28
29This file includes unit tests for the scenedetect.video_manager module, acting as
30a video container/decoder, allowing seeking and concatenation of multiple sources.
31
32These unit tests test the VideoManager object with respect to object construction,
33testing argument format/limits, opening videos and grabbing frames, and appending
34multiple videos together.  These tests rely on testvideo.mp4, available in the
35PySceneDetect git repository "resources" branch.
36
37These tests rely on the testvideo.mp4 test video file, available by checking out the
38PySceneDetect git repository "resources" branch, or the following URL to download it
39directly:  https://github.com/Breakthrough/PySceneDetect/tree/resources/tests
40"""
41
42# Standard project pylint disables for unit tests using pytest.
43# pylint: disable=no-self-use, protected-access, multiple-statements, invalid-name
44# pylint: disable=redefined-outer-name
45
46
47# Third-Party Library Imports
48import pytest
49import cv2
50
51from scenedetect.scene_manager import SceneManager
52# PySceneDetect Library Imports
53from scenedetect.video_manager import VideoManager
54from scenedetect.video_manager import VideoOpenFailure
55
56# TODO: The following exceptions still require test cases.
57# Since these are API contract violations, should they be refactored
58# into assertions instead?
59from scenedetect.video_manager import VideoDecodingInProgress
60from scenedetect.video_manager import VideoDecoderNotStarted
61
62# TODO: Need to implement a mock VideoCapture to test the exceptions below.
63# TODO: The following exceptions still require test cases:
64from scenedetect.video_manager import VideoFramerateUnavailable
65from scenedetect.video_manager import VideoParameterMismatch
66
67
68def test_video_params(test_video_file):
69    """ Test VideoManager get_framerate/get_framesize methods on test_video_file. """
70    try:
71        cap = cv2.VideoCapture(test_video_file)
72        video_manager = VideoManager([test_video_file] * 2)
73        assert cap.isOpened()
74        assert video_manager.get_framerate() == pytest.approx(cap.get(cv2.CAP_PROP_FPS))
75        assert video_manager.get_framesize() == (
76            pytest.approx(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
77            pytest.approx(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
78    finally:
79        cap.release()
80        video_manager.release()
81
82
83def test_start_release(test_video_file):
84    """ Test VideoManager start/release methods on 3 appended videos. """
85    video_manager = VideoManager([test_video_file] * 2)
86    # *must* call release() after start() or video manager process will be rogue.
87    #
88    # The start method is the only big usage differences between the
89    # VideoManager and cv2.VideoCapture objects from the point of view
90    # of a SceneManager (the other VideoManager methods function
91    # independently of it's job as a frame source).
92    try:
93        video_manager.start()
94        # even if exception thrown here, video manager process will stop.
95    finally:
96        video_manager.release()
97
98
99def test_get_property(test_video_file):
100    """ Test VideoManager get method on test_video_file. """
101    video_manager = VideoManager([test_video_file] * 3)
102    video_framerate = video_manager.get_framerate()
103    assert video_manager.get(cv2.CAP_PROP_FPS) == pytest.approx(video_framerate)
104    assert video_manager.get(cv2.CAP_PROP_FPS, 1) == pytest.approx(video_framerate)
105    assert video_manager.get(cv2.CAP_PROP_FPS, 2) == pytest.approx(video_framerate)
106    video_manager.release()
107
108
109def test_wrong_video_files_type():
110    """ Test VideoManager constructor (__init__ method) with invalid video_files
111    argument types to trigger a ValueError exception. """
112    with pytest.raises(ValueError): VideoManager([0, 1, 2])
113    with pytest.raises(ValueError): VideoManager([0, 'somefile'])
114    with pytest.raises(ValueError): VideoManager(['somefile', 1, 2, 'somefile'])
115    with pytest.raises(ValueError): VideoManager([-1])
116
117
118def test_wrong_framerate_type(test_video_file):
119    """ Test VideoManager constructor (__init__ method) with an invalid framerate
120    argument types to trigger a TypeError exception. """
121    with pytest.raises(TypeError): VideoManager([test_video_file], framerate=int(0))
122    with pytest.raises(TypeError): VideoManager([test_video_file], framerate=int(10))
123    with pytest.raises(TypeError): VideoManager([test_video_file], framerate='10')
124    VideoManager([test_video_file], framerate=float(10)).release()
125
126
127def test_video_open_failure():
128    """ Test VideoManager constructor (__init__ method) with invalid filename(s)
129    and device IDs to trigger an IOError/VideoOpenFailure exception. """
130    # Attempt to open non-existing video files should raise an IOError.
131    with pytest.raises(IOError): VideoManager(['fauxfile.mp4'])
132    with pytest.raises(IOError): VideoManager(['fauxfile.mp4', 'otherfakefile.mp4'])
133    # Attempt to open 99th video device should raise a VideoOpenFailure since
134    # the OpenCV VideoCapture open() method will likely fail (unless the test
135    # case computer has 100 webcams or more...)
136    with pytest.raises(VideoOpenFailure): VideoManager([99])
137    # Test device IDs > 100.
138    with pytest.raises(VideoOpenFailure): VideoManager([120])
139    with pytest.raises(VideoOpenFailure): VideoManager([255])
140
141
142def test_grab_retrieve(test_video_file):
143    """ Test VideoManager grab/retrieve methods. """
144    video_manager = VideoManager([test_video_file] * 2)
145    base_timecode = video_manager.get_base_timecode()
146    try:
147        video_manager.start()
148        assert video_manager.get_current_timecode() == base_timecode
149        for i in range(1, 10):
150            # VideoManager.grab() -> bool
151            ret_val = video_manager.grab()
152            assert ret_val
153            assert video_manager.get_current_timecode() == base_timecode + i
154            # VideoManager.retrieve() -> Tuple[bool, numpy.ndarray]
155            ret_val, frame_image = video_manager.retrieve()
156            assert ret_val
157            assert frame_image.shape[0] > 0
158            assert video_manager.get_current_timecode() == base_timecode + i
159    finally:
160        video_manager.release()
161
162
163def test_read(test_video_file):
164    """ Test VideoManager read method. """
165    video_manager = VideoManager([test_video_file] * 2)
166    base_timecode = video_manager.get_base_timecode()
167    try:
168        video_manager.start()
169        assert video_manager.get_current_timecode() == base_timecode
170        for i in range(1, 10):
171            # VideoManager.read() -> Tuple[bool, numpy.ndarray]
172            ret_val, frame_image = video_manager.read()
173            assert ret_val
174            assert frame_image.shape[0] > 0
175            assert video_manager.get_current_timecode() == base_timecode + i
176    finally:
177        video_manager.release()
178
179
180def test_seek(test_video_file):
181    """ Test VideoManager seek method. """
182    video_manager = VideoManager([test_video_file] * 2)
183    base_timecode = video_manager.get_base_timecode()
184    try:
185        video_manager.start()
186        assert video_manager.get_current_timecode() == base_timecode
187        ret_val, frame_image = video_manager.read()
188        assert ret_val
189        assert frame_image.shape[0] > 0
190        assert video_manager.get_current_timecode() == base_timecode + 1
191
192        assert video_manager.seek(base_timecode + 10)
193        assert video_manager.get_current_timecode() == base_timecode + 10
194        ret_val, frame_image = video_manager.read()
195        assert ret_val
196        assert frame_image.shape[0] > 0
197        assert video_manager.get_current_timecode() == base_timecode + 11
198
199    finally:
200        video_manager.release()
201
202
203def test_reset(test_video_file):
204    """ Test VideoManager reset method. """
205    video_manager = VideoManager([test_video_file] * 2)
206    base_timecode = video_manager.get_base_timecode()
207    try:
208        video_manager.start()
209        assert video_manager.get_current_timecode() == base_timecode
210        ret_val, frame_image = video_manager.read()
211        assert ret_val
212        assert frame_image.shape[0] > 0
213        assert video_manager.get_current_timecode() == base_timecode + 1
214
215        video_manager.release()
216        video_manager.reset()
217
218        video_manager.start()
219        assert video_manager.get_current_timecode() == base_timecode
220        ret_val, frame_image = video_manager.read()
221        assert ret_val
222        assert frame_image.shape[0] > 0
223        assert video_manager.get_current_timecode() == base_timecode + 1
224
225    finally:
226        video_manager.release()
227
228
229def test_multiple_videos(test_video_file):
230    """ Test VideoManager handling decoding frames across video boundaries. """
231
232    NUM_FRAMES = 10
233    NUM_VIDEOS = 3
234    # Open VideoManager and get base timecode.
235    video_manager = VideoManager([test_video_file] * NUM_VIDEOS)
236    base_timecode = video_manager.get_base_timecode()
237
238    # List of NUM_VIDEOS VideoManagers pointing to test_video_file.
239    vm_list = [
240        VideoManager([test_video_file]),
241        VideoManager([test_video_file]),
242        VideoManager([test_video_file])]
243
244    # Set duration of all VideoManagers in vm_list to NUM_FRAMES frames.
245    for vm in vm_list: vm.set_duration(duration=base_timecode+NUM_FRAMES)
246    # (FOR TESTING PURPOSES ONLY) Manually override _cap_list with the
247    # duration-limited VideoManager objects in vm_list
248    video_manager._cap_list = vm_list
249
250    try:
251        for vm in vm_list: vm.start()
252        video_manager.start()
253        assert video_manager.get_current_timecode() == base_timecode
254
255        curr_time = video_manager.get_base_timecode()
256        while True:
257            ret_val, frame_image = video_manager.read()
258            if not ret_val:
259                break
260            assert frame_image.shape[0] > 0
261            curr_time += 1
262        assert curr_time == base_timecode + ((NUM_FRAMES+1) * NUM_VIDEOS)
263
264    finally:
265        # Will release the VideoManagers in vm_list as well.
266        video_manager.release()
267
268def test_many_videos_downscale_detect_scenes(test_video_file):
269    """ Test scene detection on multiple videos in VideoManager. """
270
271    NUM_VIDEOS = 3
272    # Open VideoManager with NUM_VIDEOS test videos
273    video_manager = VideoManager([test_video_file] * NUM_VIDEOS)
274    video_manager.set_downscale_factor()
275
276    try:
277        video_manager.start()
278        scene_manager = SceneManager()
279        scene_manager.detect_scenes(frame_source=video_manager)
280    finally:
281        # Will release the VideoManagers in vm_list as well.
282        video_manager.release()
283