diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py
index 2fa96dab..126034f1 100644
--- a/mopidy/backends/local/__init__.py
+++ b/mopidy/backends/local/__init__.py
@@ -12,7 +12,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController,
BasePlaybackProvider, StoredPlaylistsController,
BaseStoredPlaylistsProvider)
from mopidy.models import Playlist, Track, Album
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
from .translator import parse_m3u, parse_mpd_tag_cache
@@ -50,12 +50,12 @@ class LocalBackend(ThreadingActor, Backend):
self.uri_handlers = [u'file://']
- self.output = None
+ self.gstreamer = None
def on_start(self):
- output_refs = ActorRegistry.get_by_class(BaseOutput)
- assert len(output_refs) == 1, 'Expected exactly one running output.'
- self.output = output_refs[0].proxy()
+ gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
+ assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ self.gstreamer = gstreamer_refs[0].proxy()
class LocalPlaybackController(PlaybackController):
@@ -67,24 +67,24 @@ class LocalPlaybackController(PlaybackController):
@property
def time_position(self):
- return self.backend.output.get_position().get()
+ return self.backend.gstreamer.get_position().get()
class LocalPlaybackProvider(BasePlaybackProvider):
def pause(self):
- return self.backend.output.set_state('PAUSED').get()
+ return self.backend.gstreamer.set_state('PAUSED').get()
def play(self, track):
- return self.backend.output.play_uri(track.uri).get()
+ return self.backend.gstreamer.play_uri(track.uri).get()
def resume(self):
- return self.backend.output.set_state('PLAYING').get()
+ return self.backend.gstreamer.set_state('PLAYING').get()
def seek(self, time_position):
- return self.backend.output.set_position(time_position).get()
+ return self.backend.gstreamer.set_position(time_position).get()
def stop(self):
- return self.backend.output.set_state('READY').get()
+ return self.backend.gstreamer.set_state('READY').get()
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py
index 1ac5f0be..9dababc0 100644
--- a/mopidy/backends/spotify/__init__.py
+++ b/mopidy/backends/spotify/__init__.py
@@ -6,7 +6,7 @@ from pykka.registry import ActorRegistry
from mopidy import settings
from mopidy.backends.base import (Backend, CurrentPlaylistController,
LibraryController, PlaybackController, StoredPlaylistsController)
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
logger = logging.getLogger('mopidy.backends.spotify')
@@ -63,13 +63,13 @@ class SpotifyBackend(ThreadingActor, Backend):
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
- self.output = None
+ self.gstreamer = None
self.spotify = None
def on_start(self):
- output_refs = ActorRegistry.get_by_class(BaseOutput)
- assert len(output_refs) == 1, 'Expected exactly one running output.'
- self.output = output_refs[0].proxy()
+ gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
+ assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ self.gstreamer = gstreamer_refs[0].proxy()
self.spotify = self._connect()
diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py
index 3721fe9c..3f2a157b 100644
--- a/mopidy/backends/spotify/playback.py
+++ b/mopidy/backends/spotify/playback.py
@@ -8,10 +8,10 @@ logger = logging.getLogger('mopidy.backends.spotify.playback')
class SpotifyPlaybackProvider(BasePlaybackProvider):
def pause(self):
- return self.backend.output.set_state('PAUSED')
+ return self.backend.gstreamer.set_state('PAUSED')
def play(self, track):
- self.backend.output.set_state('READY')
+ self.backend.gstreamer.set_state('READY')
if self.backend.playback.state == self.backend.playback.PLAYING:
self.backend.spotify.session.play(0)
if track.uri is None:
@@ -20,7 +20,8 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
- self.backend.output.play_uri('appsrc://')
+ self.backend.gstreamer.play_uri('appsrc://')
+ self.backend.gstreamer.set_metadata(track)
return True
except SpotifyError as e:
logger.info('Playback of %s failed: %s', track.uri, e)
@@ -30,12 +31,12 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
return self.seek(self.backend.playback.time_position)
def seek(self, time_position):
- self.backend.output.set_state('READY')
+ self.backend.gstreamer.set_state('READY')
self.backend.spotify.session.seek(time_position)
- self.backend.output.set_state('PLAYING')
+ self.backend.gstreamer.set_state('PLAYING')
return True
def stop(self):
- result = self.backend.output.set_state('READY')
+ result = self.backend.gstreamer.set_state('READY')
self.backend.spotify.session.play(0)
return result
diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py
index 395f3f28..09064db2 100644
--- a/mopidy/backends/spotify/session_manager.py
+++ b/mopidy/backends/spotify/session_manager.py
@@ -10,7 +10,7 @@ from mopidy import get_version, settings
from mopidy.backends.base import Backend
from mopidy.backends.spotify.translator import SpotifyTranslator
from mopidy.models import Playlist
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
from mopidy.utils.process import BaseThread
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
@@ -29,7 +29,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
BaseThread.__init__(self)
self.name = 'SpotifySMThread'
- self.output = None
+ self.gstreamer = None
self.backend = None
self.connected = threading.Event()
@@ -40,9 +40,9 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
self.connect()
def setup(self):
- output_refs = ActorRegistry.get_by_class(BaseOutput)
- assert len(output_refs) == 1, 'Expected exactly one running output.'
- self.output = output_refs[0].proxy()
+ gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
+ assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
+ self.gstreamer = gstreamer_refs[0].proxy()
backend_refs = ActorRegistry.get_by_class(Backend)
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
@@ -106,7 +106,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
'sample_rate': sample_rate,
'channels': channels,
}
- self.output.deliver_data(capabilites, bytes(frames))
+ self.gstreamer.deliver_data(capabilites, bytes(frames))
def play_token_lost(self, session):
"""Callback used by pyspotify"""
@@ -120,7 +120,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug(u'End of data stream reached')
- self.output.end_of_data_stream()
+ self.gstreamer.end_of_data_stream()
def refresh_stored_playlists(self):
"""Refresh the stored playlists in the backend with fresh meta data
diff --git a/mopidy/core.py b/mopidy/core.py
index f1a9dc36..4ca6ec29 100644
--- a/mopidy/core.py
+++ b/mopidy/core.py
@@ -5,6 +5,7 @@ import time
from pykka.registry import ActorRegistry
from mopidy import get_version, settings, OptionalDependencyError
+from mopidy.gstreamer import GStreamer
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder, get_or_create_file
@@ -18,7 +19,7 @@ def main():
setup_logging(options.verbosity_level, options.save_debug_log)
setup_settings()
setup_gobject_loop()
- setup_output()
+ setup_gstreamer()
setup_mixer()
setup_backend()
setup_frontends()
@@ -56,8 +57,8 @@ def setup_gobject_loop():
gobject_loop.start()
return gobject_loop
-def setup_output():
- return get_class(settings.OUTPUT).start().proxy()
+def setup_gstreamer():
+ return GStreamer().start().proxy()
def setup_mixer():
return get_class(settings.MIXER).start().proxy()
diff --git a/mopidy/outputs/gstreamer.py b/mopidy/gstreamer.py
similarity index 67%
rename from mopidy/outputs/gstreamer.py
rename to mopidy/gstreamer.py
index 11cddc42..6a00ca37 100644
--- a/mopidy/outputs/gstreamer.py
+++ b/mopidy/gstreamer.py
@@ -8,10 +8,10 @@ from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy import settings
+from mopidy.utils import get_class
from mopidy.backends.base import Backend
-from mopidy.outputs.base import BaseOutput
-logger = logging.getLogger('mopidy.outputs.gstreamer')
+logger = logging.getLogger('mopidy.gstreamer')
default_caps = gst.Caps("""
audio/x-raw-int,
@@ -22,7 +22,48 @@ default_caps = gst.Caps("""
signed=(boolean)true,
rate=(int)44100""")
-class GStreamerOutput(ThreadingActor, BaseOutput):
+class BaseOutput(object):
+ def connect_bin(self, pipeline, element_to_link_to):
+ """
+ Connect output bin to pipeline and given element.
+ """
+ description = 'queue ! %s' % self.describe_bin()
+ logger.debug('Adding new output to tee: %s', description)
+
+ output = self.parse_bin(description)
+ self.modify_bin(output)
+
+ pipeline.add(output)
+ output.sync_state_with_parent()
+ gst.element_link_many(element_to_link_to, output)
+
+ def parse_bin(self, description):
+ return gst.parse_bin_from_description(description, True)
+
+ def modify_bin(self, output):
+ """
+ Modifies bin before it is installed if needed
+ """
+ pass
+
+ def describe_bin(self):
+ """
+ Describe bin to be parsed.
+
+ Must be implemented by subclasses.
+ """
+ raise NotImplementedError
+
+ def set_properties(self, element, properties):
+ """
+ Set properties on element if they have a value.
+ """
+ for key, value in properties.items():
+ if value:
+ element.set_property(key, value)
+
+
+class GStreamer(ThreadingActor):
"""
Audio output through `GStreamer `_.
@@ -44,22 +85,32 @@ class GStreamerOutput(ThreadingActor, BaseOutput):
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
not enforced by :class:`GStreamerOutput` itself.
"""
-
- logger.debug(u'Setting up GStreamer pipeline')
-
- self.gst_pipeline = gst.parse_launch(' ! '.join([
+ base_pipeline = ' ! '.join([
'audioconvert name=convert',
'volume name=volume',
- settings.GSTREAMER_AUDIO_SINK,
- ]))
+ 'taginject name=tag',
+ 'tee name=tee',
+ ])
- pad = self.gst_pipeline.get_by_name('convert').get_pad('sink')
+ logger.debug(u'Setting up base GStreamer pipeline: %s', base_pipeline)
+
+ self.gst_pipeline = gst.parse_launch(base_pipeline)
+
+ self.gst_tee = self.gst_pipeline.get_by_name('tee')
+ self.gst_convert = self.gst_pipeline.get_by_name('convert')
+ self.gst_volume = self.gst_pipeline.get_by_name('volume')
+ self.gst_taginject = self.gst_pipeline.get_by_name('tag')
uridecodebin = gst.element_factory_make('uridecodebin', 'uri')
- uridecodebin.connect('pad-added', self._process_new_pad, pad)
uridecodebin.connect('notify::source', self._process_new_source)
+ uridecodebin.connect('pad-added', self._process_new_pad,
+ self.gst_convert.get_pad('sink'))
self.gst_pipeline.add(uridecodebin)
+ for output in settings.OUTPUTS:
+ output_cls = get_class(output)()
+ output_cls.connect_bin(self.gst_pipeline, self.gst_tee)
+
# Setup bus and message processor
gst_bus = self.gst_pipeline.get_bus()
gst_bus.add_signal_watch()
@@ -96,7 +147,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput):
def play_uri(self, uri):
"""Play audio at URI"""
self.set_state('READY')
- self.gst_pipeline.get_by_name('uri').set_property('uri', uri)
+ self.gst_uridecodebin.set_property('uri', uri)
return self.set_state('PLAYING')
def deliver_data(self, caps_string, data):
@@ -160,11 +211,17 @@ class GStreamerOutput(ThreadingActor, BaseOutput):
def get_volume(self):
"""Get volume in range [0..100]"""
- gst_volume = self.gst_pipeline.get_by_name('volume')
- return int(gst_volume.get_property('volume') * 100)
+ return int(self.gst_volume.get_property('volume') * 100)
def set_volume(self, volume):
"""Set volume in range [0..100]"""
- gst_volume = self.gst_pipeline.get_by_name('volume')
- gst_volume.set_property('volume', volume / 100.0)
+ self.gst_volume.set_property('volume', volume / 100.0)
return True
+
+ def set_metadata(self, track):
+ tags = u'artist="%(artist)s",title="%(title)s"' % {
+ 'artist': u', '.join([a.name for a in track.artists]),
+ 'title': track.name,
+ }
+ logger.debug('Setting tags to: %s', tags)
+ self.gst_taginject.set_property('tags', tags)
diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py
index d6365b4b..87602772 100644
--- a/mopidy/mixers/gstreamer_software.py
+++ b/mopidy/mixers/gstreamer_software.py
@@ -2,7 +2,7 @@ from pykka.actor import ThreadingActor
from pykka.registry import ActorRegistry
from mopidy.mixers.base import BaseMixer
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""
@@ -11,7 +11,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
self.output = None
def on_start(self):
- output_refs = ActorRegistry.get_by_class(BaseOutput)
+ output_refs = ActorRegistry.get_by_class(GStreamer)
assert len(output_refs) == 1, 'Expected exactly one running output.'
self.output = output_refs[0].proxy()
diff --git a/mopidy/outputs.py b/mopidy/outputs.py
new file mode 100644
index 00000000..5a57f446
--- /dev/null
+++ b/mopidy/outputs.py
@@ -0,0 +1,31 @@
+from mopidy import settings
+from mopidy.gstreamer import BaseOutput
+
+class LocalOutput(BaseOutput):
+ def describe_bin(self):
+ if settings.LOCAL_OUTPUT_OVERRIDE:
+ return settings.LOCAL_OUTPUT_OVERRIDE
+ return 'autoaudiosink'
+
+class NullOutput(BaseOutput):
+ def describe_bin(self):
+ return 'fakesink'
+
+class ShoutcastOutput(BaseOutput):
+ def describe_bin(self):
+ if settings.SHOUTCAST_OUTPUT_OVERRIDE:
+ return settings.SHOUTCAST_OUTPUT_OVERRIDE
+ return 'audioconvert ! %s ! shout2send name=shoutcast' \
+ % settings.SHOUTCAST_OUTPUT_ENCODER
+
+ def modify_bin(self, output):
+ if settings.SHOUTCAST_OUTPUT_OVERRIDE:
+ return
+
+ self.set_properties(output.get_by_name('shoutcast'), {
+ u'ip': settings.SHOUTCAST_OUTPUT_SERVER,
+ u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
+ u'port': settings.SHOUTCAST_OUTPUT_PORT,
+ u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
+ u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
+ })
diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py
deleted file mode 100644
index fbc86688..00000000
--- a/mopidy/outputs/base.py
+++ /dev/null
@@ -1,91 +0,0 @@
-class BaseOutput(object):
- """
- Base class for audio outputs.
- """
-
- def play_uri(self, uri):
- """
- Play URI.
-
- *MUST be implemented by subclass.*
-
- :param uri: the URI to play
- :type uri: string
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
- def deliver_data(self, capabilities, data):
- """
- Deliver audio data to be played.
-
- *MUST be implemented by subclass.*
-
- :param capabilities: a GStreamer capabilities string
- :type capabilities: string
- """
- raise NotImplementedError
-
- def end_of_data_stream(self):
- """
- Signal that the last audio data has been delivered.
-
- *MUST be implemented by subclass.*
- """
- raise NotImplementedError
-
- def get_position(self):
- """
- Get position in milliseconds.
-
- *MUST be implemented by subclass.*
-
- :rtype: int
- """
- raise NotImplementedError
-
- def set_position(self, position):
- """
- Set position in milliseconds.
-
- *MUST be implemented by subclass.*
-
- :param position: the position in milliseconds
- :type volume: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
- def set_state(self, state):
- """
- Set playback state.
-
- *MUST be implemented by subclass.*
-
- :param state: the state
- :type state: string
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
-
- def get_volume(self):
- """
- Get volume level for software mixer.
-
- *MUST be implemented by subclass.*
-
- :rtype: int in range [0..100]
- """
- raise NotImplementedError
-
- def set_volume(self, volume):
- """
- Set volume level for software mixer.
-
- *MUST be implemented by subclass.*
-
- :param volume: the volume in the range [0..100]
- :type volume: int
- :rtype: :class:`True` if successful, else :class:`False`
- """
- raise NotImplementedError
diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py
deleted file mode 100644
index f09965f7..00000000
--- a/mopidy/outputs/dummy.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from pykka.actor import ThreadingActor
-
-from mopidy.outputs.base import BaseOutput
-
-class DummyOutput(ThreadingActor, BaseOutput):
- """
- Audio output used for testing.
- """
-
- # pylint: disable = R0902
- # Too many instance attributes (9/7)
-
- #: For testing. Contains the last URI passed to :meth:`play_uri`.
- uri = None
-
- #: For testing. Contains the last capabilities passed to
- #: :meth:`deliver_data`.
- capabilities = None
-
- #: For testing. Contains the last data passed to :meth:`deliver_data`.
- data = None
-
- #: For testing. :class:`True` if :meth:`end_of_data_stream` has been
- #: called.
- end_of_data_stream_called = False
-
- #: For testing. Contains the current position.
- position = 0
-
- #: For testing. Contains the current state.
- state = 'NULL'
-
- #: For testing. Contains the current volume.
- volume = 100
-
- def play_uri(self, uri):
- self.uri = uri
- return True
-
- def deliver_data(self, capabilities, data):
- self.capabilities = capabilities
- self.data = data
-
- def end_of_data_stream(self):
- self.end_of_data_stream_called = True
-
- def get_position(self):
- return self.position
-
- def set_position(self, position):
- self.position = position
- return True
-
- def set_state(self, state):
- self.state = state
- return True
-
- def get_volume(self):
- return self.volume
-
- def set_volume(self, volume):
- self.volume = volume
- return True
diff --git a/mopidy/settings.py b/mopidy/settings.py
index 6e33ffaa..c0ee3569 100644
--- a/mopidy/settings.py
+++ b/mopidy/settings.py
@@ -54,12 +54,12 @@ FRONTENDS = (
u'mopidy.frontends.lastfm.LastfmFrontend',
)
-#: Which GStreamer audio sink to use in :mod:`mopidy.outputs.gstreamer`.
+#: Which GStreamer bin description to use in :mod:`mopidy.outputs.CustomOutput`.
#:
#: Default::
#:
-#: GSTREAMER_AUDIO_SINK = u'autoaudiosink'
-GSTREAMER_AUDIO_SINK = u'autoaudiosink'
+#: LOCAL_OUTPUT_OVERRIDE = None
+LOCAL_OUTPUT_OVERRIDE = None
#: Your `Last.fm `_ username.
#:
@@ -143,13 +143,6 @@ MIXER_EXT_SPEAKERS_B = None
#: MIXER_MAX_VOLUME = 100
MIXER_MAX_VOLUME = 100
-#: Audio output handler to use.
-#:
-#: Default::
-#:
-#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
-OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
-
#: Which address Mopidy's MPD server should bind to.
#:
#:Examples:
@@ -174,6 +167,77 @@ MPD_SERVER_PASSWORD = None
#: Default: 6600
MPD_SERVER_PORT = 6600
+#: List of outputs to use. See :mod:`mopidy.outputs` for all available
+#: backends
+#:
+#: Default::
+#:
+#: OUTPUTS = (
+#: u'mopidy.outputs.LocalOutput',
+#: )
+OUTPUTS = (
+ u'mopidy.outputs.LocalOutput',
+)
+
+#: Servar that runs Shoutcast server to send stream to.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1'
+SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1'
+
+#: User to authenticate as against Shoutcast server.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_USERNAME = u'source'
+SHOUTCAST_OUTPUT_USERNAME = u'source'
+
+#: Password to authenticate with against Shoutcast server.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
+SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
+
+#: Port to use for streaming to Shoutcast server.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_PORT = 8000
+SHOUTCAST_OUTPUT_PORT = 8000
+
+#: Mountpoint to use for the stream on the Shoutcast server.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_MOUNT = u'/stream'
+SHOUTCAST_OUTPUT_MOUNT = u'/stream'
+
+#: Encoder to use to process audio data before streaming.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
+SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
+
+#: Overrides to allow advanced setup of shoutcast. Using this settings implies
+#: that all other SHOUTCAST_OUTPUT_* settings will be ignored.
+#:
+#: Examples:
+#:
+#: ``vorbisenc ! oggmux ! shout2send mount=/stream port=8000``
+#: Encode with vorbis and use ogg mux.
+#: ``lame bitrate=320 ! shout2send mount=/stream port=8000``
+#: Encode with lame to bitrate=320.
+#:
+#: For all options see gst-inspect-0.10 lame, vorbisenc and shout2send.
+#:
+#: Default::
+#:
+#: SHOUTCAST_OUTPUT_OVERRIDE = None
+SHOUTCAST_OUTPUT_OVERRIDE = None
+
#: Path to the Spotify cache.
#:
#: Used by :mod:`mopidy.backends.spotify`.
diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py
index 529c6fb1..0dc6b4cb 100644
--- a/mopidy/utils/settings.py
+++ b/mopidy/utils/settings.py
@@ -97,9 +97,11 @@ def validate_settings(defaults, settings):
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
+ 'GSTREAMER_AUDIO_SINK': 'LOCAL_OUTPUT_OVERRIDE',
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
+ 'OUTPUT': None,
'SERVER': None,
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT',
diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py
index ee5e1111..427ce76d 100644
--- a/tests/backends/base/current_playlist.py
+++ b/tests/backends/base/current_playlist.py
@@ -3,7 +3,7 @@ import multiprocessing
import random
from mopidy.models import Playlist, Track
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
from tests.backends.base import populate_playlist
@@ -12,7 +12,7 @@ class CurrentPlaylistControllerTest(object):
def setUp(self):
self.backend = self.backend_class()
- self.backend.output = mock.Mock(spec=BaseOutput)
+ self.backend.gstreamer = mock.Mock(spec=GStreamer)
self.controller = self.backend.current_playlist
self.playback = self.backend.playback
diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py
index 8ea48a3a..2d455225 100644
--- a/tests/backends/base/playback.py
+++ b/tests/backends/base/playback.py
@@ -4,7 +4,7 @@ import random
import time
from mopidy.models import Track
-from mopidy.outputs.base import BaseOutput
+from mopidy.gstreamer import GStreamer
from tests import SkipTest
from tests.backends.base import populate_playlist
@@ -16,7 +16,7 @@ class PlaybackControllerTest(object):
def setUp(self):
self.backend = self.backend_class()
- self.backend.output = mock.Mock(spec=BaseOutput)
+ self.backend.gstreamer = mock.Mock(spec=GStreamer)
self.playback = self.backend.playback
self.current_playlist = self.backend.current_playlist
@@ -520,7 +520,7 @@ class PlaybackControllerTest(object):
self.assert_(wrapper.called)
- @SkipTest # Blocks for 10ms and does not work with DummyOutput
+ @SkipTest # Blocks for 10ms
@populate_playlist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
@@ -599,7 +599,7 @@ class PlaybackControllerTest(object):
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
- @SkipTest # Uses sleep and does not work with DummyOutput+LocalBackend
+ @SkipTest # Uses sleep and might not work with LocalBackend
@populate_playlist
def test_resume_continues_from_right_position(self):
self.playback.play()
@@ -729,7 +729,7 @@ class PlaybackControllerTest(object):
def test_time_position_when_stopped(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
- self.backend.output.get_position = mock.Mock(return_value=future)
+ self.backend.gstreamer.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
@@ -737,11 +737,11 @@ class PlaybackControllerTest(object):
def test_time_position_when_stopped_with_playlist(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
- self.backend.output.get_position = mock.Mock(return_value=future)
+ self.backend.gstreamer.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
- @SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput
+ @SkipTest # Uses sleep and does might not work with LocalBackend
@populate_playlist
def test_time_position_when_playing(self):
self.playback.play()
diff --git a/tests/outputs/gstreamer_test.py b/tests/gstreamer_test.py
similarity index 59%
rename from tests/outputs/gstreamer_test.py
rename to tests/gstreamer_test.py
index 31a16756..9087e0db 100644
--- a/tests/outputs/gstreamer_test.py
+++ b/tests/gstreamer_test.py
@@ -9,26 +9,28 @@ if sys.platform == 'win32':
raise SkipTest
from mopidy import settings
-from mopidy.outputs.gstreamer import GStreamerOutput
+from mopidy.gstreamer import GStreamer
from mopidy.utils.path import path_to_uri
from tests import path_to_data_dir
-class GStreamerOutputTest(unittest.TestCase):
+# TODO BaseOutputTest?
+
+class GStreamerTest(unittest.TestCase):
def setUp(self):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
- self.output = GStreamerOutput()
- self.output.on_start()
+ self.gstreamer = GStreamer()
+ self.gstreamer.on_start()
def tearDown(self):
settings.runtime.clear()
def test_play_uri_existing_file(self):
- self.assertTrue(self.output.play_uri(self.song_uri))
+ self.assertTrue(self.gstreamer.play_uri(self.song_uri))
def test_play_uri_non_existing_file(self):
- self.assertFalse(self.output.play_uri(self.song_uri + 'bogus'))
+ self.assertFalse(self.gstreamer.play_uri(self.song_uri + 'bogus'))
@SkipTest
def test_deliver_data(self):
@@ -39,19 +41,19 @@ class GStreamerOutputTest(unittest.TestCase):
pass # TODO
def test_default_get_volume_result(self):
- self.assertEqual(100, self.output.get_volume())
+ self.assertEqual(100, self.gstreamer.get_volume())
def test_set_volume(self):
- self.assertTrue(self.output.set_volume(50))
- self.assertEqual(50, self.output.get_volume())
+ self.assertTrue(self.gstreamer.set_volume(50))
+ self.assertEqual(50, self.gstreamer.get_volume())
def test_set_volume_to_zero(self):
- self.assertTrue(self.output.set_volume(0))
- self.assertEqual(0, self.output.get_volume())
+ self.assertTrue(self.gstreamer.set_volume(0))
+ self.assertEqual(0, self.gstreamer.get_volume())
def test_set_volume_to_one_hundred(self):
- self.assertTrue(self.output.set_volume(100))
- self.assertEqual(100, self.output.get_volume())
+ self.assertTrue(self.gstreamer.set_volume(100))
+ self.assertEqual(100, self.gstreamer.get_volume())
@SkipTest
def test_set_state(self):