Merge branch 'develop' into feature/mpris-frontend
This commit is contained in:
commit
55f13123a3
@ -1,20 +1,19 @@
|
||||
.. _output-api:
|
||||
|
||||
**********
|
||||
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.CustomOutput`
|
||||
* :class:`mopidy.outputs.LocalOutput`
|
||||
* :class:`mopidy.outputs.NullOutput`
|
||||
* :class:`mopidy.outputs.ShoutcastOutput`
|
||||
|
||||
@ -10,9 +10,49 @@ 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`)
|
||||
|
||||
- Support passing options to GStreamer. See :option:`--help-gst` for a list of
|
||||
available options. (Fixes: :issue:`95`)
|
||||
|
||||
|
||||
0.4.1 (2011-05-06)
|
||||
==================
|
||||
|
||||
This is a bug fix release fixing audio problems on older GStreamer and some
|
||||
minor bugs.
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10.
|
||||
The GStreamer `appsrc` bin wasn't being linked due to lack of default caps.
|
||||
(Fixes: :issue:`85`)
|
||||
|
||||
- Fix crash in :mod:`mopidy.mixers.nad` that occures at startup when the
|
||||
:mod:`io` module is available. We used an `eol` keyword argument which is
|
||||
supported by :meth:`serial.FileLike.readline`, but not by
|
||||
:meth:`io.RawBaseIO.readline`. When the :mod:`io` module is available, it is
|
||||
used by PySerial instead of the `FileLike` implementation.
|
||||
|
||||
- Fix UnicodeDecodeError in MPD frontend on non-english locale. Thanks to
|
||||
Antoine Pierlot-Garcin for the patch. (Fixes: :issue:`88`)
|
||||
|
||||
- Do not create Pykka proxies that are not going to be used in
|
||||
:mod:`mopidy.core`. The underlying actor may already intentionally be dead,
|
||||
and thus the program may crash on creating a proxy it doesn't need. Combined
|
||||
with the Pykka 0.12.2 release this fixes a crash in the Last.fm frontend
|
||||
which may occur when all dependencies are installed, but the frontend isn't
|
||||
configured. (Fixes: :issue:`84`)
|
||||
|
||||
|
||||
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:
|
||||
15
docs/modules/outputs.rst
Normal file
15
docs/modules/outputs.rst
Normal file
@ -0,0 +1,15 @@
|
||||
************************************************
|
||||
:mod:`mopidy.outputs` -- GStreamer audio outputs
|
||||
************************************************
|
||||
|
||||
The following GStreamer audio outputs implements the :ref:`output-api`.
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs
|
||||
|
||||
.. autoclass:: mopidy.outputs.CustomOutput
|
||||
|
||||
.. autoclass:: mopidy.outputs.LocalOutput
|
||||
|
||||
.. autoclass:: mopidy.outputs.NullOutput
|
||||
|
||||
.. autoclass:: mopidy.outputs.ShoutcastOutput
|
||||
@ -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:
|
||||
@ -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,26 @@ 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.pause_playback().get()
|
||||
|
||||
def play(self, track):
|
||||
return self.backend.output.play_uri(track.uri).get()
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.gstreamer.set_uri(track.uri).get()
|
||||
return self.backend.gstreamer.start_playback().get()
|
||||
|
||||
def resume(self):
|
||||
return self.backend.output.set_state('PLAYING').get()
|
||||
return self.backend.gstreamer.start_playback().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.stop_playback().get()
|
||||
|
||||
|
||||
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -8,10 +8,9 @@ logger = logging.getLogger('mopidy.backends.spotify.playback')
|
||||
|
||||
class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.output.set_state('PAUSED')
|
||||
return self.backend.gstreamer.pause_playback()
|
||||
|
||||
def play(self, track):
|
||||
self.backend.output.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 +19,10 @@ 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.prepare_change()
|
||||
self.backend.gstreamer.set_uri('appsrc://')
|
||||
self.backend.gstreamer.start_playback()
|
||||
self.backend.gstreamer.set_metadata(track)
|
||||
return True
|
||||
except SpotifyError as e:
|
||||
logger.info('Playback of %s failed: %s', track.uri, e)
|
||||
@ -30,12 +32,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.prepare_change()
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
self.backend.output.set_state('PLAYING')
|
||||
self.backend.gstreamer.start_playback()
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
result = self.backend.output.set_state('READY')
|
||||
result = self.backend.gstreamer.stop_playback()
|
||||
self.backend.spotify.session.play(0)
|
||||
return result
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,10 +1,23 @@
|
||||
import logging
|
||||
import optparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
|
||||
# processing by GStreamer. This needs to be done before GStreamer is imported,
|
||||
# so that GStreamer doesn't hijack e.g. ``--help``.
|
||||
# NOTE This naive fix does not support values like ``bar`` in
|
||||
# ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``.
|
||||
def is_gst_arg(arg):
|
||||
return arg.startswith('--gst') or arg == '--help-gst'
|
||||
gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)]
|
||||
mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)]
|
||||
sys.argv[1:] = gstreamer_args
|
||||
|
||||
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 +31,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()
|
||||
@ -32,6 +45,9 @@ def main():
|
||||
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
|
||||
parser.add_option('--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
help='show GStreamer help options')
|
||||
parser.add_option('-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
help='less output (warning level)')
|
||||
@ -44,7 +60,7 @@ def parse_options():
|
||||
parser.add_option('--list-settings',
|
||||
action='callback', callback=list_settings_optparse_callback,
|
||||
help='list current settings')
|
||||
return parser.parse_args()[0]
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
|
||||
def setup_settings():
|
||||
get_or_create_folder('~/.mopidy/')
|
||||
@ -52,25 +68,20 @@ def setup_settings():
|
||||
settings.validate()
|
||||
|
||||
def setup_gobject_loop():
|
||||
gobject_loop = GObjectEventThread()
|
||||
gobject_loop.start()
|
||||
return gobject_loop
|
||||
GObjectEventThread().start()
|
||||
|
||||
def setup_output():
|
||||
return get_class(settings.OUTPUT).start().proxy()
|
||||
def setup_gstreamer():
|
||||
GStreamer.start()
|
||||
|
||||
def setup_mixer():
|
||||
return get_class(settings.MIXER).start().proxy()
|
||||
get_class(settings.MIXER).start()
|
||||
|
||||
def setup_backend():
|
||||
return get_class(settings.BACKENDS[0]).start().proxy()
|
||||
get_class(settings.BACKENDS[0]).start()
|
||||
|
||||
def setup_frontends():
|
||||
frontends = []
|
||||
for frontend_class_name in settings.FRONTENDS:
|
||||
try:
|
||||
frontend = get_class(frontend_class_name).start().proxy()
|
||||
frontends.append(frontend)
|
||||
get_class(frontend_class_name).start()
|
||||
except OptionalDependencyError as e:
|
||||
logger.info(u'Disabled: %s (%s)', frontend_class_name, e)
|
||||
return frontends
|
||||
|
||||
@ -52,7 +52,7 @@ class MpdServer(asyncore.dispatcher):
|
||||
self._format_hostname(settings.MPD_SERVER_HOSTNAME),
|
||||
settings.MPD_SERVER_PORT)
|
||||
except IOError, e:
|
||||
logger.error('MPD server startup failed: %s' % e)
|
||||
logger.error(u'MPD server startup failed: %s' % str(e).decode('utf-8'))
|
||||
sys.exit(1)
|
||||
|
||||
def handle_accept(self):
|
||||
|
||||
264
mopidy/gstreamer.py
Normal file
264
mopidy/gstreamer.py
Normal file
@ -0,0 +1,264 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
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')
|
||||
|
||||
default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
channels=(int)2,
|
||||
width=(int)16,
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
|
||||
class GStreamer(ThreadingActor):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.OUTPUTS`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._pipeline = None
|
||||
self._source = None
|
||||
self._taginject = None
|
||||
self._tee = None
|
||||
self._uridecodebin = None
|
||||
self._volume = None
|
||||
|
||||
def on_start(self):
|
||||
self._setup_gstreamer()
|
||||
|
||||
def _setup_gstreamer(self):
|
||||
"""
|
||||
**Warning:** :class:`GStreamer` requires
|
||||
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
|
||||
not enforced by :class:`GStreamer` itself.
|
||||
"""
|
||||
description = ' ! '.join([
|
||||
'uridecodebin name=uri',
|
||||
'audioconvert name=convert',
|
||||
'volume name=volume',
|
||||
'taginject name=inject',
|
||||
'tee name=tee'])
|
||||
|
||||
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
|
||||
|
||||
self._pipeline = gst.parse_launch(description)
|
||||
self._taginject = self._pipeline.get_by_name('inject')
|
||||
self._tee = self._pipeline.get_by_name('tee')
|
||||
self._volume = self._pipeline.get_by_name('volume')
|
||||
self._uridecodebin = self._pipeline.get_by_name('uri')
|
||||
|
||||
self._uridecodebin.connect('notify::source', self._process_new_source)
|
||||
self._uridecodebin.connect('pad-added', self._process_new_pad,
|
||||
self._pipeline.get_by_name('convert').get_pad('sink'))
|
||||
|
||||
for output in settings.OUTPUTS:
|
||||
output_cls = get_class(output)()
|
||||
output_cls.connect_bin(self._pipeline, self._tee)
|
||||
|
||||
# Setup bus and message processor
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message', self._process_gstreamer_message)
|
||||
|
||||
def _process_new_source(self, element, pad):
|
||||
self._source = element.get_by_name('source')
|
||||
try:
|
||||
self._source.set_property('caps', default_caps)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def _process_new_pad(self, source, pad, target_pad):
|
||||
if not pad.is_linked():
|
||||
pad.link(target_pad)
|
||||
|
||||
def _process_gstreamer_message(self, bus, message):
|
||||
"""Process messages from GStreamer."""
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||
'Telling backend ...')
|
||||
self._get_backend().playback.on_end_of_track()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
self.stop_playback()
|
||||
error, debug = message.parse_error()
|
||||
logger.error(u'%s %s', error, debug)
|
||||
# FIXME Should we send 'stop_playback' to the backend here? Can we
|
||||
# differentiate on how serious the error is?
|
||||
elif message.type == gst.MESSAGE_WARNING:
|
||||
error, debug = message.parse_warning()
|
||||
logger.warning(u'%s %s', error, debug)
|
||||
|
||||
def _get_backend(self):
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
return backend_refs[0].proxy()
|
||||
|
||||
def set_uri(self, uri):
|
||||
"""
|
||||
Change internal uridecodebin's URI
|
||||
|
||||
:param uri: the URI to play
|
||||
:type uri: string
|
||||
"""
|
||||
self._uridecodebin.set_property('uri', uri)
|
||||
|
||||
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
|
||||
"""
|
||||
caps = gst.caps_from_string(capabilities)
|
||||
buffer_ = gst.Buffer(buffer(data))
|
||||
buffer_.set_caps(caps)
|
||||
self._source.set_property('caps', caps)
|
||||
self._source.emit('push-buffer', buffer_)
|
||||
|
||||
def end_of_data_stream(self):
|
||||
"""
|
||||
Add end-of-stream token to source.
|
||||
|
||||
We will get a GStreamer message when the stream playback reaches the
|
||||
token, and can then do any end-of-stream related tasks.
|
||||
"""
|
||||
self._source.emit('end-of-stream')
|
||||
|
||||
def get_position(self):
|
||||
"""
|
||||
Get position in milliseconds.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
if self._pipeline.get_state()[1] == gst.STATE_NULL:
|
||||
return 0
|
||||
try:
|
||||
position = self._pipeline.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
except gst.QueryError, e:
|
||||
logger.error('time_position failed: %s', e)
|
||||
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._pipeline.get_state() # block until state changes are done
|
||||
handeled = self._pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
|
||||
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
|
||||
self._pipeline.get_state() # block until seek is done
|
||||
return handeled
|
||||
|
||||
def start_playback(self):
|
||||
"""Notify GStreamer that it should start playback"""
|
||||
return self._set_state(gst.STATE_PLAYING)
|
||||
|
||||
def pause_playback(self):
|
||||
"""Notify GStreamer that it should pause playback"""
|
||||
return self._set_state(gst.STATE_PAUSED)
|
||||
|
||||
def prepare_change(self):
|
||||
"""
|
||||
Notify GStreamer that we are about to change state of playback.
|
||||
|
||||
This function always needs to be called before changing URIS or doing
|
||||
changes like updating data that is being pushed. The reason for this
|
||||
is that GStreamer will reset all its state when it changes to
|
||||
:attr:`gst.STATE_READY`.
|
||||
"""
|
||||
return self._set_state(gst.STATE_READY)
|
||||
|
||||
def stop_playback(self):
|
||||
"""Notify GStreamer that is should stop playback"""
|
||||
return self._set_state(gst.STATE_NULL)
|
||||
|
||||
def _set_state(self, state):
|
||||
"""
|
||||
Set the GStreamer state. Returns :class:`True` if successful.
|
||||
|
||||
.. digraph:: gst_state_transitions
|
||||
|
||||
"NULL" -> "READY"
|
||||
"PAUSED" -> "PLAYING"
|
||||
"PAUSED" -> "READY"
|
||||
"PLAYING" -> "PAUSED"
|
||||
"READY" -> "NULL"
|
||||
"READY" -> "PAUSED"
|
||||
|
||||
:param state: State to set pipeline to. One of: `gst.STATE_NULL`,
|
||||
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
|
||||
:type state: :class:`gst.State`
|
||||
:rtype: :class:`True` or :class:`False`
|
||||
"""
|
||||
result = self._pipeline.set_state(state)
|
||||
if result == gst.STATE_CHANGE_FAILURE:
|
||||
logger.warning('Setting GStreamer state to %s: failed',
|
||||
state.value_name)
|
||||
return False
|
||||
elif result == gst.STATE_CHANGE_ASYNC:
|
||||
logger.debug('Setting GStreamer state to %s: async',
|
||||
state.value_name)
|
||||
return True
|
||||
else:
|
||||
logger.debug('Setting GStreamer state to %s: OK',
|
||||
state.value_name)
|
||||
return True
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Get volume level for software mixer.
|
||||
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
return int(self._volume.get_property('volume') * 100)
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
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._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,
|
||||
}
|
||||
logger.debug('Setting tags to: %s', tags)
|
||||
self._taginject.set_property('tags', tags)
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -190,7 +190,7 @@ class NadTalker(ThreadingActor):
|
||||
# trailing whitespace.
|
||||
if not self._device.isOpen():
|
||||
self._device.open()
|
||||
result = self._device.readline(eol='\n').strip()
|
||||
result = self._device.readline().strip()
|
||||
if result:
|
||||
logger.debug('Read: %s', result)
|
||||
return result
|
||||
|
||||
@ -0,0 +1,153 @@
|
||||
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 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.
|
||||
|
||||
"""
|
||||
def describe_bin(self):
|
||||
return settings.CUSTOM_OUTPUT
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
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.
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return 'audioconvert ! %s ! shout2send name=shoutcast' \
|
||||
% settings.SHOUTCAST_OUTPUT_ENCODER
|
||||
|
||||
def modify_bin(self, output):
|
||||
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,
|
||||
})
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,153 +0,0 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs.gstreamer')
|
||||
|
||||
class GStreamerOutput(ThreadingActor, BaseOutput):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.gst_pipeline = None
|
||||
|
||||
def on_start(self):
|
||||
self._setup_gstreamer()
|
||||
|
||||
def _setup_gstreamer(self):
|
||||
"""
|
||||
**Warning:** :class:`GStreamerOutput` requires
|
||||
: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([
|
||||
'audioconvert name=convert',
|
||||
'volume name=volume',
|
||||
settings.GSTREAMER_AUDIO_SINK,
|
||||
]))
|
||||
|
||||
pad = self.gst_pipeline.get_by_name('convert').get_pad('sink')
|
||||
|
||||
uridecodebin = gst.element_factory_make('uridecodebin', 'uri')
|
||||
uridecodebin.connect('pad-added', self._process_new_pad, pad)
|
||||
self.gst_pipeline.add(uridecodebin)
|
||||
|
||||
# Setup bus and message processor
|
||||
gst_bus = self.gst_pipeline.get_bus()
|
||||
gst_bus.add_signal_watch()
|
||||
gst_bus.connect('message', self._process_gstreamer_message)
|
||||
|
||||
def _process_new_pad(self, source, pad, target_pad):
|
||||
pad.link(target_pad)
|
||||
|
||||
def _process_gstreamer_message(self, bus, message):
|
||||
"""Process messages from GStreamer."""
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||
'Telling backend ...')
|
||||
self._get_backend().playback.on_end_of_track()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
self.set_state('NULL')
|
||||
error, debug = message.parse_error()
|
||||
logger.error(u'%s %s', error, debug)
|
||||
# FIXME Should we send 'stop_playback' to the backend here? Can we
|
||||
# differentiate on how serious the error is?
|
||||
|
||||
def _get_backend(self):
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
return backend_refs[0].proxy()
|
||||
|
||||
def play_uri(self, uri):
|
||||
"""Play audio at URI"""
|
||||
self.set_state('READY')
|
||||
self.gst_pipeline.get_by_name('uri').set_property('uri', uri)
|
||||
return self.set_state('PLAYING')
|
||||
|
||||
def deliver_data(self, caps_string, data):
|
||||
"""Deliver audio data to be played"""
|
||||
source = self.gst_pipeline.get_by_name('source')
|
||||
caps = gst.caps_from_string(caps_string)
|
||||
buffer_ = gst.Buffer(buffer(data))
|
||||
buffer_.set_caps(caps)
|
||||
source.set_property('caps', caps)
|
||||
source.emit('push-buffer', buffer_)
|
||||
|
||||
def end_of_data_stream(self):
|
||||
"""
|
||||
Add end-of-stream token to source.
|
||||
|
||||
We will get a GStreamer message when the stream playback reaches the
|
||||
token, and can then do any end-of-stream related tasks.
|
||||
"""
|
||||
self.gst_pipeline.get_by_name('source').emit('end-of-stream')
|
||||
|
||||
def get_position(self):
|
||||
try:
|
||||
position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
except gst.QueryError, e:
|
||||
logger.error('time_position failed: %s', e)
|
||||
return 0
|
||||
|
||||
def set_position(self, position):
|
||||
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)
|
||||
self.gst_pipeline.get_state() # block until seek is done
|
||||
return handeled
|
||||
|
||||
def set_state(self, state_name):
|
||||
"""
|
||||
Set the GStreamer state. Returns :class:`True` if successful.
|
||||
|
||||
.. digraph:: gst_state_transitions
|
||||
|
||||
"NULL" -> "READY"
|
||||
"PAUSED" -> "PLAYING"
|
||||
"PAUSED" -> "READY"
|
||||
"PLAYING" -> "PAUSED"
|
||||
"READY" -> "NULL"
|
||||
"READY" -> "PAUSED"
|
||||
|
||||
:param state_name: NULL, READY, PAUSED, or PLAYING
|
||||
:type state_name: string
|
||||
:rtype: :class:`True` or :class:`False`
|
||||
"""
|
||||
result = self.gst_pipeline.set_state(
|
||||
getattr(gst, 'STATE_' + state_name))
|
||||
if result == gst.STATE_CHANGE_FAILURE:
|
||||
logger.warning('Setting GStreamer state to %s: failed', state_name)
|
||||
return False
|
||||
else:
|
||||
logger.debug('Setting GStreamer state to %s: OK', state_name)
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
return True
|
||||
@ -16,48 +16,34 @@ def translator(data):
|
||||
artist_kwargs = {}
|
||||
track_kwargs = {}
|
||||
|
||||
# FIXME replace with data.get('foo', None) ?
|
||||
def _retrieve(source_key, target_key, target):
|
||||
if source_key in data:
|
||||
target[target_key] = data[source_key]
|
||||
|
||||
if 'album' in data:
|
||||
album_kwargs['name'] = data['album']
|
||||
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
|
||||
|
||||
if 'track-count' in data:
|
||||
album_kwargs['num_tracks'] = data['track-count']
|
||||
|
||||
if 'artist' in data:
|
||||
artist_kwargs['name'] = data['artist']
|
||||
|
||||
if 'date' in data:
|
||||
date = data['date']
|
||||
if gst.TAG_DATE in data:
|
||||
date = data[gst.TAG_DATE]
|
||||
date = datetime.date(date.year, date.month, date.day)
|
||||
track_kwargs['date'] = date
|
||||
|
||||
if 'title' in data:
|
||||
track_kwargs['name'] = data['title']
|
||||
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
|
||||
|
||||
if 'track-number' in data:
|
||||
track_kwargs['track_no'] = data['track-number']
|
||||
|
||||
if 'album-artist' in data:
|
||||
albumartist_kwargs['name'] = data['album-artist']
|
||||
|
||||
if 'musicbrainz-trackid' in data:
|
||||
track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid']
|
||||
|
||||
if 'musicbrainz-artistid' in data:
|
||||
artist_kwargs['musicbrainz_id'] = data['musicbrainz-artistid']
|
||||
|
||||
if 'musicbrainz-albumid' in data:
|
||||
album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid']
|
||||
|
||||
if 'musicbrainz-albumartistid' in data:
|
||||
albumartist_kwargs['musicbrainz_id'] = data['musicbrainz-albumartistid']
|
||||
# Following keys don't seem to have TAG_* constant.
|
||||
_retrieve('album-artist', 'name', albumartist_kwargs)
|
||||
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
|
||||
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
|
||||
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
|
||||
_retrieve('musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
|
||||
|
||||
if albumartist_kwargs:
|
||||
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
|
||||
|
||||
track_kwargs['uri'] = data['uri']
|
||||
track_kwargs['length'] = data['duration']
|
||||
track_kwargs['length'] = data[gst.TAG_DURATION]
|
||||
track_kwargs['album'] = Album(**album_kwargs)
|
||||
track_kwargs['artists'] = [Artist(**artist_kwargs)]
|
||||
|
||||
|
||||
@ -26,6 +26,13 @@ BACKENDS = (
|
||||
#: details on the format.
|
||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
|
||||
|
||||
#: Which GStreamer bin description to use in :class:`mopidy.outputs.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
|
||||
@ -55,13 +62,6 @@ FRONTENDS = (
|
||||
u'mopidy.frontends.mpris.MprisFrontend',
|
||||
)
|
||||
|
||||
#: Which GStreamer audio sink to use in :mod:`mopidy.outputs.gstreamer`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: GSTREAMER_AUDIO_SINK = u'autoaudiosink'
|
||||
GSTREAMER_AUDIO_SINK = u'autoaudiosink'
|
||||
|
||||
#: Your `Last.fm <http://www.last.fm/>`_ username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
@ -144,13 +144,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:
|
||||
@ -175,6 +168,60 @@ 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'
|
||||
|
||||
#: Path to the Spotify cache.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import platform
|
||||
|
||||
from mopidy import get_version, get_platform, get_python, settings
|
||||
|
||||
|
||||
@ -97,9 +97,12 @@ def validate_settings(defaults, settings):
|
||||
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
||||
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
||||
'FRONTEND': 'FRONTENDS',
|
||||
'GSTREAMER_AUDIO_SINK': 'CUSTOM_OUTPUT',
|
||||
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
|
||||
'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',
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
80
tests/gstreamer_test.py
Normal file
80
tests/gstreamer_test.py
Normal file
@ -0,0 +1,80 @@
|
||||
import multiprocessing
|
||||
import unittest
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
# FIXME Our Windows build server does not support GStreamer yet
|
||||
import sys
|
||||
if sys.platform == 'win32':
|
||||
raise SkipTest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
# 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.gstreamer = GStreamer()
|
||||
self.gstreamer.on_start()
|
||||
|
||||
def prepare_uri(self, uri):
|
||||
self.gstreamer.prepare_change()
|
||||
self.gstreamer.set_uri(uri)
|
||||
|
||||
def tearDown(self):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_start_playback_existing_file(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.assertTrue(self.gstreamer.start_playback())
|
||||
|
||||
def test_start_playback_non_existing_file(self):
|
||||
self.prepare_uri(self.song_uri + 'bogus')
|
||||
self.assertFalse(self.gstreamer.start_playback())
|
||||
|
||||
def test_pause_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.pause_playback())
|
||||
|
||||
def test_stop_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.stop_playback())
|
||||
|
||||
@SkipTest
|
||||
def test_deliver_data(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_end_of_data_stream(self):
|
||||
pass # TODO
|
||||
|
||||
def test_default_get_volume_result(self):
|
||||
self.assertEqual(100, self.gstreamer.get_volume())
|
||||
|
||||
def test_set_volume(self):
|
||||
self.assertTrue(self.gstreamer.set_volume(50))
|
||||
self.assertEqual(50, self.gstreamer.get_volume())
|
||||
|
||||
def test_set_volume_to_zero(self):
|
||||
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.gstreamer.set_volume(100))
|
||||
self.assertEqual(100, self.gstreamer.get_volume())
|
||||
|
||||
@SkipTest
|
||||
def test_set_state_encapsulation(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_set_position(self):
|
||||
pass # TODO
|
||||
27
tests/help_test.py
Normal file
27
tests/help_test.py
Normal file
@ -0,0 +1,27 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mopidy
|
||||
|
||||
class HelpTest(unittest.TestCase):
|
||||
def test_help_has_mopidy_options(self):
|
||||
mopidy_dir = os.path.dirname(mopidy.__file__)
|
||||
args = [sys.executable, mopidy_dir, '--help']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--version' in output)
|
||||
self.assert_('--help' in output)
|
||||
self.assert_('--help-gst' in output)
|
||||
self.assert_('--quiet' in output)
|
||||
self.assert_('--verbose' in output)
|
||||
self.assert_('--save-debug-log' in output)
|
||||
self.assert_('--list-settings' in output)
|
||||
|
||||
def test_help_gst_has_gstreamer_options(self):
|
||||
mopidy_dir = os.path.dirname(mopidy.__file__)
|
||||
args = [sys.executable, mopidy_dir, '--help-gst']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--gst-version' in output)
|
||||
@ -1,62 +0,0 @@
|
||||
import multiprocessing
|
||||
import unittest
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
# FIXME Our Windows build server does not support GStreamer yet
|
||||
import sys
|
||||
if sys.platform == 'win32':
|
||||
raise SkipTest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.outputs.gstreamer import GStreamerOutput
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
class GStreamerOutputTest(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()
|
||||
|
||||
def tearDown(self):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_play_uri_existing_file(self):
|
||||
self.assertTrue(self.output.play_uri(self.song_uri))
|
||||
|
||||
def test_play_uri_non_existing_file(self):
|
||||
self.assertFalse(self.output.play_uri(self.song_uri + 'bogus'))
|
||||
|
||||
@SkipTest
|
||||
def test_deliver_data(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_end_of_data_stream(self):
|
||||
pass # TODO
|
||||
|
||||
def test_default_get_volume_result(self):
|
||||
self.assertEqual(100, self.output.get_volume())
|
||||
|
||||
def test_set_volume(self):
|
||||
self.assertTrue(self.output.set_volume(50))
|
||||
self.assertEqual(50, self.output.get_volume())
|
||||
|
||||
def test_set_volume_to_zero(self):
|
||||
self.assertTrue(self.output.set_volume(0))
|
||||
self.assertEqual(0, self.output.get_volume())
|
||||
|
||||
def test_set_volume_to_one_hundred(self):
|
||||
self.assertTrue(self.output.set_volume(100))
|
||||
self.assertEqual(100, self.output.get_volume())
|
||||
|
||||
@SkipTest
|
||||
def test_set_state(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_set_position(self):
|
||||
pass # TODO
|
||||
@ -18,7 +18,8 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV('0.2.0') < SV('0.3.0'))
|
||||
self.assert_(SV('0.3.0') < SV('0.3.1'))
|
||||
self.assert_(SV('0.3.1') < SV('0.4.0'))
|
||||
self.assert_(SV('0.4.0') < SV(get_plain_version()))
|
||||
self.assert_(SV('0.4.0') < SV('0.4.1'))
|
||||
self.assert_(SV('0.4.1') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.5.1'))
|
||||
|
||||
def test_get_platform_contains_platform(self):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user