diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 68fbe42c..1f8dc746 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -23,6 +23,10 @@ mixers.register_mixers() playlists.register_typefinders() playlists.register_elements() +_GST_STATE_MAPPING = { + gst.STATE_PLAYING: PlaybackState.PLAYING, + gst.STATE_PAUSED: PlaybackState.PAUSED, + gst.STATE_NULL: PlaybackState.STOPPED} MB = 1 << 20 @@ -321,20 +325,18 @@ class Audio(pykka.ThreadingActor): if new_state == gst.STATE_READY: return # Ignore READY state as it's GStreamer specific - if new_state == gst.STATE_PLAYING: - new_state = PlaybackState.PLAYING - elif new_state == gst.STATE_PAUSED: - new_state = PlaybackState.PAUSED - elif new_state == gst.STATE_NULL: - new_state = PlaybackState.STOPPED - + new_state = _GST_STATE_MAPPING[new_state] old_state, self.state = self.state, new_state + target_state = _GST_STATE_MAPPING[self._target_state] + if target_state == new_state: + target_state = None + logger.debug( - 'Triggering event: state_changed(old_state=%s, new_state=%s)', - old_state, new_state) - AudioListener.send( - 'state_changed', old_state=old_state, new_state=new_state) + 'Triggering event: state_changed(old_state=%s, new_state=%s, ' + 'target_state=%s)', old_state, new_state, target_state) + AudioListener.send('state_changed', old_state=old_state, + new_state=new_state, target_state=target_state) def _on_buffering(self, percent): if percent < 10 and not self._buffering: diff --git a/mopidy/audio/listener.py b/mopidy/audio/listener.py index 537a81dd..d5203ab9 100644 --- a/mopidy/audio/listener.py +++ b/mopidy/audio/listener.py @@ -27,17 +27,31 @@ class AudioListener(listener.Listener): """ pass - def state_changed(self, old_state, new_state): + def state_changed(self, old_state, new_state, target_state): """ Called after the playback state have changed. Will be called for both immediate and async state changes in GStreamer. + Target state is used to when we should be in the target state, but + temporarily need to switch to an other state. A typical example of this + is buffering. When this happens an event with + `old=PLAYING, new=PAUSED, target=PLAYING` will be emitted. Once we have + caught up a `old=PAUSED, new=PLAYING, target=None` event will be + be generated. + + Regular state changes will not have target state set as they are final + states which should be stable. + *MAY* be implemented by actor. :param old_state: the state before the change :type old_state: string from :class:`mopidy.core.PlaybackState` field :param new_state: the state after the change + :type new_state: A :class:`mopidy.core.PlaybackState` field :type new_state: string from :class:`mopidy.core.PlaybackState` field + :param target_state: the intended state + :type target_state: string from :class:`mopidy.core.PlaybackState` + field or :class:`None` if this is a final state. """ pass diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index a3dba245..9d80d04c 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -65,7 +65,7 @@ class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener): def reached_end_of_stream(self): self.playback.on_end_of_track() - def state_changed(self, old_state, new_state): + def state_changed(self, old_state, new_state, target_state): # XXX: This is a temporary fix for issue #232 while we wait for a more # permanent solution with the implementation of issue #234. When the # Spotify play token is lost, the Spotify backend pauses audio diff --git a/tests/audio/test_listener.py b/tests/audio/test_listener.py index 1690ab80..6e0366cf 100644 --- a/tests/audio/test_listener.py +++ b/tests/audio/test_listener.py @@ -15,13 +15,14 @@ class AudioListenerTest(unittest.TestCase): self.listener.state_changed = mock.Mock() self.listener.on_event( - 'state_changed', old_state='stopped', new_state='playing') + 'state_changed', old_state='stopped', new_state='playing', + target_state=None) self.listener.state_changed.assert_called_with( - old_state='stopped', new_state='playing') + old_state='stopped', new_state='playing', target_state=None) def test_listener_has_default_impl_for_reached_end_of_stream(self): self.listener.reached_end_of_stream() def test_listener_has_default_impl_for_state_changed(self): - self.listener.state_changed(None, None) + self.listener.state_changed(None, None, None)