Skip to content

Commit

Permalink
Release v1.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
jodal committed Apr 23, 2015
2 parents c4fc33e + 9c793a3 commit b000040
Show file tree
Hide file tree
Showing 15 changed files with 231 additions and 134 deletions.
29 changes: 29 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ Changelog
This changelog is used to track all major changes to Mopidy.


v1.0.1 (2015-04-23)
===================

Bug fix release.

- Core: Make the new history controller available for use. (Fixes: :js:`6`)

- Audio: Software volume control has been reworked to greatly reduce the delay
between changing the volume and the change taking effect. (Fixes:
:issue:`1097`, PR: :issue:`1101`)

- Audio: As a side effect of the previous bug fix, software volume is no longer
tied to the PulseAudio application volume when using ``pulsesink``. This
behavior was confusing for many users and doesn't work well with the plans
for multiple outputs.

- Audio: Update scanner to decode all media it finds. This should fix cases
where the scanner hangs on non-audio files like video. The scanner will now
also let us know if we found any decodeable audio. (Fixes: :issue:`726`, PR:
issue:`1124`)

- HTTP: Fix threading bug that would cause duplicate delivery of WS messages.
(PR: :issue:`1127`)

- MPD: Fix case where a playlist that is present in both browse and as a listed
playlist breaks the MPD frontend protocol output. (Fixes :issue:`1120`, PR:
:issue:`1142`)


v1.0.0 (2015-03-25)
===================

Expand Down
2 changes: 1 addition & 1 deletion mopidy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@
warnings.filterwarnings('ignore', 'could not open display')


__version__ = '1.0.0'
__version__ = '1.0.1'
108 changes: 45 additions & 63 deletions mopidy/audio/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils
import gst.pbutils # noqa

import pykka

Expand All @@ -34,25 +34,6 @@
gst.STATE_PAUSED: PlaybackState.PAUSED,
gst.STATE_NULL: PlaybackState.STOPPED}

MB = 1 << 20

# GST_PLAY_FLAG_VIDEO (1<<0)
# GST_PLAY_FLAG_AUDIO (1<<1)
# GST_PLAY_FLAG_TEXT (1<<2)
# GST_PLAY_FLAG_VIS (1<<3)
# GST_PLAY_FLAG_SOFT_VOLUME (1<<4)
# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5)
# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6)
# GST_PLAY_FLAG_DOWNLOAD (1<<7)
# GST_PLAY_FLAG_BUFFERING (1<<8)
# GST_PLAY_FLAG_DEINTERLACE (1<<9)
# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10)

# Default flags to use for playbin: AUDIO, SOFT_VOLUME
# TODO: consider removing soft volume when we do multi outputs and handling it
# ourselves.
PLAYBIN_FLAGS = (1 << 1) | (1 << 4)


class _Signals(object):
"""Helper for tracking gobject signal registrations"""
Expand Down Expand Up @@ -114,7 +95,7 @@ def configure(self, source):
source.set_property('caps', self._caps)
source.set_property('format', b'time')
source.set_property('stream-type', b'seekable')
source.set_property('max-bytes', 1 * MB)
source.set_property('max-bytes', 1 << 20) # 1MB
source.set_property('min-percent', 50)

if self._need_data_callback:
Expand Down Expand Up @@ -152,26 +133,12 @@ def _on_signal(self, element, clocktime, func):
# TODO: expose this as a property on audio when #790 gets further along.
class _Outputs(gst.Bin):
def __init__(self):
gst.Bin.__init__(self)
gst.Bin.__init__(self, 'outputs')

self._tee = gst.element_factory_make('tee')
self.add(self._tee)

# Queue element to buy us time between the about to finish event and
# the actual switch, i.e. about to switch can block for longer thanks
# to this queue.
# TODO: make the min-max values a setting?
# TODO: this does not belong in this class.
queue = gst.element_factory_make('queue')
queue.set_property('max-size-buffers', 0)
queue.set_property('max-size-bytes', 0)
queue.set_property('max-size-time', 5 * gst.SECOND)
queue.set_property('min-threshold-time', 3 * gst.SECOND)
self.add(queue)

queue.link(self._tee)

ghost_pad = gst.GhostPad('sink', queue.get_pad('sink'))
ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink'))
self.add_pad(ghost_pad)

# Add an always connected fakesink which respects the clock so the tee
Expand All @@ -195,7 +162,9 @@ def add_output(self, description):

def _add(self, element):
# All tee branches need a queue in front of them.
# But keep the queue short so the volume change isn't to slow:
queue = gst.element_factory_make('queue')
queue.set_property('max-size-buffers', 5)
self.add(element)
self.add(queue)
queue.link(element)
Expand All @@ -214,10 +183,6 @@ def __init__(self, mixer):

def setup(self, element, mixer_ref):
self._element = element

self._signals.connect(element, 'notify::volume', self._volume_changed)
self._signals.connect(element, 'notify::mute', self._mute_changed)

self._mixer.setup(mixer_ref)

def teardown(self):
Expand All @@ -229,24 +194,16 @@ def get_volume(self):

def set_volume(self, volume):
self._element.set_property('volume', volume / 100.0)
self._mixer.trigger_volume_changed(volume)

def get_mute(self):
return self._element.get_property('mute')

def set_mute(self, mute):
return self._element.set_property('mute', bool(mute))

def _volume_changed(self, element, property_):
old_volume, self._last_volume = self._last_volume, self.get_volume()
if old_volume != self._last_volume:
gst_logger.debug('Notify volume: %s', self._last_volume / 100.0)
self._mixer.trigger_volume_changed(self._last_volume)

def _mute_changed(self, element, property_):
old_mute, self._last_mute = self._last_mute, self.get_mute()
if old_mute != self._last_mute:
gst_logger.debug('Notify mute: %s', self._last_mute)
self._mixer.trigger_mute_changed(self._last_mute)
result = self._element.set_property('mute', bool(mute))
if result:
self._mixer.trigger_mute_changed(bool(mute))
return result


class _Handler(object):
Expand Down Expand Up @@ -451,8 +408,8 @@ def on_start(self):
try:
self._setup_preferences()
self._setup_playbin()
self._setup_output()
self._setup_mixer()
self._setup_outputs()
self._setup_audio_sink()
except gobject.GError as ex:
logger.exception(ex)
process.exit_process()
Expand All @@ -472,11 +429,11 @@ def _setup_preferences(self):

def _setup_playbin(self):
playbin = gst.element_factory_make('playbin2')
playbin.set_property('flags', PLAYBIN_FLAGS)
playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO

# TODO: turn into config values...
playbin.set_property('buffer-size', 2 * 1024 * 1024)
playbin.set_property('buffer-duration', 2 * gst.SECOND)
playbin.set_property('buffer-size', 5 << 20) # 5MB
playbin.set_property('buffer-duration', 5 * gst.SECOND)

self._signals.connect(playbin, 'source-setup', self._on_source_setup)
self._signals.connect(playbin, 'about-to-finish',
Expand All @@ -492,7 +449,7 @@ def _teardown_playbin(self):
self._signals.disconnect(self._playbin, 'source-setup')
self._playbin.set_state(gst.STATE_NULL)

def _setup_output(self):
def _setup_outputs(self):
# We don't want to use outputs for regular testing, so just install
# an unsynced fakesink when someone asks for a 'testoutput'.
if self._config['audio']['output'] == 'testoutput':
Expand All @@ -505,11 +462,36 @@ def _setup_output(self):
process.exit_process() # TODO: move this up the chain

self._handler.setup_event_handling(self._outputs.get_pad('sink'))
self._playbin.set_property('audio-sink', self._outputs)

def _setup_mixer(self):
def _setup_audio_sink(self):
audio_sink = gst.Bin('audio-sink')

# Queue element to buy us time between the about to finish event and
# the actual switch, i.e. about to switch can block for longer thanks
# to this queue.
# TODO: make the min-max values a setting?
queue = gst.element_factory_make('queue')
queue.set_property('max-size-buffers', 0)
queue.set_property('max-size-bytes', 0)
queue.set_property('max-size-time', 3 * gst.SECOND)
queue.set_property('min-threshold-time', 1 * gst.SECOND)

audio_sink.add(queue)
audio_sink.add(self._outputs)

if self.mixer:
self.mixer.setup(self._playbin, self.actor_ref.proxy().mixer)
volume = gst.element_factory_make('volume')
audio_sink.add(volume)
queue.link(volume)
volume.link(self._outputs)
self.mixer.setup(volume, self.actor_ref.proxy().mixer)
else:
queue.link(self._outputs)

ghost_pad = gst.GhostPad('sink', queue.get_pad('sink'))
audio_sink.add_pad(ghost_pad)

self._playbin.set_property('audio-sink', audio_sink)

def _teardown_mixer(self):
if self.mixer:
Expand Down
75 changes: 56 additions & 19 deletions mopidy/audio/scan.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import absolute_import, division, unicode_literals
from __future__ import (
absolute_import, division, print_function, unicode_literals)

import collections

import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils
import gst.pbutils # noqa

from mopidy import exceptions
from mopidy.audio import utils
Expand All @@ -14,7 +15,7 @@
_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description

_Result = collections.namedtuple(
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime'))
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable'))

_RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float')

Expand Down Expand Up @@ -51,14 +52,14 @@ def scan(self, uri):

try:
_start_pipeline(pipeline)
tags, mime = _process(pipeline, self._timeout_ms)
tags, mime, have_audio = _process(pipeline, self._timeout_ms)
duration = _query_duration(pipeline)
seekable = _query_seekable(pipeline)
finally:
pipeline.set_state(gst.STATE_NULL)
del pipeline

return _Result(uri, tags, duration, seekable, mime)
return _Result(uri, tags, duration, seekable, mime, have_audio)


# Turns out it's _much_ faster to just create a new pipeline for every as
Expand All @@ -70,30 +71,38 @@ def _setup_pipeline(uri, proxy_config=None):

typefind = gst.element_factory_make('typefind')
decodebin = gst.element_factory_make('decodebin2')
sink = gst.element_factory_make('fakesink')

pipeline = gst.element_factory_make('pipeline')
pipeline.add_many(src, typefind, decodebin, sink)
pipeline.add_many(src, typefind, decodebin)
gst.element_link_many(src, typefind, decodebin)

if proxy_config:
utils.setup_proxy(src, proxy_config)

decodebin.set_property('caps', _RAW_AUDIO)
decodebin.connect('pad-added', _pad_added, sink)
typefind.connect('have-type', _have_type, decodebin)
decodebin.connect('pad-added', _pad_added, pipeline)

return pipeline


def _have_type(element, probability, caps, decodebin):
decodebin.set_property('sink-caps', caps)
msg = gst.message_new_application(element, caps.get_structure(0))
element.get_bus().post(msg)
struct = gst.Structure('have-type')
struct['caps'] = caps.get_structure(0)
element.get_bus().post(gst.message_new_application(element, struct))


def _pad_added(element, pad, pipeline):
sink = gst.element_factory_make('fakesink')
sink.set_property('sync', False)

pipeline.add(sink)
sink.sync_state_with_parent()
pad.link(sink.get_pad('sink'))

def _pad_added(element, pad, sink):
return pad.link(sink.get_pad('sink'))
if pad.get_caps().is_subset(_RAW_AUDIO):
struct = gst.Structure('have-audio')
element.get_bus().post(gst.message_new_application(element, struct))


def _start_pipeline(pipeline):
Expand Down Expand Up @@ -123,7 +132,7 @@ def _process(pipeline, timeout_ms):
clock = pipeline.get_clock()
bus = pipeline.get_bus()
timeout = timeout_ms * gst.MSECOND
tags, mime, missing_description = {}, None, None
tags, mime, have_audio, missing_description = {}, None, False, None

types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR
| gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG)
Expand All @@ -139,19 +148,22 @@ def _process(pipeline, timeout_ms):
missing_description = encoding.locale_decode(
_missing_plugin_desc(message))
elif message.type == gst.MESSAGE_APPLICATION:
mime = message.structure.get_name()
if mime.startswith('text/') or mime == 'application/xml':
return tags, mime
if message.structure.get_name() == 'have-type':
mime = message.structure['caps'].get_name()
if mime.startswith('text/') or mime == 'application/xml':
return tags, mime, have_audio
elif message.structure.get_name() == 'have-audio':
have_audio = True
elif message.type == gst.MESSAGE_ERROR:
error = encoding.locale_decode(message.parse_error()[0])
if missing_description:
error = '%s (%s)' % (missing_description, error)
raise exceptions.ScannerError(error)
elif message.type == gst.MESSAGE_EOS:
return tags, mime
return tags, mime, have_audio
elif message.type == gst.MESSAGE_ASYNC_DONE:
if message.src == pipeline:
return tags, mime
return tags, mime, have_audio
elif message.type == gst.MESSAGE_TAG:
taglist = message.parse_tag()
# Note that this will only keep the last tag.
Expand All @@ -160,3 +172,28 @@ def _process(pipeline, timeout_ms):
timeout -= clock.get_time() - start

raise exceptions.ScannerError('Timeout after %dms' % timeout_ms)


if __name__ == '__main__':
import os
import sys

import gobject

from mopidy.utils import path

gobject.threads_init()

scanner = Scanner(5000)
for uri in sys.argv[1:]:
if not gst.uri_is_valid(uri):
uri = path.path_to_uri(os.path.abspath(uri))
try:
result = scanner.scan(uri)
for key in ('uri', 'mime', 'duration', 'playable', 'seekable'):
print('%-20s %s' % (key, getattr(result, key)))
print('tags')
for tag, value in result.tags.items():
print('%-20s %s' % (tag, value))
except exceptions.ScannerError as error:
print('%s: %s' % (uri, error))

0 comments on commit b000040

Please sign in to comment.