diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index cbec0b4c..5fc84411 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -180,6 +180,8 @@ class Audio(pykka.ThreadingActor): queue.set_property('max-size-time', 5 * gst.SECOND) queue.set_property('min-threshold-time', 3 * gst.SECOND) + queue.get_pad('src').add_event_probe(self._on_pad_event) + output.add(user_output) output.add(queue) @@ -294,6 +296,13 @@ class Audio(pykka.ThreadingActor): self._disconnect(bus, 'message') bus.remove_signal_watch() + def _on_pad_event(self, pad, event): + if event.type == gst.EVENT_NEWSEGMENT: + # update, rate, format, start, stop, position + position = event.parse_new_segment()[5] // gst.MSECOND + AudioListener.send('position_changed', position=position) + return True + def _on_message(self, bus, message): if (message.type == gst.MESSAGE_STATE_CHANGED and message.src == self._playbin): diff --git a/mopidy/audio/listener.py b/mopidy/audio/listener.py index d2690031..5b33ffe6 100644 --- a/mopidy/audio/listener.py +++ b/mopidy/audio/listener.py @@ -37,6 +37,16 @@ class AudioListener(listener.Listener): """ pass + def position_changed(self, position_changed): + """ + Called whenever the position of the stream changes. + + *MAY* be implemented by actor. + + :param int position: Position in milliseconds. + """ + pass + def state_changed(self, old_state, new_state): """ Called after the playback state have changed. diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 8c956ab3..ad88b92a 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -231,6 +231,62 @@ class AudioEventTest(unittest.TestCase): call = mock.call('stream_changed', uri=None) self.assertIn(call, send_mock.call_args_list) + def test_position_changed_on_pause(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.pause_playback() + self.audio.wait_for_state_change() + + self.audio.wait_for_state_change().get() + + call = mock.call('position_changed', position=0) + self.assertIn(call, send_mock.call_args_list) + + def test_position_changed_on_play(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.start_playback() + self.audio.wait_for_state_change() + + self.audio.wait_for_state_change().get() + + call = mock.call('position_changed', position=0) + self.assertIn(call, send_mock.call_args_list) + + def test_position_changed_on_seek(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.set_position(2000) + + self.audio.wait_for_state_change().get() + + call = mock.call('position_changed', position=0) + self.assertNotIn(call, send_mock.call_args_list) + + def test_position_changed_on_seek_after_play(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.start_playback() + self.audio.wait_for_state_change() + self.audio.set_position(2000) + + self.audio.wait_for_state_change().get() + + call = mock.call('position_changed', position=2000) + self.assertIn(call, send_mock.call_args_list) + + def test_position_changed_on_seek_after_pause(self, send_mock): + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.pause_playback() + self.audio.wait_for_state_change() + self.audio.set_position(2000) + + self.audio.wait_for_state_change().get() + + call = mock.call('position_changed', position=2000) + self.assertIn(call, send_mock.call_args_list) + # Unlike the other events, having the state changed done is not # enough to ensure our event is called. So we setup a threading # event that we can wait for with a timeout while the track playback diff --git a/tests/audio/test_listener.py b/tests/audio/test_listener.py index c579fd55..84f5b59e 100644 --- a/tests/audio/test_listener.py +++ b/tests/audio/test_listener.py @@ -27,3 +27,6 @@ class AudioListenerTest(unittest.TestCase): def test_listener_has_default_impl_for_stream_changed(self): self.listener.stream_changed(None) + + def test_listener_has_default_impl_for_position_changed(self): + self.listener.position_changed(None)