From 87ce7bbe115aabd107149e4730b7c0a1e83b6b78 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 14 Nov 2012 01:49:05 +0100 Subject: [PATCH] audio: Maintain state and trigger events based on GStreamer state changes --- mopidy/audio/actor.py | 44 +++++++++++++++++++++++++++++++---- tests/audio/actor_test.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index a7b4e8d8..49794c76 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -13,6 +13,7 @@ from mopidy import settings from mopidy.utils import process from . import mixers +from .constants import PlaybackState from .listener import AudioListener logger = logging.getLogger('mopidy.audio') @@ -29,9 +30,11 @@ class Audio(pykka.ThreadingActor): - :attr:`mopidy.settings.OUTPUT` - :attr:`mopidy.settings.MIXER` - :attr:`mopidy.settings.MIXER_TRACK` - """ + #: The GStreamer state mapped to :class:`mopidy.audio.PlaybackState` + state = PlaybackState.STOPPED + def __init__(self): super(Audio, self).__init__() @@ -160,8 +163,12 @@ class Audio(pykka.ThreadingActor): bus.remove_signal_watch() def _on_message(self, bus, message): - if message.type == gst.MESSAGE_EOS: - self._trigger_reached_end_of_stream_event() + if (message.type == gst.MESSAGE_STATE_CHANGED + 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: error, debug = message.parse_error() logger.error('%s %s', error, debug) @@ -170,8 +177,35 @@ class Audio(pykka.ThreadingActor): error, debug = message.parse_warning() logger.warning('%s %s', error, debug) - def _trigger_reached_end_of_stream_event(self): - logger.debug('Triggering reached end of stream event') + def _on_playbin_state_changed(self, old_state, new_state, pending_state): + if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL: + # XXX: We're not called on the last state cheng 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 state_changed event') + 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') def set_uri(self, uri): diff --git a/tests/audio/actor_test.py b/tests/audio/actor_test.py index b8b65e83..64666d9d 100644 --- a/tests/audio/actor_test.py +++ b/tests/audio/actor_test.py @@ -1,5 +1,9 @@ from __future__ import unicode_literals +import pygst +pygst.require('0.10') +import gst + from mopidy import audio, settings from mopidy.utils.path import path_to_uri @@ -63,3 +67,48 @@ class AudioTest(unittest.TestCase): @unittest.SkipTest def test_invalid_output_raises_error(self): 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)