From 167932278b3751d2bc138953bf6fb2a364dda0c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Dec 2012 18:57:36 +0100 Subject: [PATCH 1/4] audio: Remove blocking get_state() calls in get_position() and seek() --- mopidy/audio/actor.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 1b6c79b3..714ab0a6 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -274,13 +274,11 @@ class Audio(pykka.ThreadingActor): :rtype: int """ - if self._playbin.get_state()[1] == gst.STATE_NULL: - return 0 try: position = self._playbin.query_position(gst.FORMAT_TIME)[0] return position // gst.MSECOND - except gst.QueryError, e: - logger.error('time_position failed: %s', e) + except gst.QueryError: + logger.debug('Position query failed') return 0 def set_position(self, position): @@ -291,12 +289,9 @@ class Audio(pykka.ThreadingActor): :type position: int :rtype: :class:`True` if successful, else :class:`False` """ - self._playbin.get_state() # block until state changes are done - handeled = self._playbin.seek_simple( + return self._playbin.seek_simple( gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) - self._playbin.get_state() # block until seek is done - return handeled def start_playback(self): """ From 9fa0f5213ee59e62d889d67ea439993cad70edc1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Dec 2012 18:57:03 +0100 Subject: [PATCH 2/4] audio: Setup appsrc with seek-data callback --- mopidy/audio/actor.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 714ab0a6..bd974bed 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -39,13 +39,17 @@ class Audio(pykka.ThreadingActor): super(Audio, self).__init__() self._playbin = None + self._mixer = None self._mixer_track = None self._mixer_scale = None self._software_mixing = False - self._appsrc = None self._volume_set = None + self._appsrc = None + self._appsrc_seek_data_callback = None + self._appsrc_seek_data_id = None + self._notify_source_signal_id = None self._about_to_finish_id = None self._message_signal_id = None @@ -77,7 +81,12 @@ class Audio(pykka.ThreadingActor): 'notify::source', self._on_new_source) def _on_about_to_finish(self, element): - self._appsrc = None + source, self._appsrc = self._appsrc, None + if source is None: + return + if self._appsrc_seek_data_id is not None: + source.disconnect(self._appsrc_seek_data_id) + self._appsrc_seek_data_id = None def _on_new_source(self, element, pad): uri = element.get_property('uri') @@ -93,9 +102,19 @@ class Audio(pykka.ThreadingActor): source.set_property('caps', default_caps) # GStreamer does not like unicode source.set_property('format', b'time') + source.set_property('stream-type', b'seekable') + + self._appsrc_seek_data_id = source.connect( + 'seek-data', self._appsrc_on_seek_data) self._appsrc = source + def _appsrc_on_seek_data(self, appsrc, time_in_ns): + time_in_ms = time_in_ns // gst.MSECOND + if self._appsrc_seek_data_callback is not None: + self._appsrc_seek_data_callback(time_in_ms) + return True + def _teardown_playbin(self): if self._about_to_finish_id: self._playbin.disconnect(self._about_to_finish_id) @@ -242,6 +261,19 @@ class Audio(pykka.ThreadingActor): """ self._playbin.set_property('uri', uri) + def set_appsrc(self, seek_data=None): + """ + Switch to using appsrc for getting audio to be played. + + You *MUST* call :meth:`prepare_change` before calling this method. + + :param seek_data: callback for when data from a new position is needed + to continue playback + :type seek_data: callable which takes time position in ms + """ + self._appsrc_seek_data_callback = seek_data + self._playbin.set_property('uri', 'appsrc://') + def emit_data(self, buffer_): """ Call this to deliver raw audio data to be played. From c7656cdc15e71320fb910c6eaf2ffb4a340b9de2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Dec 2012 20:24:58 +0100 Subject: [PATCH 3/4] spotify: Replace wall clock timer with GStreamer timer --- mopidy/backends/spotify/playback.py | 93 ++++------------------ mopidy/backends/spotify/session_manager.py | 4 + 2 files changed, 19 insertions(+), 78 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index e4534172..d80ef543 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -1,113 +1,50 @@ from __future__ import unicode_literals import logging -import time +import functools from spotify import Link, SpotifyError from mopidy.backends import base -from mopidy.core import PlaybackState logger = logging.getLogger('mopidy.backends.spotify') +def seek_data_callback(spotify_backend, time_position): + logger.debug('seek_data_callback(%d) called', time_position) + spotify_backend.playback.on_seek_data(time_position) + + class SpotifyPlaybackProvider(base.BasePlaybackProvider): - def __init__(self, *args, **kwargs): - super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs) - - self._timer = TrackPositionTimer() - - def pause(self): - self._timer.pause() - - return super(SpotifyPlaybackProvider, self).pause() - def play(self, track): if track.uri is None: return False + spotify_backend = self.backend.actor_ref.proxy() + seek_data_callback_bound = functools.partial( + seek_data_callback, spotify_backend) + try: self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) self.audio.prepare_change() - self.audio.set_uri('appsrc://') + self.audio.set_appsrc(seek_data=seek_data_callback_bound) self.audio.start_playback() self.audio.set_metadata(track) - self._timer.play() - return True except SpotifyError as e: logger.info('Playback of %s failed: %s', track.uri, e) return False - def resume(self): - time_position = self.get_time_position() - self._timer.resume() - self.audio.prepare_change() - result = self.seek(time_position) - self.audio.start_playback() - return result - - def seek(self, time_position): - self.backend.spotify.session.seek(time_position) - self._timer.seek(time_position) - return True - def stop(self): self.backend.spotify.session.play(0) - return super(SpotifyPlaybackProvider, self).stop() - def get_time_position(self): - # XXX: The default implementation of get_time_position hangs/times out - # when used with the Spotify backend and GStreamer appsrc. If this can - # be resolved, we no longer need to use a wall clock based time - # position for Spotify playback. - return self._timer.get_time_position() - - -class TrackPositionTimer(object): - """ - Keeps track of time position in a track using the wall clock and playback - events. - - To not introduce a reverse dependency on the playback controller, this - class keeps track of playback state itself. - """ - - def __init__(self): - self._state = PlaybackState.STOPPED - self._accumulated = 0 - self._started = 0 - - def play(self): - self._state = PlaybackState.PLAYING - self._accumulated = 0 - self._started = self._wall_time() - - def pause(self): - self._state = PlaybackState.PAUSED - self._accumulated += self._wall_time() - self._started - - def resume(self): - self._state = PlaybackState.PLAYING - - def seek(self, time_position): - self._started = self._wall_time() - self._accumulated = time_position - - def get_time_position(self): - if self._state == PlaybackState.PLAYING: - time_since_started = self._wall_time() - self._started - return self._accumulated + time_since_started - elif self._state == PlaybackState.PAUSED: - return self._accumulated - elif self._state == PlaybackState.STOPPED: - return 0 - - def _wall_time(self): - return int(time.time() * 1000) + def on_seek_data(self, time_position): + logger.debug('playback.on_seek_data(%d) called', time_position) + self.backend.spotify.next_buffer_timestamp = time_position + self.backend.spotify.session.seek(time_position) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index f2631406..0eed9939 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -46,6 +46,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): self.backend_ref = backend_ref self.connected = threading.Event() + self.next_buffer_timestamp = None self.container_manager = None self.playlist_manager = None @@ -121,6 +122,9 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): } buffer_ = gst.Buffer(bytes(frames)) buffer_.set_caps(gst.caps_from_string(capabilites)) + if self.next_buffer_timestamp is not None: + buffer_.timestamp = self.next_buffer_timestamp * gst.MSECOND + self.next_buffer_timestamp = None if self.audio.emit_data(buffer_).get(): return num_frames From c218375100f8fbe7c7d08e4e59453c3a6597c871 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Dec 2012 01:40:35 +0100 Subject: [PATCH 4/4] audio: Move Spotify appsrc caps out of audio --- mopidy/audio/actor.py | 18 ++++++++++-------- mopidy/backends/spotify/playback.py | 10 +++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bd974bed..65edd037 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -47,6 +47,7 @@ class Audio(pykka.ThreadingActor): self._volume_set = None self._appsrc = None + self._appsrc_caps = None self._appsrc_seek_data_callback = None self._appsrc_seek_data_id = None @@ -84,6 +85,7 @@ class Audio(pykka.ThreadingActor): source, self._appsrc = self._appsrc, None if source is None: return + self._appsrc_caps = None if self._appsrc_seek_data_id is not None: source.disconnect(self._appsrc_seek_data_id) self._appsrc_seek_data_id = None @@ -93,14 +95,8 @@ class Audio(pykka.ThreadingActor): if not uri or not uri.startswith('appsrc://'): return - # These caps matches the audio data provided by libspotify - default_caps = gst.Caps( - b'audio/x-raw-int, endianness=(int)1234, channels=(int)2, ' - b'width=(int)16, depth=(int)16, signed=(boolean)true, ' - b'rate=(int)44100') source = element.get_property('source') - source.set_property('caps', default_caps) - # GStreamer does not like unicode + source.set_property('caps', self._appsrc_caps) source.set_property('format', b'time') source.set_property('stream-type', b'seekable') @@ -261,16 +257,22 @@ class Audio(pykka.ThreadingActor): """ self._playbin.set_property('uri', uri) - def set_appsrc(self, seek_data=None): + def set_appsrc(self, caps, seek_data=None): """ Switch to using appsrc for getting audio to be played. You *MUST* call :meth:`prepare_change` before calling this method. + :param caps: GStreamer caps string describing the audio format to + expect + :type caps: string :param seek_data: callback for when data from a new position is needed to continue playback :type seek_data: callable which takes time position in ms """ + if isinstance(caps, unicode): + caps = caps.encode('utf-8') + self._appsrc_caps = gst.Caps(caps) self._appsrc_seek_data_callback = seek_data self._playbin.set_property('uri', 'appsrc://') diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index d80ef543..9069ce7e 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -17,6 +17,12 @@ def seek_data_callback(spotify_backend, time_position): class SpotifyPlaybackProvider(base.BasePlaybackProvider): + # These GStreamer caps matches the audio data provided by libspotify + _caps = ( + 'audio/x-raw-int, endianness=(int)1234, channels=(int)2, ' + 'width=(int)16, depth=(int)16, signed=(boolean)true, ' + 'rate=(int)44100') + def play(self, track): if track.uri is None: return False @@ -31,7 +37,9 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): self.backend.spotify.session.play(1) self.audio.prepare_change() - self.audio.set_appsrc(seek_data=seek_data_callback_bound) + self.audio.set_appsrc( + self._caps, + seek_data=seek_data_callback_bound) self.audio.start_playback() self.audio.set_metadata(track)