diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5fc84411..64874938 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -58,6 +58,7 @@ class Audio(pykka.ThreadingActor): self._playbin = None self._signal_ids = {} # {(element, event): signal_id} + self._about_to_finish_callback = None self._mixer = None self._mixer_track = None @@ -108,15 +109,15 @@ class Audio(pykka.ThreadingActor): def _on_about_to_finish(self, element): source, self._appsrc = self._appsrc, None - if source is None: - return - self._appsrc_caps = None + if source is not None: + self._appsrc_caps = None + self._disconnect(source, 'need-data') + self._disconnect(source, 'enough-data') + self._disconnect(source, 'seek-data') - self._disconnect(source, 'need-data') - self._disconnect(source, 'enough-data') - self._disconnect(source, 'seek-data') - - logger.debug('Ready to switch to new stream') + if self._about_to_finish_callback: + logger.debug('Calling about to finish callback.') + self._about_to_finish_callback() def _on_new_source(self, element, pad): uri = element.get_property('uri') @@ -432,6 +433,19 @@ class Audio(pykka.ThreadingActor): # TODO: replace this with emit_data(None)? self._playbin.get_property('source').emit('end-of-stream') + def set_about_to_finish_callback(self, callback): + """ + Configure audio to use an about to finish callback. + + This should be used to achieve gapless playback. For this to work the + callback *MUST* call :meth:`set_uri` with the new URI to play and + block until this call has been made. :meth:`prepare_change` is not + needed before :meth:`set_uri` in this one special case. + + :param callable callback: Callback to run when we need the next URI. + """ + self._about_to_finish_callback = callback + def get_position(self): """ Get position in milliseconds. diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index ad88b92a..756c4d62 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -324,6 +324,45 @@ class AudioEventTest(unittest.TestCase): if not event.wait(timeout=5.0): self.fail('End of stream not reached within deadline') + # Make sure that gapless really works: + + def test_gapless(self, send_mock): + song2_uri = path_to_uri(path_to_data_dir('song2.wav')) + + uris = [song2_uri] + events = [] + done = threading.Event() + + def callback(): + if uris: + self.audio.set_uri(uris.pop()).get() + + def send(name, **kwargs): + events.append((name, kwargs)) + if name == 'reached_end_of_stream': + done.set() + + send_mock.side_effect = send + self.audio.set_about_to_finish_callback(callback).get() + + self.audio.prepare_change() + self.audio.set_uri(self.song_uri) + self.audio.start_playback() + self.audio.wait_for_state_change().get() + + if not done.wait(timeout=5.0): + self.fail('EOS not received') + + excepted = [ + ('position_changed', {'position': 0}), + ('stream_changed', {'uri': self.song_uri}), + ('state_changed', {'old_state': PlaybackState.STOPPED, + 'new_state': PlaybackState.PLAYING}), + ('position_changed', {'position': 0}), + ('stream_changed', {'uri': song2_uri}), + ('reached_end_of_stream', {})] + self.assertEqual(excepted, events) + class AudioStateTest(unittest.TestCase): def setUp(self):