From b3e92ff5731e4070b2a78712f76bfdd8268d8e14 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Dec 2012 12:35:52 +0100 Subject: [PATCH 01/10] audio: Log buffering messages --- mopidy/audio/actor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bd974bed..c524bae1 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -207,6 +207,9 @@ class Audio(pykka.ThreadingActor): and message.src == self._playbin): old_state, new_state, pending_state = message.parse_state_changed() self._on_playbin_state_changed(old_state, new_state, pending_state) + elif message.type == gst.MESSAGE_BUFFERING: + percent = message.parse_buffering() + logger.debug('Buffer %d%% full', percent) elif message.type == gst.MESSAGE_EOS: self._on_end_of_stream() elif message.type == gst.MESSAGE_ERROR: From a555e84225b4411a5acebac38124562f1bb19a43 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Dec 2012 12:36:37 +0100 Subject: [PATCH 02/10] audio: Add appsrc need-data and enough-data callbacks --- mopidy/audio/actor.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c524bae1..9f96c161 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -47,6 +47,10 @@ class Audio(pykka.ThreadingActor): self._volume_set = None self._appsrc = None + self._appsrc_need_data_callback = None + self._appsrc_need_data_id = None + self._appsrc_enough_data_callback = None + self._appsrc_enough_data_id = None self._appsrc_seek_data_callback = None self._appsrc_seek_data_id = None @@ -84,6 +88,12 @@ class Audio(pykka.ThreadingActor): source, self._appsrc = self._appsrc, None if source is None: return + if self._appsrc_need_data_id is not None: + source.disconnect(self._appsrc_need_data_id) + self._appsrc_need_data_id = None + if self._appsrc_enough_data_id is not None: + source.disconnect(self._appsrc_enough_data_id) + self._appsrc_enough_data_id = None if self._appsrc_seek_data_id is not None: source.disconnect(self._appsrc_seek_data_id) self._appsrc_seek_data_id = None @@ -104,11 +114,26 @@ class Audio(pykka.ThreadingActor): source.set_property('format', b'time') source.set_property('stream-type', b'seekable') + self._appsrc_need_data_id = source.connect( + 'need-data', self._appsrc_on_need_data) + self._appsrc_enough_data_id = source.connect( + 'enough-data', self._appsrc_on_enough_data) self._appsrc_seek_data_id = source.connect( 'seek-data', self._appsrc_on_seek_data) self._appsrc = source + def _appsrc_on_need_data(self, appsrc, length_hint_in_ns): + length_hint_in_ms = length_hint_in_ns // gst.MSECOND + if self._appsrc_need_data_callback is not None: + self._appsrc_need_data_callback(length_hint_in_ms) + return True + + def _appsrc_on_enough_data(self, appsrc): + if self._appsrc_enough_data_callback is not None: + self._appsrc_enough_data_callback() + return True + 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: @@ -264,16 +289,22 @@ class Audio(pykka.ThreadingActor): """ self._playbin.set_property('uri', uri) - def set_appsrc(self, seek_data=None): + def set_appsrc(self, need_data=None, enough_data=None, seek_data=None): """ Switch to using appsrc for getting audio to be played. You *MUST* call :meth:`prepare_change` before calling this method. + :param need_data: callback for when appsrc needs data + :type need_data: callable which takes data length hint in ms + :param enough_data: callback for when appsrc has enough data + :type enough_data: callable :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_need_data_callback = need_data + self._appsrc_enough_data_callback = enough_data self._appsrc_seek_data_callback = seek_data self._playbin.set_property('uri', 'appsrc://') From a02f2a96020bc4ad0f7ef2cd6c597c325c879435 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Dec 2012 12:37:12 +0100 Subject: [PATCH 03/10] audio: Make appsrc buffer between 0.5 and 1 MB of data --- mopidy/audio/actor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9f96c161..c3d11d98 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -110,9 +110,10 @@ class Audio(pykka.ThreadingActor): b'rate=(int)44100') source = element.get_property('source') source.set_property('caps', default_caps) - # GStreamer does not like unicode source.set_property('format', b'time') source.set_property('stream-type', b'seekable') + source.set_property('max-bytes', 1024 * 1024) # 1 MB + source.set_property('min-percent', 50) self._appsrc_need_data_id = source.connect( 'need-data', self._appsrc_on_need_data) From 50e8ff04b3fe6718e394567dd785fba58ec20768 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Dec 2012 12:38:37 +0100 Subject: [PATCH 04/10] spotify: Only push audio data when GStreamer wants more --- mopidy/backends/spotify/playback.py | 23 +++++++++++++++++++++- mopidy/backends/spotify/session_manager.py | 5 +++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index d80ef543..9855e124 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -11,6 +11,16 @@ from mopidy.backends import base logger = logging.getLogger('mopidy.backends.spotify') +def need_data_callback(spotify_backend, length_hint): + logger.debug('need_data_callback(%d) called', length_hint) + spotify_backend.playback.on_need_data(length_hint) + + +def enough_data_callback(spotify_backend): + logger.debug('enough_data_callback() called') + spotify_backend.playback.on_enough_data() + + 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) @@ -31,7 +41,10 @@ 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( + need_data=None, + enough_data=None, + seek_data=seek_data_callback_bound) self.audio.start_playback() self.audio.set_metadata(track) @@ -44,6 +57,14 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): self.backend.spotify.session.play(0) return super(SpotifyPlaybackProvider, self).stop() + def on_need_data(self, length_hint): + logger.debug('playback.on_need_data(%d) called', length_hint) + self.backend.spotify.push_audio_data = True + + def on_enough_data(self): + logger.debug('playback.on_enough_data() called') + self.backend.spotify.push_audio_data = False + 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 diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 0eed9939..a9c0884e 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.push_audio_data = True self.next_buffer_timestamp = None self.container_manager = None @@ -107,6 +108,10 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): """Callback used by pyspotify""" # pylint: disable = R0913 # Too many arguments (8/5) + + if not self.push_audio_data: + return 0 + assert sample_type == 0, 'Expects 16-bit signed integer samples' capabilites = """ audio/x-raw-int, From aa0c6f6dc02a9ccf114bd95499256d9a973b7573 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Dec 2012 12:18:48 +0100 Subject: [PATCH 05/10] audio: Add MB constant to make code more readable --- mopidy/audio/actor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 6aecbe8a..672aa540 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -21,6 +21,9 @@ logger = logging.getLogger('mopidy.audio') mixers.register_mixers() +MB = 1 << 20 + + class Audio(pykka.ThreadingActor): """ Audio output through `GStreamer `_. @@ -109,7 +112,7 @@ class Audio(pykka.ThreadingActor): source.set_property('caps', self._appsrc_caps) source.set_property('format', b'time') source.set_property('stream-type', b'seekable') - source.set_property('max-bytes', 1024 * 1024) # 1 MB + source.set_property('max-bytes', 1 * MB) source.set_property('min-percent', 50) self._appsrc_need_data_id = source.connect( From 7c790d61b2c046960a578725b546cec1555eb064 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Dec 2012 12:21:05 +0100 Subject: [PATCH 06/10] spotify: Hook need-data and enough-data callbacks onto appsrc --- mopidy/backends/spotify/playback.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 6899ee47..45850107 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -38,6 +38,10 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): return False spotify_backend = self.backend.actor_ref.proxy() + need_data_callback_bound = functools.partial( + need_data_callback, spotify_backend) + enough_data_callback_bound = functools.partial( + enough_data_callback, spotify_backend) seek_data_callback_bound = functools.partial( seek_data_callback, spotify_backend) @@ -49,8 +53,8 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): self.audio.prepare_change() self.audio.set_appsrc( self._caps, - need_data=None, - enough_data=None, + need_data=need_data_callback_bound, + enough_data=enough_data_callback_bound, seek_data=seek_data_callback_bound) self.audio.start_playback() self.audio.set_metadata(track) From 5e94aa19ec5419a30c219746acd5f265dfaaf75c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 24 Dec 2012 12:51:13 +0100 Subject: [PATCH 07/10] spotify: Reduce callback logging --- mopidy/backends/spotify/playback.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 45850107..c148972c 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -12,17 +12,14 @@ logger = logging.getLogger('mopidy.backends.spotify') def need_data_callback(spotify_backend, length_hint): - logger.debug('need_data_callback(%d) called', length_hint) spotify_backend.playback.on_need_data(length_hint) def enough_data_callback(spotify_backend): - logger.debug('enough_data_callback() called') spotify_backend.playback.on_enough_data() 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) From 64ae7865bb52cf5737595652fd8bf2a7629cc8d8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 19:38:31 +0100 Subject: [PATCH 08/10] audio: Add clocktime_to_millisecond util function --- mopidy/audio/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 9d0f46dd..5bb3c4ca 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -6,7 +6,8 @@ import gst def calculate_duration(num_samples, sample_rate): - """Determine duration of samples using GStreamer helper for precise math.""" + """Determine duration of samples using GStreamer helper for precise + math.""" return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate) @@ -28,10 +29,15 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): def millisecond_to_clocktime(value): - """Convert a millisecond time to internal gstreamer time.""" + """Convert a millisecond time to internal GStreamer time.""" return value * gst.MSECOND +def clocktime_to_millisecond(value): + """Convert a millisecond time to internal GStreamer time.""" + return value // gst.MSECOND + + def supported_uri_schemes(uri_schemes): """Determine which URIs we can actually support from provided whitelist. From c76ac2d2126705d77cd4f556d5cf2b66cc3eefc9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 19:40:53 +0100 Subject: [PATCH 09/10] audio: Use time conversion utils --- mopidy/audio/actor.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 672aa540..b021c35a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -12,7 +12,7 @@ import pykka from mopidy import settings from mopidy.utils import process -from . import mixers +from . import mixers, utils from .constants import PlaybackState from .listener import AudioListener @@ -124,10 +124,10 @@ class Audio(pykka.ThreadingActor): self._appsrc = source - def _appsrc_on_need_data(self, appsrc, length_hint_in_ns): - length_hint_in_ms = length_hint_in_ns // gst.MSECOND + def _appsrc_on_need_data(self, appsrc, gst_length_hint): + length_hint = utils.clocktime_to_millisecond(gst_length_hint) if self._appsrc_need_data_callback is not None: - self._appsrc_need_data_callback(length_hint_in_ms) + self._appsrc_need_data_callback(length_hint) return True def _appsrc_on_enough_data(self, appsrc): @@ -135,10 +135,10 @@ class Audio(pykka.ThreadingActor): self._appsrc_enough_data_callback() return True - def _appsrc_on_seek_data(self, appsrc, time_in_ns): - time_in_ms = time_in_ns // gst.MSECOND + def _appsrc_on_seek_data(self, appsrc, gst_position): + position = utils.clocktime_to_millisecond(gst_position) if self._appsrc_seek_data_callback is not None: - self._appsrc_seek_data_callback(time_in_ms) + self._appsrc_seek_data_callback(position) return True def _teardown_playbin(self): @@ -349,8 +349,8 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - position = self._playbin.query_position(gst.FORMAT_TIME)[0] - return position // gst.MSECOND + gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0] + return utils.clocktime_to_millisecond(gst_position) except gst.QueryError: logger.debug('Position query failed') return 0 @@ -363,9 +363,9 @@ class Audio(pykka.ThreadingActor): :type position: int :rtype: :class:`True` if successful, else :class:`False` """ + gst_position = utils.millisecond_to_clocktime(position) return self._playbin.seek_simple( - gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, - position * gst.MSECOND) + gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position) def start_playback(self): """ From edde1bc584aec31f1e2963d2197e3e78b9de0b45 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 3 Jan 2013 21:40:20 +0100 Subject: [PATCH 10/10] audio: Fix docstring --- mopidy/audio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 5bb3c4ca..15196b20 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -34,7 +34,7 @@ def millisecond_to_clocktime(value): def clocktime_to_millisecond(value): - """Convert a millisecond time to internal GStreamer time.""" + """Convert an internal GStreamer time to millisecond time.""" return value // gst.MSECOND