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