Skip to content

Commit

Permalink
Fix latency for live streams (#1845)
Browse files Browse the repository at this point in the history
Fixes #1845
  • Loading branch information
lukh authored and jodal committed Dec 9, 2019
1 parent 96d78f9 commit d3c1824
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 4 deletions.
8 changes: 7 additions & 1 deletion docs/changelog.rst
Expand Up @@ -181,7 +181,9 @@ Tracklist controller
Backend API
-----------

- (no changes yet)
- Add :meth:`mopidy.backend.PlaybackProvider.is_live` which can be
re-implemented by Playback providers, to define if the custom
URI scheme is a live stream. (PR: :issue:`1845`)

Models
------
Expand Down Expand Up @@ -264,6 +266,10 @@ Audio
- Remove the method :meth:`mopidy.audio.Audio.emit_end_of_stream`, which has
been deprecated since 1.0. (Fixes: :issue:`1465`, PR: :issue:`1705`)

- Add live_stream option to the method :meth:`mopidy.audio.Audio.set_uri`,
which disables buffering, reducing latency for stream,
and discarding data when paused. (PR: :issue:`1845`)

Internals
---------

Expand Down
11 changes: 10 additions & 1 deletion mopidy/audio/actor.py
Expand Up @@ -435,6 +435,7 @@ def __init__(self, config, mixer):
self._config = config
self._target_state = Gst.State.NULL
self._buffering = False
self._live_stream = False
self._tags = {}
self._pending_uri = None
self._pending_tags = None
Expand Down Expand Up @@ -570,16 +571,23 @@ def _on_source_setup(self, element, source):
else:
self._appsrc.reset()

if self._live_stream and hasattr(source.props, "is_live"):
gst_logger.debug("Enabling live stream mode")
source.set_live(True)

utils.setup_proxy(source, self._config["proxy"])

def set_uri(self, uri):
def set_uri(self, uri, live_stream=False):
"""
Set URI of audio to be played.
You *MUST* call :meth:`prepare_change` before calling this method.
:param uri: the URI to play
:type uri: string
:param live_stream: disables buffering, reducing latency for stream,
and discarding data when paused
:type live_stream: bool
"""

# XXX: Hack to workaround issue on Mac OS X where volume level
Expand All @@ -591,6 +599,7 @@ def set_uri(self, uri):

self._pending_uri = uri
self._pending_tags = {}
self._live_stream = live_stream
self._playbin.set_property("uri", uri)

if self.mixer is not None and current_volume is not None:
Expand Down
18 changes: 17 additions & 1 deletion mopidy/backend.py
Expand Up @@ -213,6 +213,22 @@ def translate_uri(self, uri):
"""
return uri

def is_live(self, uri):
"""
Defines if the custom URI scheme should be read as a live stream.
*MAY be reimplemented by subclass.*
Playing a source as a live stream
disables buffering, which reduces latency before playback starts,
and discards data when paused.
:param uri: the custom URI
:type uri: string
:rtype: bool
"""
return False

def change_track(self, track):
"""
Swith to provided track.
Expand All @@ -235,7 +251,7 @@ def change_track(self, track):
logger.debug("Backend translated URI from %s to %s", track.uri, uri)
if not uri:
return False
self.audio.set_uri(uri).get()
self.audio.set_uri(uri, live_stream=self.is_live(uri)).get()
return True

def resume(self):
Expand Down
52 changes: 52 additions & 0 deletions tests/audio/test_actor.py
Expand Up @@ -629,3 +629,55 @@ def test_change_to_stopped_while_buffering(self):
self.audio.stop_playback()
playbin.set_state.assert_called_with(Gst.State.NULL)
assert not self.audio._buffering


class AudioLiveTest(unittest.TestCase):
config = {
"audio": {
"buffer_time": None,
"mixer": "fakemixer track_max_volume=65536",
"mixer_track": None,
"mixer_volume": None,
"output": "testoutput",
"visualizer": None,
}
}

def setUp(self): # noqa: N802
config = {
"audio": {
"buffer_time": None,
"mixer": "foomixer",
"mixer_volume": None,
"output": "testoutput",
"visualizer": None,
},
"proxy": {"hostname": ""},
}
self.audio = audio.Audio(config=config, mixer=None)

def test_not_live_mode(self):
source = mock.MagicMock()

# Avoid appsrc.configure()
source.get_factory.get_name = mock.MagicMock(return_value="not_appsrc")

source.props = mock.MagicMock(spec=[])
self.audio._live_stream = False

self.audio._on_source_setup("dummy", source)

source.set_live.assert_not_called()

def test_live_mode(self):
source = mock.MagicMock()

# Avoid appsrc.configure()
source.get_factory.get_name = mock.MagicMock(return_value="not_appsrc")

source.props.is_live = mock.MagicMock(return_value=True)
self.audio._live_stream = True

self.audio._on_source_setup("dummy", source)

source.set_live.assert_called_with(True)
3 changes: 3 additions & 0 deletions tests/core/test_playback.py
Expand Up @@ -173,6 +173,9 @@ def test_play_tlid(self):
current_tl_track = self.core.playback.get_current_tl_track()
assert tl_tracks[1] == current_tl_track

def test_default_is_live_behaviour_is_not_live(self):
assert not self.backend.playback.is_live(self.tracks[0].uri).get()


class TestNextHandling(BaseTest):
def test_get_current_tl_track_next(self):
Expand Down
4 changes: 3 additions & 1 deletion tests/dummy_audio.py
Expand Up @@ -24,14 +24,16 @@ def __init__(self, config=None, mixer=None):
self._callback = None
self._uri = None
self._stream_changed = False
self._live_stream = False
self._tags = {}
self._bad_uris = set()

def set_uri(self, uri):
def set_uri(self, uri, live_stream=False):
assert self._uri is None, "prepare change not called before set"
self._position = 0
self._uri = uri
self._stream_changed = True
self._live_stream = live_stream
self._tags = {}

def set_appsrc(self, *args, **kwargs):
Expand Down

0 comments on commit d3c1824

Please sign in to comment.