Merge remote-tracking branch 'adamcik/feature/simplify-outputs' into develop
Conflicts: docs/changes.rst mopidy/utils/settings.py
This commit is contained in:
commit
6b41806eea
@ -26,6 +26,13 @@ v0.8 (in development)
|
|||||||
known setting, and suggests to the user what we think the setting should have
|
known setting, and suggests to the user what we think the setting should have
|
||||||
been.
|
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)
|
v0.7.3 (2012-08-11)
|
||||||
===================
|
===================
|
||||||
|
|||||||
@ -112,12 +112,9 @@ Using a custom audio sink
|
|||||||
=========================
|
=========================
|
||||||
|
|
||||||
If you for some reason want to use some other GStreamer audio sink than
|
If you for some reason want to use some other GStreamer audio sink than
|
||||||
``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the
|
``autoaudiosink``, you can set :attr:`mopidy.settings.OUTPUT` to a partial
|
||||||
:attr:`mopidy.settings.OUTPUTS` setting, and set the
|
GStreamer pipeline description describing the GStreamer sink you want to use.
|
||||||
:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline
|
|
||||||
description describing the GStreamer sink you want to use.
|
|
||||||
|
|
||||||
Example of ``settings.py`` for OSS4::
|
Example of ``settings.py`` for OSS4::
|
||||||
|
|
||||||
OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',)
|
OUTPUT = u'oss4sink'
|
||||||
CUSTOM_OUTPUT = u'oss4sink'
|
|
||||||
|
|||||||
@ -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
|
#. Install, configure and start the Icecast server. It can be found in the
|
||||||
``icecast2`` package in Debian/Ubuntu.
|
``icecast2`` package in Debian/Ubuntu.
|
||||||
|
|
||||||
#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the
|
#. Set :attr:`mopidy.settings.OUTPUT` to ``lame ! shout2send`` (an Ogg Vorbis
|
||||||
:attr:`mopidy.settings.OUTPUTS` setting.
|
encoder could be used instead of lame).
|
||||||
|
|
||||||
#. Check the default values for the following settings, and alter them to match
|
#. You might also need to change the ``shout2send`` default settings, run
|
||||||
your Icecast setup if needed:
|
``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`
|
Other advanced setups are also possible for outputs. Basically anything you can
|
||||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
|
get a ``gst-lauch`` command to output to can be plugged into
|
||||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
|
:attr:`mopidy.settings.OUTPUT``.
|
||||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
|
|
||||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
|
|
||||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
|
|
||||||
|
|
||||||
|
|
||||||
Available settings
|
Available settings
|
||||||
|
|||||||
@ -20,7 +20,7 @@ sys.argv[1:] = gstreamer_args
|
|||||||
|
|
||||||
from mopidy import (get_version, settings, OptionalDependencyError,
|
from mopidy import (get_version, settings, OptionalDependencyError,
|
||||||
SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE)
|
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 import get_class
|
||||||
from mopidy.utils.deps import list_deps_optparse_callback
|
from mopidy.utils.deps import list_deps_optparse_callback
|
||||||
from mopidy.utils.log import setup_logging
|
from mopidy.utils.log import setup_logging
|
||||||
@ -46,6 +46,8 @@ def main():
|
|||||||
loop.run()
|
loop.run()
|
||||||
except SettingsError as e:
|
except SettingsError as e:
|
||||||
logger.error(e.message)
|
logger.error(e.message)
|
||||||
|
except GStreamerError as e:
|
||||||
|
logger.error(e)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info(u'Interrupted. Exiting...')
|
logger.info(u'Interrupted. Exiting...')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import pygst
|
import pygst
|
||||||
pygst.require('0.10')
|
pygst.require('0.10')
|
||||||
|
import gobject
|
||||||
import gst
|
import gst
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -8,19 +9,22 @@ from pykka.actor import ThreadingActor
|
|||||||
from pykka.registry import ActorRegistry
|
from pykka.registry import ActorRegistry
|
||||||
|
|
||||||
from mopidy import settings
|
from mopidy import settings
|
||||||
from mopidy.utils import get_class
|
|
||||||
from mopidy.backends.base import Backend
|
from mopidy.backends.base import Backend
|
||||||
|
|
||||||
logger = logging.getLogger('mopidy.gstreamer')
|
logger = logging.getLogger('mopidy.gstreamer')
|
||||||
|
|
||||||
|
|
||||||
|
class GStreamerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GStreamer(ThreadingActor):
|
class GStreamer(ThreadingActor):
|
||||||
"""
|
"""
|
||||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||||
|
|
||||||
**Settings:**
|
**Settings:**
|
||||||
|
|
||||||
- :attr:`mopidy.settings.OUTPUTS`
|
- :attr:`mopidy.settings.OUTPUT`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -36,28 +40,25 @@ class GStreamer(ThreadingActor):
|
|||||||
rate=(int)44100""")
|
rate=(int)44100""")
|
||||||
self._pipeline = None
|
self._pipeline = None
|
||||||
self._source = None
|
self._source = None
|
||||||
self._tee = None
|
|
||||||
self._uridecodebin = None
|
self._uridecodebin = None
|
||||||
self._volume = None
|
self._volume = None
|
||||||
self._outputs = []
|
self._output = None
|
||||||
self._handlers = {}
|
|
||||||
|
|
||||||
def on_start(self):
|
|
||||||
self._setup_pipeline()
|
self._setup_pipeline()
|
||||||
self._setup_outputs()
|
self._setup_output()
|
||||||
self._setup_message_processor()
|
self._setup_message_processor()
|
||||||
|
|
||||||
def _setup_pipeline(self):
|
def _setup_pipeline(self):
|
||||||
description = ' ! '.join([
|
description = ' ! '.join([
|
||||||
'uridecodebin name=uri',
|
'uridecodebin name=uri',
|
||||||
'audioconvert name=convert',
|
'audioconvert name=convert',
|
||||||
'volume name=volume',
|
'audioresample name=resample',
|
||||||
'tee name=tee'])
|
'queue name=queue',
|
||||||
|
'volume name=volume'])
|
||||||
|
|
||||||
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
|
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
|
||||||
|
|
||||||
self._pipeline = gst.parse_launch(description)
|
self._pipeline = gst.parse_launch(description)
|
||||||
self._tee = self._pipeline.get_by_name('tee')
|
|
||||||
self._volume = self._pipeline.get_by_name('volume')
|
self._volume = self._pipeline.get_by_name('volume')
|
||||||
self._uridecodebin = self._pipeline.get_by_name('uri')
|
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._uridecodebin.connect('pad-added', self._on_new_pad,
|
||||||
self._pipeline.get_by_name('convert').get_pad('sink'))
|
self._pipeline.get_by_name('convert').get_pad('sink'))
|
||||||
|
|
||||||
def _setup_outputs(self):
|
def _setup_output(self):
|
||||||
for output in settings.OUTPUTS:
|
try:
|
||||||
get_class(output)(self).connect()
|
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):
|
def _setup_message_processor(self):
|
||||||
bus = self._pipeline.get_bus()
|
bus = self._pipeline.get_bus()
|
||||||
@ -88,10 +96,6 @@ class GStreamer(ThreadingActor):
|
|||||||
pad.link(target_pad)
|
pad.link(target_pad)
|
||||||
|
|
||||||
def _on_message(self, bus, message):
|
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:
|
if message.type == gst.MESSAGE_EOS:
|
||||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||||
'Telling backend ...')
|
'Telling backend ...')
|
||||||
@ -293,104 +297,3 @@ class GStreamer(ThreadingActor):
|
|||||||
|
|
||||||
event = gst.event_new_tag(taglist)
|
event = gst.event_new_tag(taglist)
|
||||||
self._pipeline.send_event(event)
|
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)
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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'
|
|
||||||
@ -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
|
|
||||||
@ -26,14 +26,6 @@ BACKENDS = (
|
|||||||
#: details on the format.
|
#: details on the format.
|
||||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
|
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.
|
#: The log format used for debug logging.
|
||||||
#:
|
#:
|
||||||
#: See http://docs.python.org/library/logging.html#formatter-objects for
|
#: See http://docs.python.org/library/logging.html#formatter-objects for
|
||||||
@ -185,71 +177,12 @@ MPD_SERVER_PASSWORD = None
|
|||||||
#: Default: 20
|
#: Default: 20
|
||||||
MPD_SERVER_MAX_CONNECTIONS = 20
|
MPD_SERVER_MAX_CONNECTIONS = 20
|
||||||
|
|
||||||
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
|
#: Output to use. See :mod:`mopidy.outputs` for all available backends
|
||||||
#: backends
|
|
||||||
#:
|
#:
|
||||||
#: Default::
|
#: Default::
|
||||||
#:
|
#:
|
||||||
#: OUTPUTS = (
|
#: OUTPUT = u'autoaudiosink'
|
||||||
#: u'mopidy.outputs.local.LocalOutput',
|
OUTPUT = u'autoaudiosink'
|
||||||
#: )
|
|
||||||
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'
|
|
||||||
|
|
||||||
#: Path to the Spotify cache.
|
#: Path to the Spotify cache.
|
||||||
#:
|
#:
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import sys
|
|||||||
|
|
||||||
logger = logging.getLogger('mopidy.utils')
|
logger = logging.getLogger('mopidy.utils')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: user itertools.chain.from_iterable(the_list)?
|
||||||
def flatten(the_list):
|
def flatten(the_list):
|
||||||
result = []
|
result = []
|
||||||
for element in the_list:
|
for element in the_list:
|
||||||
@ -14,22 +16,25 @@ def flatten(the_list):
|
|||||||
result.append(element)
|
result.append(element)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def import_module(name):
|
def import_module(name):
|
||||||
__import__(name)
|
__import__(name)
|
||||||
return sys.modules[name]
|
return sys.modules[name]
|
||||||
|
|
||||||
|
|
||||||
def get_class(name):
|
def get_class(name):
|
||||||
logger.debug('Loading: %s', name)
|
logger.debug('Loading: %s', name)
|
||||||
if '.' not in name:
|
if '.' not in name:
|
||||||
raise ImportError("Couldn't load: %s" % name)
|
raise ImportError("Couldn't load: %s" % name)
|
||||||
module_name = name[:name.rindex('.')]
|
module_name = name[:name.rindex('.')]
|
||||||
class_name = name[name.rindex('.') + 1:]
|
cls_name = name[name.rindex('.') + 1:]
|
||||||
try:
|
try:
|
||||||
module = import_module(module_name)
|
module = import_module(module_name)
|
||||||
class_object = getattr(module, class_name)
|
cls = getattr(module, cls_name)
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
raise ImportError("Couldn't load: %s" % name)
|
raise ImportError("Couldn't load: %s" % name)
|
||||||
return class_object
|
return cls
|
||||||
|
|
||||||
|
|
||||||
def locale_decode(bytestr):
|
def locale_decode(bytestr):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -113,6 +113,7 @@ def validate_settings(defaults, settings):
|
|||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
changed = {
|
changed = {
|
||||||
|
'CUSTOM_OUTPUT': 'OUTPUT',
|
||||||
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
||||||
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
||||||
'FRONTEND': 'FRONTENDS',
|
'FRONTEND': 'FRONTENDS',
|
||||||
@ -121,7 +122,6 @@ def validate_settings(defaults, settings):
|
|||||||
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
|
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
|
||||||
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
|
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
|
||||||
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
|
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
|
||||||
'OUTPUT': None,
|
|
||||||
'SERVER': None,
|
'SERVER': None,
|
||||||
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
|
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
|
||||||
'SERVER_PORT': 'MPD_SERVER_PORT',
|
'SERVER_PORT': 'MPD_SERVER_PORT',
|
||||||
@ -137,29 +137,37 @@ def validate_settings(defaults, settings):
|
|||||||
else:
|
else:
|
||||||
errors[setting] = u'Deprecated setting. Use %s.' % (
|
errors[setting] = u'Deprecated setting. Use %s.' % (
|
||||||
changed[setting],)
|
changed[setting],)
|
||||||
continue
|
|
||||||
|
|
||||||
if setting == 'BACKENDS':
|
elif setting == 'BACKENDS':
|
||||||
if 'mopidy.backends.despotify.DespotifyBackend' in value:
|
if 'mopidy.backends.despotify.DespotifyBackend' in value:
|
||||||
errors[setting] = (u'Deprecated setting value. ' +
|
errors[setting] = (
|
||||||
'"mopidy.backends.despotify.DespotifyBackend" is no ' +
|
u'Deprecated setting value. '
|
||||||
'longer available.')
|
u'"mopidy.backends.despotify.DespotifyBackend" is no '
|
||||||
continue
|
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):
|
if value not in (96, 160, 320):
|
||||||
errors[setting] = (u'Unavailable Spotify bitrate. ' +
|
errors[setting] = (
|
||||||
u'Available bitrates are 96, 160, and 320.')
|
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.'
|
errors[setting] = u'Unknown setting.'
|
||||||
suggestion = did_you_mean(setting, defaults)
|
suggestion = did_you_mean(setting, defaults)
|
||||||
|
|
||||||
if suggestion:
|
if suggestion:
|
||||||
errors[setting] += u' Did you mean %s?' % suggestion
|
errors[setting] += u' Did you mean %s?' % suggestion
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ class GStreamerTest(unittest.TestCase):
|
|||||||
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
|
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
|
||||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||||
self.gstreamer = GStreamer()
|
self.gstreamer = GStreamer()
|
||||||
self.gstreamer.on_start()
|
|
||||||
|
|
||||||
def prepare_uri(self, uri):
|
def prepare_uri(self, uri):
|
||||||
self.gstreamer.prepare_change()
|
self.gstreamer.prepare_change()
|
||||||
@ -71,3 +70,8 @@ class GStreamerTest(unittest.TestCase):
|
|||||||
@unittest.SkipTest
|
@unittest.SkipTest
|
||||||
def test_set_position(self):
|
def test_set_position(self):
|
||||||
pass # TODO
|
pass # TODO
|
||||||
|
|
||||||
|
@unittest.SkipTest
|
||||||
|
def test_invalid_output_raises_error(self):
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user