diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index 5ef1606d..b96c909e 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -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` diff --git a/docs/changes.rst b/docs/changes.rst index 12da4e6d..31d0a015 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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) diff --git a/docs/modules/gstreamer.rst b/docs/modules/gstreamer.rst new file mode 100644 index 00000000..adbf5fda --- /dev/null +++ b/docs/modules/gstreamer.rst @@ -0,0 +1,9 @@ +******************************************** +:mod:`mopidy.gstreamer` -- GStreamer adapter +******************************************** + +.. inheritance-diagram:: mopidy.gstreamer + +.. automodule:: mopidy.gstreamer + :synopsis: GStreamer adapter + :members: diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst new file mode 100644 index 00000000..0a6986dd --- /dev/null +++ b/docs/modules/outputs.rst @@ -0,0 +1,9 @@ +************************************************ +:mod:`mopidy.outputs` -- GStreamer audio outputs +************************************************ + +.. inheritance-diagram:: mopidy.outputs + +.. automodule:: mopidy.outputs + :synopsis: GStreamer audio outputs + :members: diff --git a/docs/modules/outputs/gstreamer.rst b/docs/modules/outputs/gstreamer.rst deleted file mode 100644 index 69c77dad..00000000 --- a/docs/modules/outputs/gstreamer.rst +++ /dev/null @@ -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: diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ff25ac58..7f0fc7b7 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -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, diff --git a/mopidy/outputs.py b/mopidy/outputs.py deleted file mode 100644 index 5a57f446..00000000 --- a/mopidy/outputs.py +++ /dev/null @@ -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, - }) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py new file mode 100644 index 00000000..e3747463 --- /dev/null +++ b/mopidy/outputs/__init__.py @@ -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, + }) diff --git a/mopidy/settings.py b/mopidy/settings.py index c0ee3569..78abb6b7 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -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:: #: