1# ----------------------------------------------------------------------------
2# pyglet
3# Copyright (c) 2006-2008 Alex Holkner
4# Copyright (c) 2008-2020 pyglet contributors
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10#
11#  * Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13#  * Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in
15#    the documentation and/or other materials provided with the
16#    distribution.
17#  * Neither the name of pyglet nor the names of its
18#    contributors may be used to endorse or promote products
19#    derived from this software without specific prior written
20#    permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33# POSSIBILITY OF SUCH DAMAGE.
34# ----------------------------------------------------------------------------
35"""High-level sound and video player."""
36
37import threading
38from collections import deque
39
40import pyglet
41from pyglet.media import buffered_logger as bl
42from pyglet.media.drivers import get_audio_driver
43from pyglet.media.codecs.base import Source, SourceGroup
44
45_debug = pyglet.options['debug_media']
46
47
48# class AudioClock(pyglet.clock.Clock):
49#     """A dedicated background Clock for refilling audio buffers."""
50#
51#     def __init__(self, interval=0.1):
52#         super().__init__()
53#         self._interval = interval
54#         self._thread = threading.Thread(target=self._tick_clock, daemon=True)
55#         self._thread.start()
56#
57#     def _tick_clock(self):
58#         while True:
59#             self.tick()
60#             self.sleep(self._interval * 1000000)
61#
62#
63# clock = AudioClock()
64
65clock = pyglet.clock.get_default()
66
67
68class PlaybackTimer:
69    """Playback Timer.
70
71    This is a simple timer object which tracks the time elapsed. It can be
72    paused and reset.
73    """
74
75    def __init__(self):
76        """Initialize the timer with time 0."""
77        self._time = 0.0
78        self._systime = None
79
80    def start(self):
81        """Start the timer."""
82        self._systime = clock.time()
83
84    def pause(self):
85        """Pause the timer."""
86        self._time = self.get_time()
87        self._systime = None
88
89    def reset(self):
90        """Reset the timer to 0."""
91        self._time = 0.0
92        if self._systime is not None:
93            self._systime = clock.time()
94
95    def get_time(self):
96        """Get the elapsed time."""
97        if self._systime is None:
98            now = self._time
99        else:
100            now = clock.time() - self._systime + self._time
101        return now
102
103    def set_time(self, value):
104        """
105        Manually set the elapsed time.
106
107        Args:
108            value (float): the new elapsed time value
109        """
110        self.reset()
111        self._time = value
112
113
114class _PlayerProperty:
115    """Descriptor for Player attributes to forward to the AudioPlayer.
116
117    We want the Player to have attributes like volume, pitch, etc. These are
118    actually implemented by the AudioPlayer. So this descriptor will forward
119    an assignement to one of the attributes to the AudioPlayer. For example
120    `player.volume = 0.5` will call `player._audio_player.set_volume(0.5)`.
121
122    The Player class has default values at the class level which are retrieved
123    if not found on the instance.
124    """
125
126    def __init__(self, attribute, doc=None):
127        self.attribute = attribute
128        self.__doc__ = doc or ''
129
130    def __get__(self, obj, objtype=None):
131        if obj is None:
132            return self
133        if '_' + self.attribute in obj.__dict__:
134            return obj.__dict__['_' + self.attribute]
135        return getattr(objtype, '_' + self.attribute)
136
137    def __set__(self, obj, value):
138        obj.__dict__['_' + self.attribute] = value
139        if obj._audio_player:
140            getattr(obj._audio_player, 'set_' + self.attribute)(value)
141
142
143class Player(pyglet.event.EventDispatcher):
144    """High-level sound and video player."""
145
146    # Spacialisation attributes, preserved between audio players
147    _volume = 1.0
148    _min_distance = 1.0
149    _max_distance = 100000000.
150
151    _position = (0, 0, 0)
152    _pitch = 1.0
153
154    _cone_orientation = (0, 0, 1)
155    _cone_inner_angle = 360.
156    _cone_outer_angle = 360.
157    _cone_outer_gain = 1.
158
159    def __init__(self):
160        """Initialize the Player with a MasterClock."""
161        self._source = None
162        self._playlists = deque()
163        self._audio_player = None
164
165        self._texture = None
166        # Desired play state (not an indication of actual state).
167        self._playing = False
168
169        self._timer = PlaybackTimer()
170        #: Loop the current source indefinitely or until
171        #: :meth:`~Player.next_source` is called. Defaults to ``False``.
172        #:
173        #: :type: bool
174        #:
175        #: .. versionadded:: 1.4
176        self.loop = False
177
178        # self.pr = cProfile.Profile()
179
180    def __del__(self):
181        """Release the Player resources."""
182        self.delete()
183
184    def queue(self, source):
185        """
186        Queue the source on this player.
187
188        If the player has no source, the player will start to play immediately
189        or pause depending on its :attr:`.playing` attribute.
190
191        Args:
192            source (Source or Iterable[Source]): The source to queue.
193        """
194        if isinstance(source, (Source, SourceGroup)):
195            source = _one_item_playlist(source)
196        else:
197            try:
198                source = iter(source)
199            except TypeError:
200                raise TypeError("source must be either a Source or an iterable."
201                                " Received type {0}".format(type(source)))
202        self._playlists.append(source)
203
204        if self.source is None:
205            source = next(self._playlists[0])
206            self._source = source.get_queue_source()
207
208        self._set_playing(self._playing)
209
210    def _set_playing(self, playing):
211        # stopping = self._playing and not playing
212        # starting = not self._playing and playing
213
214        self._playing = playing
215        source = self.source
216
217        if playing and source:
218            if source.audio_format:
219                if self._audio_player is None:
220                    self._create_audio_player()
221                if self._audio_player:
222                    # We succesfully created an audio player
223                    self._audio_player.prefill_audio()
224
225            if bl.logger is not None:
226                bl.logger.init_wall_time()
227                bl.logger.log("p.P._sp", 0.0)
228
229            if source.video_format:
230                if not self._texture:
231                    self._create_texture()
232
233            if self._audio_player:
234                self._audio_player.play()
235            if source.video_format:
236                pyglet.clock.schedule_once(self.update_texture, 0)
237            # For audio synchronization tests, the following will
238            # add a delay to de-synchronize the audio.
239            # Negative number means audio runs ahead.
240            # self._mclock._systime += -0.3
241            self._timer.start()
242            if self._audio_player is None and source.video_format is None:
243                pyglet.clock.schedule_once(lambda dt: self.dispatch_event("on_eos"), source.duration)
244
245        else:
246            if self._audio_player:
247                self._audio_player.stop()
248
249            pyglet.clock.unschedule(self.update_texture)
250            self._timer.pause()
251
252    @property
253    def playing(self):
254        """
255        bool: Read-only. Determine if the player state is playing.
256
257        The *playing* property is irrespective of whether or not there is
258        actually a source to play. If *playing* is ``True`` and a source is
259        queued, it will begin to play immediately. If *playing* is ``False``,
260        it is implied that the player is paused. There is no other possible
261        state.
262        """
263        return self._playing
264
265    def play(self):
266        """Begin playing the current source.
267
268        This has no effect if the player is already playing.
269        """
270        self._set_playing(True)
271
272    def pause(self):
273        """Pause playback of the current source.
274
275        This has no effect if the player is already paused.
276        """
277        self._set_playing(False)
278
279    def delete(self):
280        """Release the resources acquired by this player.
281
282        The internal audio player and the texture will be deleted.
283        """
284        if self._audio_player:
285            self._audio_player.delete()
286            self._audio_player = None
287        if self._texture:
288            self._texture = None
289
290    def next_source(self):
291        """Move immediately to the next source in the current playlist.
292
293        If the playlist is empty, discard it and check if another playlist
294        is queued. There may be a gap in playback while the audio buffer
295        is refilled.
296        """
297        was_playing = self._playing
298        self.pause()
299        self._timer.reset()
300
301        if self.source:
302            # Reset source to the beginning
303            self.seek(0.0)
304            self.source.is_player_source = False
305
306        playlists = self._playlists
307        if not playlists:
308            return
309
310        try:
311            source = next(playlists[0])
312        except StopIteration:
313            self._playlists.popleft()
314            if not self._playlists:
315                source = None
316            else:
317                # Could someone queue an iterator which is empty??
318                source = next(self._playlists[0])
319
320        if source is None:
321            self._source = None
322            self.delete()
323            self.dispatch_event('on_player_eos')
324        else:
325            old_audio_format = self.source.audio_format
326            old_video_format = self.source.video_format
327            self._source = source.get_queue_source()
328
329            if old_audio_format == self.source.audio_format:
330                self._audio_player.clear()
331                self._audio_player.source = self.source
332            else:
333                self._audio_player.delete()
334                self._audio_player = None
335            if old_video_format != self.source.video_format:
336                self._texture = None
337                pyglet.clock.unschedule(self.update_texture)
338
339            self._set_playing(was_playing)
340            self.dispatch_event('on_player_next_source')
341
342    def seek(self, time):
343        """
344        Seek for playback to the indicated timestamp on the current source.
345
346        Timestamp is expressed in seconds. If the timestamp is outside the
347        duration of the source, it will be clamped to the end.
348
349        Args:
350            time (float): The time where to seek in the source, clamped to the
351                beginning and end of the source.
352        """
353        playing = self._playing
354        if playing:
355            self.pause()
356        if not self.source:
357            return
358
359        if bl.logger is not None:
360            bl.logger.log("p.P.sk", time)
361
362        self._timer.set_time(time)
363        self.source.seek(time)
364        if self._audio_player:
365            # XXX: According to docstring in AbstractAudioPlayer this cannot
366            # be called when the player is not stopped
367            self._audio_player.clear()
368        if self.source.video_format:
369            self.update_texture()
370            pyglet.clock.unschedule(self.update_texture)
371        self._set_playing(playing)
372
373    def _create_audio_player(self):
374        assert not self._audio_player
375        assert self.source
376
377        source = self.source
378        audio_driver = get_audio_driver()
379        if audio_driver is None:
380            # Failed to find a valid audio driver
381            return
382
383        self._audio_player = audio_driver.create_audio_player(source, self)
384
385        # Set the audio player attributes
386        for attr in ('volume', 'min_distance', 'max_distance', 'position',
387                     'pitch', 'cone_orientation', 'cone_inner_angle',
388                     'cone_outer_angle', 'cone_outer_gain'):
389            value = getattr(self, attr)
390            setattr(self, attr, value)
391
392    @property
393    def source(self):
394        """Source: Read-only. The current :class:`Source`, or ``None``."""
395        return self._source
396
397    @property
398    def time(self):
399        """
400        float: Read-only. Current playback time of the current source.
401
402        The playback time is a float expressed in seconds, with 0.0 being the
403        beginning of the media. The playback time returned represents the
404        player master clock time which is used to synchronize both the audio
405        and the video.
406        """
407        return self._timer.get_time()
408
409    def _create_texture(self):
410        video_format = self.source.video_format
411        self._texture = pyglet.image.Texture.create(
412            video_format.width, video_format.height, rectangle=True)
413        self._texture = self._texture.get_transform(flip_y=True)
414        # After flipping the texture along the y axis, the anchor_y is set
415        # to the top of the image. We want to keep it at the bottom.
416        self._texture.anchor_y = 0
417        return self._texture
418
419    @property
420    def texture(self):
421        """
422        :class:`pyglet.image.Texture`: Get the texture for the current video frame.
423
424        You should call this method every time you display a frame of video,
425        as multiple textures might be used. The return value will be None if
426        there is no video in the current source.
427        """
428        return self._texture
429
430    def get_texture(self):
431        """
432        Get the texture for the current video frame.
433
434        You should call this method every time you display a frame of video,
435        as multiple textures might be used. The return value will be None if
436        there is no video in the current source.
437
438        Returns:
439            :class:`pyglet.image.Texture`
440
441        .. deprecated:: 1.4
442                Use :attr:`~texture` instead
443        """
444        return self.texture
445
446    def seek_next_frame(self):
447        """Step forwards one video frame in the current source."""
448        time = self.source.get_next_video_timestamp()
449        if time is None:
450            return
451        self.seek(time)
452
453    def update_texture(self, dt=None):
454        """Manually update the texture from the current source.
455
456        This happens automatically, so you shouldn't need to call this method.
457
458        Args:
459            dt (float): The time elapsed since the last call to
460                ``update_texture``.
461        """
462        # self.pr.disable()
463        # if dt > 0.05:
464        #     print("update_texture dt:", dt)
465        #     import pstats
466        #     ps = pstats.Stats(self.pr).sort_stats("cumulative")
467        #     ps.print_stats()
468        source = self.source
469        time = self.time
470        if bl.logger is not None:
471            bl.logger.log(
472                "p.P.ut.1.0", dt, time,
473                self._audio_player.get_time() if self._audio_player else 0,
474                bl.logger.rebased_wall_time()
475            )
476
477        frame_rate = source.video_format.frame_rate
478        frame_duration = 1 / frame_rate
479        ts = source.get_next_video_timestamp()
480        # Allow up to frame_duration difference
481        while ts is not None and ts + frame_duration < time:
482            source.get_next_video_frame()  # Discard frame
483            if bl.logger is not None:
484                bl.logger.log("p.P.ut.1.5", ts)
485            ts = source.get_next_video_timestamp()
486
487        if bl.logger is not None:
488            bl.logger.log("p.P.ut.1.6", ts)
489
490        if ts is None:
491            # No more video frames to show. End of video stream.
492            if bl.logger is not None:
493                bl.logger.log("p.P.ut.1.7", frame_duration)
494
495            pyglet.clock.schedule_once(self._video_finished, 0)
496            return
497
498        image = source.get_next_video_frame()
499        if image is not None:
500            if self._texture is None:
501                self._create_texture()
502            self._texture.blit_into(image, 0, 0, 0)
503        elif bl.logger is not None:
504            bl.logger.log("p.P.ut.1.8")
505
506        ts = source.get_next_video_timestamp()
507        if ts is None:
508            delay = frame_duration
509        else:
510            delay = ts - time
511
512        delay = max(0.0, delay)
513        if bl.logger is not None:
514            bl.logger.log("p.P.ut.1.9", delay, ts)
515        pyglet.clock.schedule_once(self.update_texture, delay)
516        # self.pr.enable()
517
518    def _video_finished(self, dt):
519        if self._audio_player is None:
520            self.dispatch_event("on_eos")
521
522    volume = _PlayerProperty('volume', doc="""
523    The volume level of sound playback.
524
525    The nominal level is 1.0, and 0.0 is silence.
526
527    The volume level is affected by the distance from the listener (if
528    positioned).
529    """)
530    min_distance = _PlayerProperty('min_distance', doc="""
531    The distance beyond which the sound volume drops by half, and within
532    which no attenuation is applied.
533
534    The minimum distance controls how quickly a sound is attenuated as it
535    moves away from the listener. The gain is clamped at the nominal value
536    within the min distance. By default the value is 1.0.
537
538    The unit defaults to meters, but can be modified with the listener
539    properties. """)
540    max_distance = _PlayerProperty('max_distance', doc="""
541    The distance at which no further attenuation is applied.
542
543    When the distance from the listener to the player is greater than this
544    value, attenuation is calculated as if the distance were value. By
545    default the maximum distance is infinity.
546
547    The unit defaults to meters, but can be modified with the listener
548    properties.
549    """)
550    position = _PlayerProperty('position', doc="""
551    The position of the sound in 3D space.
552
553    The position is given as a tuple of floats (x, y, z). The unit
554    defaults to meters, but can be modified with the listener properties.
555    """)
556    pitch = _PlayerProperty('pitch', doc="""
557    The pitch shift to apply to the sound.
558
559    The nominal pitch is 1.0. A pitch of 2.0 will sound one octave higher,
560    and play twice as fast. A pitch of 0.5 will sound one octave lower, and
561    play twice as slow. A pitch of 0.0 is not permitted.
562    """)
563    cone_orientation = _PlayerProperty('cone_orientation', doc="""
564    The direction of the sound in 3D space.
565
566    The direction is specified as a tuple of floats (x, y, z), and has no
567    unit. The default direction is (0, 0, -1). Directional effects are only
568    noticeable if the other cone properties are changed from their default
569    values.
570    """)
571    cone_inner_angle = _PlayerProperty('cone_inner_angle', doc="""
572    The interior angle of the inner cone.
573
574    The angle is given in degrees, and defaults to 360. When the listener
575    is positioned within the volume defined by the inner cone, the sound is
576    played at normal gain (see :attr:`volume`).
577    """)
578    cone_outer_angle = _PlayerProperty('cone_outer_angle', doc="""
579    The interior angle of the outer cone.
580
581    The angle is given in degrees, and defaults to 360. When the listener
582    is positioned within the volume defined by the outer cone, but outside
583    the volume defined by the inner cone, the gain applied is a smooth
584    interpolation between :attr:`volume` and :attr:`cone_outer_gain`.
585    """)
586    cone_outer_gain = _PlayerProperty('cone_outer_gain', doc="""
587    The gain applied outside the cone.
588
589    When the listener is positioned outside the volume defined by the outer
590    cone, this gain is applied instead of :attr:`volume`.
591    """)
592
593    # Events
594
595    def on_player_eos(self):
596        """The player ran out of sources. The playlist is empty.
597
598        :event:
599        """
600        if _debug:
601            print('Player.on_player_eos')
602
603    def on_eos(self):
604        """The current source ran out of data.
605
606        The default behaviour is to advance to the next source in the
607        playlist if the :attr:`.loop` attribute is set to ``False``.
608        If :attr:`.loop` attribute is set to ``True``, the current source
609        will start to play again until :meth:`next_source` is called or
610        :attr:`.loop` is set to ``False``.
611
612        :event:
613        """
614        if _debug:
615            print('Player.on_eos')
616        if bl.logger is not None:
617            bl.logger.log("p.P.oe")
618            bl.logger.close()
619
620        if self.loop:
621            was_playing = self._playing
622            self.pause()
623            self._timer.reset()
624
625            if self.source:
626                # Reset source to the beginning
627                self.seek(0.0)
628            self._audio_player.clear()
629            self._set_playing(was_playing)
630
631        else:
632            self.next_source()
633
634    def on_player_next_source(self):
635        """The player starts to play the next queued source in the playlist.
636
637        This is a useful event for adjusting the window size to the new
638        source :class:`VideoFormat` for example.
639
640        :event:
641        """
642        pass
643
644
645Player.register_event_type('on_eos')
646Player.register_event_type('on_player_eos')
647Player.register_event_type('on_player_next_source')
648
649
650def _one_item_playlist(source):
651    yield source
652
653
654class PlayerGroup:
655    """Group of players that can be played and paused simultaneously.
656
657    Create a player group for the given list of players.
658
659    All players in the group must currently not belong to any other group.
660
661    Args:
662        players (List[Player]): List of :class:`.Player` s in this group.
663    """
664
665    def __init__(self, players):
666        """Initialize the PlayerGroup with the players."""
667        self.players = list(players)
668
669    def play(self):
670        """Begin playing all players in the group simultaneously."""
671        audio_players = [p._audio_player
672                         for p in self.players if p._audio_player]
673        if audio_players:
674            audio_players[0]._play_group(audio_players)
675        for player in self.players:
676            player.play()
677
678    def pause(self):
679        """Pause all players in the group simultaneously."""
680        audio_players = [p._audio_player
681                         for p in self.players if p._audio_player]
682        if audio_players:
683            audio_players[0]._stop_group(audio_players)
684        for player in self.players:
685            player.pause()
686