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