1 # Implements playback controls
2 # Gstreamer stuff mostly snibbed from Panucci
4 # This file is part of Panucci.
5 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
7 # Panucci 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 # Panucci 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 Panucci. If not, see <http://www.gnu.org/licenses/>.
29 from settings import settings
30 from postoffice import postoffice
31 from fetcher import Fetcher
33 log = logging.getLogger(__name__)
35 class _Player(object):
36 """Defines the internal player interface"""
39 def play_url(self, filetype, uri):
43 def play_pause_toggle(self):
44 self.pause() if self.playing() else self.play()
51 def set_eos_callback(self, cb):
54 class GStreamer(_Player):
56 STATES = { gst.STATE_NULL : 'stopped',
57 gst.STATE_PAUSED : 'paused',
58 gst.STATE_PLAYING : 'playing' }
61 _Player.__init__(self)
62 self.time_format = gst.Format(gst.FORMAT_TIME)
65 self.filesrc_property = None
66 self.volume_control = None
67 self.volume_multiplier = 1.
68 self.volume_property = None
69 self.eos_callback = lambda: self.stop()
70 postoffice.connect('settings-changed', self, self.on_settings_changed)
72 def on_settings_changed(self, key, value):
74 self._set_volume_level(value)
75 #postoffice.disconnect(self)
78 def play_url(self, filetype, uri):
79 if None in (filetype, uri):
84 if self.player is None:
87 self._maemo_setup_playbin2_player(uri)
88 log.debug('Using playbin2 (maemo)')
89 elif util.platform == 'maemo':
90 self._maemo_setup_playbin_player()
91 log.debug('Using playbin (maemo)')
93 self._setup_playbin_player()
94 log.debug( 'Using playbin (non-maemo)' )
96 bus = self.player.get_bus()
97 bus.add_signal_watch()
98 bus.connect('message', self._on_message)
99 self._set_volume_level(settings.volume)
101 self._set_uri_to_be_played(uri)
108 state = self.player.get_state()[1]
109 return self.STATES.get(state, 'none')
112 def get_position_duration(self):
114 pos_int = self.player.query_position(self.time_format, None)[0]
115 dur_int = self.player.query_duration(self.time_format, None)[0]
117 log.exception('Error getting position')
118 pos_int = dur_int = 0
119 return pos_int, dur_int
122 return self.get_state() == 'playing'
126 self.player.set_state(gst.STATE_PLAYING)
130 self.player.set_state(gst.STATE_PAUSED)
132 def stop(self, reset = True):
134 self.player.set_state(gst.STATE_NULL)
138 def _maemo_setup_playbin2_player(self, url):
139 self.player = gst.parse_launch("playbin2 uri=%s" % (url,))
140 self.filesrc = self.player
141 self.filesrc_property = 'uri'
142 self.volume_control = self.player
143 self.volume_multiplier = 1.
144 self.volume_property = 'volume'
146 def _maemo_setup_playbin_player( self):
147 self.player = gst.element_factory_make('playbin2', 'player')
148 self.filesrc = self.player
149 self.filesrc_property = 'uri'
150 self.volume_control = self.player
151 self.volume_multiplier = 1.
152 self.volume_property = 'volume'
155 def _setup_playbin_player( self ):
156 """ This is for situations where we have a normal (read: non-maemo)
157 version of gstreamer like on a regular linux distro. """
158 self.player = gst.element_factory_make('playbin2', 'player')
159 self.filesrc = self.player
160 self.filesrc_property = 'uri'
161 self.volume_control = self.player
162 self.volume_multiplier = 10.
163 self.volume_property = 'volume'
165 def _on_decoder_pad_added(self, decoder, src_pad, sink_pad):
166 # link the decoder's new "src_pad" to "sink_pad"
167 src_pad.link( sink_pad )
169 def _get_volume_level(self):
170 if self.volume_control is not None:
171 vol = self.volume_control.get_property( self.volume_property )
172 return vol / float(self.volume_multiplier)
174 def _set_volume_level(self, value):
175 assert 0 <= value <= 1
177 if self.volume_control is not None:
178 vol = value * float(self.volume_multiplier)
179 log.debug("Setting volume to %s", vol)
180 self.volume_control.set_property( self.volume_property, vol )
182 def _set_uri_to_be_played(self, uri):
183 # Sets the right property depending on the platform of self.filesrc
184 if self.player is not None:
185 self.filesrc.set_property(self.filesrc_property, uri)
188 def _on_message(self, bus, message):
191 if t == gst.MESSAGE_EOS:
192 log.debug("Gstreamer: End of stream")
194 #elif t == gst.MESSAGE_STATE_CHANGED:
195 # if (message.src == self.player and
196 # message.structure['new-state'] == gst.STATE_PLAYING):
197 # log.debug("gstreamer: state -> playing")
198 elif t == gst.MESSAGE_ERROR:
199 err, debug = message.parse_error()
200 log.critical( 'Error: %s %s', err, debug )
203 def set_eos_callback(self, cb):
204 self.eos_callback = cb
206 if util.platform == 'maemo':
207 class OssoPlayer(_Player):
209 A player which uses osso-media-player for playback (Maemo-specific)
212 SERVICE_NAME = "com.nokia.osso_media_server"
213 OBJECT_PATH = "/com/nokia/osso_media_server"
214 AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
217 self._on_eos = lambda: self.stop()
219 self._audio = self._init_dbus()
222 def play_url(self, filetype, uri):
223 self._audio.play_media(uri)
226 return self._state == 'playing'
228 def play_pause_toggle(self):
229 self.pause() if self.playing() else self.play()
241 def set_eos_callback(self, cb):
245 def _init_dbus(self):
246 session_bus = dbus.SessionBus()
247 oms_object = session_bus.get_object(self.SERVICE_NAME,
250 follow_name_owner_changes = True)
251 return dbus.Interface(oms_object, self.AUDIO_INTERFACE_NAME)
253 def _init_signals(self):
255 "no_media_selected": "No media selected",
256 "file_not_found": "File not found",
257 "type_not_found": "Type not found",
258 "unsupported_type": "Unsupported type",
259 "gstreamer": "GStreamer Error",
261 "device_unavailable": "Device Unavailable",
262 "corrupted_file": "Corrupted File",
263 "out_of_memory": "Out of Memory",
264 "audio_codec_not_supported": "Audio codec not supported"
267 # Connect status signals
268 self._audio.connect_to_signal( "state_changed",
269 self._on_state_changed )
270 self._audio.connect_to_signal( "end_of_stream",
271 lambda x: self._call_eos() )
273 # Connect error signals
274 for error, msg in error_signals.iteritems():
275 self._audio.connect_to_signal(error, lambda *x: self._error(msg))
277 def _error(self, msg):
283 def _on_state_changed(self, state):
284 states = ("playing", "paused", "stopped")
285 self.__state = state if state in states else 'none'
287 # PlayerBackend = OssoPlayer
289 PlayerBackend = GStreamer
291 class Playlist(object):
292 def __init__(self, items = []):
293 self.radio_mode = False
295 self.radio_name = None
299 assert(isinstance(item, jamaendo.Track))
304 if isinstance(item, list):
306 assert(isinstance(i, jamaendo.Track))
307 self.items.extend(item)
309 self.items.append(item)
313 self._current = self._current + 1
314 return self.items[self._current]
319 self._current = self._current - 1
320 return self.items[self._current]
324 return self._current < (len(self.items)-1)
327 return self._current > 0
330 if self._current >= 0:
331 return self.items[self._current]
334 def jump_to(self, item_id):
335 for c, i in enumerate(self.items):
339 def current_index(self):
343 return len(self.items)
346 return "Playlist(%d of %s)"%(self._current, ", ".join([str(item.ID) for item in self.items]))
348 class Player(object):
350 self.backend = PlayerBackend()
351 self.backend.set_eos_callback(self._on_eos)
352 self.playlist = Playlist()
353 self.fetcher = None # for refilling the radio
355 def get_position_duration(self):
356 return self.backend.get_position_duration()
358 def _play_track(self, track, notify='play'):
359 self.backend.play_url('mp3', track.mp3_url())
360 log.debug("playing %s", track)
361 postoffice.notify(notify, track)
363 def _refill_radio(self):
364 log.debug("Refilling radio %s", self.playlist)
365 #self.playlist.add(jamaendo.get_radio_tracks(self.playlist.radio_id))
366 self._start_radio_fetcher()
368 def _start_radio_fetcher(self):
372 self.fetcher = Fetcher(lambda: jamaendo.get_radio_tracks(self.playlist.radio_id),
374 on_item = self._on_radio_result,
375 on_ok = self._on_radio_complete,
376 on_fail = self._on_radio_complete)
377 self.fetcher.has_no_results = True
380 def _on_radio_result(self, wnd, item):
382 self.playlist.add(item)
383 if not self.playing():
384 if self.fetcher.has_no_results:
385 self.fetcher.has_no_results = False
386 entry = self.playlist.next()
387 self._play_track(entry)
389 def _on_radio_complete(self, wnd, error=None):
396 def play(self, playlist = None):
398 self.playlist = playlist
399 elif self.playlist is None:
400 self.playlist = Playlist()
402 if self.playlist.current():
403 entry = self.playlist.current()
404 self._play_track(entry)
405 elif self.playlist.has_next():
406 entry = self.playlist.next()
407 self._play_track(entry)
408 elif self.playlist.radio_mode:
413 if self.playlist.has_next():
414 self.backend.stop(reset=False)
415 entry = self.playlist.next()
416 self._play_track(entry, notify='next')
417 elif self.playlist.radio_mode:
419 #if self.playlist.has_next():
427 if self.playlist.has_prev():
428 self.backend.stop(reset=False)
429 entry = self.playlist.prev()
430 self._play_track(entry, 'prev')
434 postoffice.notify('pause', self.playlist.current())
438 postoffice.notify('stop', self.playlist.current())
441 return self.backend.playing()
446 the_player = Player() # the player instance