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!
42 class gPodderPlayer(dbus
.service
.Object
):
43 # Empty class with method definitions to send D-Bus signals
45 def __init__(self
, path
, name
):
46 dbus
.service
.Object
.__init
__(self
, object_path
=path
, bus_name
=name
)
48 # Signals for gPodder's media player integration
49 @dbus.service
.signal(dbus_interface
='org.gpodder.player', signature
='us')
50 def PlaybackStarted(self
, position
, file_uri
):
53 @dbus.service
.signal(dbus_interface
='org.gpodder.player', signature
='uuus')
54 def PlaybackStopped(self
, start_position
, end_position
, total_time
, \
58 class MafwResumeHandler(object):
59 # Simple state machine for handling resume with MAFW
61 # NoResume ... No desire to resume / error state ("do nothing")
62 # Init ....... Want to resume, filename and position set
63 # Loaded ..... The correct filename has been loaded
64 # Seekable ... The media is seekable (metadata "is-seekable" is True)
65 # Playing .... The media is being played back (state_changed with 1)
66 # Done ....... The "resume" action has been carried out
67 NoResume
, Init
, Loaded
, Seekable
, Playing
, Done
= range(6)
69 def __init__(self
, playback_monitor
):
70 self
.playback_monitor
= playback_monitor
71 self
.state
= MafwResumeHandler
.NoResume
75 def set_resume_point(self
, filename
, position
):
76 self
.filename
= filename
77 self
.position
= position
79 self
.state
= MafwResumeHandler
.Init
81 self
.state
= MafwResumeHandler
.NoResume
83 def on_media_changed(self
, filename
):
84 if self
.state
== MafwResumeHandler
.Init
:
85 if filename
.startswith('file://'):
86 filename
= urllib
.unquote(filename
[len('file://'):])
87 if self
.filename
== filename
:
88 self
.state
= MafwResumeHandler
.Loaded
90 def on_metadata_changed(self
, key
, value
):
91 if self
.state
== MafwResumeHandler
.Loaded
:
92 if key
== 'is-seekable' and value
== True:
93 self
.state
= MafwResumeHandler
.Seekable
95 def on_state_changed(self
, new_state
):
96 if self
.state
== MafwResumeHandler
.Seekable
:
98 self
.state
= MafwResumeHandler
.Playing
99 self
.playback_monitor
.set_position(self
.position
)
100 self
.state
= MafwResumeHandler
.Done
103 class MafwPlaybackMonitor(object):
104 MAFW_RENDERER_OBJECT
= 'com.nokia.mafw.renderer.Mafw-Gst-Renderer-Plugin.gstrenderer'
105 MAFW_RENDERER_PATH
= '/com/nokia/mafw/renderer/gstrenderer'
106 MAFW_RENDERER_INTERFACE
= 'com.nokia.mafw.renderer'
107 MAFW_RENDERER_SIGNAL_MEDIA
= 'media_changed'
108 MAFW_RENDERER_SIGNAL_STATE
= 'state_changed'
109 MAFW_RENDERER_SIGNAL_METADATA
= 'metadata_changed'
111 MAFW_SENDER_PATH
= '/org/gpodder/maemo/mafw'
113 class MafwPlayState(object):
119 def __init__(self
, bus
):
121 self
._filename
= None
122 self
._is
_playing
= False
123 self
._start
_time
= time
.time()
124 self
._start
_position
= 0
127 self
._resume
_handler
= MafwResumeHandler(self
)
129 self
._player
= gPodderPlayer(self
.MAFW_SENDER_PATH
, \
130 dbus
.service
.BusName(gpodder
.dbus_bus_name
, self
.bus
))
132 state
, object_id
= self
.get_status()
134 self
.on_media_changed(0, object_id
)
135 self
.on_state_changed(state
)
137 self
.bus
.add_signal_receiver(self
.on_media_changed
, \
138 self
.MAFW_RENDERER_SIGNAL_MEDIA
, \
139 self
.MAFW_RENDERER_INTERFACE
, \
141 self
.MAFW_RENDERER_PATH
)
143 self
.bus
.add_signal_receiver(self
.on_state_changed
, \
144 self
.MAFW_RENDERER_SIGNAL_STATE
, \
145 self
.MAFW_RENDERER_INTERFACE
, \
147 self
.MAFW_RENDERER_PATH
)
149 self
.bus
.add_signal_receiver(self
.on_metadata_changed
, \
150 self
.MAFW_RENDERER_SIGNAL_METADATA
, \
151 self
.MAFW_RENDERER_INTERFACE
, \
153 self
.MAFW_RENDERER_PATH
)
155 # Capture requests to the renderer where the position is to be set to
156 # something else because we don't get normal signals in these cases
157 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',member='set_position'")
158 bus
.add_message_filter(self
._message
_filter
)
160 def set_resume_point(self
, filename
, position
):
161 self
._resume
_handler
.set_resume_point(filename
, position
)
163 def _message_filter(self
, bus
, message
):
165 # message type 1 = dbus.lowlevel.MESSAGE_TYPE_METHOD_CALL
166 if message
.get_path() == self
.MAFW_RENDERER_PATH
and \
167 message
.get_interface() == self
.MAFW_RENDERER_INTERFACE
and \
168 message
.get_destination() == self
.MAFW_RENDERER_OBJECT
and \
169 message
.get_type() == 1 and \
170 message
.get_member() == 'set_position' and \
172 # Fake stop-of-old / start-of-new
173 self
.on_state_changed(-1)
174 self
.on_state_changed(self
.MafwPlayState
.Playing
)
176 return 1 # = dbus.lowlevel.HANDLER_RESULT_NOT_YET_HANDLED
178 def object_id_to_filename(self
, object_id
):
179 # Naive, but works for now...
180 if object_id
.startswith('localtagfs::'):
181 if isinstance(object_id
, unicode):
182 object_id
= object_id
.encode('utf-8')
183 return 'file://'+urllib
.quote(urllib
.unquote(object_id
[object_id
.index('%2F'):]))
184 elif object_id
.startswith('urisource::'):
185 return object_id
[len('urisource::'):]
187 # This is pretty bad, actually (can happen with other
188 # sources, but should not happen for gPodder episodes)
193 o
= self
.bus
.get_object(self
.MAFW_RENDERER_OBJECT
, \
194 self
.MAFW_RENDERER_PATH
)
195 return dbus
.Interface(o
, self
.MAFW_RENDERER_INTERFACE
)
197 def get_position(self
):
198 return self
.renderer
.get_position()
200 def set_position(self
, position
):
201 self
.renderer
.set_position(0, position
)
202 self
._start
_position
= position
203 self
._start
_time
= time
.time()
206 def get_status(self
):
207 """Returns playing status and updates filename"""
208 playlist
, index
, state
, object_id
= self
.renderer
.get_status()
209 return (state
, object_id
)
211 def on_media_changed(self
, position
, object_id
):
212 # We don't know the duration for newly-loaded files at first
216 # Fake stop-of-old / start-of-new
217 self
.on_state_changed(-1) # (see below where we catch the "-1")
218 self
._filename
= self
.object_id_to_filename(object_id
)
219 self
.on_state_changed(self
.MafwPlayState
.Playing
)
221 self
._filename
= self
.object_id_to_filename(object_id
)
223 self
._resume
_handler
.on_media_changed(self
._filename
)
225 def on_state_changed(self
, state
):
226 if state
== self
.MafwPlayState
.Playing
:
227 self
._is
_playing
= True
229 self
._start
_position
= self
.get_position()
233 self
._start
_time
= time
.time()
234 self
._player
.PlaybackStarted(self
._start
_position
, self
._filename
)
238 # Lame: if state is -1 (a faked "stop" event), don't try to
239 # get the "current" position, but use the wall time method
240 # from below to calculate the stop time
243 position
= self
.get_position()
245 # Happens if the assertion fails or if the position cannot
246 # be determined for whatever reason. Use wall time and
247 # assume that the media file has advanced the same amount.
248 position
= self
._start
_position
+ (time
.time()-self
._start
_time
)
249 if self
._start
_position
!= position
:
250 self
._player
.PlaybackStopped(self
._start
_position
, \
251 position
, self
._duration
, self
._filename
)
252 self
._is
_playing
= False
254 self
._resume
_handler
.on_state_changed(state
)
256 def on_metadata_changed(self
, key
, count
, value
):
257 if key
== 'duration':
258 # Remember the duration of the media - right now, we don't care
259 # if this is for the right file, as we re-set the internally-saved
260 # duration when the media is changed (see on_media_changed above)
261 self
._duration
= int(value
)
262 elif key
== 'is-seekable':
263 self
._resume
_handler
.on_metadata_changed(key
, value
)