70d261a99da9b9daecf19ce4609d9dc6f658ae02
[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
32 log = logging.getLogger(__name__)
33
34 class _Player(object):
35     """Defines the internal player interface"""
36     def __init__(self):
37         pass
38     def play_url(self, filetype, uri):
39         raise NotImplemented
40     def playing(self):
41         raise NotImplemented
42     def play_pause_toggle(self):
43         self.pause() if self.playing() else self.play()
44     def play(self):
45         raise NotImplemented
46     def pause(self):
47         raise NotImplemented
48     def stop(self):
49         raise NotImplemented
50     def set_eos_callback(self, cb):
51         raise NotImplemented
52
53 class GStreamer(_Player):
54     """Wraps GStreamer"""
55     STATES = { gst.STATE_NULL    : 'stopped',
56                gst.STATE_PAUSED  : 'paused',
57                gst.STATE_PLAYING : 'playing' }
58
59     def __init__(self):
60         _Player.__init__(self)
61         self.time_format = gst.Format(gst.FORMAT_TIME)
62         self.player = None
63         self.filesrc = None
64         self.filesrc_property = None
65         self.volume_control = None
66         self.volume_multiplier = 1.
67         self.volume_property = None
68         self.eos_callback = lambda: self.stop()
69         postoffice.connect('settings-changed', self, self.on_settings_changed)
70
71     def on_settings_changed(self, key, value):
72         if key == 'volume':
73             self._set_volume_level(value)
74         #postoffice.disconnect(self)
75
76
77     def play_url(self, filetype, uri):
78         if None in (filetype, uri):
79             self.player = None
80             return False
81
82         _first = False
83         if self.player is None:
84             _first = True
85             if False:
86                 self._maemo_setup_playbin2_player(uri)
87                 log.debug('Using playbin2 (maemo)')
88             elif util.platform == 'maemo':
89                 self._maemo_setup_playbin_player()
90                 log.debug('Using playbin (maemo)')
91             else:
92                 self._setup_playbin_player()
93                 log.debug( 'Using playbin (non-maemo)' )
94
95             bus = self.player.get_bus()
96             bus.add_signal_watch()
97             bus.connect('message', self._on_message)
98             self._set_volume_level(settings.volume)
99
100         self._set_uri_to_be_played(uri)
101
102         self.play()
103         return True
104
105     def get_state(self):
106         if self.player:
107             state = self.player.get_state()[1]
108             return self.STATES.get(state, 'none')
109         return 'none'
110
111     def get_position_duration(self):
112         try:
113             pos_int = self.player.query_position(self.time_format, None)[0]
114             dur_int = self.player.query_duration(self.time_format, None)[0]
115         except Exception, e:
116             log.exception('Error getting position')
117             pos_int = dur_int = 0
118         return pos_int, dur_int
119
120     def playing(self):
121         return self.get_state() == 'playing'
122
123     def play(self):
124         if self.player:
125             self.player.set_state(gst.STATE_PLAYING)
126
127     def pause(self):
128         if self.player:
129             self.player.set_state(gst.STATE_PAUSED)
130
131     def stop(self, reset = True):
132         if self.player:
133             self.player.set_state(gst.STATE_NULL)
134             if reset:
135                 self.player = None
136
137     def _maemo_setup_playbin2_player(self, url):
138         self.player = gst.parse_launch("playbin2 uri=%s" % (url,))
139         self.filesrc = self.player
140         self.filesrc_property = 'uri'
141         self.volume_control = self.player
142         self.volume_multiplier = 1.
143         self.volume_property = 'volume'
144
145     def _maemo_setup_playbin_player( self):
146         self.player = gst.element_factory_make('playbin2', 'player')
147         self.filesrc = self.player
148         self.filesrc_property = 'uri'
149         self.volume_control = self.player
150         self.volume_multiplier = 1.
151         self.volume_property = 'volume'
152         return True
153
154     def _setup_playbin_player( self ):
155         """ This is for situations where we have a normal (read: non-maemo)
156         version of gstreamer like on a regular linux distro. """
157         self.player = gst.element_factory_make('playbin2', 'player')
158         self.filesrc = self.player
159         self.filesrc_property = 'uri'
160         self.volume_control = self.player
161         self.volume_multiplier = 1.
162         self.volume_property = 'volume'
163
164     def _on_decoder_pad_added(self, decoder, src_pad, sink_pad):
165         # link the decoder's new "src_pad" to "sink_pad"
166         src_pad.link( sink_pad )
167
168     def _get_volume_level(self):
169         if self.volume_control is not None:
170             vol = self.volume_control.get_property( self.volume_property )
171             return  vol / float(self.volume_multiplier)
172
173     def _set_volume_level(self, value):
174         assert  0 <= value <= 1
175
176         if self.volume_control is not None:
177             vol = value * float(self.volume_multiplier)
178             log.debug("Setting volume to %s", vol)
179             self.volume_control.set_property( self.volume_property, vol )
180
181     def _set_uri_to_be_played(self, uri):
182         # Sets the right property depending on the platform of self.filesrc
183         if self.player is not None:
184             self.filesrc.set_property(self.filesrc_property, uri)
185             log.info("%s", uri)
186
187     def _on_message(self, bus, message):
188         t = message.type
189
190         if t == gst.MESSAGE_EOS:
191             log.debug("Gstreamer: End of stream")
192             self.eos_callback()
193         #elif t == gst.MESSAGE_STATE_CHANGED:
194         #    if (message.src == self.player and
195         #        message.structure['new-state'] == gst.STATE_PLAYING):
196         #        log.debug("gstreamer: state -> playing")
197         elif t == gst.MESSAGE_ERROR:
198             err, debug = message.parse_error()
199             log.critical( 'Error: %s %s', err, debug )
200             self.stop()
201
202     def set_eos_callback(self, cb):
203         self.eos_callback = cb
204
205 if util.platform == 'maemo':
206     class OssoPlayer(_Player):
207         """
208         A player which uses osso-media-player for playback (Maemo-specific)
209         """
210
211         SERVICE_NAME         = "com.nokia.osso_media_server"
212         OBJECT_PATH          = "/com/nokia/osso_media_server"
213         AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
214
215         def __init__(self):
216             self._on_eos = lambda: self.stop()
217             self._state = 'none'
218             self._audio = self._init_dbus()
219             self._init_signals()
220
221         def play_url(self, filetype, uri):
222             self._audio.play_media(uri)
223
224         def playing(self):
225             return self._state == 'playing'
226
227         def play_pause_toggle(self):
228             self.pause() if self.playing() else self.play()
229
230         def play(self):
231             self._audio.play()
232
233         def pause(self):
234             if self.playing():
235                 self._audio.pause()
236
237         def stop(self):
238             self._audio.stop()
239
240         def set_eos_callback(self, cb):
241             self._on_eos = cb
242
243
244         def _init_dbus(self):
245             session_bus = dbus.SessionBus()
246             oms_object = session_bus.get_object(self.SERVICE_NAME,
247                                                 self.OBJECT_PATH,
248                                                 introspect = False,
249                                                 follow_name_owner_changes = True)
250             return dbus.Interface(oms_object, self.AUDIO_INTERFACE_NAME)
251
252         def _init_signals(self):
253             error_signals = {
254                 "no_media_selected":            "No media selected",
255                 "file_not_found":               "File not found",
256                 "type_not_found":               "Type not found",
257                 "unsupported_type":             "Unsupported type",
258                 "gstreamer":                    "GStreamer Error",
259                 "dsp":                          "DSP Error",
260                 "device_unavailable":           "Device Unavailable",
261                 "corrupted_file":               "Corrupted File",
262                 "out_of_memory":                "Out of Memory",
263                 "audio_codec_not_supported":    "Audio codec not supported"
264             }
265
266             # Connect status signals
267             self._audio.connect_to_signal( "state_changed",
268                                                 self._on_state_changed )
269             self._audio.connect_to_signal( "end_of_stream",
270                                                 lambda x: self._call_eos() )
271
272             # Connect error signals
273             for error, msg in error_signals.iteritems():
274                 self._audio.connect_to_signal(error, lambda *x: self._error(msg))
275
276         def _error(self, msg):
277             log.error(msg)
278
279         def _call_eos(self):
280             self._on_eos()
281
282         def _on_state_changed(self, state):
283             states = ("playing", "paused", "stopped")
284             self.__state = state if state in states else 'none'
285
286 #    PlayerBackend = OssoPlayer
287 #else:
288 PlayerBackend = GStreamer
289
290 class Playlist(object):
291     def __init__(self, items = []):
292         self.radio_mode = False
293         self.radio_id = None
294         self.radio_name = None
295         if items is None:
296             items = []
297         for item in items:
298             assert(isinstance(item, jamaendo.Track))
299         self.items = items
300         self._current = -1
301
302     def add(self, item):
303         if isinstance(item, list):
304             for i in item:
305                 assert(isinstance(i, jamaendo.Track))
306             self.items.extend(item)
307         else:
308             self.items.append(item)
309
310     def next(self):
311         if self.has_next():
312             self._current = self._current + 1
313             return self.items[self._current]
314         return None
315
316     def prev(self):
317         if self.has_prev():
318             self._current = self._current - 1
319             return self.items[self._current]
320         return None
321
322     def has_next(self):
323         return self._current < (len(self.items)-1)
324
325     def has_prev(self):
326         return self._current > 0
327
328     def current(self):
329         if self._current >= 0:
330             return self.items[self._current]
331         return None
332
333     def jump_to(self, item_id):
334         for c, i in enumerate(self.items):
335             if i.ID == item_id:
336                 self._current = c
337
338     def current_index(self):
339         return self._current
340
341     def size(self):
342         return len(self.items)
343
344     def __repr__(self):
345         return "Playlist(%d of %s)"%(self._current, ", ".join([str(item.ID) for item in self.items]))
346
347 class Player(object):
348     def __init__(self):
349         self.backend = PlayerBackend()
350         self.backend.set_eos_callback(self._on_eos)
351         self.playlist = Playlist()
352
353     def get_position_duration(self):
354         return self.backend.get_position_duration()
355
356     def play(self, playlist = None):
357         if playlist:
358             self.playlist = playlist
359         elif self.playlist is None:
360             self.playlist = Playlist()
361         if self.playlist.size():
362             if self.playlist.current():
363                 entry = self.playlist.current()
364                 self.backend.play_url('mp3', entry.mp3_url())
365                 log.debug("playing %s", entry)
366             elif self.playlist.has_next():
367                 entry = self.playlist.next()
368                 self.backend.play_url('mp3', entry.mp3_url())
369                 log.debug("playing %s", entry)
370             postoffice.notify('play', entry)
371
372     def pause(self):
373         self.backend.pause()
374         postoffice.notify('pause', self.playlist.current())
375
376     def stop(self):
377         self.backend.stop()
378         postoffice.notify('stop', self.playlist.current())
379
380     def playing(self):
381         return self.backend.playing()
382
383     def next(self):
384         if self.playlist.has_next():
385             self.backend.stop(reset=False)
386             entry = self.playlist.next()
387             self.backend.play_url('mp3', entry.mp3_url())
388             log.debug("playing %s:%s", entry.ID, entry.name)
389             postoffice.notify('next', entry)
390         elif self.playlist.radio_mode:
391             log.debug("Refilling radio %s", self.playlist)
392             self.playlist.add(jamaendo.get_radio_tracks(self.playlist.radio_id))
393             if self.playlist.has_next():
394                 self.next()
395             else:
396                 self.stop()
397         else:
398             self.stop()
399
400     def prev(self):
401         if self.playlist.has_prev():
402             self.backend.stop(reset=False)
403             entry = self.playlist.prev()
404             self.backend.play_url('mp3', entry.mp3_url())
405             log.debug("playing %s", entry)
406             postoffice.notify('prev', entry)
407
408     def _on_eos(self):
409         self.next()
410
411 the_player = Player() # the player instance