Merge pull request #240 from jodal/feature/audio-state-events

Audio state events
This commit is contained in:
Thomas Adamcik 2012-11-14 15:00:09 -08:00
commit 4d4c560882
9 changed files with 153 additions and 21 deletions

View File

@ -3,3 +3,4 @@ from __future__ import unicode_literals
# flake8: noqa # flake8: noqa
from .actor import Audio from .actor import Audio
from .listener import AudioListener from .listener import AudioListener
from .constants import PlaybackState

View File

@ -13,6 +13,7 @@ from mopidy import settings
from mopidy.utils import process from mopidy.utils import process
from . import mixers from . import mixers
from .constants import PlaybackState
from .listener import AudioListener from .listener import AudioListener
logger = logging.getLogger('mopidy.audio') logger = logging.getLogger('mopidy.audio')
@ -29,9 +30,11 @@ class Audio(pykka.ThreadingActor):
- :attr:`mopidy.settings.OUTPUT` - :attr:`mopidy.settings.OUTPUT`
- :attr:`mopidy.settings.MIXER` - :attr:`mopidy.settings.MIXER`
- :attr:`mopidy.settings.MIXER_TRACK` - :attr:`mopidy.settings.MIXER_TRACK`
""" """
#: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState`
state = PlaybackState.STOPPED
def __init__(self): def __init__(self):
super(Audio, self).__init__() super(Audio, self).__init__()
@ -164,8 +167,12 @@ class Audio(pykka.ThreadingActor):
bus.remove_signal_watch() bus.remove_signal_watch()
def _on_message(self, bus, message): def _on_message(self, bus, message):
if message.type == gst.MESSAGE_EOS: if (message.type == gst.MESSAGE_STATE_CHANGED
self._trigger_reached_end_of_stream_event() and message.src == self._playbin):
old_state, new_state, pending_state = message.parse_state_changed()
self._on_playbin_state_changed(old_state, new_state, pending_state)
elif message.type == gst.MESSAGE_EOS:
self._on_end_of_stream()
elif message.type == gst.MESSAGE_ERROR: elif message.type == gst.MESSAGE_ERROR:
error, debug = message.parse_error() error, debug = message.parse_error()
logger.error('%s %s', error, debug) logger.error('%s %s', error, debug)
@ -174,8 +181,37 @@ class Audio(pykka.ThreadingActor):
error, debug = message.parse_warning() error, debug = message.parse_warning()
logger.warning('%s %s', error, debug) logger.warning('%s %s', error, debug)
def _trigger_reached_end_of_stream_event(self): def _on_playbin_state_changed(self, old_state, new_state, pending_state):
logger.debug('Triggering reached end of stream event') if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL:
# XXX: We're not called on the last state change when going down to
# NULL, so we rewrite the second to last call to get the expected
# behavior.
new_state = gst.STATE_NULL
pending_state = gst.STATE_VOID_PENDING
if pending_state != gst.STATE_VOID_PENDING:
return # Ignore intermediate state changes
if new_state == gst.STATE_READY:
return # Ignore READY state as it's GStreamer specific
if new_state == gst.STATE_PLAYING:
new_state = PlaybackState.PLAYING
elif new_state == gst.STATE_PAUSED:
new_state = PlaybackState.PAUSED
elif new_state == gst.STATE_NULL:
new_state = PlaybackState.STOPPED
old_state, self.state = self.state, new_state
logger.debug(
'Triggering event: state_changed(old_state=%s, new_state=%s)',
old_state, new_state)
AudioListener.send('state_changed',
old_state=old_state, new_state=new_state)
def _on_end_of_stream(self):
logger.debug('Triggering reached_end_of_stream event')
AudioListener.send('reached_end_of_stream') AudioListener.send('reached_end_of_stream')
def set_uri(self, uri): def set_uri(self, uri):

16
mopidy/audio/constants.py Normal file
View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
class PlaybackState(object):
"""
Enum of playback states.
"""
#: Constant representing the paused state.
PAUSED = 'paused'
#: Constant representing the playing state.
PLAYING = 'playing'
#: Constant representing the stopped state.
STOPPED = 'stopped'

View File

@ -28,3 +28,18 @@ class AudioListener(object):
*MAY* be implemented by actor. *MAY* be implemented by actor.
""" """
pass pass
def state_changed(self, old_state, new_state):
"""
Called after the playback state have changed.
Will be called for both immediate and async state changes in GStreamer.
*MAY* be implemented by actor.
:param old_state: the state before the change
:type old_state: string from :class:`mopidy.core.PlaybackState` field
:param new_state: the state after the change
:type new_state: string from :class:`mopidy.core.PlaybackState` field
"""
pass

View File

@ -4,7 +4,7 @@ import itertools
import pykka import pykka
from mopidy.audio import AudioListener from mopidy.audio import AudioListener, PlaybackState
from .library import LibraryController from .library import LibraryController
from .playback import PlaybackController from .playback import PlaybackController
@ -55,6 +55,18 @@ class Core(pykka.ThreadingActor, AudioListener):
def reached_end_of_stream(self): def reached_end_of_stream(self):
self.playback.on_end_of_track() self.playback.on_end_of_track()
def state_changed(self, old_state, new_state):
# XXX: This is a temporary fix for issue #232 while we wait for a more
# permanent solution with the implementation of issue #234. When the
# Spotify play token is lost, the Spotify backend pauses audio
# playback, but mopidy.core doesn't know this, so we need to update
# mopidy.core's state to match the actual state in mopidy.audio. If we
# don't do this, clients will think that we're still playing.
if (new_state == PlaybackState.PAUSED
and self.playback.state != PlaybackState.PAUSED):
self.playback.state = new_state
self.playback._trigger_track_playback_paused()
class Backends(list): class Backends(list):
def __init__(self, backends): def __init__(self, backends):

View File

@ -4,6 +4,8 @@ import logging
import random import random
import urlparse import urlparse
from mopidy.audio import PlaybackState
from . import listener from . import listener
@ -24,21 +26,6 @@ def option_wrapper(name, default):
return property(get_option, set_option) return property(get_option, set_option)
class PlaybackState(object):
"""
Enum of playback states.
"""
#: Constant representing the paused state.
PAUSED = 'paused'
#: Constant representing the playing state.
PLAYING = 'playing'
#: Constant representing the stopped state.
STOPPED = 'stopped'
class PlaybackController(object): class PlaybackController(object):
# pylint: disable = R0902 # pylint: disable = R0902
# Too many instance attributes # Too many instance attributes

0
tests/audio/__init__.py Normal file
View File

View File

@ -1,5 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
from mopidy import audio, settings from mopidy import audio, settings
from mopidy.utils.path import path_to_uri from mopidy.utils.path import path_to_uri
@ -63,3 +67,48 @@ class AudioTest(unittest.TestCase):
@unittest.SkipTest @unittest.SkipTest
def test_invalid_output_raises_error(self): def test_invalid_output_raises_error(self):
pass # TODO pass # TODO
class AudioStateTest(unittest.TestCase):
def setUp(self):
self.audio = audio.Audio()
def test_state_starts_as_stopped(self):
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_does_not_change_when_in_gst_ready_state(self):
self.audio._on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_changes_from_stopped_to_playing_on_play(self):
self.audio._on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING)
self.audio._on_playbin_state_changed(
gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING)
self.audio._on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state)
def test_state_changes_from_playing_to_paused_on_pause(self):
self.audio.state = audio.PlaybackState.PLAYING
self.audio._on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state)
def test_state_changes_from_playing_to_stopped_on_stop(self):
self.audio.state = audio.PlaybackState.PLAYING
self.audio._on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL)
self.audio._on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL)
# We never get the following call, so the logic must work without it
#self.audio._on_playbin_state_changed(
# gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)

View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
from mopidy import audio
from tests import unittest
class AudioListenerTest(unittest.TestCase):
def setUp(self):
self.listener = audio.AudioListener()
def test_listener_has_default_impl_for_reached_end_of_stream(self):
self.listener.reached_end_of_stream()
def test_listener_has_default_impl_for_state_changed(self):
self.listener.state_changed(None, None)