1# Copyright 2004-2011 Joe Wreschnig, Michael Urman, Steven Robertson, 2# 2011-2014 Christoph Reiter 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8 9import gi 10try: 11 gi.require_version("Gst", "1.0") 12 gi.require_version("GstPbutils", "1.0") 13except ValueError as e: 14 raise ImportError(e) 15 16from gi.repository import Gst, GLib, GstPbutils 17 18from quodlibet import const 19from quodlibet import config 20from quodlibet import util 21from quodlibet import app 22from quodlibet import _ 23 24from quodlibet.util import fver, sanitize_tags, MainRunner, MainRunnerError, \ 25 MainRunnerAbortedError, MainRunnerTimeoutError, print_w, print_d, \ 26 print_e, print_ 27from quodlibet.player import PlayerError 28from quodlibet.player._base import BasePlayer 29from quodlibet.qltk.notif import Task 30from quodlibet.formats.mod import ModFile 31 32from .util import (parse_gstreamer_taglist, TagListWrapper, iter_to_list, 33 GStreamerSink, link_many, bin_debug) 34from .plugins import GStreamerPluginHandler 35from .prefs import GstPlayerPreferences 36 37STATE_CHANGE_TIMEOUT = Gst.SECOND * 4 38 39 40const.MinVersions.GSTREAMER.check(Gst.version()) 41 42 43class BufferingWrapper(object): 44 """A wrapper for a Gst.Element (usually GstPlayBin) which pauses the 45 elmement in case buffering is needed, but hides the fact that it does. 46 47 Probably not perfect... 48 49 You have to call destroy() before it gets gc'd 50 """ 51 52 def __init__(self, bin_, player): 53 """ 54 bin_ -- the GstPlaybin instance to wrap 55 player -- the GStreamerPlayer instance used 56 for aborting the buffer process 57 """ 58 59 self.bin = bin_ 60 self._wanted_state = None 61 self._task = None 62 self._inhibit_play = False 63 self._player = player 64 65 bus = self.bin.get_bus() 66 bus.add_signal_watch() 67 self.__bus_id = bus.connect('message', self.__message) 68 69 def __message(self, bus, message): 70 if message.type == Gst.MessageType.BUFFERING: 71 percent = message.parse_buffering() 72 self.__update_buffering(percent) 73 74 def __getattr__(self, name): 75 return getattr(self.bin, name) 76 77 def __update_buffering(self, percent): 78 """Call this with buffer percent values from the bus""" 79 80 if self._task: 81 self._task.update(percent / 100.0) 82 83 self.__set_inhibit_play(percent < 100) 84 85 def __set_inhibit_play(self, inhibit): 86 """Change the inhibit state""" 87 88 if inhibit == self._inhibit_play: 89 return 90 self._inhibit_play = inhibit 91 92 # task management 93 if inhibit: 94 if not self._task: 95 def stop_buf(*args): 96 self._player.paused = True 97 98 self._task = Task(_("Stream"), _("Buffering"), stop=stop_buf) 99 elif self._task: 100 self._task.finish() 101 self._task = None 102 103 # state management 104 if inhibit: 105 # save the current state 106 status, state, pending = self.bin.get_state( 107 timeout=STATE_CHANGE_TIMEOUT) 108 if status == Gst.StateChangeReturn.SUCCESS and \ 109 state == Gst.State.PLAYING: 110 self._wanted_state = state 111 else: 112 # no idea, at least don't play 113 self._wanted_state = Gst.State.PAUSED 114 115 self.bin.set_state(Gst.State.PAUSED) 116 else: 117 # restore the old state 118 self.bin.set_state(self._wanted_state) 119 self._wanted_state = None 120 121 def set_state(self, state): 122 if self._inhibit_play: 123 # PLAYING, PAUSED change the state for after buffering is finished, 124 # everything else aborts buffering 125 if state not in (Gst.State.PLAYING, Gst.State.PAUSED): 126 # abort 127 self.__set_inhibit_play(False) 128 self.bin.set_state(state) 129 return 130 self._wanted_state = state 131 else: 132 self.bin.set_state(state) 133 134 def get_state(self, *args, **kwargs): 135 # get_state also is a barrier (seek/positioning), 136 # so call every time but ignore the result in the inhibit case 137 res = self.bin.get_state(*args, **kwargs) 138 if self._inhibit_play: 139 return (Gst.StateChangeReturn.SUCCESS, 140 self._wanted_state, Gst.State.VOID_PENDING) 141 return res 142 143 def destroy(self): 144 if self.__bus_id: 145 bus = self.bin.get_bus() 146 bus.disconnect(self.__bus_id) 147 bus.remove_signal_watch() 148 self.__bus_id = None 149 150 self.__set_inhibit_play(False) 151 self.bin = None 152 153 154def sink_has_external_state(sink): 155 sink_name = sink.get_factory().get_name() 156 157 if sink_name == "wasapisink": 158 # https://bugzilla.gnome.org/show_bug.cgi?id=796386 159 return hasattr(sink.props, "volume") 160 else: 161 return sink_name == "pulsesink" 162 163 164def sink_state_is_valid(sink): 165 if not sink_has_external_state(sink): 166 return True 167 168 # pulsesink volume is only valid in PAUSED/PLAYING 169 # https://bugzilla.gnome.org/show_bug.cgi?id=748577 170 current_state = sink.get_state(0)[1] 171 return current_state >= Gst.State.PAUSED 172 173 174class Seeker(object): 175 """Manages async seeking and position reporting for a pipeline. 176 177 You have to call destroy() before it gets gc'd 178 """ 179 180 def __init__(self, playbin, player): 181 self._player = player 182 self._playbin = playbin 183 184 self._last_position = 0 185 self._seek_requests = [] 186 self._active_seeks = [] 187 self._seekable = False 188 189 bus = playbin.get_bus() 190 bus.add_signal_watch() 191 self._bus_id = bus.connect('message', self._on_message) 192 player.notify("seekable") 193 194 @property 195 def seekable(self): 196 """If the current stream is seekable""" 197 198 return self._seekable 199 200 def destroy(self): 201 """This needs to be called before it gets GC'ed""" 202 203 del self._seek_requests[:] 204 del self._active_seeks[:] 205 206 if self._bus_id: 207 bus = self._playbin.get_bus() 208 bus.disconnect(self._bus_id) 209 bus.remove_signal_watch() 210 self._bus_id = None 211 212 self._player = None 213 self._playbin = None 214 215 def set_position(self, pos): 216 """Set the position. Async and might not succeed. 217 218 Args: 219 pos (int): position in milliseconds 220 """ 221 222 pos = max(0, int(pos)) 223 224 # We need at least a paused state to seek, if there is non active 225 # or pending, request one async. 226 res, next_state, pending = self._playbin.get_state(timeout=0) 227 if pending != Gst.State.VOID_PENDING: 228 next_state = pending 229 if next_state < Gst.State.PAUSED: 230 self._playbin.set_state(Gst.State.PAUSED) 231 232 self._set_position(self._player.song, pos) 233 234 def get_position(self): 235 """Get the position 236 237 Returns: 238 int: the position in milliseconds 239 """ 240 241 if self._seek_requests: 242 self._last_position = self._seek_requests[-1][1] 243 elif self._active_seeks: 244 self._last_position = self._active_seeks[-1][1] 245 else: 246 # While we are actively seeking return the last wanted position. 247 # query_position() returns 0 while in this state 248 ok, p = self._playbin.query_position(Gst.Format.TIME) 249 if ok: 250 p //= Gst.MSECOND 251 # During stream seeking querying the position fails. 252 # Better return the last valid one instead of 0. 253 self._last_position = p 254 255 return self._last_position 256 257 def reset(self): 258 """In case the underlying stream has changed, call this to 259 abort any pending seeking actions and update the seekable state 260 """ 261 262 self._last_position = 0 263 del self._seek_requests[:] 264 del self._active_seeks[:] 265 self._refresh_seekable() 266 267 def _refresh_seekable(self): 268 query = Gst.Query.new_seeking(Gst.Format.TIME) 269 if self._playbin.query(query): 270 seekable = query.parse_seeking()[1] 271 elif self._player.song is None: 272 seekable = False 273 else: 274 seekable = True 275 276 if seekable != self._seekable: 277 self._seekable = seekable 278 self._player.notify("seekable") 279 280 def _on_message(self, bus, message): 281 if message.type == Gst.MessageType.ASYNC_DONE: 282 # we only get one ASYNC_DONE for multiple seeks, so flush all 283 284 if self._active_seeks: 285 song, pos = self._active_seeks[-1] 286 if song is self._player.song: 287 self._player.emit("seek", song, pos) 288 del self._active_seeks[:] 289 elif message.type == Gst.MessageType.STATE_CHANGED: 290 if message.src is self._playbin.bin: 291 new_state = message.parse_state_changed()[1] 292 if new_state >= Gst.State.PAUSED: 293 self._refresh_seekable() 294 295 if self._seek_requests: 296 song, pos = self._seek_requests[-1] 297 if song is self._player.song: 298 self._set_position(song, pos) 299 del self._seek_requests[:] 300 301 def _set_position(self, song, pos): 302 event = Gst.Event.new_seek( 303 1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH, 304 Gst.SeekType.SET, pos * Gst.MSECOND, Gst.SeekType.NONE, 0) 305 306 if self._playbin.send_event(event): 307 self._active_seeks.append((song, pos)) 308 else: 309 self._seek_requests.append((song, pos)) 310 311 312class GStreamerPlayer(BasePlayer, GStreamerPluginHandler): 313 314 def PlayerPreferences(self): 315 return GstPlayerPreferences(self, const.DEBUG) 316 317 def __init__(self, librarian=None): 318 GStreamerPluginHandler.__init__(self) 319 BasePlayer.__init__(self) 320 321 self._librarian = librarian 322 323 self.version_info = "GStreamer: %s" % fver(Gst.version()) 324 self._pipeline_desc = None 325 326 self._volume = 1.0 327 self._paused = True 328 self._mute = False 329 330 self._in_gapless_transition = False 331 self._active_error = False 332 333 self.bin = None 334 self._seeker = None 335 self._int_vol_element = None 336 self._ext_vol_element = None 337 self._ext_mute_element = None 338 self._use_eq = False 339 self._eq_element = None 340 self.__info_buffer = None 341 342 self._lib_id = librarian.connect("changed", self.__songs_changed) 343 self.__atf_id = None 344 self.__bus_id = None 345 self._runner = MainRunner() 346 347 def __songs_changed(self, librarian, songs): 348 # replaygain values might have changed, recalc volume 349 if self.song and self.song in songs: 350 self._reset_replaygain() 351 352 def _destroy(self): 353 self._librarian.disconnect(self._lib_id) 354 self._runner.abort() 355 self.__destroy_pipeline() 356 357 @property 358 def name(self): 359 name = "GStreamer" 360 if self._pipeline_desc: 361 name += " (%s)" % self._pipeline_desc 362 return name 363 364 @property 365 def has_external_volume(self): 366 ext = self._ext_vol_element 367 if ext is None or not sink_has_external_state(ext): 368 return False 369 return True 370 371 def _set_buffer_duration(self, duration): 372 """Set the stream buffer duration in msecs""" 373 374 config.set("player", "gst_buffer", float(duration) / 1000) 375 376 if self.bin: 377 value = duration * Gst.MSECOND 378 self.bin.set_property('buffer-duration', value) 379 380 def _print_pipeline(self): 381 """Print debug information for the active pipeline to stdout 382 (elements, formats, ...) 383 """ 384 385 if self.bin: 386 # self.bin is just a wrapper, so get the real one 387 for line in bin_debug([self.bin.bin]): 388 print_(line) 389 else: 390 print_e("No active pipeline.") 391 392 def __init_pipeline(self): 393 """Creates a gstreamer pipeline. Returns True on success.""" 394 395 if self.bin: 396 return True 397 398 # reset error state 399 self.error = False 400 401 pipeline = config.get("player", "gst_pipeline") 402 try: 403 pipeline, self._pipeline_desc = GStreamerSink(pipeline) 404 except PlayerError as e: 405 self._error(e) 406 return False 407 408 if self._use_eq and Gst.ElementFactory.find('equalizer-10bands'): 409 # The equalizer only operates on 16-bit ints or floats, and 410 # will only pass these types through even when inactive. 411 # We push floats through to this point, then let the second 412 # audioconvert handle pushing to whatever the rest of the 413 # pipeline supports. As a bonus, this seems to automatically 414 # select the highest-precision format supported by the 415 # rest of the chain. 416 filt = Gst.ElementFactory.make('capsfilter', None) 417 filt.set_property('caps', 418 Gst.Caps.from_string('audio/x-raw,format=F32LE')) 419 eq = Gst.ElementFactory.make('equalizer-10bands', None) 420 self._eq_element = eq 421 self.update_eq_values() 422 conv = Gst.ElementFactory.make('audioconvert', None) 423 resample = Gst.ElementFactory.make('audioresample', None) 424 pipeline = [filt, eq, conv, resample] + pipeline 425 426 # playbin2 has started to control the volume through pulseaudio, 427 # which means the volume property can change without us noticing. 428 # Use our own volume element for now until this works with PA. 429 self._int_vol_element = Gst.ElementFactory.make('volume', None) 430 pipeline.insert(0, self._int_vol_element) 431 432 # Get all plugin elements and append audio converters. 433 # playbin already includes one at the end 434 plugin_pipeline = [] 435 for plugin in self._get_plugin_elements(): 436 plugin_pipeline.append(plugin) 437 plugin_pipeline.append( 438 Gst.ElementFactory.make('audioconvert', None)) 439 plugin_pipeline.append( 440 Gst.ElementFactory.make('audioresample', None)) 441 pipeline = plugin_pipeline + pipeline 442 443 bufbin = Gst.Bin() 444 for element in pipeline: 445 assert element is not None, pipeline 446 bufbin.add(element) 447 448 if len(pipeline) > 1: 449 if not link_many(pipeline): 450 print_w("Linking the GStreamer pipeline failed") 451 self._error( 452 PlayerError(_("Could not create GStreamer pipeline"))) 453 return False 454 455 # see if the sink provides a volume property, if yes, use it 456 sink_element = pipeline[-1] 457 if isinstance(sink_element, Gst.Bin): 458 sink_element = iter_to_list(sink_element.iterate_recurse)[-1] 459 460 self._ext_vol_element = None 461 if hasattr(sink_element.props, "volume"): 462 self._ext_vol_element = sink_element 463 464 # In case we use the sink volume directly we can increase buffering 465 # without affecting the volume change delay too much and safe some 466 # CPU time... (2x default for now). 467 if hasattr(sink_element.props, "buffer_time"): 468 sink_element.set_property("buffer-time", 400000) 469 470 def ext_volume_notify(*args): 471 # gets called from a thread 472 GLib.idle_add(self.notify, "volume") 473 474 self._ext_vol_element.connect("notify::volume", ext_volume_notify) 475 476 self._ext_mute_element = None 477 if hasattr(sink_element.props, "mute") and \ 478 sink_element.get_factory().get_name() != "directsoundsink": 479 # directsoundsink has a mute property but it doesn't work 480 # https://bugzilla.gnome.org/show_bug.cgi?id=755106 481 self._ext_mute_element = sink_element 482 483 def mute_notify(*args): 484 # gets called from a thread 485 GLib.idle_add(self.notify, "mute") 486 487 self._ext_mute_element.connect("notify::mute", mute_notify) 488 489 # Make the sink of the first element the sink of the bin 490 gpad = Gst.GhostPad.new('sink', pipeline[0].get_static_pad('sink')) 491 bufbin.add_pad(gpad) 492 493 bin_ = Gst.ElementFactory.make('playbin', None) 494 assert bin_ 495 496 self.bin = BufferingWrapper(bin_, self) 497 self._seeker = Seeker(self.bin, self) 498 499 bus = bin_.get_bus() 500 bus.add_signal_watch() 501 self.__bus_id = bus.connect('message', self.__message, self._librarian) 502 503 self.__atf_id = self.bin.connect('about-to-finish', 504 self.__about_to_finish) 505 506 # set buffer duration 507 duration = config.getfloat("player", "gst_buffer") 508 self._set_buffer_duration(int(duration * 1000)) 509 510 # connect playbin to our pluing/volume/eq pipeline 511 self.bin.set_property('audio-sink', bufbin) 512 513 # by default playbin will render video -> suppress using fakesink 514 fakesink = Gst.ElementFactory.make('fakesink', None) 515 self.bin.set_property('video-sink', fakesink) 516 517 # disable all video/text decoding in playbin 518 GST_PLAY_FLAG_VIDEO = 1 << 0 519 GST_PLAY_FLAG_TEXT = 1 << 2 520 flags = self.bin.get_property("flags") 521 flags &= ~(GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_TEXT) 522 self.bin.set_property("flags", flags) 523 524 if not self.has_external_volume: 525 # Restore volume/ReplayGain and mute state 526 self.props.volume = self._volume 527 self.mute = self._mute 528 529 # ReplayGain information gets lost when destroying 530 self._reset_replaygain() 531 532 if self.song: 533 self.bin.set_property('uri', self.song("~uri")) 534 535 return True 536 537 def __destroy_pipeline(self): 538 self._remove_plugin_elements() 539 540 if self.__bus_id: 541 bus = self.bin.get_bus() 542 bus.disconnect(self.__bus_id) 543 bus.remove_signal_watch() 544 self.__bus_id = None 545 546 if self.__atf_id: 547 self.bin.disconnect(self.__atf_id) 548 self.__atf_id = None 549 550 if self._seeker is not None: 551 self._seeker.destroy() 552 self._seeker = None 553 self.notify("seekable") 554 555 if self.bin: 556 self.bin.set_state(Gst.State.NULL) 557 self.bin.get_state(timeout=STATE_CHANGE_TIMEOUT) 558 # BufferingWrapper cleanup 559 self.bin.destroy() 560 self.bin = None 561 562 self._in_gapless_transition = False 563 564 self._ext_vol_element = None 565 self._int_vol_element = None 566 self._ext_mute_element = None 567 self._eq_element = None 568 569 def _rebuild_pipeline(self): 570 """If a pipeline is active, rebuild it and restore vol, position etc""" 571 572 if not self.bin: 573 return 574 575 paused = self.paused 576 pos = self.get_position() 577 578 self.__destroy_pipeline() 579 self.paused = True 580 self.__init_pipeline() 581 self.paused = paused 582 self.seek(pos) 583 584 def __message(self, bus, message, librarian): 585 if message.type == Gst.MessageType.EOS: 586 print_d("Stream EOS") 587 if not self._in_gapless_transition: 588 self._source.next_ended() 589 self._end(False) 590 elif message.type == Gst.MessageType.TAG: 591 self.__tag(message.parse_tag(), librarian) 592 elif message.type == Gst.MessageType.ERROR: 593 gerror, debug_info = message.parse_error() 594 message = u"" 595 if gerror: 596 message = gerror.message.rstrip(".") 597 details = None 598 if debug_info: 599 # strip the first line, not user friendly 600 debug_info = "\n".join(debug_info.splitlines()[1:]) 601 # can contain paths, so not sure if utf-8 in all cases 602 details = debug_info 603 self._error(PlayerError(message, details)) 604 elif message.type == Gst.MessageType.STATE_CHANGED: 605 # pulsesink doesn't notify a volume change on startup 606 # and the volume is only valid in > paused states. 607 if message.src is self._ext_vol_element: 608 self.notify("volume") 609 if message.src is self._ext_mute_element: 610 self.notify("mute") 611 elif message.type == Gst.MessageType.STREAM_START: 612 if self._in_gapless_transition: 613 print_d("Stream changed") 614 self._end(False) 615 elif message.type == Gst.MessageType.ELEMENT: 616 message_name = message.get_structure().get_name() 617 618 if message_name == "missing-plugin": 619 self.__handle_missing_plugin(message) 620 elif message.type == Gst.MessageType.CLOCK_LOST: 621 print_d("Clock lost") 622 self.bin.set_state(Gst.State.PAUSED) 623 self.bin.set_state(Gst.State.PLAYING) 624 elif message.type == Gst.MessageType.LATENCY: 625 print_d("Recalculate latency") 626 self.bin.recalculate_latency() 627 elif message.type == Gst.MessageType.REQUEST_STATE: 628 state = message.parse_request_state() 629 print_d("State requested: %s" % Gst.Element.state_get_name(state)) 630 self.bin.set_state(state) 631 elif message.type == Gst.MessageType.DURATION_CHANGED: 632 if self.song.fill_length: 633 ok, p = self.bin.query_duration(Gst.Format.TIME) 634 if ok: 635 p /= float(Gst.SECOND) 636 self.song["~#length"] = p 637 librarian.changed([self.song]) 638 639 def __handle_missing_plugin(self, message): 640 get_installer_detail = \ 641 GstPbutils.missing_plugin_message_get_installer_detail 642 get_description = GstPbutils.missing_plugin_message_get_description 643 644 details = get_installer_detail(message) 645 if details is None: 646 return 647 648 self.stop() 649 650 format_desc = get_description(message) 651 title = _(u"No GStreamer element found to handle media format") 652 error_details = _(u"Media format: %(format-description)s") % { 653 "format-description": format_desc} 654 655 def install_done_cb(plugins_return, *args): 656 print_d("Gstreamer plugin install return: %r" % plugins_return) 657 Gst.update_registry() 658 659 context = GstPbutils.InstallPluginsContext.new() 660 661 # new in 1.6 662 if hasattr(context, "set_desktop_id"): 663 from gi.repository import Gtk 664 context.set_desktop_id(app.id) 665 666 # new in 1.6 667 if hasattr(context, "set_startup_notification_id"): 668 current_time = Gtk.get_current_event_time() 669 context.set_startup_notification_id("_TIME%d" % current_time) 670 671 gdk_window = app.window.get_window() 672 if gdk_window: 673 try: 674 xid = gdk_window.get_xid() 675 except AttributeError: # non X11 676 pass 677 else: 678 context.set_xid(xid) 679 680 res = GstPbutils.install_plugins_async( 681 [details], context, install_done_cb, None) 682 print_d("Gstreamer plugin install result: %r" % res) 683 684 if res in (GstPbutils.InstallPluginsReturn.HELPER_MISSING, 685 GstPbutils.InstallPluginsReturn.INTERNAL_FAILURE): 686 self._error(PlayerError(title, error_details)) 687 688 def __about_to_finish_sync(self): 689 """Returns the next song uri to play or None""" 690 691 print_d("About to finish (sync)") 692 693 # Chained oggs falsely trigger a gapless transition. 694 # At least for radio streams we can safely ignore it because 695 # transitions don't occur there. 696 # https://github.com/quodlibet/quodlibet/issues/1454 697 # https://bugzilla.gnome.org/show_bug.cgi?id=695474 698 if self.song.multisong: 699 print_d("multisong: ignore about to finish") 700 return 701 702 # mod + gapless deadlocks 703 # https://github.com/quodlibet/quodlibet/issues/2780 704 if isinstance(self.song, ModFile): 705 return 706 707 if config.getboolean("player", "gst_disable_gapless"): 708 print_d("Gapless disabled") 709 return 710 711 # this can trigger twice, see issue 987 712 if self._in_gapless_transition: 713 return 714 self._in_gapless_transition = True 715 716 print_d("Select next song in mainloop..") 717 self._source.next_ended() 718 print_d("..done.") 719 720 song = self._source.current 721 if song is not None: 722 return song("~uri") 723 724 def __about_to_finish(self, playbin): 725 print_d("About to finish (async)") 726 727 try: 728 uri = self._runner.call(self.__about_to_finish_sync, 729 priority=GLib.PRIORITY_HIGH, 730 timeout=0.5) 731 except MainRunnerTimeoutError as e: 732 # Due to some locks being held during this signal we can get 733 # into a deadlock when a seek or state change event happens 734 # in the mainloop before our function gets scheduled. 735 # In this case abort and do nothing, which results 736 # in a non-gapless transition. 737 print_d("About to finish (async): %s" % e) 738 return 739 except MainRunnerAbortedError as e: 740 print_d("About to finish (async): %s" % e) 741 return 742 except MainRunnerError: 743 util.print_exc() 744 return 745 746 if uri is not None: 747 print_d("About to finish (async): setting uri") 748 playbin.set_property('uri', uri) 749 print_d("About to finish (async): done") 750 751 def stop(self): 752 super(GStreamerPlayer, self).stop() 753 self.__destroy_pipeline() 754 755 def do_get_property(self, property): 756 if property.name == 'volume': 757 if self._ext_vol_element is not None and \ 758 sink_has_external_state(self._ext_vol_element) and \ 759 sink_state_is_valid(self._ext_vol_element): 760 # never read back the volume if we don't have to, e.g. 761 # directsoundsink maps volume to an int which makes UI 762 # sliders jump if we read the value back 763 self._volume = self._ext_vol_element.get_property("volume") 764 return self._volume 765 elif property.name == "mute": 766 if self._ext_mute_element is not None and \ 767 sink_has_external_state(self._ext_mute_element) and \ 768 sink_state_is_valid(self._ext_mute_element): 769 self._mute = self._ext_mute_element.get_property("mute") 770 return self._mute 771 elif property.name == "seekable": 772 if self._seeker is not None: 773 return self._seeker.seekable 774 return False 775 else: 776 raise AttributeError 777 778 def _reset_replaygain(self): 779 if not self.bin: 780 return 781 782 v = 1.0 if self._ext_vol_element is not None else self._volume 783 v = self.calc_replaygain_volume(v) 784 v = min(10.0, max(0.0, v)) 785 self._int_vol_element.set_property('volume', v) 786 787 def do_set_property(self, property, v): 788 if property.name == 'volume': 789 self._volume = v 790 if self._ext_vol_element: 791 v = min(10.0, max(0.0, v)) 792 self._ext_vol_element.set_property("volume", v) 793 else: 794 v = self.calc_replaygain_volume(v) 795 if self.bin: 796 v = min(10.0, max(0.0, v)) 797 self._int_vol_element.set_property('volume', v) 798 elif property.name == 'mute': 799 self._mute = v 800 if self._ext_mute_element is not None: 801 self._ext_mute_element.set_property("mute", v) 802 else: 803 if self.bin: 804 self._int_vol_element.set_property("mute", v) 805 else: 806 raise AttributeError 807 808 def get_position(self): 809 """Return the current playback position in milliseconds, 810 or 0 if no song is playing.""" 811 812 if self._seeker: 813 return self._seeker.get_position() 814 return 0 815 816 @property 817 def paused(self): 818 return self._paused 819 820 @paused.setter 821 def paused(self, paused): 822 if paused == self._paused: 823 return 824 825 self._paused = paused 826 self.emit((paused and 'paused') or 'unpaused') 827 # in case a signal handler changed the paused state, abort this 828 if self._paused != paused: 829 return 830 831 if paused: 832 if self.bin: 833 if not self.song: 834 # Something wants us to pause between songs, or when 835 # we've got no song playing (probably StopAfterMenu). 836 self.__destroy_pipeline() 837 elif self.seekable: 838 self.bin.set_state(Gst.State.PAUSED) 839 else: 840 q = Gst.Query.new_buffering(Gst.Format.DEFAULT) 841 if self.bin.query(q): 842 # destroy so that we rebuffer on resume i.e. we don't 843 # want to continue unseekable streams from where we 844 # paused but from where we unpaused. 845 self.__destroy_pipeline() 846 else: 847 self.bin.set_state(Gst.State.PAUSED) 848 else: 849 if self.song and self.__init_pipeline(): 850 self.bin.set_state(Gst.State.PLAYING) 851 852 def _error(self, player_error): 853 """Destroy the pipeline and set the error state. 854 855 The passed PlayerError will be emitted through the 'error' signal. 856 """ 857 858 # prevent recursive errors 859 if self._active_error: 860 return 861 self._active_error = True 862 863 self.__destroy_pipeline() 864 self.error = True 865 self.paused = True 866 867 print_w(player_error) 868 self.emit('error', self.song, player_error) 869 self._active_error = False 870 871 def seek(self, pos): 872 """Seek to a position in the song, in milliseconds.""" 873 874 # Don't allow seeking during gapless. We can't go back to the old song. 875 if not self.song or self._in_gapless_transition: 876 return 877 878 if self.__init_pipeline(): 879 self._seeker.set_position(pos) 880 881 def sync(self, timeout): 882 if self.bin is not None: 883 # XXX: This is flaky, try multiple times 884 for i in range(5): 885 self.bin.get_state(Gst.SECOND * timeout) 886 # we have some logic in the main loop, so iterate there 887 while GLib.MainContext.default().iteration(False): 888 pass 889 890 def _end(self, stopped, next_song=None): 891 print_d("End song") 892 song, info = self.song, self.info 893 894 # set the new volume before the signals to avoid delays 895 if self._in_gapless_transition: 896 self.song = self._source.current 897 self._reset_replaygain() 898 899 # We need to set self.song to None before calling our signal 900 # handlers. Otherwise, if they try to end the song they're given 901 # (e.g. by removing it), then we get in an infinite loop. 902 self.__info_buffer = self.song = self.info = None 903 if song is not info: 904 self.emit('song-ended', info, stopped) 905 self.emit('song-ended', song, stopped) 906 907 current = self._source.current if next_song is None else next_song 908 909 # Then, set up the next song. 910 self.song = self.info = current 911 912 print_d("Next song") 913 if self.song is not None: 914 if not self._in_gapless_transition: 915 # Due to extensive problems with playbin2, we destroy the 916 # entire pipeline and recreate it each time we're not in 917 # a gapless transition. 918 self.__destroy_pipeline() 919 self.__init_pipeline() 920 if self.bin: 921 if self.paused: 922 self.bin.set_state(Gst.State.PAUSED) 923 else: 924 # something unpaused while no song was active 925 if song is None: 926 self.emit("unpaused") 927 self.bin.set_state(Gst.State.PLAYING) 928 else: 929 self.__destroy_pipeline() 930 931 self._in_gapless_transition = False 932 933 if self._seeker is not None: 934 # we could have a gapless transition to a non-seekable -> update 935 self._seeker.reset() 936 937 self.emit('song-started', self.song) 938 939 if self.song is None: 940 self.paused = True 941 942 def __tag(self, tags, librarian): 943 if self.song and self.song.multisong: 944 self._fill_stream(tags, librarian) 945 elif self.song and self.song.fill_metadata: 946 pass 947 948 def _fill_stream(self, tags, librarian): 949 # get a new remote file 950 new_info = self.__info_buffer 951 if not new_info: 952 new_info = type(self.song)(self.song["~filename"]) 953 new_info.multisong = False 954 new_info.streamsong = True 955 956 # copy from the old songs 957 # we should probably listen to the library for self.song changes 958 new_info.update(self.song) 959 new_info.update(self.info) 960 961 changed = False 962 info_changed = False 963 964 tags = TagListWrapper(tags, merge=True) 965 tags = parse_gstreamer_taglist(tags) 966 967 for key, value in sanitize_tags(tags, stream=False).items(): 968 if self.song.get(key) != value: 969 changed = True 970 self.song[key] = value 971 972 for key, value in sanitize_tags(tags, stream=True).items(): 973 if new_info.get(key) != value: 974 info_changed = True 975 new_info[key] = value 976 977 if info_changed: 978 # in case the title changed, make self.info a new instance 979 # and emit ended/started for the the old/new one 980 if self.info.get("title") != new_info.get("title"): 981 if self.info is not self.song: 982 self.emit('song-ended', self.info, False) 983 self.info = new_info 984 self.__info_buffer = None 985 self.emit('song-started', self.info) 986 else: 987 # in case title didn't changed, update the values of the 988 # old instance if there is one and tell the library. 989 if self.info is not self.song: 990 self.info.update(new_info) 991 librarian.changed([self.info]) 992 else: 993 # So we don't loose all tags before the first title 994 # save it for later 995 self.__info_buffer = new_info 996 997 if changed: 998 librarian.changed([self.song]) 999 1000 @property 1001 def eq_bands(self): 1002 if Gst.ElementFactory.find('equalizer-10bands'): 1003 return [29, 59, 119, 237, 474, 947, 1889, 3770, 7523, 15011] 1004 else: 1005 return [] 1006 1007 def update_eq_values(self): 1008 need_eq = any(self._eq_values) 1009 if need_eq != self._use_eq: 1010 self._use_eq = need_eq 1011 self._rebuild_pipeline() 1012 1013 if self._eq_element: 1014 for band, val in enumerate(self._eq_values): 1015 self._eq_element.set_property('band%d' % band, val) 1016 1017 def can_play_uri(self, uri): 1018 if not Gst.uri_is_valid(uri): 1019 return False 1020 try: 1021 Gst.Element.make_from_uri(Gst.URIType.SRC, uri, '') 1022 except GLib.GError: 1023 return False 1024 return True 1025 1026 1027def init(librarian): 1028 # Enable error messages by default 1029 if Gst.debug_get_default_threshold() == Gst.DebugLevel.NONE: 1030 Gst.debug_set_default_threshold(Gst.DebugLevel.ERROR) 1031 1032 return GStreamerPlayer(librarian) 1033