From ae0d1937085329138f391a9c9d70e4be85d59251 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 11 May 2010 22:05:38 -0500 Subject: [PATCH] Integrating and testing of playback --- src/__init__.py | 2 +- src/index.py | 36 +++++++++-- src/mormonchannel_gtk.py | 1 + src/playcontrol.py | 65 ++++++++++---------- src/player.py | 58 ++++++++++++------ src/presenter.py | 2 +- src/stream.py | 144 +++++++++++++++++++++++++++++++++++++++++++ src/windows.py | 152 +++++++++++++++++++++++++++++++--------------- 8 files changed, 354 insertions(+), 106 deletions(-) create mode 100644 src/stream.py diff --git a/src/__init__.py b/src/__init__.py index 7132119..d3a51da 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python -import utils +import util diff --git a/src/index.py b/src/index.py index e8deff6..2ffb6c2 100644 --- a/src/index.py +++ b/src/index.py @@ -120,6 +120,10 @@ class Node(object): def get_properties(self): return self._data + @property + def title(self): + raise NotImplementedError("On %s" % type(self)) + def is_leaf(self): raise NotImplementedError("") @@ -193,11 +197,11 @@ class LeafNode(Node): raise NotImplementedError("On %s" % type(self)) @property - def title(self): + def subtitle(self): raise NotImplementedError("On %s" % type(self)) @property - def subtitle(self): + def uri(self): raise NotImplementedError("On %s" % type(self)) def _get_children(self, on_success, on_error): @@ -209,6 +213,10 @@ class RadioNode(ParentNode): def __init__(self, connection): ParentNode.__init__(self, connection, None, {}) + @property + def title(self): + return "Radio" + def _get_func(self): return "get_radio_channels", (), {} @@ -235,6 +243,10 @@ class RadioChannelNode(LeafNode): def subtitle(self): return "" + @property + def uri(self): + return self._data["url"] + def get_programming(self, date, on_success, on_error): date = date.strftime("%Y-%m-%d") try: @@ -287,6 +299,10 @@ class ConferencesNode(ParentNode): ParentNode.__init__(self, connection, None, {}) self._langId = langId + @property + def title(self): + return "Conferences" + def _get_func(self): return "get_conferences", (self._langId, ), {} @@ -299,6 +315,10 @@ class ConferenceNode(ParentNode): def __init__(self, connection, parent, data): ParentNode.__init__(self, connection, parent, data) + @property + def title(self): + return self._data["title"] + def _get_func(self): return "get_conference_sessions", (self._data["id"], ), {} @@ -311,6 +331,10 @@ class SessionNode(ParentNode): def __init__(self, connection, parent, data): ParentNode.__init__(self, connection, parent, data) + @property + def title(self): + return self._data["title"] + def _get_func(self): return "get_conference_talks", (self._data["id"], ), {} @@ -329,8 +353,12 @@ class TalkNode(LeafNode): @property def title(self): - return self._date["title"] + return self._data["title"] @property def subtitle(self): - return self._date["speaker"] + return self._data["speaker"] + + @property + def uri(self): + return self._data["url"] diff --git a/src/mormonchannel_gtk.py b/src/mormonchannel_gtk.py index 6484fff..85d068e 100755 --- a/src/mormonchannel_gtk.py +++ b/src/mormonchannel_gtk.py @@ -4,6 +4,7 @@ """ @todo Reverse order option. Toggle between playing ascending/descending chronological order @todo Track recent +@bug All connect's need disconnects or else we will leak a bunch of objects """ from __future__ import with_statement diff --git a/src/playcontrol.py b/src/playcontrol.py index 9f55bd9..8576237 100644 --- a/src/playcontrol.py +++ b/src/playcontrol.py @@ -45,7 +45,7 @@ class NavControl(gobject.GObject): self._controlBox.connect("action", self._on_nav_action) self._controlBox.connect("navigating", self._on_navigating) - self._titleButton = gtk.Label("") + self._titleButton = gtk.Label(self._player.title) self._displayBox = presenter.NavigationBox() self._displayBox.toplevel.add(self._titleButton) @@ -57,8 +57,25 @@ class NavControl(gobject.GObject): self._layout.pack_start(self._displayBox.toplevel, True, True) def refresh(self): - if not self._player.title: + self._titleButton.set_label(self._player.title) + self._set_context(self._player.state) + + def _set_context(self, state): + if state == self._player.STATE_PLAY: + stateImage = self._store.STORE_LOOKUP["small_pause"] + self._store.set_image_from_store(self._controlButton, stateImage) + self.toplevel.show() + elif state == self._player.STATE_PAUSE: + stateImage = self._store.STORE_LOOKUP["small_play"] + self._store.set_image_from_store(self._controlButton, stateImage) + self.toplevel.show() + elif state == self._player.STATE_STOP: + self._titleButton.set_label("") self.toplevel.hide() + else: + _moduleLogger.info("Unhandled player state %s" % state) + stateImage = self._store.STORE_LOOKUP["small_pause"] + self._store.set_image_from_store(self._controlButton, stateImage) @property def toplevel(self): @@ -72,45 +89,32 @@ class NavControl(gobject.GObject): if self._controlBox.is_active() or self._displayBox.is_active(): return - if newState == "play": - stateImage = self._store.STORE_LOOKUP["small_play"] - self._store.set_image_from_store(self._controlButton, stateImage) - self.toplevel.show() - elif newState == "pause": - stateImage = self._store.STORE_LOOKUP["small_pause"] - self._store.set_image_from_store(self._controlButton, stateImage) - self.toplevel.show() - elif newState == "stop": - self._titleButton.set_label("") - self.toplevel.hide() - else: - _moduleLogger.info("Unhandled player state %s" % newState) - stateImage = self._store.STORE_LOOKUP["small_pause"] - self._store.set_image_from_store(self._controlButton, stateImage) + self._set_context(newState) @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, newState): + def _on_player_title_change(self, player, node): + _moduleLogger.info("Title change: %s" % self._player.title) self._titleButton.set_label(self._player.title) @misc_utils.log_exception(_moduleLogger) def _on_navigating(self, widget, navState): if navState == "down": imageName = "small_home" - elif navState == "up": - imageName = "small_play" elif navState == "clicking" or not self._player.can_navigate: if widget is self._controlBox: if self._player.state == "play": - imageName = "small_pause" - else: - imageName = "small_play" - elif widget is self._displayBox: - if self._player.state == "play": imageName = "small_play" else: imageName = "small_pause" + elif widget is self._displayBox: + if self._player.state == self._player.STATE_PLAY: + imageName = "small_pause" + else: + imageName = "small_play" else: raise NotImplementedError() + elif navState == "up": + imageName = "small_play" elif navState == "left": imageName = "small_next" elif navState == "right": @@ -121,16 +125,11 @@ class NavControl(gobject.GObject): @misc_utils.log_exception(_moduleLogger) def _on_nav_action(self, widget, navState): - if self._player.state == "play": - imageName = "small_play" - else: - imageName = "small_pause" - imagePath = self._store.STORE_LOOKUP[imageName] - self._store.set_image_from_store(self._controlButton, imagePath) + self._set_context(self._player.state) if navState == "clicking": if widget is self._controlBox: - if self._player.state == "play": + if self._player.state == self._player.STATE_PLAY: self._player.pause() else: self._player.play() @@ -276,7 +275,7 @@ class PlayControl(object): self._set_state(newState) @misc_utils.log_exception(_moduleLogger) - def _on_player_nav_change(self, player, newState): + def _on_player_nav_change(self, player, node): self._set_navigate(player.can_navigate) @misc_utils.log_exception(_moduleLogger) diff --git a/src/player.py b/src/player.py index c4a06de..9288470 100644 --- a/src/player.py +++ b/src/player.py @@ -2,6 +2,9 @@ import logging import gobject +import util.misc as misc_utils +import stream + _moduleLogger = logging.getLogger(__name__) @@ -19,20 +22,35 @@ class Player(gobject.GObject): gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, ), ), + 'error' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), + ), } + STATE_PLAY = stream.GSTStream.STATE_PLAY + STATE_PAUSE = stream.GSTStream.STATE_PAUSE + STATE_STOP = stream.GSTStream.STATE_STOP + def __init__(self, index): gobject.GObject.__init__(self) self._index = index self._node = None - self._state = "play" + self._stream = stream.GSTStream() + self._stream.connect("state-change", self._on_stream_state) + self._stream.connect("eof", self._on_stream_eof) + self._stream.connect("error", self._on_stream_error) def set_piece_by_node(self, node): - assert node.is_leaf() or node is None + assert node is None or node.is_leaf(), node if self._node is node: return self._node = node - self.emit("title_change", self._state) + if self._node is not None: + self._stream.set_file(self._node.uri) + _moduleLogger.info("New node %r" % self._node) + self.emit("title_change", self._node) @property def node(self): @@ -58,29 +76,20 @@ class Player(gobject.GObject): @property def state(self): - return self._state + return self._stream.state def play(self): - if self._state == "play": - return - self._state = "play" - self.emit("state_change", self._state) _moduleLogger.info("play") + self._stream.play() def pause(self): - if self._state == "pause": - return - self._state = "pause" - self.emit("state_change", self._state) _moduleLogger.info("pause") + self._stream.pause() def stop(self): - if self._state == "stop": - return - self._state = "stop" - self.set_piece_by_node(None) - self.emit("state_change", self._state) _moduleLogger.info("stop") + self._stream.stop() + self.set_piece_by_node(None) def back(self): _moduleLogger.info("back") @@ -88,5 +97,20 @@ class Player(gobject.GObject): def next(self): _moduleLogger.info("next") + @misc_utils.log_exception(_moduleLogger) + def _on_stream_state(self, s, state): + _moduleLogger.info("State change %r" % state) + self.emit("state_change", state) + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_eof(self, s, uri): + _moduleLogger.info("EOF %s" % uri) + self.next() + + @misc_utils.log_exception(_moduleLogger) + def _on_stream_error(self, s, error, debug): + _moduleLogger.info("Error %s %s" % (error, debug)) + self.emit("error", error, debug) + gobject.type_register(Player) diff --git a/src/presenter.py b/src/presenter.py index 65899b7..fd57533 100644 --- a/src/presenter.py +++ b/src/presenter.py @@ -99,9 +99,9 @@ class NavigationBox(gobject.GObject): mousePosition = event.get_coords() state = self.get_state(mousePosition) assert state - self.emit("action", state) finally: self._clickPosition = self._NO_POSITION + self.emit("action", state) @misc_utils.log_exception(_moduleLogger) def _on_motion_notify(self, widget, event): diff --git a/src/stream.py b/src/stream.py new file mode 100644 index 0000000..5ae4313 --- /dev/null +++ b/src/stream.py @@ -0,0 +1,144 @@ +import logging + +import gobject +import gst + +import util.misc as misc_utils + + +_moduleLogger = logging.getLogger(__name__) + + +class GSTStream(gobject.GObject): + + STATE_PLAY = "play" + STATE_PAUSE = "pause" + STATE_STOP = "stop" + + __gsignals__ = { + 'state-change' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'eof' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, ), + ), + 'error' : ( + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT), + ), + } + + + def __init__(self): + gobject.GObject.__init__(self) + #Fields + self._state = self.STATE_STOP + self._uri = "" + self._elapsed = 0 + self._duration = 0 + + #Set up GStreamer + self._player = gst.element_factory_make("playbin2", "player") + bus = self._player.get_bus() + bus.add_signal_watch() + bus.connect("message", self._on_message) + + #Constants + self._timeFormat = gst.Format(gst.FORMAT_TIME) + self._seekFlag = gst.SEEK_FLAG_FLUSH + + @property + def playing(self): + return self.state == self.STATE_PLAY + + @property + def has_file(self): + return 0 < len(self._uri) + + @property + def state(self): + state = self._player.get_state()[1] + return self._translate_state(state) + + def set_file(self, uri): + if self._uri != file: + self._invalidate_cache() + if self.playing: + self.stop() + + self._uri = uri + self._player.set_property("uri", uri) + + def play(self): + if self.state == self.STATE_PLAY: + _moduleLogger.info("Already play") + return + _moduleLogger.info("Play") + self._player.set_state(gst.STATE_PLAYING) + self.emit("state-change", self.STATE_PLAY) + + def pause(self): + if self.state == self.STATE_PAUSE: + _moduleLogger.info("Already pause") + return + _moduleLogger.info("Pause") + self._player.set_state(gst.STATE_PAUSED) + self.emit("state-change", self.STATE_PAUSE) + + def stop(self): + if self.state == self.STATE_STOP: + _moduleLogger.info("Already stop") + return + self._player.set_state(gst.STATE_NULL) + _moduleLogger.info("Stopped") + self.emit("state-change", self.STATE_STOP) + + def elapsed(self): + try: + self._elapsed = self._player.query_position(self._timeFormat, None)[0] + except: + pass + return self._elapsed + + def duration(self): + try: + self._duration = self._player.query_duration(self._timeFormat, None)[0] + except: + _moduleLogger.exception("Query failed") + return self._duration + + def seek_time(self, ns): + _moduleLogger.debug("Seeking to: %s", ns) + self._elapsed = ns + self._player.seek_simple(self._timeFormat, self._seekFlag, ns) + + def _invalidate_cache(self): + self._elapsed = 0 + self._duration = 0 + + def _translate_state(self, gstState): + return { + gst.STATE_NULL: self.STATE_STOP, + gst.STATE_PAUSED: self.STATE_PAUSE, + gst.STATE_PLAYING: self.STATE_PLAY, + }.get(gstState, self.STATE_STOP) + + @misc_utils.log_exception(_moduleLogger) + def _on_message(self, bus, message): + t = message.type + if t == gst.MESSAGE_EOS: + self._player.set_state(gst.STATE_NULL) + self.emit("eof", self._uri) + elif t == gst.MESSAGE_ERROR: + self._player.set_state(gst.STATE_NULL) + err, debug = message.parse_error() + _moduleLogger.error("Error: %s, (%s)" % (err, debug)) + self.emit("error", err, debug) + + +gobject.type_register(GSTStream) diff --git a/src/windows.py b/src/windows.py index 0a493d7..fd7ece3 100644 --- a/src/windows.py +++ b/src/windows.py @@ -310,10 +310,6 @@ class RadioWindow(BasicWindow): self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self._presenter = presenter.StreamMiniPresenter(self._store) - if self._player.state == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) self._presenterNavigation = presenter.NavigationBox() self._presenterNavigation.toplevel.add(self._presenter.toplevel) self._presenterNavigation.connect("action", self._on_nav_action) @@ -327,7 +323,7 @@ class RadioWindow(BasicWindow): self._layout.pack_start(self._loadingBanner.toplevel, False, False) self._layout.pack_start(self._radioLayout, True, True) - self._window.set_title("Radio") + self._window.set_title(self._node.title) self._dateShown = datetime.datetime.now() def show(self): @@ -338,6 +334,24 @@ class RadioWindow(BasicWindow): self._refresh() + @property + def _active(self): + return self._player.node is self._childNode + + def _set_context(self, state): + if state == self._player.STATE_PLAY: + if self._active: + self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + else: + self._presenter.set_state(self._store.STORE_LOOKUP["stop"]) + elif state == self._player.STATE_PAUSE: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_STOP: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + else: + _moduleLogger.info("Unhandled player state %s" % state) + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + def _show_loading(self): animationPath = self._store.STORE_LOOKUP["loading"] animation = self._store.get_pixbuf_animation_from_store(animationPath) @@ -353,6 +367,7 @@ class RadioWindow(BasicWindow): self._on_channels, self._on_load_error, ) + self._set_context(self._player.state) def _get_current_row(self): nowTime = self._dateShown.strftime("%H:%M:%S") @@ -371,26 +386,29 @@ class RadioWindow(BasicWindow): if self._headerNavigation.is_active() or self._presenterNavigation.is_active(): return - if newState == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif newState == "pause": - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) - else: - _moduleLogger.info("Unhandled player state %s" % newState) - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + self._set_context(newState) @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, newState): - _moduleLogger.info("Player title magically changed to %s" % player.title) - self._destroy() + def _on_player_title_change(self, player, node): + if node is not self._childNode or node is None: + _moduleLogger.info("Player title magically changed to %s" % player.title) + return @misc_utils.log_exception(_moduleLogger) def _on_navigating(self, widget, navState): if navState == "clicking": - if self._player.state == "play": - imageName = "pause" + if self._player.state == self._player.STATE_PLAY: + if self._active: + imageName = "pause" + else: + imageName = "stop" + elif self._player.state == self._player.STATE_PAUSE: + imageName = "play" + elif self._player.state == self._player.STATE_STOP: + imageName = "play" else: imageName = "play" + _moduleLogger.info("Unhandled player state %s" % self._player.state) elif navState == "down": imageName = "home" elif navState == "up": @@ -404,16 +422,21 @@ class RadioWindow(BasicWindow): @misc_utils.log_exception(_moduleLogger) def _on_nav_action(self, widget, navState): - if self._player.state == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + self._set_context(self._player.state) if navState == "clicking": - if self._player.state == "play": - self._player.pause() - else: + if self._player.state == self._player.STATE_PLAY: + if self._active: + self._player.pause() + else: + self._player.stop() + elif self._player.state == self._player.STATE_PAUSE: + self._player.play() + elif self._player.state == self._player.STATE_STOP: + self._player.set_piece_by_node(self._childNode) self._player.play() + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) elif navState == "down": self.window.destroy() elif navState == "up": @@ -555,7 +578,7 @@ class ConferencesWindow(ListWindow): def __init__(self, player, store, node): ListWindow.__init__(self, player, store, node) - self._window.set_title("Conferences") + self._window.set_title(self._node.title) @classmethod def _get_columns(cls): @@ -626,7 +649,7 @@ class ConferenceSessionsWindow(ListWindow): def __init__(self, player, store, node): ListWindow.__init__(self, player, store, node) - self._window.set_title("Sessions") + self._window.set_title(self._node.title) @classmethod def _get_columns(cls): @@ -691,7 +714,7 @@ class ConferenceTalksWindow(ListWindow): def __init__(self, player, store, node): ListWindow.__init__(self, player, store, node) - self._window.set_title("Talks") + self._window.set_title(self._node.title) @classmethod def _get_columns(cls): @@ -756,9 +779,11 @@ class ConferenceTalkWindow(BasicWindow): def __init__(self, player, store, node): BasicWindow.__init__(self, player, store) + self._node = node self._player.connect("state-change", self._on_player_state_change) self._player.connect("title-change", self._on_player_title_change) + self._player.connect("error", self._on_player_error) self._loadingBanner = banners.GenericBanner() @@ -771,7 +796,7 @@ class ConferenceTalkWindow(BasicWindow): self._layout.pack_start(self._loadingBanner.toplevel, False, False) self._layout.pack_start(self._presenterNavigation.toplevel, True, True) - self._window.set_title("Talk") + self._window.set_title(self._node.title) def show(self): BasicWindow.show(self) @@ -784,10 +809,11 @@ class ConferenceTalkWindow(BasicWindow): self._player.title, self._player.subtitle, ) - if self._player.state == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + self._set_context(self._player.state) + + @property + def _active(self): + return self._player.node is self._node def _show_loading(self): animationPath = self._store.STORE_LOOKUP["loading"] @@ -797,21 +823,31 @@ class ConferenceTalkWindow(BasicWindow): def _hide_loading(self): self._loadingBanner.hide() + def _set_context(self, state): + if state == self._player.STATE_PLAY: + if self._active: + self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + else: + self._presenter.set_state(self._store.STORE_LOOKUP["stop"]) + elif state == self._player.STATE_PAUSE: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + elif state == self._player.STATE_STOP: + self._presenter.set_state(self._store.STORE_LOOKUP["play"]) + else: + _moduleLogger.info("Unhandled player state %s" % state) + @misc_utils.log_exception(_moduleLogger) def _on_player_state_change(self, player, newState): if self._presenterNavigation.is_active(): return - if newState == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - elif newState == "pause": - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) - else: - _moduleLogger.info("Unhandled player state %s" % newState) - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + self._set_context(newState) @misc_utils.log_exception(_moduleLogger) - def _on_player_title_change(self, player, newState): + def _on_player_title_change(self, player, node): + if node is not self._node or node is None: + _moduleLogger.info("Player title magically changed to %s" % player.title) + return self._presenter.set_context( self._store.STORE_LOOKUP["conference_background"], self._player.title, @@ -819,12 +855,23 @@ class ConferenceTalkWindow(BasicWindow): ) @misc_utils.log_exception(_moduleLogger) + def _on_player_error(self, player, err, debug): + _moduleLogger.error("%r - %r" % (err, debug)) + + @misc_utils.log_exception(_moduleLogger) def _on_navigating(self, widget, navState): if navState == "clicking": - if self._player.state == "play": - imageName = "pause" - else: + if self._player.state == self._player.STATE_PLAY: + if self._active: + imageName = "pause" + else: + imageName = "stop" + elif self._player.state == self._player.STATE_PAUSE: imageName = "play" + elif self._player.state == self._player.STATE_STOP: + imageName = "play" + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) elif navState == "down": imageName = "home" elif navState == "up": @@ -838,16 +885,21 @@ class ConferenceTalkWindow(BasicWindow): @misc_utils.log_exception(_moduleLogger) def _on_nav_action(self, widget, navState): - if self._player.state == "play": - self._presenter.set_state(self._store.STORE_LOOKUP["play"]) - else: - self._presenter.set_state(self._store.STORE_LOOKUP["pause"]) + self._set_context(self._player.state) if navState == "clicking": - if self._player.state == "play": - self._player.pause() - else: + if self._player.state == self._player.STATE_PLAY: + if self._active: + self._player.pause() + else: + self._player.stop() + elif self._player.state == self._player.STATE_PAUSE: self._player.play() + elif self._player.state == self._player.STATE_STOP: + self._player.set_piece_by_node(self._node) + self._player.play() + else: + _moduleLogger.info("Unhandled player state %s" % self._player.state) elif navState == "down": self.emit("home") self._window.destroy() -- 1.7.9.5