Maemo 5: Track playback status from MAFW
[gpodder.git] / src / gpodder / gtkui / frmntl / mafw.py
blobc4dbb6ca60c1ee7f0cfe9b777e542ef3bcdb97e2
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # gPodder - A media aggregator and podcast client
5 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
7 # gPodder is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # gPodder is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # Maemo 5 Media Player / MAFW Playback Monitor
23 # Send playback status information to gPodder using D-Bus
24 # Thomas Perl <thp@gpodder.org>; 2010-08-16 / 2010-08-17
27 # The code below is based on experimentation with MAFW and real files,
28 # so it might not work in the general case. It worked fine for me with
29 # local and streaming files (audio/video), though. Blame missing docs!
31 import gtk
32 import gobject
33 import dbus
34 import dbus.service
35 import urllib
36 import time
38 class gPodderPlayer(dbus.service.Object):
39 # Empty class with method definitions to send D-Bus signals
41 def __init__(self, path, name):
42 dbus.service.Object.__init__(self, object_path=path, bus_name=name)
44 # Signals for gPodder's media player integration
45 @dbus.service.signal(dbus_interface='org.gpodder.player', signature='us')
46 def PlaybackStarted(self, position, file_uri):
47 pass
49 @dbus.service.signal(dbus_interface='org.gpodder.player', signature='uuus')
50 def PlaybackStopped(self, start_position, end_position, total_time, \
51 file_uri):
52 pass
54 class MafwPlaybackMonitor(object):
55 MAFW_RENDERER_OBJECT = 'com.nokia.mafw.renderer.Mafw-Gst-Renderer-Plugin.gstrenderer'
56 MAFW_RENDERER_PATH = '/com/nokia/mafw/renderer/gstrenderer'
57 MAFW_RENDERER_INTERFACE = 'com.nokia.mafw.renderer'
58 MAFW_RENDERER_SIGNAL_MEDIA = 'media_changed'
59 MAFW_RENDERER_SIGNAL_STATE = 'state_changed'
61 MAFW_SENDER_PATH = '/org/gpodder/maemo/mafw'
62 MAFW_SENDER_NAME = 'org.gpodder.maemo.mafw'
64 class MafwPlayState(object):
65 Stopped = 0
66 Playing = 1
67 Paused = 2
68 Transitioning = 3
70 def __init__(self, bus):
71 self.bus = bus
72 self._filename = None
73 self._is_playing = False
74 self._start_time = time.time()
75 self._start_position = 0
77 self._player = gPodderPlayer(self.MAFW_SENDER_PATH, \
78 dbus.service.BusName(self.MAFW_SENDER_NAME, self.bus))
80 state, object_id = self.get_status()
82 self.on_media_changed(0, object_id)
83 self.on_state_changed(state)
85 self.bus.add_signal_receiver(self.on_media_changed, \
86 self.MAFW_RENDERER_SIGNAL_MEDIA, \
87 self.MAFW_RENDERER_INTERFACE, \
88 None, \
89 self.MAFW_RENDERER_PATH)
91 self.bus.add_signal_receiver(self.on_state_changed, \
92 self.MAFW_RENDERER_SIGNAL_STATE, \
93 self.MAFW_RENDERER_INTERFACE, \
94 None, \
95 self.MAFW_RENDERER_PATH)
97 # Capture requests to the renderer where the position is to
98 # be set to something else (or when it is to be stopped),
99 # because we don't get normal signals in these cases
100 bus.add_match_string("type='method_call',destination='com.nokia.mafw.renderer.Mafw-Gst-Renderer-Plugin.gstrenderer',path='/com/nokia/mafw/renderer/gstrenderer',interface='com.nokia.mafw.renderer'")
101 bus.add_message_filter(self._message_filter)
103 def _message_filter(self, bus, message):
104 if message.get_path() == self.MAFW_RENDERER_PATH and \
105 message.get_interface() == self.MAFW_RENDERER_INTERFACE and \
106 message.get_destination() == self.MAFW_RENDERER_OBJECT and \
107 message.get_type() == 1: # message type 1 == method call?
108 if message.get_member() in ('set_position', 'stop'):
109 if self._is_playing:
110 # Fake stop-of-old / start-of-new
111 self.on_state_changed(-1)
112 self.on_state_changed(self.MafwPlayState.Playing)
114 def object_id_to_filename(self, object_id):
115 # Naive, but works for now...
116 if object_id.startswith('localtagfs::'):
117 return 'file://'+urllib.quote(urllib.unquote(object_id[object_id.index('%2F'):]))
118 elif object_id.startswith('urisource::'):
119 return object_id[len('urisource::'):]
120 else:
121 # This is pretty bad, actually (can happen with other
122 # sources, but should not happen for gPodder episodes)
123 return object_id
125 @property
126 def renderer(self):
127 o = self.bus.get_object(self.MAFW_RENDERER_OBJECT, \
128 self.MAFW_RENDERER_PATH)
129 return dbus.Interface(o, self.MAFW_RENDERER_INTERFACE)
131 def get_position(self):
132 return self.renderer.get_position()
134 def set_position(self, position):
135 self.renderer.set_position(0, position)
136 return False
138 def get_status(self):
139 """Returns playing status and updates filename"""
140 playlist, index, state, object_id = self.renderer.get_status()
141 return (state, object_id)
143 def on_media_changed(self, position, object_id):
144 if self._is_playing:
145 # Fake stop-of-old / start-of-new
146 self.on_state_changed(-1) # (see below where we catch the "-1")
147 self._filename = self.object_id_to_filename(object_id)
148 self.on_state_changed(self.MafwPlayState.Playing)
149 else:
150 self._filename = self.object_id_to_filename(object_id)
152 def on_state_changed(self, state):
153 if state == self.MafwPlayState.Playing:
154 self._is_playing = True
155 try:
156 self._start_position = self.get_position()
157 except:
158 # XXX: WTF?
159 pass
160 self._start_time = time.time()
161 self._player.PlaybackStarted(self._start_position, self._filename)
162 else:
163 if self._is_playing:
164 try:
165 # Lame: if state is -1 (a faked "stop" event), don't try to
166 # get the "current" position, but use the wall time method
167 # from below to calculate the stop time
168 assert state != -1
170 position = self.get_position()
171 except:
172 # Happens if the assertion fails or if the position cannot
173 # be determined for whatever reason. Use wall time and
174 # assume that the media file has advanced the same amount.
175 position = self._start_position + (time.time()-self._start_time)
176 if self._start_position != position:
177 self._player.PlaybackStopped(self._start_position, position, 0, self._filename)
178 self._is_playing = False
181 if __name__ == "__main__":
182 from dbus.mainloop.glib import DBusGMainLoop
183 DBusGMainLoop(set_as_default=True)
184 bus = dbus.SessionBus()
185 monitor = MafwPlaybackMonitor(bus)
186 gtk.main()