From 9f77f801ba2d7cdd71b77ac81999f174f1d5aba7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Apr 2011 21:45:58 +0200 Subject: [PATCH 01/74] Swith to tee based pipeline to allow for multiple outputs --- mopidy/outputs/gstreamer.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index a6d1e9dd..0a4c0d85 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -35,21 +35,28 @@ class GStreamerOutput(ThreadingActor, BaseOutput): :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([ + base_pipeline = ' ! '.join([ 'audioconvert name=convert', 'volume name=volume', - settings.GSTREAMER_AUDIO_SINK, - ])) + 'taginject name=tag', + 'tee name=tee', + ]) - pad = self.gst_pipeline.get_by_name('convert').get_pad('sink') + logger.debug(u'Setting up base GStreamer pipeline: %s', base_pipeline) + + self.gst_pipeline = gst.parse_launch(base_pipeline) + + tee = self.gst_pipeline.get_by_name('tee') + convert = self.gst_pipeline.get_by_name('convert') uridecodebin = gst.element_factory_make('uridecodebin', 'uri') uridecodebin.connect('pad-added', self._process_new_pad, pad) self.gst_pipeline.add(uridecodebin) + output_bin = gst.parse_bin_from_description('queue ! %s' % + settings.GSTREAMER_AUDIO_SINK, True) + gst.element_link_many(tee, output_bin) + # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() gst_bus.add_signal_watch() From ecdf689301895e82a786ef6462723a5e809af46b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Apr 2011 23:43:29 +0200 Subject: [PATCH 02/74] Factor out method to add output bin --- mopidy/outputs/gstreamer.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 0a4c0d85..7f1d35eb 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -46,22 +46,28 @@ class GStreamerOutput(ThreadingActor, BaseOutput): self.gst_pipeline = gst.parse_launch(base_pipeline) - tee = self.gst_pipeline.get_by_name('tee') - convert = self.gst_pipeline.get_by_name('convert') + self.gst_tee = self.gst_pipeline.get_by_name('tee') + self.gst_convert = self.gst_pipeline.get_by_name('convert') uridecodebin = gst.element_factory_make('uridecodebin', 'uri') uridecodebin.connect('pad-added', self._process_new_pad, pad) self.gst_pipeline.add(uridecodebin) - output_bin = gst.parse_bin_from_description('queue ! %s' % - settings.GSTREAMER_AUDIO_SINK, True) - gst.element_link_many(tee, output_bin) + self._add_output(settings.GSTREAMER_AUDIO_SINK) # 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 _add_output(self, description): + bin = 'queue ! %s' % description + logger.debug('Adding output bin to tee: %s', bin) + output = gst.parse_bin_from_description(bin, True) + self.gst_pipeline.add(output) + output.sync_state_with_parent() + gst.element_link_many(self.gst_tee, output) + def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) From a81113e1a7a3117ef25b77ee2c17ab030c7e833d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 02:15:26 +0200 Subject: [PATCH 03/74] Add _build_shoutcast_description to construct shoutcast bin --- mopidy/outputs/gstreamer.py | 23 ++++++++++++++ mopidy/settings.py | 45 ++++++++++++++++++++++++++ tests/outputs/gstreamer_test.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 7f1d35eb..cdf3829e 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -68,6 +68,29 @@ class GStreamerOutput(ThreadingActor, BaseOutput): output.sync_state_with_parent() gst.element_link_many(self.gst_tee, output) + def _build_shoutcast_description(self): + if settings.SHOUTCAST_OVERRIDE: + return settings.SHOUTCAST_OVERRIDE + + if not settings.SHOUTCAST_SERVER: + return None + + description = ['%s ! shout2send' % settings.SHOUTCAST_ENCODER] + options = { + u'ip': settings.SHOUTCAST_SERVER, + u'mount': settings.SHOUTCAST_MOUNT, + u'port': settings.SHOUTCAST_PORT, + u'username': settings.SHOUTCAST_USER, + u'password': settings.SHOUTCAST_PASSWORD, + } + + for key, value in sorted(options.items()): + if value: + description.append('%s="%s"' % (key, value)) + + return u' '.join(description) + + def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) diff --git a/mopidy/settings.py b/mopidy/settings.py index 6e33ffaa..cd8f445e 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -174,6 +174,51 @@ MPD_SERVER_PASSWORD = None #: Default: 6600 MPD_SERVER_PORT = 6600 +#: Servar that runs Shoutcast server to send stream to. +#: +#: Default: :class:`None`, disables shoutcast if set to :class:`None` +SHOUTCAST_SERVER = None + +#: User to authenticate as against Shoutcast server. +#: +#: Default: 'source' +SHOUTCAST_USER = u'source' + +#: Password to authenticate with against Shoutcast server. +#: +#: Default: 'hackme' +SHOUTCAST_PASSWORD = u'hackme' + +#: Port to use for streaming to Shoutcast server. +#: +#: Default: 8000 +SHOUTCAST_PORT = 8000 + +#: Mountpoint to use for the stream on the Shoutcast server. +#: +#: Default: /stream +SHOUTCAST_MOUNT = u'/stream' + +#: Encoder to use to process audio data before streaming. +#: +#: Default: vorbisenc ! oggmux +SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' + +#: Overrides to allow advanced setup of shoutcast. Using this settings implies +#: that all other SHOUTCAST_* settings will be ignored. +#: +#: Examples: +#: +#: ``vorbisenc ! oggmux ! shout2send mount=/stream port=8000`` +#: Encode with vorbis and use ogg mux. +#: ``lame bitrate=320 ! shout2send mount=/stream port=8000`` +#: Encode with lame to bitrate=320. +#: +#: For all options see gst-inspect-0.10 lame, vorbisenc and shout2send. +#: +#: Default: :class:`None` +SHOUTCAST_OVERRIDE = None + #: Path to the Spotify cache. #: #: Used by :mod:`mopidy.backends.spotify`. diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 31a16756..b39e5583 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -60,3 +60,59 @@ class GStreamerOutputTest(unittest.TestCase): @SkipTest def test_set_position(self): pass # TODO + + def test_build_shoutcast_description_without_server(self): + self.assertEqual(None, self.output._build_shoutcast_description()) + + def test_build_shoutcast_description_with_server(self): + settings.SHOUTCAST_SERVER = '127.0.0.1' + + expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + u'shout2send ip="127.0.0.1" mount="/stream" ' \ + u'password="hackme" port="8000" username="source"' + result = self.output._build_shoutcast_description() + self.assertEqual(expected, result) + + def test_build_shoutcast_description_with_mount(self): + settings.SHOUTCAST_SERVER = '127.0.0.1' + settings.SHOUTCAST_MOUNT = '/stream.mp3' + + expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + u'shout2send ip="127.0.0.1" mount="/stream.mp3" ' \ + u'password="hackme" port="8000" username="source"' + result = self.output._build_shoutcast_description() + self.assertEqual(expected, result) + + def test_build_shoutcast_description_with_user_and_passwod(self): + settings.SHOUTCAST_SERVER = '127.0.0.1' + settings.SHOUTCAST_USER = 'john' + settings.SHOUTCAST_PASSWORD = 'doe' + + expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + u'shout2send ip="127.0.0.1" mount="/stream" ' \ + u'password="doe" port="8000" username="john"' + result = self.output._build_shoutcast_description() + self.assertEqual(expected, result) + + def test_build_shoutcast_description_unset_user_and_pass(self): + settings.SHOUTCAST_SERVER = '127.0.0.1' + settings.SHOUTCAST_USER = None + settings.SHOUTCAST_PASSWORD = None + + expected = u'%s ! shout2send ' % settings.SHOUTCAST_ENCODER + \ + u'ip="127.0.0.1" mount="/stream" port="8000"' + result = self.output._build_shoutcast_description() + self.assertEqual(expected, result) + + def test_build_shoutcast_description_with_override(self): + settings.SHOUTCAST_OVERRIDE = 'foobar' + + result = self.output._build_shoutcast_description() + self.assertEqual('foobar', result) + + def test_build_shoutcast_description_with_override_and_server(self): + settings.SHOUTCAST_OVERRIDE = 'foobar' + settings.SHOUTCAST_SERVER = '127.0.0.1' + + result = self.output._build_shoutcast_description() + self.assertEqual('foobar', result) From 700792f4a0372495957325392fd87f93d236168b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 02:55:14 +0200 Subject: [PATCH 04/74] Add basic streaming support --- mopidy/outputs/gstreamer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index cdf3829e..984efef1 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -55,6 +55,10 @@ class GStreamerOutput(ThreadingActor, BaseOutput): self._add_output(settings.GSTREAMER_AUDIO_SINK) + shoutcast_bin = self._build_shoutcast_description() + if shoutcast_bin: + self._add_output(shoutcast_bin) + # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() gst_bus.add_signal_watch() From b58436aaf3b09a56bfe17df171a579188d98b8c4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 03:03:43 +0200 Subject: [PATCH 05/74] Use audioconvert to ensure that volume element is handeled --- mopidy/outputs/gstreamer.py | 2 +- tests/outputs/gstreamer_test.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 984efef1..9430f47c 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -79,7 +79,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): if not settings.SHOUTCAST_SERVER: return None - description = ['%s ! shout2send' % settings.SHOUTCAST_ENCODER] + description = ['audioconvert ! %s ! shout2send' % settings.SHOUTCAST_ENCODER] options = { u'ip': settings.SHOUTCAST_SERVER, u'mount': settings.SHOUTCAST_MOUNT, diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index b39e5583..14493665 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -67,7 +67,7 @@ class GStreamerOutputTest(unittest.TestCase): def test_build_shoutcast_description_with_server(self): settings.SHOUTCAST_SERVER = '127.0.0.1' - expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ u'shout2send ip="127.0.0.1" mount="/stream" ' \ u'password="hackme" port="8000" username="source"' result = self.output._build_shoutcast_description() @@ -77,7 +77,7 @@ class GStreamerOutputTest(unittest.TestCase): settings.SHOUTCAST_SERVER = '127.0.0.1' settings.SHOUTCAST_MOUNT = '/stream.mp3' - expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ u'shout2send ip="127.0.0.1" mount="/stream.mp3" ' \ u'password="hackme" port="8000" username="source"' result = self.output._build_shoutcast_description() @@ -88,7 +88,7 @@ class GStreamerOutputTest(unittest.TestCase): settings.SHOUTCAST_USER = 'john' settings.SHOUTCAST_PASSWORD = 'doe' - expected = u'%s ! ' % settings.SHOUTCAST_ENCODER + \ + expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ u'shout2send ip="127.0.0.1" mount="/stream" ' \ u'password="doe" port="8000" username="john"' result = self.output._build_shoutcast_description() @@ -99,7 +99,7 @@ class GStreamerOutputTest(unittest.TestCase): settings.SHOUTCAST_USER = None settings.SHOUTCAST_PASSWORD = None - expected = u'%s ! shout2send ' % settings.SHOUTCAST_ENCODER + \ + expected = u'audioconvert ! %s ! shout2send ' % settings.SHOUTCAST_ENCODER + \ u'ip="127.0.0.1" mount="/stream" port="8000"' result = self.output._build_shoutcast_description() self.assertEqual(expected, result) From 3a44f130aa264aa570afbd161394b480beacc674 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 03:07:14 +0200 Subject: [PATCH 06/74] Refactor shoutcast tests --- tests/outputs/gstreamer_test.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 14493665..9f380815 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -77,32 +77,23 @@ class GStreamerOutputTest(unittest.TestCase): settings.SHOUTCAST_SERVER = '127.0.0.1' settings.SHOUTCAST_MOUNT = '/stream.mp3' - expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ - u'shout2send ip="127.0.0.1" mount="/stream.mp3" ' \ - u'password="hackme" port="8000" username="source"' - result = self.output._build_shoutcast_description() - self.assertEqual(expected, result) + self.check_shoutcast_options(u'ip="127.0.0.1" mount="/stream.mp3" ' + u'password="hackme" port="8000" username="source"') def test_build_shoutcast_description_with_user_and_passwod(self): settings.SHOUTCAST_SERVER = '127.0.0.1' settings.SHOUTCAST_USER = 'john' settings.SHOUTCAST_PASSWORD = 'doe' - expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ - u'shout2send ip="127.0.0.1" mount="/stream" ' \ - u'password="doe" port="8000" username="john"' - result = self.output._build_shoutcast_description() - self.assertEqual(expected, result) + self.check_shoutcast_options('ip="127.0.0.1" mount="/stream" ' + u'password="doe" port="8000" username="john"') def test_build_shoutcast_description_unset_user_and_pass(self): settings.SHOUTCAST_SERVER = '127.0.0.1' settings.SHOUTCAST_USER = None settings.SHOUTCAST_PASSWORD = None - expected = u'audioconvert ! %s ! shout2send ' % settings.SHOUTCAST_ENCODER + \ - u'ip="127.0.0.1" mount="/stream" port="8000"' - result = self.output._build_shoutcast_description() - self.assertEqual(expected, result) + self.check_shoutcast_options(u'ip="127.0.0.1" mount="/stream" port="8000"') def test_build_shoutcast_description_with_override(self): settings.SHOUTCAST_OVERRIDE = 'foobar' @@ -116,3 +107,10 @@ class GStreamerOutputTest(unittest.TestCase): result = self.output._build_shoutcast_description() self.assertEqual('foobar', result) + + def check_shoutcast_options(self, options): + expected = u'audioconvert ! %s ! shout2send ' % settings.SHOUTCAST_ENCODER + expected += options + + result = self.output._build_shoutcast_description() + self.assertEqual(expected, result) From 971132d539f017fe82e257ebdcb190ec49b2b800 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 03:11:55 +0200 Subject: [PATCH 07/74] Support just local or shoutcast audio output --- mopidy/outputs/gstreamer.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 9430f47c..e4563744 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -53,11 +53,16 @@ class GStreamerOutput(ThreadingActor, BaseOutput): uridecodebin.connect('pad-added', self._process_new_pad, pad) self.gst_pipeline.add(uridecodebin) - self._add_output(settings.GSTREAMER_AUDIO_SINK) + localaudio = settings.GSTREAMER_AUDIO_SINK + shoutcast = self._build_shoutcast_description() - shoutcast_bin = self._build_shoutcast_description() - if shoutcast_bin: - self._add_output(shoutcast_bin) + if localaudio: + self._add_output(localaudio) + if shoutcast: + self._add_output(shoutcast) + if not localaudio and not shoutcast: + logger.error('No proper output channels have been setup.') + self._add_output('fakesink') # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() From 9f862fe1b1d63f38e011aa96efb948d11f7fa98b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 03:17:17 +0200 Subject: [PATCH 08/74] Update settings docs for shoutcast --- mopidy/settings.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index cd8f445e..7daa2257 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -176,32 +176,44 @@ MPD_SERVER_PORT = 6600 #: Servar that runs Shoutcast server to send stream to. #: -#: Default: :class:`None`, disables shoutcast if set to :class:`None` +#: Default:: +#: +#: SHOUTCAST_SERVER = None # Must be set to enable shoutcase SHOUTCAST_SERVER = None #: User to authenticate as against Shoutcast server. #: -#: Default: 'source' +#: Default:: +#: +#: SHOUTCAST_USER = u'source' SHOUTCAST_USER = u'source' #: Password to authenticate with against Shoutcast server. #: -#: Default: 'hackme' +#: Default:: +#: +#: SHOUTCAST_PASSWORD = u'hackme' SHOUTCAST_PASSWORD = u'hackme' #: Port to use for streaming to Shoutcast server. #: -#: Default: 8000 +#: Default:: +#: +#: SHOUTCAST_PORT = 8000 SHOUTCAST_PORT = 8000 #: Mountpoint to use for the stream on the Shoutcast server. #: -#: Default: /stream +#: Default:: +#: +#: SHOUTCAST_MOUNT = u'/stream' SHOUTCAST_MOUNT = u'/stream' #: Encoder to use to process audio data before streaming. #: -#: Default: vorbisenc ! oggmux +#: Default:: +#: +#: SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' #: Overrides to allow advanced setup of shoutcast. Using this settings implies @@ -216,7 +228,9 @@ SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' #: #: For all options see gst-inspect-0.10 lame, vorbisenc and shout2send. #: -#: Default: :class:`None` +#: Default:: +#: +#: SHOUTCAST_OVERRIDE = None SHOUTCAST_OVERRIDE = None #: Path to the Spotify cache. From 44316c7cfc46908a41ad2e93d1d33c78c6dd3adb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 16:39:05 +0200 Subject: [PATCH 09/74] Reduce number of get_by_name lookups --- mopidy/outputs/gstreamer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index e4563744..f5e7a1a3 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -48,10 +48,13 @@ class GStreamerOutput(ThreadingActor, BaseOutput): self.gst_tee = self.gst_pipeline.get_by_name('tee') self.gst_convert = self.gst_pipeline.get_by_name('convert') + self.gst_volume = self.gst_pipeline.get_by_name('volume') + self.gst_taginject = self.gst_pipeline.get_by_name('tag') - uridecodebin = gst.element_factory_make('uridecodebin', 'uri') - uridecodebin.connect('pad-added', self._process_new_pad, pad) - self.gst_pipeline.add(uridecodebin) + self.gst_uridecodebin = gst.element_factory_make('uridecodebin', 'uri') + self.gst_uridecodebin.connect('pad-added', self._process_new_pad, + self.gst_convert.get_pad('sink')) + self.gst_pipeline.add(self.gst_uridecodebin) localaudio = settings.GSTREAMER_AUDIO_SINK shoutcast = self._build_shoutcast_description() @@ -124,7 +127,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def play_uri(self, uri): """Play audio at URI""" self.set_state('READY') - self.gst_pipeline.get_by_name('uri').set_property('uri', uri) + self.gst_uridecodebin.set_property('uri', uri) return self.set_state('PLAYING') def deliver_data(self, caps_string, data): @@ -188,11 +191,9 @@ class GStreamerOutput(ThreadingActor, BaseOutput): 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) + return int(self.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) + self.gst_volume.set_property('volume', volume / 100.0) return True From 85970dbc3bcc52ae43b3de073cdc426f61cc1a5b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 16:39:42 +0200 Subject: [PATCH 10/74] Switch to lame encoding for shoutcast --- mopidy/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 7daa2257..7f41cf69 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -213,8 +213,8 @@ SHOUTCAST_MOUNT = u'/stream' #: #: Default:: #: -#: SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' -SHOUTCAST_ENCODER = u'vorbisenc ! oggmux' +#: SHOUTCAST_ENCODER = u'lame mode=stereo bitrate=320' +SHOUTCAST_ENCODER = u'lame mode=stereo bitrate=320' #: Overrides to allow advanced setup of shoutcast. Using this settings implies #: that all other SHOUTCAST_* settings will be ignored. From f4db449f0e80a9d23974d0f7532bf4ba7cced006 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 16:40:37 +0200 Subject: [PATCH 11/74] Add set_metadata to allow taginjection for shoutcast stream --- mopidy/backends/spotify/playback.py | 1 + mopidy/outputs/base.py | 14 ++++++++++++++ mopidy/outputs/gstreamer.py | 9 +++++++++ 3 files changed, 24 insertions(+) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index b02c2d9f..f4db490c 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -21,6 +21,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) self.backend.output.play_uri('appsrc://') + self.backend.output.set_metadata(track) return True except SpotifyError as e: logger.warning('Play %s failed: %s', track.uri, e) diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index fbc86688..f81fe905 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -89,3 +89,17 @@ class BaseOutput(object): :rtype: :class:`True` if successful, else :class:`False` """ raise NotImplementedError + + 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. + + *MUST be implemented by subclass.* + + :param track: Track containing metadata for current song. + :type track: :class:`mopidy.modes.Track` + """ + raise NotImplementedError diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index f5e7a1a3..c92e51d4 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -197,3 +197,12 @@ class GStreamerOutput(ThreadingActor, BaseOutput): """Set volume in range [0..100]""" self.gst_volume.set_property('volume', volume / 100.0) return True + + def set_metadata(self, track): + tags = u'artist="%(artist)s",title="%(title)s",album="%(album)s"' % { + 'artist': u', '.join([a.name for a in track.artists]), + 'title': track.name, + 'album': track.album.name, + } + logger.debug('Setting tags to: %s', tags) + self.gst_taginject.set_property('tags', tags) From 7016a2081179eacd6f9940727fac6de9e73d1813 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 21:30:30 +0200 Subject: [PATCH 12/74] Kill of BaseOutput --- mopidy/backends/local/__init__.py | 4 +- mopidy/backends/spotify/__init__.py | 4 +- mopidy/outputs/base.py | 105 ------------------------ mopidy/outputs/dummy.py | 63 -------------- mopidy/outputs/gstreamer.py | 3 +- tests/backends/base/current_playlist.py | 4 +- tests/backends/base/playback.py | 4 +- 7 files changed, 9 insertions(+), 178 deletions(-) delete mode 100644 mopidy/outputs/base.py delete mode 100644 mopidy/outputs/dummy.py diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 2fa96dab..3d10a63c 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -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.outputs.gstreamer import GStreamerOutput from .translator import parse_m3u, parse_mpd_tag_cache @@ -53,7 +53,7 @@ class LocalBackend(ThreadingActor, Backend): self.output = None def on_start(self): - output_refs = ActorRegistry.get_by_class(BaseOutput) + output_refs = ActorRegistry.get_by_class(GStreamerOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 1ac5f0be..79750a5d 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -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.outputs.gstreamer import GStreamerOutput logger = logging.getLogger('mopidy.backends.spotify') @@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend): self.spotify = None def on_start(self): - output_refs = ActorRegistry.get_by_class(BaseOutput) + output_refs = ActorRegistry.get_by_class(GStreamerOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py deleted file mode 100644 index f81fe905..00000000 --- a/mopidy/outputs/base.py +++ /dev/null @@ -1,105 +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 - - 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. - - *MUST be implemented by subclass.* - - :param track: Track containing metadata for current song. - :type track: :class:`mopidy.modes.Track` - """ - raise NotImplementedError diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py deleted file mode 100644 index f09965f7..00000000 --- a/mopidy/outputs/dummy.py +++ /dev/null @@ -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 diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index c92e51d4..738c346d 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -9,11 +9,10 @@ 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): +class GStreamerOutput(ThreadingActor): """ Audio output through `GStreamer `_. diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index ee5e1111..fa5e760c 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -3,7 +3,7 @@ import multiprocessing import random from mopidy.models import Playlist, Track -from mopidy.outputs.base import BaseOutput +from mopidy.outputs.gstreamer import GStreamerOutput 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.output = mock.Mock(spec=GStreamerOutput) self.controller = self.backend.current_playlist self.playback = self.backend.playback diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 8ea48a3a..d5f04655 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -4,7 +4,7 @@ import random import time from mopidy.models import Track -from mopidy.outputs.base import BaseOutput +from mopidy.outputs.gstreamer import GStreamerOutput 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.output = mock.Mock(spec=GStreamerOutput) self.playback = self.backend.playback self.current_playlist = self.backend.current_playlist From 4a1df118fbd059d21134e048a10289545c1c1fca Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 21:46:19 +0200 Subject: [PATCH 13/74] Move GStreamerOutput up one level to mopidy.gstreamer and rename to GStreamer --- mopidy/backends/local/__init__.py | 4 +-- mopidy/backends/spotify/__init__.py | 4 +-- mopidy/{outputs => }/gstreamer.py | 2 +- mopidy/outputs/__init__.py | 0 tests/backends/base/current_playlist.py | 4 +-- tests/backends/base/playback.py | 4 +-- tests/{outputs => }/gstreamer_test.py | 36 ++++++++++++------------- 7 files changed, 27 insertions(+), 27 deletions(-) rename mopidy/{outputs => }/gstreamer.py (99%) delete mode 100644 mopidy/outputs/__init__.py rename tests/{outputs => }/gstreamer_test.py (74%) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 3d10a63c..cc09271a 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -12,7 +12,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album -from mopidy.outputs.gstreamer import GStreamerOutput +from mopidy.gstreamer import GStreamer from .translator import parse_m3u, parse_mpd_tag_cache @@ -53,7 +53,7 @@ class LocalBackend(ThreadingActor, Backend): self.output = None def on_start(self): - output_refs = ActorRegistry.get_by_class(GStreamerOutput) + output_refs = ActorRegistry.get_by_class(GStreamer) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 79750a5d..641f5377 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -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.gstreamer import GStreamerOutput +from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') @@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend): self.spotify = None def on_start(self): - output_refs = ActorRegistry.get_by_class(GStreamerOutput) + output_refs = ActorRegistry.get_by_class(GStreamer) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/gstreamer.py similarity index 99% rename from mopidy/outputs/gstreamer.py rename to mopidy/gstreamer.py index 738c346d..e3edf69c 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/gstreamer.py @@ -12,7 +12,7 @@ from mopidy.backends.base import Backend logger = logging.getLogger('mopidy.outputs.gstreamer') -class GStreamerOutput(ThreadingActor): +class GStreamer(ThreadingActor): """ Audio output through `GStreamer `_. diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index fa5e760c..a298817a 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -3,7 +3,7 @@ import multiprocessing import random from mopidy.models import Playlist, Track -from mopidy.outputs.gstreamer import GStreamerOutput +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=GStreamerOutput) + self.backend.output = mock.Mock(spec=GStreamer) self.controller = self.backend.current_playlist self.playback = self.backend.playback diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index d5f04655..972e5b5e 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -4,7 +4,7 @@ import random import time from mopidy.models import Track -from mopidy.outputs.gstreamer import GStreamerOutput +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=GStreamerOutput) + self.backend.output = mock.Mock(spec=GStreamer) self.playback = self.backend.playback self.current_playlist = self.backend.current_playlist diff --git a/tests/outputs/gstreamer_test.py b/tests/gstreamer_test.py similarity index 74% rename from tests/outputs/gstreamer_test.py rename to tests/gstreamer_test.py index 9f380815..5601160e 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -9,26 +9,26 @@ if sys.platform == 'win32': raise SkipTest from mopidy import settings -from mopidy.outputs.gstreamer import GStreamerOutput +from mopidy.gstreamer import GStreamer from mopidy.utils.path import path_to_uri from tests import path_to_data_dir -class GStreamerOutputTest(unittest.TestCase): +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.output = GStreamerOutput() - self.output.on_start() + self.gstreamer = GStreamer() + self.gstreamer.on_start() def tearDown(self): settings.runtime.clear() def test_play_uri_existing_file(self): - self.assertTrue(self.output.play_uri(self.song_uri)) + self.assertTrue(self.gstreamer.play_uri(self.song_uri)) def test_play_uri_non_existing_file(self): - self.assertFalse(self.output.play_uri(self.song_uri + 'bogus')) + self.assertFalse(self.gstreamer.play_uri(self.song_uri + 'bogus')) @SkipTest def test_deliver_data(self): @@ -39,19 +39,19 @@ class GStreamerOutputTest(unittest.TestCase): pass # TODO def test_default_get_volume_result(self): - self.assertEqual(100, self.output.get_volume()) + self.assertEqual(100, self.gstreamer.get_volume()) def test_set_volume(self): - self.assertTrue(self.output.set_volume(50)) - self.assertEqual(50, self.output.get_volume()) + self.assertTrue(self.gstreamer.set_volume(50)) + self.assertEqual(50, self.gstreamer.get_volume()) def test_set_volume_to_zero(self): - self.assertTrue(self.output.set_volume(0)) - self.assertEqual(0, self.output.get_volume()) + 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.output.set_volume(100)) - self.assertEqual(100, self.output.get_volume()) + self.assertTrue(self.gstreamer.set_volume(100)) + self.assertEqual(100, self.gstreamer.get_volume()) @SkipTest def test_set_state(self): @@ -62,7 +62,7 @@ class GStreamerOutputTest(unittest.TestCase): pass # TODO def test_build_shoutcast_description_without_server(self): - self.assertEqual(None, self.output._build_shoutcast_description()) + self.assertEqual(None, self.gstreamer._build_shoutcast_description()) def test_build_shoutcast_description_with_server(self): settings.SHOUTCAST_SERVER = '127.0.0.1' @@ -70,7 +70,7 @@ class GStreamerOutputTest(unittest.TestCase): expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ u'shout2send ip="127.0.0.1" mount="/stream" ' \ u'password="hackme" port="8000" username="source"' - result = self.output._build_shoutcast_description() + result = self.gstreamer._build_shoutcast_description() self.assertEqual(expected, result) def test_build_shoutcast_description_with_mount(self): @@ -98,19 +98,19 @@ class GStreamerOutputTest(unittest.TestCase): def test_build_shoutcast_description_with_override(self): settings.SHOUTCAST_OVERRIDE = 'foobar' - result = self.output._build_shoutcast_description() + result = self.gstreamer._build_shoutcast_description() self.assertEqual('foobar', result) def test_build_shoutcast_description_with_override_and_server(self): settings.SHOUTCAST_OVERRIDE = 'foobar' settings.SHOUTCAST_SERVER = '127.0.0.1' - result = self.output._build_shoutcast_description() + result = self.gstreamer._build_shoutcast_description() self.assertEqual('foobar', result) def check_shoutcast_options(self, options): expected = u'audioconvert ! %s ! shout2send ' % settings.SHOUTCAST_ENCODER expected += options - result = self.output._build_shoutcast_description() + result = self.gstreamer._build_shoutcast_description() self.assertEqual(expected, result) From c7ccd0c2d4d332c1d24c1f956c5863decdd37480 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 23:06:33 +0200 Subject: [PATCH 14/74] More fixes with respect to refactoring --- mopidy/backends/spotify/session_manager.py | 4 ++-- mopidy/core.py | 7 ++++--- mopidy/mixers/gstreamer_software.py | 4 ++-- mopidy/settings.py | 7 ------- mopidy/utils/settings.py | 2 ++ 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index e92fe89e..2b768b20 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -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') @@ -40,7 +40,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.connect() def setup(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() diff --git a/mopidy/core.py b/mopidy/core.py index 093f783d..60e25ef1 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -5,6 +5,7 @@ import time 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 +19,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() @@ -54,8 +55,8 @@ def setup_gobject_loop(): gobject_loop.start() return gobject_loop -def setup_output(): - return get_class(settings.OUTPUT).start().proxy() +def setup_gstreamer(): + return GStreamer().start().proxy() def setup_mixer(): return get_class(settings.MIXER).start().proxy() diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index d6365b4b..87602772 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -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() diff --git a/mopidy/settings.py b/mopidy/settings.py index 7f41cf69..e2d85c88 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -143,13 +143,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: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 529c6fb1..b907e85f 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -97,9 +97,11 @@ 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_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', From f218bc060bbe88ff1e2f2bde854f8eec09306755 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 23:30:03 +0200 Subject: [PATCH 15/74] Don't bother with album in taginject --- mopidy/gstreamer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index e3edf69c..3aeb7384 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -198,10 +198,9 @@ class GStreamer(ThreadingActor): return True def set_metadata(self, track): - tags = u'artist="%(artist)s",title="%(title)s",album="%(album)s"' % { + tags = u'artist="%(artist)s",title="%(title)s"' % { 'artist': u', '.join([a.name for a in track.artists]), 'title': track.name, - 'album': track.album.name, } logger.debug('Setting tags to: %s', tags) self.gst_taginject.set_property('tags', tags) From 4e232c981cdb9680375ae5f8cfefbf7ac04971f2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 00:50:15 +0200 Subject: [PATCH 16/74] Switch to modularised output for playback --- mopidy/gstreamer.py | 79 +++++++++++++++++++++------------------------ mopidy/outputs.py | 37 +++++++++++++++++++++ mopidy/settings.py | 18 +++++++++-- 3 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 mopidy/outputs.py diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 3aeb7384..ab70b0ba 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -8,9 +8,42 @@ 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.outputs.gstreamer') +logger = logging.getLogger('mopidy.gstreamer') + +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 class GStreamer(ThreadingActor): """ @@ -55,53 +88,15 @@ class GStreamer(ThreadingActor): self.gst_convert.get_pad('sink')) self.gst_pipeline.add(self.gst_uridecodebin) - localaudio = settings.GSTREAMER_AUDIO_SINK - shoutcast = self._build_shoutcast_description() - - if localaudio: - self._add_output(localaudio) - if shoutcast: - self._add_output(shoutcast) - if not localaudio and not shoutcast: - logger.error('No proper output channels have been setup.') - self._add_output('fakesink') + for output in settings.OUTPUTS: + output_cls = get_class(output)() + output_cls.connect_bin(self.gst_pipeline, self.gst_tee) # 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 _add_output(self, description): - bin = 'queue ! %s' % description - logger.debug('Adding output bin to tee: %s', bin) - output = gst.parse_bin_from_description(bin, True) - self.gst_pipeline.add(output) - output.sync_state_with_parent() - gst.element_link_many(self.gst_tee, output) - - def _build_shoutcast_description(self): - if settings.SHOUTCAST_OVERRIDE: - return settings.SHOUTCAST_OVERRIDE - - if not settings.SHOUTCAST_SERVER: - return None - - description = ['audioconvert ! %s ! shout2send' % settings.SHOUTCAST_ENCODER] - options = { - u'ip': settings.SHOUTCAST_SERVER, - u'mount': settings.SHOUTCAST_MOUNT, - u'port': settings.SHOUTCAST_PORT, - u'username': settings.SHOUTCAST_USER, - u'password': settings.SHOUTCAST_PASSWORD, - } - - for key, value in sorted(options.items()): - if value: - description.append('%s="%s"' % (key, value)) - - return u' '.join(description) - - def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) diff --git a/mopidy/outputs.py b/mopidy/outputs.py new file mode 100644 index 00000000..60569a95 --- /dev/null +++ b/mopidy/outputs.py @@ -0,0 +1,37 @@ +from mopidy import settings +from mopidy.gstreamer import BaseOutput + +class LocalAudioOutput(BaseOutput): + def describe_bin(self): + return 'autoaudiosink' + +class CustomOutput(BaseOutput): + def describe_bin(self): + return settings.CUSTOM_OUTPUT + +class NullOutput(BaseOutput): + def describe_bin(self): + return 'fakesink' + +class ShoutcastOutput(BaseOutput): + def describe_bin(self): + if settings.SHOUTCAST_OVERRIDE: + return settings.SHOUTCAST_OVERRIDE + + if not settings.SHOUTCAST_SERVER: + return None + + description = ['audioconvert ! %s ! shout2send' % settings.SHOUTCAST_ENCODER] + options = { + u'ip': settings.SHOUTCAST_SERVER, + u'mount': settings.SHOUTCAST_MOUNT, + u'port': settings.SHOUTCAST_PORT, + u'username': settings.SHOUTCAST_USER, + u'password': settings.SHOUTCAST_PASSWORD, + } + + for key, value in sorted(options.items()): + if value: + description.append('%s="%s"' % (key, value)) + + return u' '.join(description) diff --git a/mopidy/settings.py b/mopidy/settings.py index e2d85c88..ab299eb6 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -20,6 +20,18 @@ BACKENDS = ( u'mopidy.backends.spotify.SpotifyBackend', ) +#: List of outputs to use. See :mod:`mopidy.outputs` for all available +#: backends +#: +#: Default:: +#: +#: OUTPUTS = ( +#: u'mopidy.outputs.LocalAudioOutput', +#: ) +OUTPUTS = ( + u'mopidy.outputs.LocalAudioOutput', +) + #: The log format used for informational logging. #: #: See http://docs.python.org/library/logging.html#formatter-objects for @@ -54,12 +66,12 @@ FRONTENDS = ( u'mopidy.frontends.lastfm.LastfmFrontend', ) -#: Which GStreamer audio sink to use in :mod:`mopidy.outputs.gstreamer`. +#: Which GStreamer bin description to use in :mod:`mopidy.outputs.CustomOutput`. #: #: Default:: #: -#: GSTREAMER_AUDIO_SINK = u'autoaudiosink' -GSTREAMER_AUDIO_SINK = u'autoaudiosink' +#: CUSTOM_OUTPUT = None +CUSTOM_OUTPUT= None #: Your `Last.fm `_ username. #: From ebe9bba9a92f5a7e95827a087b8f33b0079ba0e1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 00:59:53 +0200 Subject: [PATCH 17/74] Use modify_bin and set_property to construct shoutcast output --- mopidy/outputs.py | 15 +++++++-------- mopidy/settings.py | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/mopidy/outputs.py b/mopidy/outputs.py index 60569a95..229f273e 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -18,11 +18,12 @@ class ShoutcastOutput(BaseOutput): if settings.SHOUTCAST_OVERRIDE: return settings.SHOUTCAST_OVERRIDE - if not settings.SHOUTCAST_SERVER: - return None + return 'audioconvert ! %s ! shout2send name=shoutcast' \ + % settings.SHOUTCAST_ENCODER - description = ['audioconvert ! %s ! shout2send' % settings.SHOUTCAST_ENCODER] - options = { + def modify_bin(self, output): + shoutcast = output.get_by_name('shoutcast') + properties = { u'ip': settings.SHOUTCAST_SERVER, u'mount': settings.SHOUTCAST_MOUNT, u'port': settings.SHOUTCAST_PORT, @@ -30,8 +31,6 @@ class ShoutcastOutput(BaseOutput): u'password': settings.SHOUTCAST_PASSWORD, } - for key, value in sorted(options.items()): + for key, value in properties.items(): if value: - description.append('%s="%s"' % (key, value)) - - return u' '.join(description) + shoutcast.set_property(key, value) diff --git a/mopidy/settings.py b/mopidy/settings.py index ab299eb6..9db30a11 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -183,8 +183,8 @@ MPD_SERVER_PORT = 6600 #: #: Default:: #: -#: SHOUTCAST_SERVER = None # Must be set to enable shoutcase -SHOUTCAST_SERVER = None +#: SHOUTCAST_SERVER = u'127.0.0.1' +SHOUTCAST_SERVER = u'127.0.0.1' #: User to authenticate as against Shoutcast server. #: From 1c233a3f8a971377de4ee594ba3f3e9817bb40db Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 17:14:31 +0200 Subject: [PATCH 18/74] Replace CustomOutput with override for LocalOutput --- mopidy/outputs.py | 8 +++----- mopidy/settings.py | 4 ++-- mopidy/utils/settings.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mopidy/outputs.py b/mopidy/outputs.py index 229f273e..6aff0a0f 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -1,14 +1,12 @@ from mopidy import settings from mopidy.gstreamer import BaseOutput -class LocalAudioOutput(BaseOutput): +class LocalOutput(BaseOutput): def describe_bin(self): + if settings.LOCALOUTPUT_OVERRIDE: + return settings.LOCALOUTPUT_OVERRIDE return 'autoaudiosink' -class CustomOutput(BaseOutput): - def describe_bin(self): - return settings.CUSTOM_OUTPUT - class NullOutput(BaseOutput): def describe_bin(self): return 'fakesink' diff --git a/mopidy/settings.py b/mopidy/settings.py index 9db30a11..915363b4 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -70,8 +70,8 @@ FRONTENDS = ( #: #: Default:: #: -#: CUSTOM_OUTPUT = None -CUSTOM_OUTPUT= None +#: LOCALOUTPUT_OVERRIDE = None +LOCALOUTPUT_OVERRIDE = None #: Your `Last.fm `_ username. #: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index b907e85f..6c678e4b 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -97,7 +97,7 @@ def validate_settings(defaults, settings): 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', 'FRONTEND': 'FRONTENDS', - 'GSTREAMER_AUDIO_SINK': 'CUSTOM_OUTPUT', + 'GSTREAMER_AUDIO_SINK': 'LOCALOUTPUT_OVERRIDE', 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH', 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', From 5c19bf843463adee36745087b314b979e582d229 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 17:15:43 +0200 Subject: [PATCH 19/74] Create set_properties helper for BaseOutput --- mopidy/gstreamer.py | 9 +++++++++ mopidy/outputs.py | 13 +++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ab70b0ba..5c242a66 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -45,6 +45,15 @@ class BaseOutput(object): """ 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): """ Audio output through `GStreamer `_. diff --git a/mopidy/outputs.py b/mopidy/outputs.py index 6aff0a0f..1a767d58 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -15,20 +15,17 @@ class ShoutcastOutput(BaseOutput): def describe_bin(self): if settings.SHOUTCAST_OVERRIDE: return settings.SHOUTCAST_OVERRIDE - return 'audioconvert ! %s ! shout2send name=shoutcast' \ % settings.SHOUTCAST_ENCODER def modify_bin(self, output): - shoutcast = output.get_by_name('shoutcast') - properties = { + if settings.SHOUTCAST_OVERRIDE: + return + + self.set_properties(output.get_by_name('shoutcast'), { u'ip': settings.SHOUTCAST_SERVER, u'mount': settings.SHOUTCAST_MOUNT, u'port': settings.SHOUTCAST_PORT, u'username': settings.SHOUTCAST_USER, u'password': settings.SHOUTCAST_PASSWORD, - } - - for key, value in properties.items(): - if value: - shoutcast.set_property(key, value) + }) From eceba712737a77ed73640a82d7756150d9aef0c7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 17:30:55 +0200 Subject: [PATCH 20/74] Unify naming of output settings --- mopidy/outputs.py | 22 +++++++++++----------- mopidy/settings.py | 34 +++++++++++++++++----------------- mopidy/utils/settings.py | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/mopidy/outputs.py b/mopidy/outputs.py index 1a767d58..ee1d20b0 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -3,8 +3,8 @@ from mopidy.gstreamer import BaseOutput class LocalOutput(BaseOutput): def describe_bin(self): - if settings.LOCALOUTPUT_OVERRIDE: - return settings.LOCALOUTPUT_OVERRIDE + if settings.LOCAL_OUTPUT_OVERRIDE: + return settings.LOCAL_OUTPUT_OVERRIDE return 'autoaudiosink' class NullOutput(BaseOutput): @@ -13,19 +13,19 @@ class NullOutput(BaseOutput): class ShoutcastOutput(BaseOutput): def describe_bin(self): - if settings.SHOUTCAST_OVERRIDE: - return settings.SHOUTCAST_OVERRIDE + if settings.SHOUTCAST_OUTPUT_OVERRIDE: + return settings.SHOUTCAST_OUTPUT_OVERRIDE return 'audioconvert ! %s ! shout2send name=shoutcast' \ - % settings.SHOUTCAST_ENCODER + % settings.SHOUTCAST_OUTPUT_ENCODER def modify_bin(self, output): - if settings.SHOUTCAST_OVERRIDE: + if settings.SHOUTCAST_OUTPUT_OVERRIDE: return self.set_properties(output.get_by_name('shoutcast'), { - u'ip': settings.SHOUTCAST_SERVER, - u'mount': settings.SHOUTCAST_MOUNT, - u'port': settings.SHOUTCAST_PORT, - u'username': settings.SHOUTCAST_USER, - u'password': settings.SHOUTCAST_PASSWORD, + u'ip': settings.SHOUTCAST_OUTPUT_SERVER, + u'mount': settings.SHOUTCAST_OUTPUT_MOUNT, + u'port': settings.SHOUTCAST_OUTPUT_PORT, + u'username': settings.SHOUTCAST_OUTPUT_USER, + u'password': settings.SHOUTCAST_OUTPUT_PASSWORD, }) diff --git a/mopidy/settings.py b/mopidy/settings.py index 915363b4..483d89dd 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -70,8 +70,8 @@ FRONTENDS = ( #: #: Default:: #: -#: LOCALOUTPUT_OVERRIDE = None -LOCALOUTPUT_OVERRIDE = None +#: LOCAL_OUTPUT_OVERRIDE = None +LOCAL_OUTPUT_OVERRIDE = None #: Your `Last.fm `_ username. #: @@ -183,46 +183,46 @@ MPD_SERVER_PORT = 6600 #: #: Default:: #: -#: SHOUTCAST_SERVER = u'127.0.0.1' -SHOUTCAST_SERVER = u'127.0.0.1' +#: 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_USER = u'source' -SHOUTCAST_USER = u'source' +#: SHOUTCAST_OUTPUT_USER = u'source' +SHOUTCAST_OUTPUT_USER = u'source' #: Password to authenticate with against Shoutcast server. #: #: Default:: #: -#: SHOUTCAST_PASSWORD = u'hackme' -SHOUTCAST_PASSWORD = u'hackme' +#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme' +SHOUTCAST_OUTPUT_PASSWORD = u'hackme' #: Port to use for streaming to Shoutcast server. #: #: Default:: #: -#: SHOUTCAST_PORT = 8000 -SHOUTCAST_PORT = 8000 +#: SHOUTCAST_OUTPUT_PORT = 8000 +SHOUTCAST_OUTPUT_PORT = 8000 #: Mountpoint to use for the stream on the Shoutcast server. #: #: Default:: #: -#: SHOUTCAST_MOUNT = u'/stream' -SHOUTCAST_MOUNT = u'/stream' +#: SHOUTCAST_OUTPUT_MOUNT = u'/stream' +SHOUTCAST_OUTPUT_MOUNT = u'/stream' #: Encoder to use to process audio data before streaming. #: #: Default:: #: -#: SHOUTCAST_ENCODER = u'lame mode=stereo bitrate=320' -SHOUTCAST_ENCODER = u'lame mode=stereo bitrate=320' +#: SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' +SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' #: Overrides to allow advanced setup of shoutcast. Using this settings implies -#: that all other SHOUTCAST_* settings will be ignored. +#: that all other SHOUTCAST_OUTPUT_* settings will be ignored. #: #: Examples: #: @@ -235,8 +235,8 @@ SHOUTCAST_ENCODER = u'lame mode=stereo bitrate=320' #: #: Default:: #: -#: SHOUTCAST_OVERRIDE = None -SHOUTCAST_OVERRIDE = None +#: SHOUTCAST_OUTPUT_OVERRIDE = None +SHOUTCAST_OUTPUT_OVERRIDE = None #: Path to the Spotify cache. #: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 6c678e4b..0dc6b4cb 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -97,7 +97,7 @@ def validate_settings(defaults, settings): 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', 'FRONTEND': 'FRONTENDS', - 'GSTREAMER_AUDIO_SINK': 'LOCALOUTPUT_OVERRIDE', + 'GSTREAMER_AUDIO_SINK': 'LOCAL_OUTPUT_OVERRIDE', 'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH', 'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH', 'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE', From 169fbae695616074d95985b44e412ff79a3f11eb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 17:41:33 +0200 Subject: [PATCH 21/74] Fix outputs setting --- mopidy/settings.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index 483d89dd..f803c932 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -20,18 +20,6 @@ BACKENDS = ( u'mopidy.backends.spotify.SpotifyBackend', ) -#: List of outputs to use. See :mod:`mopidy.outputs` for all available -#: backends -#: -#: Default:: -#: -#: OUTPUTS = ( -#: u'mopidy.outputs.LocalAudioOutput', -#: ) -OUTPUTS = ( - u'mopidy.outputs.LocalAudioOutput', -) - #: The log format used for informational logging. #: #: See http://docs.python.org/library/logging.html#formatter-objects for @@ -179,6 +167,18 @@ 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.LocalAudioOutput', +#: ) +OUTPUTS = ( + u'mopidy.outputs.LocalOutput', +) + #: Servar that runs Shoutcast server to send stream to. #: #: Default:: From a4c526774bfe29f11f02485c21881c5a3585bfe9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 17:44:02 +0200 Subject: [PATCH 22/74] Kill off stale shoutcast tests --- tests/gstreamer_test.py | 56 ++--------------------------------------- 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 5601160e..9087e0db 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -14,6 +14,8 @@ 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',) @@ -60,57 +62,3 @@ class GStreamerTest(unittest.TestCase): @SkipTest def test_set_position(self): pass # TODO - - def test_build_shoutcast_description_without_server(self): - self.assertEqual(None, self.gstreamer._build_shoutcast_description()) - - def test_build_shoutcast_description_with_server(self): - settings.SHOUTCAST_SERVER = '127.0.0.1' - - expected = u'audioconvert ! %s ! ' % settings.SHOUTCAST_ENCODER + \ - u'shout2send ip="127.0.0.1" mount="/stream" ' \ - u'password="hackme" port="8000" username="source"' - result = self.gstreamer._build_shoutcast_description() - self.assertEqual(expected, result) - - def test_build_shoutcast_description_with_mount(self): - settings.SHOUTCAST_SERVER = '127.0.0.1' - settings.SHOUTCAST_MOUNT = '/stream.mp3' - - self.check_shoutcast_options(u'ip="127.0.0.1" mount="/stream.mp3" ' - u'password="hackme" port="8000" username="source"') - - def test_build_shoutcast_description_with_user_and_passwod(self): - settings.SHOUTCAST_SERVER = '127.0.0.1' - settings.SHOUTCAST_USER = 'john' - settings.SHOUTCAST_PASSWORD = 'doe' - - self.check_shoutcast_options('ip="127.0.0.1" mount="/stream" ' - u'password="doe" port="8000" username="john"') - - def test_build_shoutcast_description_unset_user_and_pass(self): - settings.SHOUTCAST_SERVER = '127.0.0.1' - settings.SHOUTCAST_USER = None - settings.SHOUTCAST_PASSWORD = None - - self.check_shoutcast_options(u'ip="127.0.0.1" mount="/stream" port="8000"') - - def test_build_shoutcast_description_with_override(self): - settings.SHOUTCAST_OVERRIDE = 'foobar' - - result = self.gstreamer._build_shoutcast_description() - self.assertEqual('foobar', result) - - def test_build_shoutcast_description_with_override_and_server(self): - settings.SHOUTCAST_OVERRIDE = 'foobar' - settings.SHOUTCAST_SERVER = '127.0.0.1' - - result = self.gstreamer._build_shoutcast_description() - self.assertEqual('foobar', result) - - def check_shoutcast_options(self, options): - expected = u'audioconvert ! %s ! shout2send ' % settings.SHOUTCAST_ENCODER - expected += options - - result = self.gstreamer._build_shoutcast_description() - self.assertEqual(expected, result) From 472e4d2790f9abd4be48ab30498673576fb1971b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 21:30:01 +0200 Subject: [PATCH 23/74] Rename output to gstreamer in backends --- mopidy/backends/local/__init__.py | 20 ++++++++++---------- mopidy/backends/spotify/__init__.py | 8 ++++---- mopidy/backends/spotify/playback.py | 14 +++++++------- mopidy/backends/spotify/session_manager.py | 12 ++++++------ tests/backends/base/current_playlist.py | 2 +- tests/backends/base/playback.py | 12 ++++++------ 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index cc09271a..126034f1 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -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(GStreamer) - 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,24 @@ 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.set_state('PAUSED').get() def play(self, track): - return self.backend.output.play_uri(track.uri).get() + return self.backend.gstreamer.play_uri(track.uri).get() def resume(self): - return self.backend.output.set_state('PLAYING').get() + return self.backend.gstreamer.set_state('PLAYING').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.set_state('READY').get() class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 641f5377..9dababc0 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -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(GStreamer) - 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() diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index f4db490c..496bd83c 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -8,10 +8,10 @@ logger = logging.getLogger('mopidy.backends.spotify.playback') class SpotifyPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.output.set_state('PAUSED') + return self.backend.gstreamer.set_state('PAUSED') def play(self, track): - self.backend.output.set_state('READY') + self.backend.gstreamer.set_state('READY') if self.backend.playback.state == self.backend.playback.PLAYING: self.backend.spotify.session.play(0) if track.uri is None: @@ -20,8 +20,8 @@ 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.output.set_metadata(track) + self.backend.gstreamer.play_uri('appsrc://') + self.backend.gstreamer.set_metadata(track) return True except SpotifyError as e: logger.warning('Play %s failed: %s', track.uri, e) @@ -31,12 +31,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.set_state('READY') self.backend.spotify.session.seek(time_position) - self.backend.output.set_state('PLAYING') + self.backend.gstreamer.set_state('PLAYING') return True def stop(self): - result = self.backend.output.set_state('READY') + result = self.backend.gstreamer.set_state('READY') self.backend.spotify.session.play(0) return result diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 2b768b20..1d997156 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -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(GStreamer) - 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.' @@ -102,7 +102,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""" @@ -116,7 +116,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 diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index a298817a..427ce76d 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -12,7 +12,7 @@ class CurrentPlaylistControllerTest(object): def setUp(self): self.backend = self.backend_class() - self.backend.output = mock.Mock(spec=GStreamer) + self.backend.gstreamer = mock.Mock(spec=GStreamer) self.controller = self.backend.current_playlist self.playback = self.backend.playback diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 972e5b5e..2d455225 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -16,7 +16,7 @@ class PlaybackControllerTest(object): def setUp(self): self.backend = self.backend_class() - self.backend.output = mock.Mock(spec=GStreamer) + 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() From bb8296ceb70c9b942593aee320264da468d0a3c5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 21:30:15 +0200 Subject: [PATCH 24/74] Fix stale documentation in settings.py --- mopidy/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/settings.py b/mopidy/settings.py index f803c932..b6176f9b 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -173,7 +173,7 @@ MPD_SERVER_PORT = 6600 #: Default:: #: #: OUTPUTS = ( -#: u'mopidy.outputs.LocalAudioOutput', +#: u'mopidy.outputs.LocalOutput', #: ) OUTPUTS = ( u'mopidy.outputs.LocalOutput', From 9b9419fcb3a41accd1db0164da34342528c6eb72 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 21:32:34 +0200 Subject: [PATCH 25/74] s/USER/USERNAME/ --- mopidy/outputs.py | 2 +- mopidy/settings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/outputs.py b/mopidy/outputs.py index ee1d20b0..5a57f446 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -26,6 +26,6 @@ class ShoutcastOutput(BaseOutput): u'ip': settings.SHOUTCAST_OUTPUT_SERVER, u'mount': settings.SHOUTCAST_OUTPUT_MOUNT, u'port': settings.SHOUTCAST_OUTPUT_PORT, - u'username': settings.SHOUTCAST_OUTPUT_USER, + u'username': settings.SHOUTCAST_OUTPUT_USERNAME, u'password': settings.SHOUTCAST_OUTPUT_PASSWORD, }) diff --git a/mopidy/settings.py b/mopidy/settings.py index b6176f9b..c0ee3569 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -190,8 +190,8 @@ SHOUTCAST_OUTPUT_SERVER = u'127.0.0.1' #: #: Default:: #: -#: SHOUTCAST_OUTPUT_USER = u'source' -SHOUTCAST_OUTPUT_USER = u'source' +#: SHOUTCAST_OUTPUT_USERNAME = u'source' +SHOUTCAST_OUTPUT_USERNAME = u'source' #: Password to authenticate with against Shoutcast server. #: From 16955072101fa82542c002865fc0b51dd77f642c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Apr 2011 20:25:07 +0200 Subject: [PATCH 26/74] Remove eol kwarg that are only supported by pyserial.FileLike.readline, and not io.RawIOBase.readline --- mopidy/mixers/nad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index bd53376e..62f38bb7 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -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 From b2e4f3903c6e34b7652c5018aabe710a9ac90ca1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Apr 2011 20:30:04 +0200 Subject: [PATCH 27/74] Add NadMixer fix to changelog --- docs/changes.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 5a7db6ad..c91b2b40 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,18 @@ Changes This change log is used to track all major changes to Mopidy. +0.4.1 (in development) +====================== + +**Bugfixes** + +- 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. + + 0.4.0 (2011-04-27) ================== From 9f130f4dfcf7cd6ec1754d51612b21ce4eeeb8ea Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Apr 2011 21:44:57 +0200 Subject: [PATCH 28/74] Bump version number to 0.4.1 --- mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 1fbf99c8..35f5f94d 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -5,7 +5,7 @@ if not (2, 6) <= sys.version_info < (3,): from subprocess import PIPE, Popen -VERSION = (0, 4, 0) +VERSION = (0, 4, 1) def get_version(): try: diff --git a/tests/version_test.py b/tests/version_test.py index 7f204283..7c452c63 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -17,8 +17,9 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.1.0') < SV('1.0.0')) 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(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.4.1')) + self.assert_(SV('0.3.1') < SV('0.4.0')) + self.assert_(SV('0.4.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.4.2')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From e3235e96208732e410a8ea807bc072970dbba619 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 29 Apr 2011 22:17:50 +0200 Subject: [PATCH 29/74] Fix for #85 - appsrc wasn't being linked due to lack of default caps --- mopidy/outputs/gstreamer.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index a6d1e9dd..11cddc42 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -13,6 +13,15 @@ from mopidy.outputs.base import BaseOutput logger = logging.getLogger('mopidy.outputs.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 GStreamerOutput(ThreadingActor, BaseOutput): """ Audio output through `GStreamer `_. @@ -48,6 +57,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): uridecodebin = gst.element_factory_make('uridecodebin', 'uri') uridecodebin.connect('pad-added', self._process_new_pad, pad) + uridecodebin.connect('notify::source', self._process_new_source) self.gst_pipeline.add(uridecodebin) # Setup bus and message processor @@ -55,6 +65,13 @@ class GStreamerOutput(ThreadingActor, BaseOutput): gst_bus.add_signal_watch() gst_bus.connect('message', self._process_gstreamer_message) + def _process_new_source(self, element, pad): + source = element.get_by_name('source') + try: + source.set_property('caps', default_caps) + except TypeError: + pass + def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) From cec3c30400e80f0fafcf7924f720091501522152 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Apr 2011 23:52:40 +0200 Subject: [PATCH 30/74] Update changelog with fix for GH-85 --- docs/changes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index c91b2b40..bf7efc5f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,6 +10,10 @@ This change log is used to track all major changes to Mopidy. **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 From 5a16b2ec55a6021c1fe5021afc8d15747cd96305 Mon Sep 17 00:00:00 2001 From: Antoine Pierlot-Garcin Date: Fri, 29 Apr 2011 20:46:34 -0400 Subject: [PATCH 31/74] Fix UnicodeDecodeError in MPD frontend on non-english locale --- mopidy/frontends/mpd/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 8507e266..1be46ef4 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -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): From 5830ab3377e4837f819629ab5115bd3d62884cd6 Mon Sep 17 00:00:00 2001 From: Antoine Pierlot-Garcin Date: Fri, 29 Apr 2011 20:46:34 -0400 Subject: [PATCH 32/74] Fix UnicodeDecodeError in MPD frontend on non-english locale --- mopidy/frontends/mpd/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 8507e266..1be46ef4 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -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): From f39d98f5050853aee78e234ff2ab5823048fac5a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 2 May 2011 21:55:11 +0200 Subject: [PATCH 33/74] Fix merge error in gstreamer.py --- mopidy/gstreamer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 6a00ca37..7b444df2 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -101,11 +101,11 @@ class GStreamer(ThreadingActor): self.gst_volume = self.gst_pipeline.get_by_name('volume') self.gst_taginject = self.gst_pipeline.get_by_name('tag') - uridecodebin = gst.element_factory_make('uridecodebin', 'uri') - uridecodebin.connect('notify::source', self._process_new_source) - uridecodebin.connect('pad-added', self._process_new_pad, + self.gst_uridecodebin = gst.element_factory_make('uridecodebin', 'uri') + self.gst_uridecodebin.connect('notify::source', self._process_new_source) + self.gst_uridecodebin.connect('pad-added', self._process_new_pad, self.gst_convert.get_pad('sink')) - self.gst_pipeline.add(uridecodebin) + self.gst_pipeline.add(self.gst_uridecodebin) for output in settings.OUTPUTS: output_cls = get_class(output)() From e57f3f39c43a8f9aaf61558dc36565f8fa6048ce Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 2 May 2011 23:49:39 +0200 Subject: [PATCH 34/74] Short circuit retrival of position when we are in stopped state, should fix #87 --- mopidy/gstreamer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 7b444df2..ddcc6a56 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -169,6 +169,8 @@ class GStreamer(ThreadingActor): self.gst_pipeline.get_by_name('source').emit('end-of-stream') def get_position(self): + if self.gst_pipeline.get_state()[1] == gst.STATE_NULL: + return 0 try: position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] return position // gst.MSECOND From dc018024f93121a8f7e16b6eff2a034a66de9f8a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 3 May 2011 10:04:20 +0200 Subject: [PATCH 35/74] Move BaseOutput from mopidy.gstreamer to mopidy.outputs --- mopidy/gstreamer.py | 40 ----------------------------- mopidy/outputs.py | 61 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ddcc6a56..0c356c45 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): """ diff --git a/mopidy/outputs.py b/mopidy/outputs.py index 5a57f446..3aac4105 100644 --- a/mopidy/outputs.py +++ b/mopidy/outputs.py @@ -1,17 +1,76 @@ +import logging + +import pygst +pygst.require('0.10') +import gst + from mopidy import settings -from mopidy.gstreamer import BaseOutput + +logger = logging.getLogger('mopidy.outputs') + + +class BaseOutput(object): + """TODO adamcik""" + + 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 LocalOutput(BaseOutput): + """TODO adamcik""" + def describe_bin(self): if settings.LOCAL_OUTPUT_OVERRIDE: return settings.LOCAL_OUTPUT_OVERRIDE return 'autoaudiosink' + class NullOutput(BaseOutput): + """TODO adamcik""" + def describe_bin(self): return 'fakesink' + class ShoutcastOutput(BaseOutput): + """TODO adamcik""" + def describe_bin(self): if settings.SHOUTCAST_OUTPUT_OVERRIDE: return settings.SHOUTCAST_OUTPUT_OVERRIDE From ed5cd8f52b2b73d4aaacff1bf6b11ec8a0a2a61f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 3 May 2011 10:08:21 +0200 Subject: [PATCH 36/74] docs: Refresh docs wrt. mopidy.{gstreamer,outputs} --- docs/api/outputs.rst | 14 +++++--------- docs/modules/gstreamer.rst | 9 +++++++++ docs/modules/outputs.rst | 9 +++++++++ docs/modules/outputs/gstreamer.rst | 9 --------- 4 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 docs/modules/gstreamer.rst create mode 100644 docs/modules/outputs.rst delete mode 100644 docs/modules/outputs/gstreamer.rst 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/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: From 882ba520d89eba517be33cd4a0cb1a235685a99c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:04:15 +0200 Subject: [PATCH 37/74] Replace set_state with _playback helpers --- mopidy/backends/local/__init__.py | 10 ++++++---- mopidy/backends/spotify/playback.py | 13 +++++++------ mopidy/gstreamer.py | 29 ++++++++++++++++++++--------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 126034f1..5fb7f1cb 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -72,19 +72,21 @@ class LocalPlaybackController(PlaybackController): class LocalPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.gstreamer.set_state('PAUSED').get() + return self.backend.gstreamer.pause_playback().get() def play(self, track): - return self.backend.gstreamer.play_uri(track.uri).get() + self.backend.gstreamer.prepare_playback() + self.backend.gstreamer.set_uri(track.uri).get() + return self.backend.gstreamer.start_playback().get() def resume(self): - return self.backend.gstreamer.set_state('PLAYING').get() + return self.backend.gstreamer.start_playback().get() def seek(self, time_position): return self.backend.gstreamer.set_position(time_position).get() def stop(self): - return self.backend.gstreamer.set_state('READY').get() + return self.backend.gstreamer.stop_playback().get() class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 3f2a157b..7c67a7c1 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -8,10 +8,9 @@ logger = logging.getLogger('mopidy.backends.spotify.playback') class SpotifyPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.gstreamer.set_state('PAUSED') + return self.backend.gstreamer.pause_playback() def play(self, track): - self.backend.gstreamer.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,9 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) - self.backend.gstreamer.play_uri('appsrc://') + self.backend.gstreamer.prepare_playback() + self.backend.gstreamer.set_uri('appsrc://') + self.backend.gstreamer.start_playback() self.backend.gstreamer.set_metadata(track) return True except SpotifyError as e: @@ -31,12 +32,12 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return self.seek(self.backend.playback.time_position) def seek(self, time_position): - self.backend.gstreamer.set_state('READY') + self.backend.gstreamer.prepare_playback() self.backend.spotify.session.seek(time_position) - self.backend.gstreamer.set_state('PLAYING') + self.backend.gstreamer.start_playback() return True def stop(self): - result = self.backend.gstreamer.set_state('READY') + result = self.backend.gstreamer.stop_playback() self.backend.spotify.session.play(0) return result diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ddcc6a56..052c0827 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -133,7 +133,7 @@ class GStreamer(ThreadingActor): 'Telling backend ...') self._get_backend().playback.on_end_of_track() elif message.type == gst.MESSAGE_ERROR: - self.set_state('NULL') + 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 @@ -144,11 +144,9 @@ class GStreamer(ThreadingActor): assert len(backend_refs) == 1, 'Expected exactly one running backend.' return backend_refs[0].proxy() - def play_uri(self, uri): + def set_uri(self, uri): """Play audio at URI""" - self.set_state('READY') self.gst_uridecodebin.set_property('uri', uri) - return self.set_state('PLAYING') def deliver_data(self, caps_string, data): """Deliver audio data to be played""" @@ -185,7 +183,19 @@ class GStreamer(ThreadingActor): self.gst_pipeline.get_state() # block until seek is done return handeled - def set_state(self, state_name): + def start_playback(self): + return self.set_state(gst.STATE_PLAYING) + + def pause_playback(self): + return self.set_state(gst.STATE_PAUSE) + + def prepare_playback(self): + return self.set_state(gst.STATE_READY) + + def stop_playback(self): + return self.set_state(gst.STATE_NULL) + + def set_state(self, state): """ Set the GStreamer state. Returns :class:`True` if successful. @@ -202,13 +212,14 @@ class GStreamer(ThreadingActor): :type state_name: string :rtype: :class:`True` or :class:`False` """ - result = self.gst_pipeline.set_state( - getattr(gst, 'STATE_' + state_name)) + result = self.gst_pipeline.set_state(state) if result == gst.STATE_CHANGE_FAILURE: - logger.warning('Setting GStreamer state to %s: failed', state_name) + logger.warning('Setting GStreamer state to %s: failed', + state.value_name) return False else: - logger.debug('Setting GStreamer state to %s: OK', state_name) + logger.debug('Setting GStreamer state to %s: OK', + state.value_name) return True def get_volume(self): From 42547563eaae2c5c1dd9efa923482b9c32935040 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:08:31 +0200 Subject: [PATCH 38/74] Convert set_state to internal method --- mopidy/gstreamer.py | 10 +++++----- tests/gstreamer_test.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 052c0827..901597a7 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -184,18 +184,18 @@ class GStreamer(ThreadingActor): return handeled def start_playback(self): - return self.set_state(gst.STATE_PLAYING) + return self._set_state(gst.STATE_PLAYING) def pause_playback(self): - return self.set_state(gst.STATE_PAUSE) + return self._set_state(gst.STATE_PAUSE) def prepare_playback(self): - return self.set_state(gst.STATE_READY) + return self._set_state(gst.STATE_READY) def stop_playback(self): - return self.set_state(gst.STATE_NULL) + return self._set_state(gst.STATE_NULL) - def set_state(self, state): + def _set_state(self, state): """ Set the GStreamer state. Returns :class:`True` if successful. diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 9087e0db..d05b3bda 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -56,7 +56,7 @@ class GStreamerTest(unittest.TestCase): self.assertEqual(100, self.gstreamer.get_volume()) @SkipTest - def test_set_state(self): + def test_set_state_encapsulation(self): pass # TODO @SkipTest From 32cf5588eb5a7e01dcbaa10857c44502a558190a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:25:33 +0200 Subject: [PATCH 39/74] Recovered old BastOutput docstrings and added them to GStreamer class where apropriate --- mopidy/gstreamer.py | 58 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 0c356c45..bd1467b2 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -29,7 +29,7 @@ class GStreamer(ThreadingActor): **Settings:** - - :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK` + - :attr:`mopidy.settings.OUTPUTS` """ @@ -105,15 +105,27 @@ class GStreamer(ThreadingActor): return backend_refs[0].proxy() def play_uri(self, uri): - """Play audio at URI""" + """ + Play audio at URI + + :param uri: the URI to play + :type uri: string + :rtype: :class:`True` if successful, else :class:`False` + """ self.set_state('READY') self.gst_uridecodebin.set_property('uri', uri) return self.set_state('PLAYING') - 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) @@ -129,6 +141,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: @@ -139,6 +156,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) @@ -172,15 +196,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, From 9b47870404389bbbf4d07211a63cbaa3b5ee25d5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:27:23 +0200 Subject: [PATCH 40/74] Make mopidy.ouputs a module again --- mopidy/{outputs.py => outputs/__init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mopidy/{outputs.py => outputs/__init__.py} (100%) diff --git a/mopidy/outputs.py b/mopidy/outputs/__init__.py similarity index 100% rename from mopidy/outputs.py rename to mopidy/outputs/__init__.py From 8e8a7cd037fbc2c34173960be70d1629339d0a4d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:41:01 +0200 Subject: [PATCH 41/74] Add docstring for BaseOutput --- mopidy/outputs/__init__.py | 49 ++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index 3aac4105..8ff43a5f 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -10,45 +10,68 @@ logger = logging.getLogger('mopidy.outputs') class BaseOutput(object): - """TODO adamcik""" + """Base class for providing support for multiple pluggable outputs.""" - def connect_bin(self, pipeline, element_to_link_to): + 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 = self.parse_bin(description) + output = gst.parse_bin_from_description(description, True) 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) + 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 + 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. """ pass def describe_bin(self): """ - Describe bin to be parsed. + Return text string describing bin in gst-launch format. - Must be implemented by subclasses. + 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): """ - Set properties on element if they have a value. + 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 element: dict """ for key, value in properties.items(): - if value: + if value is not None: element.set_property(key, value) From f69e01c501b14921b74c57e3536ba703af33a92a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:56:24 +0200 Subject: [PATCH 42/74] docstrings for outputs --- mopidy/outputs/__init__.py | 41 +++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index 8ff43a5f..9623681f 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -76,7 +76,21 @@ class BaseOutput(object): class LocalOutput(BaseOutput): - """TODO adamcik""" + """ + 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 and osssink. 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. + """ def describe_bin(self): if settings.LOCAL_OUTPUT_OVERRIDE: @@ -85,14 +99,35 @@ class LocalOutput(BaseOutput): class NullOutput(BaseOutput): - """TODO adamcik""" + """ + 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): - """TODO adamcik""" + """ + 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: From 0a2a582b25a52517bdebcaf864db4e817fc028e6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 3 May 2011 23:57:38 +0200 Subject: [PATCH 43/74] Remove last refrenceto GStreamerOutput --- mopidy/gstreamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index bd1467b2..4c69da91 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -41,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', From 4df54ffdef57524683e0a9962804e61680dd4991 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 00:00:38 +0200 Subject: [PATCH 44/74] Fix refrence to mopidy.outputs.CustomOutput --- mopidy/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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:: #: From fad015ba7e424a4ca9e9e0872a77821874ad4e0f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 00:15:21 +0200 Subject: [PATCH 45/74] Update changelog with modularised output and time error bug --- docs/changes.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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) From 173b5cfb827adc2e6f15d382640965c3a977276c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 00:23:21 +0200 Subject: [PATCH 46/74] Minor doc fixes for outputs --- mopidy/outputs/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index 9623681f..f7a796a8 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -43,6 +43,9 @@ class BaseOutput(object): 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 @@ -68,7 +71,7 @@ class BaseOutput(object): :param element: gst.Element to set properties on. :type element: :class:`gst.Element` :param properties: Dictionary of properties to set on element. - :type element: dict + :type properties: dict """ for key, value in properties.items(): if value is not None: From 6feb4f4c419c8c6442a6814d4d5b93ec90823e2c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 11:18:17 +0200 Subject: [PATCH 47/74] Add pulsesink to docstring and mention gst-launch --- mopidy/outputs/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index f7a796a8..e3747463 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -90,9 +90,10 @@ class LocalOutput(BaseOutput): 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 and osssink. 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. + 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): From d0e0594a780706dfc3e0ca084ad75bf379415819 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 23:35:32 +0200 Subject: [PATCH 48/74] s/PAUSE/PAUSED/ in pause_playback --- mopidy/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 901597a7..66735beb 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -187,7 +187,7 @@ class GStreamer(ThreadingActor): return self._set_state(gst.STATE_PLAYING) def pause_playback(self): - return self._set_state(gst.STATE_PAUSE) + return self._set_state(gst.STATE_PAUSED) def prepare_playback(self): return self._set_state(gst.STATE_READY) From 9998a0e80f2c677895fee1955586b5f2acac407c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 23:35:50 +0200 Subject: [PATCH 49/74] Update gstreamer test --- tests/gstreamer_test.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index d05b3bda..81231d37 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -26,11 +26,15 @@ class GStreamerTest(unittest.TestCase): def tearDown(self): settings.runtime.clear() - def test_play_uri_existing_file(self): - self.assertTrue(self.gstreamer.play_uri(self.song_uri)) + def test_set_uri_existing_file(self): + self.gstreamer.prepare_playback() + self.gstreamer.set_uri(self.song_uri) + self.assertTrue(self.gstreamer.start_playback()) - def test_play_uri_non_existing_file(self): - self.assertFalse(self.gstreamer.play_uri(self.song_uri + 'bogus')) + def test_set_uri_non_existing_file(self): + self.gstreamer.prepare_playback() + self.gstreamer.set_uri(self.song_uri + 'bogus') + self.assertFalse(self.gstreamer.start_playback()) @SkipTest def test_deliver_data(self): From 76a33f37dbc8b2dd7c33648cc75d7ddd647d44ff Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 23:42:14 +0200 Subject: [PATCH 50/74] Add some more tests --- tests/gstreamer_test.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 81231d37..d01e694c 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -23,19 +23,31 @@ class GStreamerTest(unittest.TestCase): self.gstreamer = GStreamer() self.gstreamer.on_start() + def prepare_uri(self, uri): + self.gstreamer.prepare_playback() + self.gstreamer.set_uri(uri) + def tearDown(self): settings.runtime.clear() - def test_set_uri_existing_file(self): - self.gstreamer.prepare_playback() - self.gstreamer.set_uri(self.song_uri) + def test_start_playback_existing_file(self): + self.prepare_uri(self.song_uri) self.assertTrue(self.gstreamer.start_playback()) - def test_set_uri_non_existing_file(self): - self.gstreamer.prepare_playback() - self.gstreamer.set_uri(self.song_uri + 'bogus') + 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 From e1cc1dfe483de0658caeb6d1b408148db05982c4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 5 May 2011 20:17:28 +0200 Subject: [PATCH 51/74] Rename prepare_playback to prepare_change --- mopidy/backends/local/__init__.py | 2 +- mopidy/backends/spotify/playback.py | 4 ++-- mopidy/gstreamer.py | 2 +- tests/gstreamer_test.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 5fb7f1cb..cc039ce0 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -75,7 +75,7 @@ class LocalPlaybackProvider(BasePlaybackProvider): return self.backend.gstreamer.pause_playback().get() def play(self, track): - self.backend.gstreamer.prepare_playback() + self.backend.gstreamer.prepare_change() self.backend.gstreamer.set_uri(track.uri).get() return self.backend.gstreamer.start_playback().get() diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index 7c67a7c1..dc328fc9 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -19,7 +19,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) - self.backend.gstreamer.prepare_playback() + self.backend.gstreamer.prepare_change() self.backend.gstreamer.set_uri('appsrc://') self.backend.gstreamer.start_playback() self.backend.gstreamer.set_metadata(track) @@ -32,7 +32,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): return self.seek(self.backend.playback.time_position) def seek(self, time_position): - self.backend.gstreamer.prepare_playback() + self.backend.gstreamer.prepare_change() self.backend.spotify.session.seek(time_position) self.backend.gstreamer.start_playback() return True diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 66735beb..95953d4a 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -189,7 +189,7 @@ class GStreamer(ThreadingActor): def pause_playback(self): return self._set_state(gst.STATE_PAUSED) - def prepare_playback(self): + def prepare_change(self): return self._set_state(gst.STATE_READY) def stop_playback(self): diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index d01e694c..0b9a559e 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -24,7 +24,7 @@ class GStreamerTest(unittest.TestCase): self.gstreamer.on_start() def prepare_uri(self, uri): - self.gstreamer.prepare_playback() + self.gstreamer.prepare_change() self.gstreamer.set_uri(uri) def tearDown(self): From b966c0fd1b68883cd9e80adaa784dd2ebe0da55e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 5 May 2011 20:21:17 +0200 Subject: [PATCH 52/74] Basic docstrings for new helpers --- mopidy/gstreamer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 95953d4a..ff25ac58 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -184,15 +184,24 @@ class GStreamer(ThreadingActor): 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. + """ 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): From 6f9be1159425867a3d260cfd3379f61c97b461b2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 4 May 2011 23:21:26 +0200 Subject: [PATCH 53/74] Cleanup gstreamer. Simplified code and reduced get_by_name usage. --- mopidy/gstreamer.py | 85 ++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 7f0fc7b7..5b715175 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -34,7 +34,12 @@ class GStreamer(ThreadingActor): """ def __init__(self): - self.gst_pipeline = None + 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() @@ -45,46 +50,44 @@ class GStreamer(ThreadingActor): :class:`mopidy.utils.process.GObjectEventThread` to be running. This is not enforced by :class:`GStreamer` itself. """ - base_pipeline = ' ! '.join([ + description = ' ! '.join([ + 'uridecodebin name=uri', 'audioconvert name=convert', 'volume name=volume', - 'taginject name=tag', - 'tee name=tee', - ]) + 'taginject name=inject', + 'tee name=tee']) - logger.debug(u'Setting up base GStreamer pipeline: %s', base_pipeline) + logger.debug(u'Setting up base GStreamer pipeline: %s', description) - self.gst_pipeline = gst.parse_launch(base_pipeline) + 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.gst_tee = self.gst_pipeline.get_by_name('tee') - self.gst_convert = self.gst_pipeline.get_by_name('convert') - self.gst_volume = self.gst_pipeline.get_by_name('volume') - self.gst_taginject = self.gst_pipeline.get_by_name('tag') - - self.gst_uridecodebin = gst.element_factory_make('uridecodebin', 'uri') - self.gst_uridecodebin.connect('notify::source', self._process_new_source) - self.gst_uridecodebin.connect('pad-added', self._process_new_pad, - self.gst_convert.get_pad('sink')) - self.gst_pipeline.add(self.gst_uridecodebin) + 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.gst_pipeline, self.gst_tee) + output_cls.connect_bin(self._pipeline, self._tee) # 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) + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._process_gstreamer_message) def _process_new_source(self, element, pad): - source = element.get_by_name('source') + self._source = element.get_by_name('source') try: - source.set_property('caps', default_caps) + self._source.set_property('caps', default_caps) except TypeError: pass def _process_new_pad(self, source, pad, target_pad): - pad.link(target_pad) + if not pad.is_linked(): + pad.link(target_pad) def _process_gstreamer_message(self, bus, message): """Process messages from GStreamer.""" @@ -105,8 +108,13 @@ class GStreamer(ThreadingActor): return backend_refs[0].proxy() def set_uri(self, uri): - """Change internal uridecodebin's URI""" - self.gst_uridecodebin.set_property('uri', 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): """ @@ -116,12 +124,11 @@ class GStreamer(ThreadingActor): :type capabilities: string :param data: raw audio data to be played """ - source = self.gst_pipeline.get_by_name('source') caps = gst.caps_from_string(capabilities) buffer_ = gst.Buffer(buffer(data)) buffer_.set_caps(caps) - source.set_property('caps', caps) - source.emit('push-buffer', buffer_) + self._source.set_property('caps', caps) + self._source.emit('push-buffer', buffer_) def end_of_data_stream(self): """ @@ -130,7 +137,7 @@ class GStreamer(ThreadingActor): 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') + self._source.emit('end-of-stream') def get_position(self): """ @@ -138,10 +145,10 @@ class GStreamer(ThreadingActor): :rtype: int """ - if self.gst_pipeline.get_state()[1] == gst.STATE_NULL: + if self._pipeline.get_state()[1] == gst.STATE_NULL: return 0 try: - position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] + position = self._pipeline.query_position(gst.FORMAT_TIME)[0] return position // gst.MSECOND except gst.QueryError, e: logger.error('time_position failed: %s', e) @@ -155,10 +162,10 @@ class GStreamer(ThreadingActor): :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), + 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.gst_pipeline.get_state() # block until seek is done + self._pipeline.get_state() # block until seek is done return handeled def start_playback(self): @@ -199,7 +206,7 @@ class GStreamer(ThreadingActor): :type state_name: string :rtype: :class:`True` or :class:`False` """ - result = self.gst_pipeline.set_state(state) + result = self._pipeline.set_state(state) if result == gst.STATE_CHANGE_FAILURE: logger.warning('Setting GStreamer state to %s: failed', state.value_name) @@ -215,7 +222,7 @@ class GStreamer(ThreadingActor): :rtype: int in range [0..100] """ - return int(self.gst_volume.get_property('volume') * 100) + return int(self._volume.get_property('volume') * 100) def set_volume(self, volume): """ @@ -225,7 +232,7 @@ class GStreamer(ThreadingActor): :type volume: int :rtype: :class:`True` if successful, else :class:`False` """ - self.gst_volume.set_property('volume', volume / 100.0) + self._volume.set_property('volume', volume / 100.0) return True def set_metadata(self, track): @@ -244,4 +251,4 @@ class GStreamer(ThreadingActor): 'title': track.name, } logger.debug('Setting tags to: %s', tags) - self.gst_taginject.set_property('tags', tags) + self._taginject.set_property('tags', tags) From 8922ebe0aa155852559171aa09f649bd17326bec Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 5 May 2011 21:45:42 +0200 Subject: [PATCH 54/74] Log gst warnings as well --- mopidy/gstreamer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5b715175..5b9077a5 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -101,6 +101,9 @@ class GStreamer(ThreadingActor): 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) From f34ab8761ad57e10b52a7bf59ae0ee06ca9bf43c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 5 May 2011 21:50:40 +0200 Subject: [PATCH 55/74] Log async state changes --- mopidy/gstreamer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5b9077a5..79e87de4 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -214,6 +214,10 @@ class GStreamer(ThreadingActor): 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) From 49db06d249ba1ac8bbca52b4960023cd0299cb77 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 5 May 2011 22:28:27 +0200 Subject: [PATCH 56/74] No need for an instance of GStreamer before the actor is started --- mopidy/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/core.py b/mopidy/core.py index 4ca6ec29..0c341a1a 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -58,7 +58,7 @@ def setup_gobject_loop(): return gobject_loop def setup_gstreamer(): - return GStreamer().start().proxy() + return GStreamer.start().proxy() def setup_mixer(): return get_class(settings.MIXER).start().proxy() From d536990ad4907603d10ece1c77f52045275296f7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 5 May 2011 22:37:55 +0200 Subject: [PATCH 57/74] Update _set_state docstring --- mopidy/gstreamer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 79e87de4..7125efe9 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -205,8 +205,9 @@ class GStreamer(ThreadingActor): "READY" -> "NULL" "READY" -> "PAUSED" - :param state_name: NULL, READY, PAUSED, or PLAYING - :type state_name: string + :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) From df11f0523dc8bf6e169ad4b217f1824ea2bd9024 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 5 May 2011 23:57:20 +0200 Subject: [PATCH 58/74] Do not create Pykka proxies we do not use. The underlying actor may already be dead. --- mopidy/core.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index f1a9dc36..bf5c4c2a 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -52,25 +52,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() + get_class(settings.OUTPUT).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 From 9b0499bc1af169bfdf04945a158192610af1c51f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 5 May 2011 23:59:02 +0200 Subject: [PATCH 59/74] Update changelog with UnicodeDecodeError fix from bok --- docs/changes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index bf7efc5f..fdc18a95 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -20,6 +20,8 @@ This change log is used to track all major changes to Mopidy. :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. + 0.4.0 (2011-04-27) ================== From 79fc6a11da18043a3cb88060abb8afc89c96d8ee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 6 May 2011 00:01:03 +0200 Subject: [PATCH 60/74] Update changelog with removal of redundant Pykka proxy creation --- docs/changes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index fdc18a95..ef28b86b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,6 +22,10 @@ This change log is used to track all major changes to Mopidy. - Fix UnicodeDecodeError in MPD frontend on non-english locale. +- 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. + 0.4.0 (2011-04-27) ================== From eb13a8abd1a8abab99b050bcbc2129fc8e38e631 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 6 May 2011 00:14:09 +0200 Subject: [PATCH 61/74] Add credits and issue reference to changelog --- docs/changes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index ef28b86b..e40baffe 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -20,7 +20,8 @@ This change log is used to track all major changes to Mopidy. :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. +- 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, From ffb2985fda98e6d505be67e43147de55ecafb42c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 6 May 2011 00:21:24 +0200 Subject: [PATCH 62/74] Update changelog for 0.4.1 release --- docs/changes.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e40baffe..f86cef92 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,8 +5,12 @@ Changes This change log is used to track all major changes to Mopidy. -0.4.1 (in development) -====================== +0.4.1 (2011-05-06) +================== + +This is a bug fix release fixing audio problems on older GStreamer and some +minor bugs. + **Bugfixes** @@ -25,7 +29,10 @@ This change log is used to track all major changes to Mopidy. - 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. + 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) From 65db8c4a7bb5ef7975208e75dcac040f1d009906 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 6 May 2011 22:01:39 +0200 Subject: [PATCH 63/74] Kill off *_OUTPUT_OVERRIDEs in favour of just having CustomOutput to handle corner cases --- mopidy/outputs/__init__.py | 49 +++++++++++++++++++------------------- mopidy/settings.py | 31 ++++++------------------ mopidy/utils/settings.py | 3 ++- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index e3747463..5cdb2baa 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -78,27 +78,40 @@ class BaseOutput(object): element.set_property(key, value) +class CustomOutput(BaseOutput): + """ + Custom output for using alternate setups. + + This output is intended to handle to 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 + `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 `CUSTOM_OUTPUT` with a `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. - - 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' @@ -124,25 +137,13 @@ class ShoutcastOutput(BaseOutput): 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, diff --git a/mopidy/settings.py b/mopidy/settings.py index 78abb6b7..1aa4a630 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -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 @@ -54,13 +61,6 @@ FRONTENDS = ( u'mopidy.frontends.lastfm.LastfmFrontend', ) -#: Which GStreamer bin description to use in :class:`mopidy.outputs.LocalOutput`. -#: -#: Default:: -#: -#: LOCAL_OUTPUT_OVERRIDE = None -LOCAL_OUTPUT_OVERRIDE = None - #: Your `Last.fm `_ username. #: #: Used by :mod:`mopidy.frontends.lastfm`. @@ -221,23 +221,6 @@ SHOUTCAST_OUTPUT_MOUNT = u'/stream' #: SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' -#: Overrides to allow advanced setup of shoutcast. Using this settings implies -#: that all other SHOUTCAST_OUTPUT_* settings will be ignored. -#: -#: Examples: -#: -#: ``vorbisenc ! oggmux ! shout2send mount=/stream port=8000`` -#: Encode with vorbis and use ogg mux. -#: ``lame bitrate=320 ! shout2send mount=/stream port=8000`` -#: Encode with lame to bitrate=320. -#: -#: For all options see gst-inspect-0.10 lame, vorbisenc and shout2send. -#: -#: Default:: -#: -#: SHOUTCAST_OUTPUT_OVERRIDE = None -SHOUTCAST_OUTPUT_OVERRIDE = None - #: Path to the Spotify cache. #: #: Used by :mod:`mopidy.backends.spotify`. diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 0dc6b4cb..7f541c21 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -97,8 +97,9 @@ def validate_settings(defaults, settings): 'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME', 'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT', 'FRONTEND': 'FRONTENDS', - 'GSTREAMER_AUDIO_SINK': 'LOCAL_OUTPUT_OVERRIDE', + '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, From ebee9620201ad9609e8b6d6a5322672d4bd52479 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 May 2011 01:38:04 +0200 Subject: [PATCH 64/74] Test that --help returns the options we expect it to --- tests/help_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/help_test.py diff --git a/tests/help_test.py b/tests/help_test.py new file mode 100644 index 00000000..a64877c6 --- /dev/null +++ b/tests/help_test.py @@ -0,0 +1,19 @@ +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_('--quiet' in output) + self.assert_('--verbose' in output) + self.assert_('--save-debug-log' in output) + self.assert_('--list-settings' in output) From 644c87128b1fef5f5b6568b54d176a192d72eca0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 May 2011 01:41:30 +0200 Subject: [PATCH 65/74] Naive workaround for #95 --- mopidy/core.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mopidy/core.py b/mopidy/core.py index d0f2a668..cd49dfa1 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,7 +1,17 @@ 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``. +gstreamer_args = [arg for arg in sys.argv[1:] if arg.startswith('--gst')] +mopidy_args = [arg for arg in sys.argv[1:] if not arg.startswith('--gst')] +sys.argv[1:] = gstreamer_args + from pykka.registry import ActorRegistry from mopidy import get_version, settings, OptionalDependencyError @@ -45,7 +55,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/') From 3d705b3e022171d5c1148d9d3826f73b14ff1edc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 8 May 2011 17:01:05 +0200 Subject: [PATCH 66/74] restify docstrings of CustomOutput --- mopidy/outputs/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index 5cdb2baa..d7aacaab 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -82,12 +82,12 @@ class CustomOutput(BaseOutput): """ Custom output for using alternate setups. - This output is intended to handle to main cases: + 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 - `CUSTOM_OUTPUT` to `alsasink` and you are good to go. Some possible - sinks include: + :attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good + to go. Some possible sinks include: - alsasink - osssink @@ -95,8 +95,8 @@ class CustomOutput(BaseOutput): - ...and many more 2. Advanced setups that require complete control of the output bin. For - these cases setup `CUSTOM_OUTPUT` with a `gst-launch` compatible string - describing the target setup. + these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a + :cmd:`gst-launch` compatible string describing the target setup. """ def describe_bin(self): From 10e1f2abab7b47047f88aeb67e8ebda20f1b637d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 8 May 2011 19:53:09 +0200 Subject: [PATCH 67/74] Switch to using gst.TAG_* in scanner --- mopidy/scanner.py | 48 +++++++++++++++++------------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 93224331..c603c578 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -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)] From f035ea31ef45e66324e01944be974de60256b79e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 9 May 2011 23:13:06 +0200 Subject: [PATCH 68/74] Allow GStreamer to process --help-gst --- mopidy/core.py | 6 ++++-- tests/help_test.py | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index cd49dfa1..61a47c4b 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -8,8 +8,10 @@ import time # 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``. -gstreamer_args = [arg for arg in sys.argv[1:] if arg.startswith('--gst')] -mopidy_args = [arg for arg in sys.argv[1:] if not arg.startswith('--gst')] +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 diff --git a/tests/help_test.py b/tests/help_test.py index a64877c6..502673be 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -17,3 +17,10 @@ class HelpTest(unittest.TestCase): 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) From b796661dc1cfd28853cb8bbf24485dbde8972c1a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 9 May 2011 23:16:45 +0200 Subject: [PATCH 69/74] List --help-gst in Mopidy's --help listing --- mopidy/core.py | 3 +++ tests/help_test.py | 1 + 2 files changed, 4 insertions(+) diff --git a/mopidy/core.py b/mopidy/core.py index 61a47c4b..e510b698 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -45,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)') diff --git a/tests/help_test.py b/tests/help_test.py index 502673be..dccccc9c 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -13,6 +13,7 @@ class HelpTest(unittest.TestCase): 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) From b6c1f5fc1518613aacfe3a5f6db2347a8a04f288 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 9 May 2011 23:25:29 +0200 Subject: [PATCH 70/74] Update changelog with fix for #95 --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index cca12aab..f8f01129 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,9 @@ No description 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) ================== From 3dd9fc8564917eefef3d53f0d0b0e4b078a3d7b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 9 May 2011 23:37:56 +0200 Subject: [PATCH 71/74] Should be :command:, not :cmd: --- mopidy/outputs/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/outputs/__init__.py b/mopidy/outputs/__init__.py index d7aacaab..d1512617 100644 --- a/mopidy/outputs/__init__.py +++ b/mopidy/outputs/__init__.py @@ -96,7 +96,7 @@ class CustomOutput(BaseOutput): 2. Advanced setups that require complete control of the output bin. For these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a - :cmd:`gst-launch` compatible string describing the target setup. + :command:`gst-launch` compatible string describing the target setup. """ def describe_bin(self): From 9d3a5aa054b1f309266688bea5db0efc58241e47 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 9 May 2011 23:47:11 +0200 Subject: [PATCH 72/74] Fix error due to including BaseOutput twice in the docs --- docs/api/outputs.rst | 3 +++ docs/modules/outputs.rst | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index b96c909e..062eabdd 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -1,3 +1,5 @@ +.. _output-api: + ********** Output API ********** @@ -11,6 +13,7 @@ Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way. Output implementations ====================== +* :class:`mopidy.outputs.CustomOutput` * :class:`mopidy.outputs.LocalOutput` * :class:`mopidy.outputs.NullOutput` * :class:`mopidy.outputs.ShoutcastOutput` diff --git a/docs/modules/outputs.rst b/docs/modules/outputs.rst index 0a6986dd..7da29fbc 100644 --- a/docs/modules/outputs.rst +++ b/docs/modules/outputs.rst @@ -2,8 +2,14 @@ :mod:`mopidy.outputs` -- GStreamer audio outputs ************************************************ +The following GStreamer audio outputs implements the :ref:`output-api`. + .. inheritance-diagram:: mopidy.outputs -.. automodule:: mopidy.outputs - :synopsis: GStreamer audio outputs - :members: +.. autoclass:: mopidy.outputs.CustomOutput + +.. autoclass:: mopidy.outputs.LocalOutput + +.. autoclass:: mopidy.outputs.NullOutput + +.. autoclass:: mopidy.outputs.ShoutcastOutput From 1c83fe236dfb2531fef5bee31227c9d124f7738f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 10 May 2011 17:37:04 +0200 Subject: [PATCH 73/74] Remove unused import --- mopidy/utils/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 531b68b6..03b85b48 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -1,6 +1,5 @@ import logging import logging.handlers -import platform from mopidy import get_version, get_platform, get_python, settings From d98e218baa38749b6633f4fa2f2af3a9581567f7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 10 May 2011 22:24:23 +0200 Subject: [PATCH 74/74] Explain a bit more about why prepare_change is needed --- mopidy/gstreamer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 7125efe9..3c8941fa 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -184,7 +184,9 @@ class GStreamer(ThreadingActor): 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. + 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)