1import random
2from unittest import mock
3
4from tests.base.future_test import FutureTestCase
5
6from pyglet.media.player import Player, PlayerGroup
7from pyglet.media.codecs.base import AudioFormat, VideoFormat, Source
8
9
10class PlayerTestCase(FutureTestCase):
11    # Default values to use
12    audio_format_1 = AudioFormat(1, 8, 11025)
13    audio_format_2 = AudioFormat(2, 8, 11025)
14    audio_format_3 = AudioFormat(2, 16, 44100)
15    video_format_1 = VideoFormat(800, 600)
16    video_format_2 = VideoFormat(1920, 1280)
17    video_format_2.frame_rate = 25
18
19    def setUp(self):
20        self.player = Player()
21
22        self._get_audio_driver_patcher = mock.patch('pyglet.media.player.get_audio_driver')
23        self.mock_get_audio_driver = self._get_audio_driver_patcher.start()
24        self.mock_audio_driver = self.mock_get_audio_driver.return_value
25        self.mock_audio_driver_player = self.mock_audio_driver.create_audio_player.return_value
26
27        self._clock_patcher = mock.patch('pyglet.clock')
28        self.mock_clock = self._clock_patcher.start()
29
30        self._texture_patcher = mock.patch('pyglet.image.Texture.create')
31        self.mock_texture_create = self._texture_patcher.start()
32        self.mock_texture = self.mock_texture_create.return_value
33        # Need to do this as side_effect instead of return_value, or reset_mock will recurse
34        self.mock_texture.get_transform.side_effect = lambda flip_y: self.mock_texture
35
36    def tearDown(self):
37        self._get_audio_driver_patcher.stop()
38        self._clock_patcher.stop()
39        self._texture_patcher.stop()
40
41    def reset_mocks(self):
42        # These mocks will recursively reset their children
43        self.mock_get_audio_driver.reset_mock()
44        self.mock_clock.reset_mock()
45        self.mock_texture_create.reset_mock()
46
47    def create_mock_source(self, audio_format, video_format):
48        mock_source = mock.MagicMock(spec=Source)
49        type(mock_source).audio_format = mock.PropertyMock(return_value=audio_format)
50        type(mock_source).video_format = mock.PropertyMock(return_value=video_format)
51        type(mock_source.get_queue_source.return_value).audio_format = mock.PropertyMock(return_value=audio_format)
52        type(mock_source.get_queue_source.return_value).video_format = mock.PropertyMock(return_value=video_format)
53        if video_format:
54            mock_source.video_format.frame_rate = 30
55        return mock_source
56
57    def set_video_data_for_mock_source(self, mock_source, timestamp_data_pairs):
58        """Make the given mock source return video data. Video data is given in pairs of
59        timestamp and data to return."""
60        def _get_frame():
61            if timestamp_data_pairs:
62                current_frame = timestamp_data_pairs.pop(0)
63                return current_frame[1]
64
65        def _get_timestamp():
66            if timestamp_data_pairs:
67                return timestamp_data_pairs[0][0]
68        queue_source = mock_source.get_queue_source.return_value
69        queue_source.get_next_video_timestamp.side_effect = _get_timestamp
70        queue_source.get_next_video_frame.side_effect = _get_frame
71
72    def assert_not_playing_yet(self, current_source=None):
73        """Assert the the player did not start playing yet."""
74        self.assertFalse(self.mock_get_audio_driver.called, msg='No audio driver required yet')
75        self.assertAlmostEqual(self.player.time, 0.)
76        self.assert_not_playing(current_source)
77
78    def assert_not_playing(self, current_source=None):
79        self._assert_playing(False, current_source)
80
81    def assert_now_playing(self, current_source):
82        self._assert_playing(True, current_source)
83
84    def _assert_playing(self, playing, current_source=None):
85        self.assertEqual(self.player.playing, playing)
86        queued_source = (current_source.get_queue_source.return_value
87                         if current_source is not None
88                         else None)
89        self.assertIs(self.player.source, queued_source)
90
91    def assert_driver_player_created_for(self, source):
92        """Assert that a driver specific audio player is created to play back given source"""
93        self._assert_player_created_for(self.mock_get_audio_driver, self.mock_audio_driver, source)
94
95    def _assert_player_created_for(self, mock_get_audio_driver, mock_audio_driver, source):
96        mock_get_audio_driver.assert_called_once_with()
97        self.assertEqual(mock_audio_driver.create_audio_player.call_count, 1)
98        call_args = mock_audio_driver.create_audio_player.call_args
99        args, kwargs = call_args
100        arg_source, arg_player = args
101        self.assertIs(arg_source, source.get_queue_source.return_value)
102        self.assertIs(arg_player, self.player)
103
104    def assert_no_new_driver_player_created(self):
105        """Assert that no new driver specific audio player is created."""
106        self.assertFalse(self.mock_get_audio_driver.called, msg='No new audio driver should be created')
107
108    # def assert_in_current_play_list(self, *sources):
109    #     self.assertIsNotNone(self.current_play_list, msg='No previous call to create driver player')
110
111    #     queue_sources = deque(source.get_queue_source.return_value for source in sources)
112    #     self.assertSequenceEqual(self.current_play_list._sources, queue_sources)
113
114    def assert_driver_player_destroyed(self):
115        self.mock_audio_driver_player.delete.assert_called_once_with()
116
117    def assert_driver_player_not_destroyed(self):
118        self.assertFalse(self.mock_audio_driver_player.delete.called)
119
120    def assert_silent_driver_player_destroyed(self):
121        self.mock_silent_audio_driver_player.delete.assert_called_once_with()
122
123    def assert_driver_player_started(self):
124        self.mock_audio_driver_player.play.assert_called_once_with()
125
126    def assert_driver_player_stopped(self):
127        self.mock_audio_driver_player.stop.assert_called_once_with()
128
129    def assert_driver_player_cleared(self):
130        self.mock_audio_driver_player.clear.assert_called_once_with()
131
132    def assert_source_seek(self, source, time):
133        source.get_queue_source.return_value.seek.assert_called_once_with(time)
134
135    def assert_new_texture_created(self, video_format):
136        self.mock_texture_create.assert_called_once_with(video_format.width, video_format.height, rectangle=True)
137
138    def assert_no_new_texture_created(self):
139        self.assertFalse(self.mock_texture_create.called)
140
141    def assert_texture_updated(self, frame_data):
142        self.mock_texture.blit_into.assert_called_once_with(frame_data, 0, 0, 0)
143
144    def assert_texture_not_updated(self):
145        self.assertFalse(self.mock_texture.blit_into.called)
146
147    def assert_update_texture_scheduled(self):
148        self.mock_clock.schedule_once.assert_called_once_with(self.player.update_texture, 0)
149
150    def assert_update_texture_unscheduled(self):
151        self.mock_clock.unschedule.assert_called_with(self.player.update_texture)
152
153    def pretend_player_at_time(self, t):
154        self.player._timer.set_time(t)
155
156    def pretend_silent_driver_player_at_time(self, t):
157        self.mock_silent_audio_driver_player.get_time.return_value = t
158
159    def test_queue_single_audio_source_and_play(self):
160        """Queue a single audio source and start playing it."""
161
162        mock_source = self.create_mock_source(self.audio_format_1, None)
163        self.player.queue(mock_source)
164        self.assert_not_playing_yet(mock_source)
165
166        self.player.play()
167        self.assert_driver_player_created_for(mock_source)
168        self.assert_driver_player_started()
169        self.assert_now_playing(mock_source)
170
171    def test_queue_multiple_audio_sources_same_format_and_play(self):
172        """Queue multiple audio sources using the same audio format and start playing."""
173        mock_source1 = self.create_mock_source(self.audio_format_1, None)
174        mock_source2 = self.create_mock_source(self.audio_format_1, None)
175        mock_source3 = self.create_mock_source(self.audio_format_1, None)
176
177        self.player.queue(mock_source1)
178        self.assert_not_playing_yet(mock_source1)
179
180        self.player.queue(mock_source2)
181        self.assert_not_playing_yet(mock_source1)
182
183        self.player.queue(mock_source3)
184        self.assert_not_playing_yet(mock_source1)
185
186        self.player.play()
187        self.assert_driver_player_created_for(mock_source1)
188        self.assert_driver_player_started()
189        self.assert_now_playing(mock_source1)
190
191    def test_queue_multiple_audio_sources_different_format_and_play_and_skip(self):
192        """Queue multiple audio sources having different formats and start playing. Different
193        formats should be played by seperate driver players."""
194        mock_source1 = self.create_mock_source(self.audio_format_1, None)
195        mock_source2 = self.create_mock_source(self.audio_format_2, None)
196        mock_source3 = self.create_mock_source(self.audio_format_3, None)
197
198        self.player.queue(mock_source1)
199        self.assert_not_playing_yet(mock_source1)
200
201        self.player.queue(mock_source2)
202        self.assert_not_playing_yet(mock_source1)
203
204        self.player.queue(mock_source3)
205        self.assert_not_playing_yet(mock_source1)
206
207        self.player.play()
208        self.assert_driver_player_created_for(mock_source1)
209        self.assert_driver_player_started()
210        self.assert_now_playing(mock_source1)
211
212        self.reset_mocks()
213        self.player.next_source()
214        self.assert_driver_player_destroyed()
215        self.assert_driver_player_created_for(mock_source2)
216        self.assert_driver_player_started()
217        self.assert_now_playing(mock_source2)
218
219        self.reset_mocks()
220        self.player.next_source()
221        self.assert_driver_player_destroyed()
222        self.assert_driver_player_created_for(mock_source3)
223        self.assert_driver_player_started()
224        self.assert_now_playing(mock_source3)
225
226    def test_queue_multiple_audio_sources_same_format_and_play_and_skip(self):
227        """When multiple audio sources with the same format are queued, they are played
228        using the same driver player. Skipping to the next source is just advancing the
229        source group.
230        """
231        mock_source1 = self.create_mock_source(self.audio_format_1, None)
232        mock_source2 = self.create_mock_source(self.audio_format_1, None)
233        mock_source3 = self.create_mock_source(self.audio_format_1, None)
234
235        self.player.queue(mock_source1)
236        self.player.queue(mock_source2)
237        self.player.queue(mock_source3)
238
239        self.player.play()
240        self.assert_driver_player_created_for(mock_source1)
241        self.assert_driver_player_started()
242        self.assert_now_playing(mock_source1)
243
244        self.reset_mocks()
245        self.player.next_source()
246        self.assert_driver_player_not_destroyed()
247        self.assert_no_new_driver_player_created()
248        self.assert_now_playing(mock_source2)
249
250        self.reset_mocks()
251        self.player.next_source()
252        self.assert_driver_player_not_destroyed()
253        self.assert_no_new_driver_player_created()
254        self.assert_now_playing(mock_source3)
255
256    def test_on_eos(self):
257        """The player receives on_eos for every source, but does not need to do anything.
258        """
259        mock_source1 = self.create_mock_source(self.audio_format_1, None)
260        mock_source2 = self.create_mock_source(self.audio_format_1, None)
261        mock_source3 = self.create_mock_source(self.audio_format_1, None)
262
263        self.player.queue(mock_source1)
264        self.player.queue(mock_source2)
265        self.player.queue(mock_source3)
266
267        self.player.play()
268        self.assert_driver_player_created_for(mock_source1)
269        self.assert_driver_player_started()
270
271        self.reset_mocks()
272        self.player.dispatch_event('on_eos')
273        self.assert_driver_player_not_destroyed()
274
275    def test_player_stops_after_last_eos(self):
276        """If the last playlist source is eos, the player stops."""
277        mock_source = self.create_mock_source(self.audio_format_1, None)
278        self.player.queue(mock_source)
279        self.assert_not_playing_yet(mock_source)
280
281        self.player.play()
282        self.assert_driver_player_created_for(mock_source)
283        self.assert_driver_player_started()
284        self.assert_now_playing(mock_source)
285
286        self.reset_mocks()
287        self.player.dispatch_event('on_eos')
288        self.assert_driver_player_destroyed()
289        self.assert_not_playing(None)
290
291    def test_eos_events(self):
292        """Test receiving various eos events: on source eos,
293        on playlist exhausted and on player eos and on player next source.
294        """
295        on_eos_mock = mock.MagicMock(return_value=None)
296        self.player.event('on_eos')(on_eos_mock)
297        on_player_eos_mock = mock.MagicMock(return_value=None)
298        self.player.event('on_player_eos')(on_player_eos_mock)
299        on_player_next_source_mock = mock.MagicMock(return_value=None)
300        self.player.event('on_player_next_source')(on_player_next_source_mock)
301
302        def reset_eos_mocks():
303            on_eos_mock.reset_mock()
304            on_player_eos_mock.reset_mock()
305            on_player_next_source_mock.reset_mock()
306
307        def assert_eos_events_received(on_eos=False, on_player_eos=False,
308                                       on_player_next_source=False):
309            self.assertEqual(on_eos_mock.called, on_eos)
310            self.assertEqual(on_player_eos_mock.called, on_player_eos)
311            self.assertEqual(on_player_next_source_mock.called, on_player_next_source)
312
313        mock_source1 = self.create_mock_source(self.audio_format_1, None)
314        mock_source2 = self.create_mock_source(self.audio_format_1, None)
315        mock_source3 = self.create_mock_source(self.audio_format_2, None)
316
317        self.player.queue(mock_source1)
318        self.player.queue(mock_source2)
319        self.player.queue(mock_source3)
320        self.assert_not_playing_yet(mock_source1)
321
322        self.player.play()
323        self.assert_driver_player_created_for(mock_source1)
324
325        self.reset_mocks()
326        reset_eos_mocks()
327        # Pretend the current source in the group was eos and next source started
328        self.player.dispatch_event('on_eos')
329        self.assert_driver_player_not_destroyed()
330        assert_eos_events_received(on_eos=True, on_player_next_source=True)
331        self.assertEqual(len(self.player._playlists), 2)
332
333        # Pretend playlist is exhausted. Should be no more sources to play.
334        self.player.dispatch_event('on_eos')
335        self.reset_mocks()
336        reset_eos_mocks()
337        self.player.dispatch_event('on_eos')
338        self.assert_not_playing(None)
339        assert_eos_events_received(on_eos=True, on_player_eos=True)
340
341    def test_pause_resume(self):
342        """A stream can be paused. After that play will resume where paused."""
343        mock_source = self.create_mock_source(self.audio_format_1, None)
344        self.player.queue(mock_source)
345        self.player.play()
346        self.assert_driver_player_created_for(mock_source)
347        self.assert_driver_player_started()
348        self.assert_now_playing(mock_source)
349
350        self.reset_mocks()
351        self.pretend_player_at_time(0.5)
352        self.player.pause()
353        self.assert_driver_player_stopped()
354        self.assert_driver_player_not_destroyed()
355
356        self.reset_mocks()
357        self.player.play()
358        self.assertAlmostEqual(
359            self.player.time, 0.5, places=2,
360            msg='While playing, player should return time from driver player'
361        )
362        self.assert_driver_player_started()
363        self.assert_no_new_driver_player_created()
364        self.assert_now_playing(mock_source)
365
366    def test_delete(self):
367        """Test clean up of the player when delete() is called."""
368        mock_source1 = self.create_mock_source(self.audio_format_1, None)
369        mock_source2 = self.create_mock_source(self.audio_format_2, None)
370        mock_source3 = self.create_mock_source(self.audio_format_3, None)
371
372        self.player.queue(mock_source1)
373        self.player.queue(mock_source2)
374        self.player.queue(mock_source3)
375        self.assert_not_playing_yet(mock_source1)
376
377        self.player.play()
378        self.assert_driver_player_created_for(mock_source1)
379        self.assert_driver_player_started()
380
381        self.reset_mocks()
382        self.pretend_player_at_time(1.)
383        self.player.delete()
384        self.assert_driver_player_destroyed()
385
386    def test_empty_player(self):
387        """A player without queued sources should not start a driver player and should not raise
388        exceptions"""
389        self.assert_not_playing_yet(None)
390
391        self.reset_mocks()
392        self.player.play()
393        self.assert_no_new_driver_player_created()
394
395        self.reset_mocks()
396        self.player.pause()
397        self.assert_no_new_driver_player_created()
398        self.assert_driver_player_not_destroyed()
399
400        self.reset_mocks()
401        self.player.next_source()
402        self.assert_no_new_driver_player_created()
403        self.assert_driver_player_not_destroyed()
404
405        self.reset_mocks()
406        self.player.seek(0.8)
407        self.assert_no_new_driver_player_created()
408        self.assert_driver_player_not_destroyed()
409
410        self.player.delete()
411
412    def test_set_player_properties_before_playing(self):
413        """When setting player properties before a driver specific player is
414        created, these settings should be propagated after creating the
415        player.
416        """
417        mock_source1 = self.create_mock_source(self.audio_format_1, None)
418        mock_source2 = self.create_mock_source(self.audio_format_2, None)
419        self.player.queue(mock_source1)
420        self.player.queue(mock_source2)
421        self.assert_not_playing_yet(mock_source1)
422
423        self.reset_mocks()
424        self.player.volume = 10.
425        self.player.min_distance = 2.
426        self.player.max_distance = 3.
427        self.player.position = (4, 4, 4)
428        self.player.pitch = 5.0
429        self.player.cone_orientation = (6, 6, 6)
430        self.player.cone_inner_angle = 7.
431        self.player.cone_outer_angle = 8.
432        self.player.cone_outer_gain = 9.
433
434        def assert_properties_set():
435            self.mock_audio_driver_player.set_volume.assert_called_once_with(10.)
436            self.mock_audio_driver_player.set_min_distance.assert_called_once_with(2.)
437            self.mock_audio_driver_player.set_max_distance.assert_called_once_with(3.)
438            self.mock_audio_driver_player.set_position.assert_called_once_with((4, 4, 4))
439            self.mock_audio_driver_player.set_pitch.assert_called_once_with(5.)
440            self.mock_audio_driver_player.set_cone_orientation.assert_called_once_with((6, 6, 6))
441            self.mock_audio_driver_player.set_cone_inner_angle.assert_called_once_with(7.)
442            self.mock_audio_driver_player.set_cone_outer_angle.assert_called_once_with(8.)
443            self.mock_audio_driver_player.set_cone_outer_gain.assert_called_once_with(9.)
444
445        self.reset_mocks()
446        self.player.play()
447        self.assert_driver_player_created_for(mock_source1)
448        self.assert_now_playing(mock_source1)
449        assert_properties_set()
450
451        self.reset_mocks()
452        self.player.next_source()
453        self.assert_driver_player_destroyed()
454        self.assert_driver_player_created_for(mock_source2)
455        assert_properties_set()
456
457    def test_set_player_properties_while_playing(self):
458        """When setting player properties while playing, the properties should
459        be propagated to the driver specific player right away."""
460        mock_source1 = self.create_mock_source(self.audio_format_1, None)
461        mock_source2 = self.create_mock_source(self.audio_format_2, None)
462        self.player.queue(mock_source1)
463        self.player.queue(mock_source2)
464        self.assert_not_playing_yet(mock_source1)
465
466        self.reset_mocks()
467        self.player.play()
468        self.assert_driver_player_created_for(mock_source1)
469        self.assert_now_playing(mock_source1)
470
471        self.reset_mocks()
472        self.player.volume = 10.
473        self.mock_audio_driver_player.set_volume.assert_called_once_with(10.)
474
475        self.reset_mocks()
476        self.player.min_distance = 2.
477        self.mock_audio_driver_player.set_min_distance.assert_called_once_with(2.)
478
479        self.reset_mocks()
480        self.player.max_distance = 3.
481        self.mock_audio_driver_player.set_max_distance.assert_called_once_with(3.)
482
483        self.reset_mocks()
484        self.player.position = (4, 4, 4)
485        self.mock_audio_driver_player.set_position.assert_called_once_with((4, 4, 4))
486
487        self.reset_mocks()
488        self.player.pitch = 5.0
489        self.mock_audio_driver_player.set_pitch.assert_called_once_with(5.)
490
491        self.reset_mocks()
492        self.player.cone_orientation = (6, 6, 6)
493        self.mock_audio_driver_player.set_cone_orientation.assert_called_once_with((6, 6, 6))
494
495        self.reset_mocks()
496        self.player.cone_inner_angle = 7.
497        self.mock_audio_driver_player.set_cone_inner_angle.assert_called_once_with(7.)
498
499        self.reset_mocks()
500        self.player.cone_outer_angle = 8.
501        self.mock_audio_driver_player.set_cone_outer_angle.assert_called_once_with(8.)
502
503        self.reset_mocks()
504        self.player.cone_outer_gain = 9.
505        self.mock_audio_driver_player.set_cone_outer_gain.assert_called_once_with(9.)
506
507        self.reset_mocks()
508        self.player.next_source()
509        self.assert_driver_player_destroyed()
510        self.assert_driver_player_created_for(mock_source2)
511        self.mock_audio_driver_player.set_volume.assert_called_once_with(10.)
512        self.mock_audio_driver_player.set_min_distance.assert_called_once_with(2.)
513        self.mock_audio_driver_player.set_max_distance.assert_called_once_with(3.)
514        self.mock_audio_driver_player.set_position.assert_called_once_with((4, 4, 4))
515        self.mock_audio_driver_player.set_pitch.assert_called_once_with(5.)
516        self.mock_audio_driver_player.set_cone_orientation.assert_called_once_with((6, 6, 6))
517        self.mock_audio_driver_player.set_cone_inner_angle.assert_called_once_with(7.)
518        self.mock_audio_driver_player.set_cone_outer_angle.assert_called_once_with(8.)
519        self.mock_audio_driver_player.set_cone_outer_gain.assert_called_once_with(9.)
520
521    def test_seek(self):
522        """Test seeking to a specific time in the current source."""
523        mock_source = self.create_mock_source(self.audio_format_1, None)
524        self.player.queue(mock_source)
525        self.assert_not_playing_yet(mock_source)
526
527        self.reset_mocks()
528        mock_source.reset_mock()
529        self.player.seek(0.7)
530        self.assert_source_seek(mock_source, 0.7)
531
532        self.reset_mocks()
533        mock_source.reset_mock()
534        self.player.play()
535        self.assert_driver_player_created_for(mock_source)
536        self.assert_now_playing(mock_source)
537
538        self.reset_mocks()
539        mock_source.reset_mock()
540        self.player.seek(0.2)
541        self.assert_source_seek(mock_source, 0.2)
542        # Clear buffers for immediate result
543        self.assert_driver_player_cleared()
544
545    def test_video_queue_and_play(self):
546        """Sources can also include video. Instead of using a player to
547        continuously play the video, a texture is updated based on the
548        video packet timestamp."""
549        mock_source = self.create_mock_source(self.audio_format_1, self.video_format_1)
550        self.set_video_data_for_mock_source(mock_source, [(0.2, 'a')])
551        self.player.queue(mock_source)
552        self.assert_not_playing_yet(mock_source)
553
554        self.reset_mocks()
555        self.player.play()
556        self.assert_driver_player_created_for(mock_source)
557        self.assert_driver_player_started()
558        self.assert_now_playing(mock_source)
559        self.assert_new_texture_created(self.video_format_1)
560        self.assert_update_texture_scheduled()
561
562        self.reset_mocks()
563        self.pretend_player_at_time(0.2)
564        self.player.update_texture()
565        self.assert_texture_updated('a')
566        self.assertIs(self.player.texture, self.mock_texture)
567
568    def test_video_seek(self):
569        """Sources with video can also be seeked. It's the Source
570        responsibility to present the Player with audio and video at the
571        correct time."""
572        mock_source = self.create_mock_source(self.audio_format_1, self.video_format_1)
573        self.set_video_data_for_mock_source(mock_source, [(0.0, 'a'), (0.1, 'b'), (0.2, 'c'),
574                                                          (0.3, 'd'), (0.4, 'e'), (0.5, 'f')])
575        self.player.queue(mock_source)
576        self.player.play()
577        self.assert_new_texture_created(self.video_format_1)
578        self.assert_update_texture_scheduled()
579
580        self.reset_mocks()
581        self.pretend_player_at_time(0.0)
582        self.player.update_texture()
583        self.assert_texture_updated('a')
584
585        self.reset_mocks()
586        self.player.seek(0.3)
587        self.assert_source_seek(mock_source, 0.3)
588        self.assert_no_new_texture_created()
589        self.assert_texture_updated('d')
590
591        self.reset_mocks()
592        self.pretend_player_at_time(0.4)
593        self.player.update_texture()
594        self.assert_texture_updated('e')
595
596    def test_video_frame_rate(self):
597        """Videos texture are scheduled according to the video packet
598        timestamp."""
599        mock_source1 = self.create_mock_source(self.audio_format_1, self.video_format_1)
600        mock_source2 = self.create_mock_source(self.audio_format_1, self.video_format_2)
601        for mock_source in (mock_source1, mock_source2):
602            self.set_video_data_for_mock_source(
603                mock_source,
604                [(0.0, 'a'), (0.1, 'b'), (0.2, 'c'),
605                 (0.3, 'd'), (0.4, 'e'), (0.5, 'f')]
606            )
607
608        self.player.queue(mock_source1)
609        self.player.queue(mock_source2)
610
611        self.player.play()
612        self.assert_new_texture_created(self.video_format_1)
613        self.assert_update_texture_scheduled()
614
615        self.reset_mocks()
616        self.player.next_source()
617        self.assert_new_texture_created(self.video_format_2)
618        self.assert_update_texture_unscheduled()
619        # schedule_once called twice:
620        #   - Once for seeking back to 0 the previous source in next_source()
621        #   - Once for scheduling the next source update_texture
622        assert self.mock_clock.schedule_once.call_count == 2
623        self.mock_clock.schedule_once.assert_called_with(self.player.update_texture, 0)
624
625    def test_video_seek_next_frame(self):
626        """It is possible to jump directly to the next frame of video and adjust the audio player
627        accordingly."""
628        mock_source = self.create_mock_source(self.audio_format_1, self.video_format_1)
629        self.set_video_data_for_mock_source(mock_source, [(0.0, 'a'), (0.2, 'b')])
630        self.player.queue(mock_source)
631        self.player.play()
632        self.assert_new_texture_created(self.video_format_1)
633        self.assert_update_texture_scheduled()
634
635        self.reset_mocks()
636        self.pretend_player_at_time(0.0)
637        self.player.update_texture()
638        self.assert_texture_updated('a')
639
640        self.reset_mocks()
641        self.player.seek_next_frame()
642        self.assert_source_seek(mock_source, 0.2)
643        self.assert_texture_updated('b')
644
645    def test_video_runs_out_of_frames(self):
646        """When the video runs out of frames, it stops updating the texture. The audio player is
647        responsible for triggering eos."""
648        mock_source = self.create_mock_source(self.audio_format_1, self.video_format_1)
649        self.set_video_data_for_mock_source(mock_source, [(0.0, 'a'), (0.1, 'b')])
650        self.player.queue(mock_source)
651        self.player.play()
652        self.assert_new_texture_created(self.video_format_1)
653        self.assert_update_texture_scheduled()
654
655        self.reset_mocks()
656        self.pretend_player_at_time(0.0)
657        self.player.update_texture()
658        self.assert_texture_updated('a')
659
660        self.reset_mocks()
661        self.pretend_player_at_time(0.1)
662        self.player.update_texture()
663        self.assert_texture_updated('b')
664
665        self.reset_mocks()
666        self.pretend_player_at_time(0.2)
667        self.player.update_texture()
668        self.assert_texture_not_updated()
669
670        self.reset_mocks()
671        self.player.seek_next_frame()
672        self.assert_texture_not_updated()
673
674    def test_video_without_audio(self):
675        """It is possible to have videos without audio streams."""
676        mock_source = self.create_mock_source(None, self.video_format_1)
677        self.player.queue(mock_source)
678        self.player.play()
679        self.assert_new_texture_created(self.video_format_1)
680        self.assert_update_texture_scheduled()
681        self.assert_no_new_driver_player_created()
682
683    def test_audio_source_with_silent_driver(self):
684        """An audio source with a silent driver."""
685        mock_source = self.create_mock_source(self.audio_format_3, None)
686        self.mock_get_audio_driver.return_value = None
687        self.player.queue(mock_source)
688        self.player.play()
689
690
691class PlayerGroupTestCase(FutureTestCase):
692    def create_mock_player(self, has_audio=True):
693        player = mock.MagicMock()
694        if has_audio:
695            audio_player = mock.PropertyMock(return_value=mock.MagicMock())
696        else:
697            audio_player = mock.PropertyMock(return_value=None)
698        type(player)._audio_player = audio_player
699        return player
700
701    def assert_players_started(self, *players):
702        for player in players:
703            player.play.assert_called_once_with()
704
705    def assert_audio_players_started(self, *players):
706        # Find the one player that was used to start the group,
707        # the rest should not be used
708        call_args = None
709        audio_players = []
710        for player in players:
711            audio_player = player._audio_player
712            audio_players.append(audio_player)
713            if call_args is not None:
714                self.assertFalse(audio_player._play_group.called,
715                                 msg='Only one player should be used to start the group')
716            elif audio_player._play_group.called:
717                call_args = audio_player._play_group.call_args
718
719        self.assertIsNotNone(call_args,
720                             msg='No player was used to start all audio players.')
721        started_players = call_args[0][0]
722        self.assertCountEqual(started_players, audio_players,
723                              msg='Not all players with audio players were started')
724
725    def assert_players_stopped(self, *players):
726        for player in players:
727            player.pause.assert_called_once_with()
728
729    def assert_audio_players_stopped(self, *players):
730        # Find the one player that was used to start the group,
731        # the rest should not be used
732        call_args = None
733        audio_players = []
734        for player in players:
735            audio_player = player._audio_player
736            audio_players.append(audio_player)
737            if call_args is not None:
738                self.assertFalse(audio_player._stop_group.called,
739                                 msg='Only one player should be used to stop the group')
740            elif audio_player._stop_group.called:
741                call_args = audio_player._stop_group.call_args
742
743        self.assertIsNotNone(call_args,
744                             msg='No player was used to stop all audio players.')
745        stopped_players = call_args[0][0]
746        self.assertCountEqual(stopped_players, audio_players,
747                              msg='Not all players with audio players were stopped')
748
749    def reset_mocks(self, *mocks):
750        for m in mocks:
751            m.reset_mock()
752
753    def test_empty_group(self):
754        """Just check nothing explodes on an empty group."""
755        group = PlayerGroup([])
756        group.play()
757        group.pause()
758
759    def test_only_with_audio(self):
760        """Test a group containing only players with audio."""
761        players = [self.create_mock_player(has_audio=True) for _ in range(10)]
762        group = PlayerGroup(players)
763
764        group.play()
765        self.assert_audio_players_started(*players)
766        self.assert_players_started(*players)
767        self.reset_mocks(*players)
768
769        group.pause()
770        self.assert_audio_players_stopped(*players)
771        self.assert_players_stopped(*players)
772
773    def test_only_without_audio(self):
774        """Test a group containing only players without audio."""
775        players = [self.create_mock_player(has_audio=False) for _ in range(10)]
776        group = PlayerGroup(players)
777
778        group.play()
779        self.assert_players_started(*players)
780        self.reset_mocks(*players)
781
782        group.pause()
783        self.assert_players_stopped(*players)
784
785    def test_mixed_players(self):
786        """Test a group containing both players with audio and players without audio."""
787        players_with_audio = [self.create_mock_player(has_audio=True) for _ in range(10)]
788        players_without_audio = [self.create_mock_player(has_audio=False) for _ in range(10)]
789        players = players_with_audio + players_without_audio
790        random.shuffle(players)
791        group = PlayerGroup(players)
792
793        group.play()
794        self.assert_audio_players_started(*players_with_audio)
795        self.assert_players_started(*players)
796        self.reset_mocks(*players)
797
798        group.pause()
799        self.assert_audio_players_stopped(*players_with_audio)
800        self.assert_players_stopped(*players)
801