Merge pull request #1346 from adamcik/feature/eot-seek-handling

Gapless aware seek handling
This commit is contained in:
Stein Magnus Jodal 2015-12-05 21:34:51 +01:00
commit 23cdeb208b
4 changed files with 74 additions and 15 deletions

View File

@ -16,6 +16,10 @@ Core API
- Start ``tlid`` counting at 1 instead of 0 to keep in sync with MPD's - Start ``tlid`` counting at 1 instead of 0 to keep in sync with MPD's
``songid``. ``songid``.
- :meth:`~mopidy.core.PlaybackController.get_time_position` now returns the
seek target while a seek is in progress. This gives better results than just
failing the position query. (Fixes: :issue:`312` PR: :issue:`1346`)
Models Models
------ ------
@ -83,6 +87,11 @@ Gapless
- Tests have been updated to always use a core actor so async state changes - Tests have been updated to always use a core actor so async state changes
don't trip us up. don't trip us up.
- Seek events are now triggered when the seek completes. Previously the event
was emitted when the seek was requested, not when it completed. Further
changes have been made to make seek work correctly for gapless related corner
cases. (Fixes: :issue:`1305` PR: :issue:`1346`)
v1.1.2 (UNRELEASED) v1.1.2 (UNRELEASED)
=================== ===================

View File

@ -90,6 +90,9 @@ class Core(
def stream_changed(self, uri): def stream_changed(self, uri):
self.playback._on_stream_changed(uri) self.playback._on_stream_changed(uri)
def position_changed(self, position):
self.playback._on_position_changed(position)
def state_changed(self, old_state, new_state, target_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 # 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 # permanent solution with the implementation of issue #234. When the

View File

@ -26,6 +26,7 @@ class PlaybackController(object):
self._current_tl_track = None self._current_tl_track = None
self._pending_tl_track = None self._pending_tl_track = None
self._pending_position = None
self._last_position = None self._last_position = None
self._previous = False self._previous = False
@ -130,6 +131,8 @@ class PlaybackController(object):
def get_time_position(self): def get_time_position(self):
"""Get time position in milliseconds.""" """Get time position in milliseconds."""
if self._pending_position is not None:
return self._pending_position
backend = self._get_backend(self.get_current_tl_track()) backend = self._get_backend(self.get_current_tl_track())
if backend: if backend:
return backend.playback.get_time_position().get() return backend.playback.get_time_position().get()
@ -211,14 +214,24 @@ class PlaybackController(object):
# This code path handles the stop() case, uri should be none. # This code path handles the stop() case, uri should be none.
position, self._last_position = self._last_position, None position, self._last_position = self._last_position, None
self._trigger_track_playback_ended(position) if self._pending_position is None:
self._trigger_track_playback_ended(position)
self._stream_title = None self._stream_title = None
if self._pending_tl_track: if self._pending_tl_track:
self._set_current_tl_track(self._pending_tl_track) self._set_current_tl_track(self._pending_tl_track)
self._pending_tl_track = None self._pending_tl_track = None
self.set_state(PlaybackState.PLAYING)
self._trigger_track_playback_started() if self._pending_position is None:
self.set_state(PlaybackState.PLAYING)
self._trigger_track_playback_started()
else:
self._seek(self._pending_position)
def _on_position_changed(self, position):
if self._pending_position == position:
self._trigger_seeked(position)
self._pending_position = None
def _on_about_to_finish_callback(self): def _on_about_to_finish_callback(self):
"""Callback that performs a blocking actor call to the real callback. """Callback that performs a blocking actor call to the real callback.
@ -437,11 +450,6 @@ class PlaybackController(object):
if self.get_state() == PlaybackState.STOPPED: if self.get_state() == PlaybackState.STOPPED:
self.play() self.play()
# TODO: uncomment once we have tests for this. Should fix seek after
# about to finish doing wrong track.
# if self._current_tl_track and self._pending_tl_track:
# self.play(self._current_tl_track)
# We need to prefer the still playing track, but if nothing is playing # We need to prefer the still playing track, but if nothing is playing
# we fall back to the pending one. # we fall back to the pending one.
tl_track = self._current_tl_track or self._pending_tl_track tl_track = self._current_tl_track or self._pending_tl_track
@ -455,14 +463,21 @@ class PlaybackController(object):
self.next() self.next()
return True return True
# Store our target position.
self._pending_position = time_position
# Make sure we switch back to previous track if we get a seek while we
# have a pending track.
if self._current_tl_track and self._pending_tl_track:
self._change(self._current_tl_track, self.get_state())
else:
return self._seek(time_position)
def _seek(self, time_position):
backend = self._get_backend(self.get_current_tl_track()) backend = self._get_backend(self.get_current_tl_track())
if not backend: if not backend:
return False return False
return backend.playback.seek(time_position).get()
success = backend.playback.seek(time_position).get()
if success:
self._trigger_seeked(time_position)
return success
def stop(self): def stop(self):
"""Stop playing.""" """Stop playing."""

View File

@ -432,6 +432,7 @@ class EventEmissionTest(BaseTest):
self.core.playback.play(tl_tracks[0]) self.core.playback.play(tl_tracks[0])
self.replay_events() self.replay_events()
self.core.playback.seek(1000) self.core.playback.seek(1000)
self.replay_events()
listener_mock.reset_mock() listener_mock.reset_mock()
self.core.playback.stop() self.core.playback.stop()
@ -454,6 +455,7 @@ class EventEmissionTest(BaseTest):
self.core.playback.play(tl_tracks[0]) self.core.playback.play(tl_tracks[0])
self.replay_events() self.replay_events()
self.core.playback.seek(1000) self.core.playback.seek(1000)
self.replay_events()
listener_mock.reset_mock() listener_mock.reset_mock()
self.core.playback.next() self.core.playback.next()
@ -502,6 +504,7 @@ class EventEmissionTest(BaseTest):
listener_mock.reset_mock() listener_mock.reset_mock()
self.core.playback.seek(1000) self.core.playback.seek(1000)
self.replay_events()
listener_mock.send.assert_called_once_with( listener_mock.send.assert_called_once_with(
'seeked', time_position=1000) 'seeked', time_position=1000)
@ -529,6 +532,24 @@ class EventEmissionTest(BaseTest):
], ],
listener_mock.send.mock_calls) listener_mock.send.mock_calls)
def test_seek_race_condition_emits_events(self, listener_mock):
tl_tracks = self.core.tracklist.get_tl_tracks()
self.core.playback.play(tl_tracks[0])
self.trigger_about_to_finish(replay_until='stream_changed')
listener_mock.reset_mock()
self.core.playback.seek(1000)
self.replay_events()
# When we trigger seek after an about to finish the other code that
# emits track stopped/started and playback state changed events gets
# triggered as we have to switch back to the previous track.
# The correct behavior would be to only emit seeked.
self.assertListEqual(
[mock.call('seeked', time_position=1000)],
listener_mock.send.mock_calls)
def test_previous_emits_events(self, listener_mock): def test_previous_emits_events(self, listener_mock):
tl_tracks = self.core.tracklist.get_tl_tracks() tl_tracks = self.core.tracklist.get_tl_tracks()
@ -632,6 +653,19 @@ class SeekTest(BaseTest):
self.core.playback.seek(1000) self.core.playback.seek(1000)
self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED) self.assertEqual(self.core.playback.state, core.PlaybackState.PAUSED)
def test_seek_race_condition_after_about_to_finish(self):
tl_tracks = self.core.tracklist.get_tl_tracks()
self.core.playback.play(tl_tracks[0])
self.replay_events()
self.trigger_about_to_finish(replay_until='stream_changed')
self.core.playback.seek(1000)
self.replay_events()
current_tl_track = self.core.playback.get_current_tl_track()
self.assertEqual(current_tl_track, tl_tracks[0])
class TestStream(BaseTest): class TestStream(BaseTest):
@ -821,7 +855,6 @@ class BackendSelectionTest(unittest.TestCase):
self.core.playback.play(self.tl_tracks[0]) self.core.playback.play(self.tl_tracks[0])
self.trigger_stream_changed() self.trigger_stream_changed()
self.core.playback.seek(10000)
self.core.playback.time_position self.core.playback.time_position
self.playback1.get_time_position.assert_called_once_with() self.playback1.get_time_position.assert_called_once_with()
@ -831,7 +864,6 @@ class BackendSelectionTest(unittest.TestCase):
self.core.playback.play(self.tl_tracks[1]) self.core.playback.play(self.tl_tracks[1])
self.trigger_stream_changed() self.trigger_stream_changed()
self.core.playback.seek(10000)
self.core.playback.time_position self.core.playback.time_position
self.assertFalse(self.playback1.get_time_position.called) self.assertFalse(self.playback1.get_time_position.called)