From da67213a5f998e8013e7db5f15374de75088f1f6 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 9 Aug 2010 09:53:46 +0200 Subject: [PATCH 1/5] initial prototype to have libspotify backend to use gstreamer --- mopidy/backends/libspotify.py | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 4f1b2049..32444612 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -14,8 +14,7 @@ from mopidy.backends import (BaseBackend, BaseCurrentPlaylistController, BaseStoredPlaylistsController) from mopidy.models import Artist, Album, Track, Playlist -import alsaaudio - +import gst logger = logging.getLogger('mopidy.backends.libspotify') ENCODING = 'utf-8' @@ -200,9 +199,29 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): threading.Thread.__init__(self) self.core_queue = core_queue self.connected = threading.Event() - self.audio = audio_controller_class(alsaaudio.PCM_NORMAL) self.session = None + cap_string = """audio/x-raw-int, + endianness=(int)1234, + channels=(int)2, + width=(int)16, + depth=(int)16, + signed=True, + rate=(int)44100""" + caps = gst.caps_from_string(cap_string) + + self.gsrc = gst.element_factory_make("appsrc", "app-source") + self.gsrc.set_property('caps', caps) + + self.gsink = gst.element_factory_make("autoaudiosink", "autosink") + + self.pipeline = gst.Pipeline("spotify_pipeline") + self.pipeline.add(self.gsrc, self.gsink) + + gst.element_link_many(self.gsrc, self.gsink) + + self.pipeline.set_state(gst.STATE_PLAYING) + def run(self): self.connect() @@ -243,8 +262,17 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def music_delivery(self, session, frames, frame_size, num_frames, sample_type, sample_rate, channels): """Callback used by pyspotify""" - self.audio.music_delivery(session, frames, frame_size, num_frames, - sample_type, sample_rate, channels) + cap_string = """audio/x-raw-int, + endianness=(int)1234, + channels=(int)2, + width=(int)16, + depth=(int)16, + signed=True, + rate=(int)44100""" + caps = gst.caps_from_string(cap_string) + b = gst.Buffer(frames) + b.set_caps(caps) + self.gsrc.emit('push-buffer', b) def play_token_lost(self, session): """Callback used by pyspotify""" From 659b879e9c18892dfd7b9c96e319011fb4da9dde Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Mon, 9 Aug 2010 18:23:46 +0200 Subject: [PATCH 2/5] moved gstreamer pipeline initialization to backend class to flush the buffer on stop/play and removed audiocontroller arguments --- mopidy/backends/libspotify.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 047f1328..8e193949 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -43,8 +43,7 @@ class LibspotifyBackend(BaseBackend): self.stored_playlists = LibspotifyStoredPlaylistsController( backend=self) self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] - self.audio_controller_class = kwargs.get( - 'audio_controller_class', AlsaController) + self.gstreamer_pipeline = gst.Pipeline("spotify_pipeline") self.spotify = self._connect() def _connect(self): @@ -52,7 +51,7 @@ class LibspotifyBackend(BaseBackend): spotify = LibspotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, core_queue=self.core_queue, - audio_controller_class=self.audio_controller_class) + gstreamer_pipeline=self.gstreamer_pipeline) spotify.start() return spotify @@ -99,6 +98,7 @@ class LibspotifyPlaybackController(BasePlaybackController): return False def _play(self, track): + self.backend.gstreamer_pipeline.set_state(gst.STATE_READY) if self.state == self.PLAYING: self.stop() if track.uri is None: @@ -107,6 +107,7 @@ class LibspotifyPlaybackController(BasePlaybackController): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) + self.backend.gstreamer_pipeline.set_state(gst.STATE_PLAYING) return True except SpotifyError as e: logger.warning('Play %s failed: %s', track.uri, e) @@ -120,6 +121,7 @@ class LibspotifyPlaybackController(BasePlaybackController): pass # TODO def _stop(self): + self.backend.gstreamer_pipeline.set_state(gst.STATE_READY) self.backend.spotify.session.play(0) return True @@ -201,12 +203,13 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) user_agent = 'Mopidy %s' % get_version() - def __init__(self, username, password, core_queue, audio_controller_class): + def __init__(self, username, password, core_queue, gstreamer_pipeline): SpotifySessionManager.__init__(self, username, password) threading.Thread.__init__(self) self.core_queue = core_queue self.connected = threading.Event() self.session = None + self.gstreamer_pipeline = gstreamer_pipeline cap_string = """audio/x-raw-int, endianness=(int)1234, @@ -222,13 +225,10 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): self.gsink = gst.element_factory_make("autoaudiosink", "autosink") - self.pipeline = gst.Pipeline("spotify_pipeline") - self.pipeline.add(self.gsrc, self.gsink) + self.gstreamer_pipeline.add(self.gsrc, self.gsink) gst.element_link_many(self.gsrc, self.gsink) - self.pipeline.set_state(gst.STATE_PLAYING) - def run(self): self.connect() From d509c12c1e02e79c247c162ca7f28b15532301d5 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 10 Aug 2010 13:24:58 +0200 Subject: [PATCH 3/5] emit end-of-stream in end of track callback, pull for gstreamer end-of-stream messages and send end of track mpd command --- mopidy/backends/libspotify.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 8e193949..159cbf75 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -196,6 +196,21 @@ class LibspotifyTranslator(object): tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], ) +class GstreamerMessageBusProcess(threading.Thread): + def __init__(self, core_queue, pipeline): + super(GstreamerMessageBusProcess, self).__init__() + self.core_queue = core_queue + self.bus = pipeline.get_bus() + + def run(self): + while True: + message = self.bus.pop() + if message is not None: + logger.debug('Got Gstreamer message of type: %s' % message.type) + if message is not None and ( + message.type == gst.MESSAGE_EOS + or message.type == gst.MESSAGE_ERROR): + self.core_queue.put({'command': 'end_of_track'}) class LibspotifySessionManager(SpotifySessionManager, threading.Thread): cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) @@ -229,6 +244,9 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): gst.element_link_many(self.gsrc, self.gsink) + message_process = GstreamerMessageBusProcess(self.core_queue, self.gstreamer_pipeline) + message_process.start() + def run(self): self.connect() @@ -292,8 +310,9 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def end_of_track(self, session): """Callback used by pyspotify""" - logger.debug('End of track') - self.core_queue.put({'command': 'end_of_track'}) + logger.debug('End of track.') + self.gsrc.emit('end-of-stream') + logger.debug('End of stream sent to gstreamer.') def search(self, query, connection): """Search method used by Mopidy backend""" From ae46832e62a1270146c506faaca46acf254d58d6 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 10 Aug 2010 14:52:16 +0200 Subject: [PATCH 4/5] block gstreamer loop to avoid 100% cpu and avoid C-threads to freeze --- mopidy/backends/libspotify.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/libspotify.py b/mopidy/backends/libspotify.py index 159cbf75..63cb62e4 100644 --- a/mopidy/backends/libspotify.py +++ b/mopidy/backends/libspotify.py @@ -1,4 +1,5 @@ import datetime as dt +import gobject import logging import os import multiprocessing @@ -203,14 +204,15 @@ class GstreamerMessageBusProcess(threading.Thread): self.bus = pipeline.get_bus() def run(self): + loop = gobject.MainLoop() + gobject.threads_init() + context = loop.get_context() while True: - message = self.bus.pop() + message = self.bus.pop_filtered(gst.MESSAGE_EOS) if message is not None: - logger.debug('Got Gstreamer message of type: %s' % message.type) - if message is not None and ( - message.type == gst.MESSAGE_EOS - or message.type == gst.MESSAGE_ERROR): self.core_queue.put({'command': 'end_of_track'}) + logger.debug('Got and handled Gstreamer message of type: %s' % message.type) + context.iteration(True) class LibspotifySessionManager(SpotifySessionManager, threading.Thread): cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) From ada20b7af100e9067056c6f76fe643e601d55969 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Tue, 10 Aug 2010 16:46:46 +0200 Subject: [PATCH 5/5] simple libspotify pause/resume implementation --- mopidy/backends/libspotify/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 5afaa293..0b1f2483 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -95,8 +95,9 @@ class LibspotifyLibraryController(BaseLibraryController): class LibspotifyPlaybackController(BasePlaybackController): def _pause(self): - # TODO - return False + result = self.backend.gstreamer_pipeline.set_state(gst.STATE_PAUSED) + logger.debug('Changed gstreamer state to paused. Result was: %s' % result) + return result == gst.STATE_CHANGE_SUCCESS def _play(self, track): self.backend.gstreamer_pipeline.set_state(gst.STATE_READY) @@ -115,8 +116,9 @@ class LibspotifyPlaybackController(BasePlaybackController): return False def _resume(self): - # TODO - return False + result = self.backend.gstreamer_pipeline.set_state(gst.STATE_PLAYING) + logger.debug('Changed gstreamer state to playing. Result was: %s' % result) + return result == gst.STATE_CHANGE_SUCCESS def _seek(self, time_position): pass # TODO