Merge remote-tracking branch 'adamcik/feature/simplify-outputs' into develop

Conflicts:
	docs/changes.rst
	mopidy/utils/settings.py
This commit is contained in:
Stein Magnus Jodal 2012-09-03 22:08:52 +02:00
commit 6b41806eea
13 changed files with 81 additions and 439 deletions

View File

@ -26,6 +26,13 @@ v0.8 (in development)
known setting, and suggests to the user what we think the setting should have
been.
- Removed most traces of multiple outputs support. Having this feature
currently seems to be more trouble than what it is worth.
:attr:`mopidy.settings.OUTPUTS` setting is no longer supported, and has been
replaced with :attr:`mopidy.settings.OUTPUT` which is a GStreamer
bin described in the same format as ``gst-launch`` expects. Default value is
``autoaudiosink``.
v0.7.3 (2012-08-11)
===================

View File

@ -112,12 +112,9 @@ Using a custom audio sink
=========================
If you for some reason want to use some other GStreamer audio sink than
``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the
:attr:`mopidy.settings.OUTPUTS` setting, and set the
:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline
description describing the GStreamer sink you want to use.
``autoaudiosink``, you can set :attr:`mopidy.settings.OUTPUT` to a partial
GStreamer pipeline description describing the GStreamer sink you want to use.
Example of ``settings.py`` for OSS4::
OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',)
CUSTOM_OUTPUT = u'oss4sink'
OUTPUT = u'oss4sink'

View File

@ -157,18 +157,18 @@ server simultaneously. To use the SHOUTcast output, do the following:
#. Install, configure and start the Icecast server. It can be found in the
``icecast2`` package in Debian/Ubuntu.
#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the
:attr:`mopidy.settings.OUTPUTS` setting.
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send`` (an Ogg Vorbis
encoder could be used instead of lame).
#. Check the default values for the following settings, and alter them to match
your Icecast setup if needed:
#. You might also need to change the ``shout2send`` default settings, run
``gst-inspect-0.10 shout2send`` to see the available settings. Most likely
you want to change ``ip``, ``username``, ``password`` and ``mount``. For
example, to set the password use:
``lame ! shout2send username="foobar" password="s3cret"``.
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
Other advanced setups are also possible for outputs. Basically anything you can
get a ``gst-lauch`` command to output to can be plugged into
:attr:`mopidy.settings.OUTPUT``.
Available settings

View File

@ -20,7 +20,7 @@ sys.argv[1:] = gstreamer_args
from mopidy import (get_version, settings, OptionalDependencyError,
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
from mopidy.gstreamer import GStreamer
from mopidy.gstreamer import GStreamer, GStreamerError
from mopidy.utils import get_class
from mopidy.utils.deps import list_deps_optparse_callback
from mopidy.utils.log import setup_logging
@ -46,6 +46,8 @@ def main():
loop.run()
except SettingsError as e:
logger.error(e.message)
except GStreamerError as e:
logger.error(e)
except KeyboardInterrupt:
logger.info(u'Interrupted. Exiting...')
except Exception as e:

View File

@ -1,5 +1,6 @@
import pygst
pygst.require('0.10')
import gobject
import gst
import logging
@ -8,19 +9,22 @@ 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
logger = logging.getLogger('mopidy.gstreamer')
class GStreamerError(Exception):
pass
class GStreamer(ThreadingActor):
"""
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
**Settings:**
- :attr:`mopidy.settings.OUTPUTS`
- :attr:`mopidy.settings.OUTPUT`
"""
@ -36,28 +40,25 @@ class GStreamer(ThreadingActor):
rate=(int)44100""")
self._pipeline = None
self._source = None
self._tee = None
self._uridecodebin = None
self._volume = None
self._outputs = []
self._handlers = {}
self._output = None
def on_start(self):
self._setup_pipeline()
self._setup_outputs()
self._setup_output()
self._setup_message_processor()
def _setup_pipeline(self):
description = ' ! '.join([
'uridecodebin name=uri',
'audioconvert name=convert',
'volume name=volume',
'tee name=tee'])
'audioresample name=resample',
'queue name=queue',
'volume name=volume'])
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
self._pipeline = gst.parse_launch(description)
self._tee = self._pipeline.get_by_name('tee')
self._volume = self._pipeline.get_by_name('volume')
self._uridecodebin = self._pipeline.get_by_name('uri')
@ -65,9 +66,16 @@ class GStreamer(ThreadingActor):
self._uridecodebin.connect('pad-added', self._on_new_pad,
self._pipeline.get_by_name('convert').get_pad('sink'))
def _setup_outputs(self):
for output in settings.OUTPUTS:
get_class(output)(self).connect()
def _setup_output(self):
try:
self._output = gst.parse_bin_from_description(settings.OUTPUT, True)
except gobject.GError as e:
raise GStreamerError('%r while creating %r' % (e.message,
settings.OUTPUT))
self._pipeline.add(self._output)
gst.element_link_many(self._volume, self._output)
logger.debug('Output set to %s', settings.OUTPUT)
def _setup_message_processor(self):
bus = self._pipeline.get_bus()
@ -88,10 +96,6 @@ class GStreamer(ThreadingActor):
pad.link(target_pad)
def _on_message(self, bus, message):
if message.src in self._handlers:
if self._handlers[message.src](message):
return # Message was handeled by output
if message.type == gst.MESSAGE_EOS:
logger.debug(u'GStreamer signalled end-of-stream. '
'Telling backend ...')
@ -293,104 +297,3 @@ class GStreamer(ThreadingActor):
event = gst.event_new_tag(taglist)
self._pipeline.send_event(event)
def connect_output(self, output):
"""
Connect output to pipeline.
:param output: output to connect to the pipeline
:type output: :class:`gst.Bin`
"""
self._pipeline.add(output)
output.sync_state_with_parent() # Required to add to running pipe
gst.element_link_many(self._tee, output)
self._outputs.append(output)
logger.debug('GStreamer added %s', output.get_name())
def list_outputs(self):
"""
Get list with the name of all active outputs.
:rtype: list of strings
"""
return [output.get_name() for output in self._outputs]
def remove_output(self, output):
"""
Remove output from our pipeline.
:param output: output to remove from the pipeline
:type output: :class:`gst.Bin`
"""
if output not in self._outputs:
raise LookupError('Ouput %s not present in pipeline'
% output.get_name)
teesrc = output.get_pad('sink').get_peer()
handler = teesrc.add_event_probe(self._handle_event_probe)
struct = gst.Structure('mopidy-unlink-tee')
struct.set_value('handler', handler)
event = gst.event_new_custom(gst.EVENT_CUSTOM_DOWNSTREAM, struct)
self._tee.send_event(event)
def _handle_event_probe(self, teesrc, event):
if (event.type == gst.EVENT_CUSTOM_DOWNSTREAM
and event.has_name('mopidy-unlink-tee')):
data = self._get_structure_data(event.get_structure())
output = teesrc.get_peer().get_parent()
teesrc.unlink(teesrc.get_peer())
teesrc.remove_event_probe(data['handler'])
output.set_state(gst.STATE_NULL)
self._pipeline.remove(output)
logger.warning('Removed %s', output.get_name())
return False
return True
def _get_structure_data(self, struct):
# Ugly hack to get around missing get_value in pygst bindings :/
data = {}
def get_data(key, value):
data[key] = value
struct.foreach(get_data)
return data
def connect_message_handler(self, element, handler):
"""
Attach custom message handler for given element.
Hook to allow outputs (or other code) to register custom message
handlers for all messages coming from the element in question.
In the case of outputs, :meth:`mopidy.outputs.BaseOutput.on_connect`
should be used to attach such handlers and care should be taken to
remove them in :meth:`mopidy.outputs.BaseOutput.on_remove` using
:meth:`remove_message_handler`.
The handler callback will only be given the message in question, and
is free to ignore the message. However, if the handler wants to prevent
the default handling of the message it should return :class:`True`
indicating that the message has been handled.
Note that there can only be one handler per element.
:param element: element to watch messages from
:type element: :class:`gst.Element`
:param handler: callable that takes :class:`gst.Message` and returns
:class:`True` if the message has been handeled
:type handler: callable
"""
self._handlers[element] = handler
def remove_message_handler(self, element):
"""
Remove custom message handler.
:param element: element to remove message handling from.
:type element: :class:`gst.Element`
"""
self._handlers.pop(element, None)

View File

@ -1,105 +0,0 @@
import pygst
pygst.require('0.10')
import gst
import logging
logger = logging.getLogger('mopidy.outputs')
class BaseOutput(object):
"""Base class for pluggable audio outputs."""
MESSAGE_EOS = gst.MESSAGE_EOS
MESSAGE_ERROR = gst.MESSAGE_ERROR
MESSAGE_WARNING = gst.MESSAGE_WARNING
def __init__(self, gstreamer):
self.gstreamer = gstreamer
self.bin = self._build_bin()
self.bin.set_name(self.get_name())
self.modify_bin()
def _build_bin(self):
description = 'queue ! %s' % self.describe_bin()
logger.debug('Creating new output: %s', description)
return gst.parse_bin_from_description(description, True)
def connect(self):
"""Attach output to GStreamer pipeline."""
self.gstreamer.connect_output(self.bin)
self.on_connect()
def on_connect(self):
"""
Called after output has been connected to GStreamer pipeline.
*MAY be implemented by subclass.*
"""
pass
def remove(self):
"""Remove output from GStreamer pipeline."""
self.gstreamer.remove_output(self.bin)
self.on_remove()
def on_remove(self):
"""
Called after output has been removed from GStreamer pipeline.
*MAY be implemented by subclass.*
"""
pass
def get_name(self):
"""
Get name of the output. Defaults to the output's class name.
*MAY be implemented by subclass.*
:rtype: string
"""
return self.__class__.__name__
def modify_bin(self):
"""
Modifies ``self.bin`` before it is installed if needed.
Overriding this method allows for outputs to modify the constructed bin
before it is installed. This can for instance be a good place to call
`set_properties` on elements that need to be configured.
*MAY be implemented by subclass.*
"""
pass
def describe_bin(self):
"""
Return string describing the output bin in :command:`gst-launch`
format.
For simple cases this can just be a sink such as ``autoaudiosink``,
or it can be a chain like ``element1 ! element2 ! sink``. See the
manpage of :command:`gst-launch` for details on the format.
*MUST be implemented by subclass.*
:rtype: string
"""
raise NotImplementedError
def set_properties(self, element, properties):
"""
Helper method for setting of properties on elements.
Will call :meth:`gst.Element.set_property` on ``element`` for each key
in ``properties`` that has a value that is not :class:`None`.
:param element: element to set properties on
:type element: :class:`gst.Element`
:param properties: properties to set on element
:type properties: dict
"""
for key, value in properties.items():
if value is not None:
element.set_property(key, value)

View File

@ -1,34 +0,0 @@
from mopidy import settings
from mopidy.outputs import BaseOutput
class CustomOutput(BaseOutput):
"""
Custom output for using alternate setups.
This output is intended to handle two main cases:
1. Simple things like switching which sink to use. Say :class:`LocalOutput`
doesn't work for you and you want to switch to ALSA, simple. Set
:attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good
to go. Some possible sinks include:
- alsasink
- osssink
- pulsesink
- ...and many more
2. Advanced setups that require complete control of the output bin. For
these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
:command:`gst-launch` compatible string describing the target setup.
**Dependencies:**
- None
**Settings:**
- :attr:`mopidy.settings.CUSTOM_OUTPUT`
"""
def describe_bin(self):
return settings.CUSTOM_OUTPUT

View File

@ -1,20 +0,0 @@
from mopidy.outputs import BaseOutput
class LocalOutput(BaseOutput):
"""
Basic output to local audio sink.
This output will normally tell GStreamer to choose whatever it thinks is
best for your system. In other words this is usually a sane choice.
**Dependencies:**
- None
**Settings:**
- None
"""
def describe_bin(self):
return 'autoaudiosink'

View File

@ -1,58 +0,0 @@
import logging
from mopidy import settings
from mopidy.outputs import BaseOutput
logger = logging.getLogger('mopidy.outputs.shoutcast')
class ShoutcastOutput(BaseOutput):
"""
Shoutcast streaming output.
This output allows for streaming to an icecast server or anything else that
supports Shoutcast. The output supports setting for: server address, port,
mount point, user, password and encoder to use. Please see
:class:`mopidy.settings` for details about settings.
**Dependencies:**
- A SHOUTcast/Icecast server
**Settings:**
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
"""
def describe_bin(self):
return 'audioconvert ! %s ! shout2send name=shoutcast' \
% settings.SHOUTCAST_OUTPUT_ENCODER
def modify_bin(self):
self.set_properties(self.bin.get_by_name('shoutcast'), {
u'ip': settings.SHOUTCAST_OUTPUT_HOSTNAME,
u'port': settings.SHOUTCAST_OUTPUT_PORT,
u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
})
def on_connect(self):
self.gstreamer.connect_message_handler(
self.bin.get_by_name('shoutcast'), self.message_handler)
def on_remove(self):
self.gstreamer.remove_message_handler(
self.bin.get_by_name('shoutcast'))
def message_handler(self, message):
if message.type != self.MESSAGE_ERROR:
return False
error, debug = message.parse_error()
logger.warning('%s (%s)', error, debug)
self.remove()
return True

View File

@ -26,14 +26,6 @@ BACKENDS = (
#: details on the format.
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
#: Which GStreamer bin description to use in
#: :class:`mopidy.outputs.custom.CustomOutput`.
#:
#: Default::
#:
#: CUSTOM_OUTPUT = u'fakesink'
CUSTOM_OUTPUT = u'fakesink'
#: The log format used for debug logging.
#:
#: See http://docs.python.org/library/logging.html#formatter-objects for
@ -185,71 +177,12 @@ MPD_SERVER_PASSWORD = None
#: Default: 20
MPD_SERVER_MAX_CONNECTIONS = 20
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
#: backends
#: Output to use. See :mod:`mopidy.outputs` for all available backends
#:
#: Default::
#:
#: OUTPUTS = (
#: u'mopidy.outputs.local.LocalOutput',
#: )
OUTPUTS = (
u'mopidy.outputs.local.LocalOutput',
)
#: Hostname of the SHOUTcast server which Mopidy should stream audio to.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
#: Port of the SHOUTcast server.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_PORT = 8000
SHOUTCAST_OUTPUT_PORT = 8000
#: User to authenticate as against SHOUTcast server.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_USERNAME = u'source'
SHOUTCAST_OUTPUT_USERNAME = u'source'
#: Password to authenticate with against SHOUTcast server.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
#: Mountpoint to use for the stream on the SHOUTcast server.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_MOUNT = u'/stream'
SHOUTCAST_OUTPUT_MOUNT = u'/stream'
#: Encoder to use to process audio data before streaming to SHOUTcast server.
#:
#: Used by :mod:`mopidy.outputs.shoutcast`.
#:
#: Default::
#:
#: SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
#: OUTPUT = u'autoaudiosink'
OUTPUT = u'autoaudiosink'
#: Path to the Spotify cache.
#:

View File

@ -5,6 +5,8 @@ import sys
logger = logging.getLogger('mopidy.utils')
# TODO: user itertools.chain.from_iterable(the_list)?
def flatten(the_list):
result = []
for element in the_list:
@ -14,22 +16,25 @@ def flatten(the_list):
result.append(element)
return result
def import_module(name):
__import__(name)
return sys.modules[name]
def get_class(name):
logger.debug('Loading: %s', name)
if '.' not in name:
raise ImportError("Couldn't load: %s" % name)
module_name = name[:name.rindex('.')]
class_name = name[name.rindex('.') + 1:]
cls_name = name[name.rindex('.') + 1:]
try:
module = import_module(module_name)
class_object = getattr(module, class_name)
cls = getattr(module, cls_name)
except (ImportError, AttributeError):
raise ImportError("Couldn't load: %s" % name)
return class_object
return cls
def locale_decode(bytestr):
try:

View File

@ -113,6 +113,7 @@ def validate_settings(defaults, settings):
errors = {}
changed = {
'CUSTOM_OUTPUT': 'OUTPUT',
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
'FRONTEND': 'FRONTENDS',
@ -121,7 +122,6 @@ def validate_settings(defaults, settings):
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
'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',
@ -137,29 +137,37 @@ def validate_settings(defaults, settings):
else:
errors[setting] = u'Deprecated setting. Use %s.' % (
changed[setting],)
continue
if setting == 'BACKENDS':
elif setting == 'BACKENDS':
if 'mopidy.backends.despotify.DespotifyBackend' in value:
errors[setting] = (u'Deprecated setting value. ' +
'"mopidy.backends.despotify.DespotifyBackend" is no ' +
'longer available.')
continue
errors[setting] = (
u'Deprecated setting value. '
u'"mopidy.backends.despotify.DespotifyBackend" is no '
u'longer available.')
if setting == 'SPOTIFY_BITRATE':
elif setting == 'OUTPUTS':
errors[setting] = (
u'Deprecated setting, please change to OUTPUT. OUTPUT expectes '
u'a GStreamer bin describing your desired output.')
elif setting == 'SPOTIFY_BITRATE':
if value not in (96, 160, 320):
errors[setting] = (u'Unavailable Spotify bitrate. ' +
u'Available bitrates are 96, 160, and 320.')
errors[setting] = (
u'Unavailable Spotify bitrate. Available bitrates are 96, '
u'160, and 320.')
if setting not in defaults:
elif setting.startswith('SHOUTCAST_OUTPUT_'):
errors[setting] = (
u'Deprecated setting, please set the value via the GStreamer '
u'bin in OUTPUT.')
elif setting not in defaults:
errors[setting] = u'Unknown setting.'
suggestion = did_you_mean(setting, defaults)
if suggestion:
errors[setting] += u' Did you mean %s?' % suggestion
continue
return errors

View File

@ -14,7 +14,6 @@ class GStreamerTest(unittest.TestCase):
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
self.gstreamer = GStreamer()
self.gstreamer.on_start()
def prepare_uri(self, uri):
self.gstreamer.prepare_change()
@ -71,3 +70,8 @@ class GStreamerTest(unittest.TestCase):
@unittest.SkipTest
def test_set_position(self):
pass # TODO
@unittest.SkipTest
def test_invalid_output_raises_error(self):
pass # TODO