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