Merge branch 'develop' into feature/encapsulate-gstreamer-state-changes
Conflicts: mopidy/gstreamer.py
This commit is contained in:
commit
03c30369b3
@ -2,19 +2,15 @@
|
||||
Output API
|
||||
**********
|
||||
|
||||
Outputs are responsible for playing audio.
|
||||
Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way.
|
||||
|
||||
.. warning::
|
||||
|
||||
A stable output API is not available yet, as we've only implemented a
|
||||
single output module.
|
||||
|
||||
.. automodule:: mopidy.outputs.base
|
||||
:synopsis: Base class for outputs
|
||||
.. autoclass:: mopidy.outputs.BaseOutput
|
||||
:members:
|
||||
|
||||
|
||||
Output implementations
|
||||
======================
|
||||
|
||||
* :mod:`mopidy.outputs.gstreamer`
|
||||
* :class:`mopidy.outputs.LocalOutput`
|
||||
* :class:`mopidy.outputs.NullOutput`
|
||||
* :class:`mopidy.outputs.ShoutcastOutput`
|
||||
|
||||
@ -10,9 +10,16 @@ This change log is used to track all major changes to Mopidy.
|
||||
|
||||
No description yet.
|
||||
|
||||
**Important changes**
|
||||
|
||||
- Mopidy now supports running with 1-n outputs at the same time. This feature
|
||||
was mainly added to facilitate Shoutcast support, which Mopidy has also
|
||||
gained. In its current state outputs can not be toggled during runtime.
|
||||
|
||||
**Changes**
|
||||
|
||||
No changes yet.
|
||||
- Fix local backend time query errors that where coming from stopped pipeline.
|
||||
(Fixes: :issue:`87`)
|
||||
|
||||
|
||||
0.4.0 (2011-04-27)
|
||||
|
||||
9
docs/modules/gstreamer.rst
Normal file
9
docs/modules/gstreamer.rst
Normal file
@ -0,0 +1,9 @@
|
||||
********************************************
|
||||
:mod:`mopidy.gstreamer` -- GStreamer adapter
|
||||
********************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.gstreamer
|
||||
|
||||
.. automodule:: mopidy.gstreamer
|
||||
:synopsis: GStreamer adapter
|
||||
:members:
|
||||
9
docs/modules/outputs.rst
Normal file
9
docs/modules/outputs.rst
Normal file
@ -0,0 +1,9 @@
|
||||
************************************************
|
||||
:mod:`mopidy.outputs` -- GStreamer audio outputs
|
||||
************************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs
|
||||
|
||||
.. automodule:: mopidy.outputs
|
||||
:synopsis: GStreamer audio outputs
|
||||
:members:
|
||||
@ -1,9 +0,0 @@
|
||||
*********************************************************************
|
||||
:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms
|
||||
*********************************************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.gstreamer
|
||||
|
||||
.. automodule:: mopidy.outputs.gstreamer
|
||||
:synopsis: GStreamer output for all platforms
|
||||
:members:
|
||||
@ -22,46 +22,6 @@ default_caps = gst.Caps("""
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -69,7 +29,7 @@ class GStreamer(ThreadingActor):
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||
- :attr:`mopidy.settings.OUTPUTS`
|
||||
|
||||
"""
|
||||
|
||||
@ -81,9 +41,9 @@ class GStreamer(ThreadingActor):
|
||||
|
||||
def _setup_gstreamer(self):
|
||||
"""
|
||||
**Warning:** :class:`GStreamerOutput` requires
|
||||
**Warning:** :class:`GStreamer` requires
|
||||
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
|
||||
not enforced by :class:`GStreamerOutput` itself.
|
||||
not enforced by :class:`GStreamer` itself.
|
||||
"""
|
||||
base_pipeline = ' ! '.join([
|
||||
'audioconvert name=convert',
|
||||
@ -145,13 +105,19 @@ class GStreamer(ThreadingActor):
|
||||
return backend_refs[0].proxy()
|
||||
|
||||
def set_uri(self, uri):
|
||||
"""Play audio at URI"""
|
||||
"""Change internal uridecodebin's URI"""
|
||||
self.gst_uridecodebin.set_property('uri', uri)
|
||||
|
||||
def deliver_data(self, caps_string, data):
|
||||
"""Deliver audio data to be played"""
|
||||
def deliver_data(self, capabilities, data):
|
||||
"""
|
||||
Deliver audio data to be played
|
||||
|
||||
:param capabilities: a GStreamer capabilities string
|
||||
:type capabilities: string
|
||||
:param data: raw audio data to be played
|
||||
"""
|
||||
source = self.gst_pipeline.get_by_name('source')
|
||||
caps = gst.caps_from_string(caps_string)
|
||||
caps = gst.caps_from_string(capabilities)
|
||||
buffer_ = gst.Buffer(buffer(data))
|
||||
buffer_.set_caps(caps)
|
||||
source.set_property('caps', caps)
|
||||
@ -167,6 +133,11 @@ class GStreamer(ThreadingActor):
|
||||
self.gst_pipeline.get_by_name('source').emit('end-of-stream')
|
||||
|
||||
def get_position(self):
|
||||
"""
|
||||
Get position in milliseconds.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
if self.gst_pipeline.get_state()[1] == gst.STATE_NULL:
|
||||
return 0
|
||||
try:
|
||||
@ -177,6 +148,13 @@ class GStreamer(ThreadingActor):
|
||||
return 0
|
||||
|
||||
def set_position(self, position):
|
||||
"""
|
||||
Set position in milliseconds.
|
||||
|
||||
:param position: the position in milliseconds
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.gst_pipeline.get_state() # block until state changes are done
|
||||
handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
|
||||
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
|
||||
@ -232,15 +210,35 @@ class GStreamer(ThreadingActor):
|
||||
return True
|
||||
|
||||
def get_volume(self):
|
||||
"""Get volume in range [0..100]"""
|
||||
"""
|
||||
Get volume level for software mixer.
|
||||
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
return int(self.gst_volume.get_property('volume') * 100)
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""Set volume in range [0..100]"""
|
||||
"""
|
||||
Set volume level for software mixer.
|
||||
|
||||
:param volume: the volume in the range [0..100]
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self.gst_volume.set_property('volume', volume / 100.0)
|
||||
return True
|
||||
|
||||
def set_metadata(self, track):
|
||||
"""
|
||||
Set track metadata for currently playing song.
|
||||
|
||||
Only needs to be called by sources such as appsrc which don't already
|
||||
inject tags in pipeline.
|
||||
|
||||
:param track: Track containing metadata for current song.
|
||||
:type track: :class:`mopidy.modes.Track`
|
||||
"""
|
||||
# FIXME what if we want to unset taginject tags?
|
||||
tags = u'artist="%(artist)s",title="%(title)s"' % {
|
||||
'artist': u', '.join([a.name for a in track.artists]),
|
||||
'title': track.name,
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
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,
|
||||
})
|
||||
152
mopidy/outputs/__init__.py
Normal file
152
mopidy/outputs/__init__.py
Normal file
@ -0,0 +1,152 @@
|
||||
import logging
|
||||
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
from mopidy import settings
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs')
|
||||
|
||||
|
||||
class BaseOutput(object):
|
||||
"""Base class for providing support for multiple pluggable outputs."""
|
||||
|
||||
def connect_bin(self, pipeline, element):
|
||||
"""
|
||||
Connect output bin to pipeline and given element.
|
||||
|
||||
In normal cases the element will probably be a `tee`,
|
||||
thus allowing us to connect any number of outputs. This
|
||||
however is why each bin is forced to have its own `queue`
|
||||
after the `tee`.
|
||||
|
||||
:param pipeline: gst.Pipeline to add output to.
|
||||
:type pipeline: :class:`gst.Pipeline`
|
||||
:param element: gst.Element in pipeline to connect output to.
|
||||
:type element: :class:`gst.Element`
|
||||
"""
|
||||
description = 'queue ! %s' % self.describe_bin()
|
||||
logger.debug('Adding new output to tee: %s', description)
|
||||
|
||||
output = gst.parse_bin_from_description(description, True)
|
||||
self.modify_bin(output)
|
||||
|
||||
pipeline.add(output)
|
||||
output.sync_state_with_parent() # Required to add to running pipe
|
||||
gst.element_link_many(element, output)
|
||||
|
||||
def modify_bin(self, output):
|
||||
"""
|
||||
Modifies 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.
|
||||
|
||||
:param output: gst.Bin to modify in some way.
|
||||
:type output: :class:`gst.Bin`
|
||||
"""
|
||||
pass
|
||||
|
||||
def describe_bin(self):
|
||||
"""
|
||||
Return text string describing bin in gst-launch format.
|
||||
|
||||
For simple cases this can just be a plain sink such as `autoaudiosink`
|
||||
or it can be a chain `element1 ! element2 ! sink`. See `man
|
||||
gst-launch0.10` for details on format.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_properties(self, element, properties):
|
||||
"""
|
||||
Helper to allow for simple setting of properties on elements.
|
||||
|
||||
Will call `set_property` on the element for each key that has a value
|
||||
that is not None.
|
||||
|
||||
:param element: gst.Element to set properties on.
|
||||
:type element: :class:`gst.Element`
|
||||
:param properties: Dictionary of properties to set on element.
|
||||
:type properties: dict
|
||||
"""
|
||||
for key, value in properties.items():
|
||||
if value is not None:
|
||||
element.set_property(key, value)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Advanced:
|
||||
|
||||
However, there are chases when you want to explicitly set what GStreamer
|
||||
should use. This can be achieved by setting `settings.LOCAL_OUTPUT_OVERRIDE`
|
||||
to the sink you want to use. Some of the possible values are: alsasink,
|
||||
esdsink, jackaudiosink, oss4sink, osssink and pulsesink. Exact values that
|
||||
will work on your system will depend on your sound setup and installed
|
||||
GStreamer plugins. Run `gst-inspect0.10` for list of all available plugins.
|
||||
Also note that this accepts properties and bins in `gst-launch` format.
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
if settings.LOCAL_OUTPUT_OVERRIDE:
|
||||
return settings.LOCAL_OUTPUT_OVERRIDE
|
||||
return 'autoaudiosink'
|
||||
|
||||
|
||||
class NullOutput(BaseOutput):
|
||||
"""
|
||||
Fall-back null output.
|
||||
|
||||
This output will not output anything. It is intended as a fall-back for
|
||||
when setup of all other outputs have failed and should not be used by end
|
||||
users. Inserting this output in such a case ensures that the pipeline does
|
||||
not crash.
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return 'fakesink'
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Advanced:
|
||||
|
||||
If you need to do something special that this output has not taken into
|
||||
account the setting `settings.SHOUTCAST_OUTPUT_OVERRIDE` has been provided
|
||||
to allow for manual setup of the bin using a gst-launch string. If this
|
||||
setting is set all other shoutcast settings will be ignored.
|
||||
"""
|
||||
|
||||
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,
|
||||
})
|
||||
@ -54,7 +54,7 @@ FRONTENDS = (
|
||||
u'mopidy.frontends.lastfm.LastfmFrontend',
|
||||
)
|
||||
|
||||
#: Which GStreamer bin description to use in :mod:`mopidy.outputs.CustomOutput`.
|
||||
#: Which GStreamer bin description to use in :class:`mopidy.outputs.LocalOutput`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user