More screens updated to do background fetching
[jamaendo] / jamaui / player.py
1 # Implements playback controls
2 # Gstreamer stuff mostly snibbed from Panucci
3 #
4 # This file is part of Panucci.
5 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
6 #
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.
11 #
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.
16 #
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/>.
19 #
20
21 import logging
22 import pygst
23 pygst.require('0.10')
24 import gst
25 import util
26 import dbus
27
28 import jamaendo
29 from settings import settings
30 from postoffice import postoffice
31 from fetcher import Fetcher
32
33 log = logging.getLogger(__name__)
34
35 class _Player(object):
36     """Defines the internal player interface"""
37     def __init__(self):
38         pass
39     def play_url(self, filetype, uri):
40         raise NotImplemented
41     def playing(self):
42         raise NotImplemented
43     def play_pause_toggle(self):
44         self.pause() if self.playing() else self.play()
45     def play(self):
46         raise NotImplemented
47     def pause(self):
48         raise NotImplemented
49     def stop(self):
50         raise NotImplemented
51     def set_eos_callback(self, cb):
52         raise NotImplemented
53
54 class GStreamer(_Player):
55     """Wraps GStreamer"""
56     STATES = { gst.STATE_NULL    : 'stopped',
57                gst.STATE_PAUSED  : 'paused',
58                gst.STATE_PLAYING : 'playing' }
59
60     def __init__(self):
61         _Player.__init__(self)
62         self.time_format = gst.Format(gst.FORMAT_TIME)
63         self.player = None
64         self.filesrc = None
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)
71
72     def on_settings_changed(self, key, value):
73         if key == 'volume':
74             self._set_volume_level(value)
75         #postoffice.disconnect(self)
76
77
78     def play_url(self, filetype, uri):
79         if None in (filetype, uri):
80             self.player = None
81             return False
82
83         _first = False
84         if self.player is None:
85             _first = True
86             if False:
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)')
92             else:
93                 self._setup_playbin_player()
94                 log.debug( 'Using playbin (non-maemo)' )
95
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)
100
101         self._set_uri_to_be_played(uri)
102
103         self.play()
104         return True
105
106     def get_state(self):
107         if self.player:
108             state = self.player.get_state()[1]
109             return self.STATES.get(state, 'none')
110         return 'none'
111
112     def get_position_duration(self):
113         try:
114             pos_int = self.player.query_position(self.time_format, None)[0]
115             dur_int = self.player.query_duration(self.time_format, None)[0]
116         except Exception, e:
117             log.exception('Error getting position')
118             pos_int = dur_int = 0
119         return pos_int, dur_int
120
121     def playing(self):
122         return self.get_state() == 'playing'
123
124     def play(self):
125         if self.player:
126             self.player.set_state(gst.STATE_PLAYING)
127
128     def pause(self):
129         if self.player:
130             self.player.set_state(gst.STATE_PAUSED)
131
132     def stop(self, reset = True):
133         if self.player:
134             self.player.set_state(gst.STATE_NULL)
135             if reset:
136                 self.player = None
137
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'
145
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'
153         return True
154
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'
164
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 )
168
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)
173
174     def _set_volume_level(self, value):
175         assert  0 <= value <= 1
176
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 )
181
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)
186             log.info("%s", uri)
187
188     def _on_message(self, bus, message):
189         t = message.type
190
191         if t == gst.MESSAGE_EOS:
192             log.debug("Gstreamer: End of stream")
193             self.eos_callback()
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 )
201             self.stop()
202
203     def set_eos_callback(self, cb):
204         self.eos_callback = cb
205
206 if util.platform == 'maemo':
207     class OssoPlayer(_Player):
208         """
209         A player which uses osso-media-player for playback (Maemo-specific)
210         """
211
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"
215
216         def __init__(self):
217             self._on_eos = lambda: self.stop()
218             self._state = 'none'
219             self._audio = self._init_dbus()
220             self._init_signals()
221
222         def play_url(self, filetype, uri):
223             self._audio.play_media(uri)
224
225         def playing(self):
226             return self._state == 'playing'
227
228         def play_pause_toggle(self):
229             self.pause() if self.playing() else self.play()
230
231         def play(self):
232             self._audio.play()
233
234         def pause(self):
235             if self.playing():
236                 self._audio.pause()
237
238         def stop(self):
239             self._audio.stop()
240
241         def set_eos_callback(self, cb):
242             self._on_eos = cb
243
244
245         def _init_dbus(self):
246             session_bus = dbus.SessionBus()
247             oms_object = session_bus.get_object(self.SERVICE_NAME,
248                                                 self.OBJECT_PATH,
249                                                 introspect = False,
250                                                 follow_name_owner_changes = True)
251             return dbus.Interface(oms_object, self.AUDIO_INTERFACE_NAME)
252
253         def _init_signals(self):
254             error_signals = {
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",
260                 "dsp":                          "DSP 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"
265             }
266
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() )
272
273             # Connect error signals
274             for error, msg in error_signals.iteritems():
275                 self._audio.connect_to_signal(error, lambda *x: self._error(msg))
276
277         def _error(self, msg):
278             log.error(msg)
279
280         def _call_eos(self):
281             self._on_eos()
282
283         def _on_state_changed(self, state):
284             states = ("playing", "paused", "stopped")
285             self.__state = state if state in states else 'none'
286
287 #    PlayerBackend = OssoPlayer
288 #else:
289 PlayerBackend = GStreamer
290
291 class Playlist(object):
292     def __init__(self, items = []):
293         self.radio_mode = False
294         self.radio_id = None
295         self.radio_name = None
296         if items is None:
297             items = []
298         for item in items:
299             assert(isinstance(item, jamaendo.Track))
300         self.items = items
301         self._current = -1
302
303     def add(self, item):
304         if isinstance(item, list):
305             for i in item:
306                 assert(isinstance(i, jamaendo.Track))
307             self.items.extend(item)
308         else:
309             self.items.append(item)
310
311     def next(self):
312         if self.has_next():
313             self._current = self._current + 1
314             return self.items[self._current]
315         return None
316
317     def prev(self):
318         if self.has_prev():
319             self._current = self._current - 1
320             return self.items[self._current]
321         return None
322
323     def has_next(self):
324         return self._current < (len(self.items)-1)
325
326     def has_prev(self):
327         return self._current > 0
328
329     def current(self):
330         if self._current >= 0:
331             return self.items[self._current]
332         return None
333
334     def jump_to(self, item_id):
335         for c, i in enumerate(self.items):
336             if i.ID == item_id:
337                 self._current = c
338
339     def current_index(self):
340         return self._current
341
342     def size(self):
343         return len(self.items)
344
345     def __repr__(self):
346         return "Playlist(%d of %s)"%(self._current, ", ".join([str(item.ID) for item in self.items]))
347
348 class Player(object):
349     def __init__(self):
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
354
355     def get_position_duration(self):
356         return self.backend.get_position_duration()
357
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)
362
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()
367
368     def _start_radio_fetcher(self):
369         if self.fetcher:
370             self.fetcher.stop()
371             self.fetcher = None
372         self.fetcher = Fetcher(lambda: jamaendo.get_radio_tracks(self.playlist.radio_id),
373                                self,
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
378         self.fetcher.start()
379
380     def _on_radio_result(self, wnd, item):
381         if wnd is self:
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)
388
389     def _on_radio_complete(self, wnd, error=None):
390         if wnd is self:
391             if error:
392                 self.stop()
393             self.fetcher.stop()
394             self.fetcher = None
395
396     def play(self, playlist = None):
397         if playlist:
398             self.playlist = playlist
399         elif self.playlist is None:
400             self.playlist = Playlist()
401
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:
409             self._refill_radio()
410             #self.play()
411
412     def next(self):
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:
418             self._refill_radio()
419             #if self.playlist.has_next():
420             #    self.next()
421             #else:
422             #    self.stop()
423         else:
424             self.stop()
425
426     def prev(self):
427         if self.playlist.has_prev():
428             self.backend.stop(reset=False)
429             entry = self.playlist.prev()
430             self._play_track(entry, 'prev')
431
432     def pause(self):
433         self.backend.pause()
434         postoffice.notify('pause', self.playlist.current())
435
436     def stop(self):
437         self.backend.stop()
438         postoffice.notify('stop', self.playlist.current())
439
440     def playing(self):
441         return self.backend.playing()
442
443     def _on_eos(self):
444         self.next()
445
446 the_player = Player() # the player instance