We have sound!
[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 import dbus.service
28
29 log = logging.getLogger(__name__)
30
31 class _Player(object):
32     """Defines the internal player interface"""
33     def __init__(self):
34         pass
35     def play_url(self, filetype, uri):
36         raise NotImplemented
37     def playing(self):
38         raise NotImplemented
39     def play_pause_toggle(self):
40         self.pause() if self.playing() else self.play()
41     def play(self):
42         raise NotImplemented
43     def pause(self):
44         raise NotImplemented
45     def stop(self):
46         raise NotImplemented
47     def set_eos_callback(self, cb):
48         raise NotImplemented
49
50 class GStreamer(_Player):
51     """Wraps GStreamer"""
52     STATES = { gst.STATE_NULL    : 'stopped',
53                gst.STATE_PAUSED  : 'paused',
54                gst.STATE_PLAYING : 'playing' }
55
56     def __init__(self):
57         _Player.__init__(self)
58         self.time_format = gst.Format(gst.FORMAT_TIME)
59         self.player = None
60         self.filesrc = None
61         self.filesrc_property = None
62         self.volume_control = None
63         self.volume_multiplier = 1
64         self.volume_property = None
65         self.eos_callback = lambda: self.stop()
66
67     def play_url(self, filetype, uri):
68         if None in (filetype, uri):
69             self.player = None
70             return False
71
72         log.debug("Setting up for %s : %s", filetype, uri)
73
74         # On maemo use software decoding to workaround some bugs their gst:
75         # 1. Weird volume bugs in playbin when playing ogg or wma files
76         # 2. When seeking the DSPs sometimes lie about the real position info
77         if True:
78             self._maemo_setup_playbin_player(uri)
79         elif util.platform == 'maemo':
80             if not self._maemo_setup_hardware_player(filetype):
81                 self._maemo_setup_software_player()
82                 log.debug( 'Using software decoding (maemo)' )
83             else:
84                 log.debug( 'Using hardware decoding (maemo)' )
85         else:
86             # This is for *ahem* "normal" versions of gstreamer
87             self._setup_playbin_player()
88             log.debug( 'Using playbin (non-maemo)' )
89
90         self._set_uri_to_be_played(uri)
91
92         bus = self.player.get_bus()
93         bus.add_signal_watch()
94         bus.connect('message', self._on_message)
95
96         self._set_volume_level( 1 )
97
98         self.play()
99         return True
100
101     def get_state(self):
102         if self.player:
103             state = self.player.get_state()[1]
104             return self.STATES.get(state, 'none')
105         return 'none'
106
107     def playing(self):
108         return self.get_state() == 'playing'
109
110     def play(self):
111         if self.player:
112             log.debug("playing")
113             self.player.set_state(gst.STATE_PLAYING)
114
115     def pause(self):
116         if self.player:
117             self.player.set_state(gst.STATE_PAUSED)
118
119     def stop(self):
120         if self.player:
121             self.player.set_state(gst.STATE_NULL)
122             self.player = None
123
124     def _maemo_setup_playbin_player(self, url):
125         self.player = gst.parse_launch("playbin2 uri=%s" % (url,))
126         self.filesrc = self.player
127         self.filesrc_property = 'uri'
128         self.volume_control = self.player
129         self.volume_multiplier = 1.
130         self.volume_property = 'volume'
131
132     def _maemo_setup_hardware_player( self, filetype ):
133         """ Setup a hardware player for mp3 or aac audio using
134         dspaacsink or dspmp3sink """
135
136         if filetype in [ 'mp3', 'aac', 'mp4', 'm4a' ]:
137             self.player = gst.element_factory_make('playbin', 'player')
138             self.filesrc = self.player
139             self.filesrc_property = 'uri'
140             self.volume_control = self.player
141             self.volume_multiplier = 10.
142             self.volume_property = 'volume'
143             return True
144         else:
145             return False
146
147     def _maemo_setup_software_player( self ):
148         """
149         Setup a software decoding player for maemo, this is the only choice
150         for decoding wma and ogg or if audio is to be piped to a bluetooth
151         headset (this is because the audio must first be decoded only to be
152         re-encoded using sbcenc.
153         """
154
155         self.player = gst.Pipeline('player')
156         src = gst.element_factory_make('gnomevfssrc', 'src')
157         decoder = gst.element_factory_make('decodebin', 'decoder')
158         convert = gst.element_factory_make('audioconvert', 'convert')
159         resample = gst.element_factory_make('audioresample', 'resample')
160         sink = gst.element_factory_make('dsppcmsink', 'sink')
161
162         self.filesrc = src # pointer to the main source element
163         self.filesrc_property = 'location'
164         self.volume_control = sink
165         self.volume_multiplier = 1
166         self.volume_property = 'fvolume'
167
168         # Add the various elements to the player pipeline
169         self.player.add( src, decoder, convert, resample, sink )
170
171         # Link what can be linked now, the decoder->convert happens later
172         gst.element_link_many( src, decoder )
173         gst.element_link_many( convert, resample, sink )
174
175         # We can't link the two halves of the pipeline until it comes
176         # time to start playing, this singal lets us know when it's time.
177         # This is because the output from decoder can't be determined until
178         # decoder knows what it's decoding.
179         decoder.connect('pad-added',
180                         self._on_decoder_pad_added,
181                         convert.get_pad('sink') )
182
183     def _setup_playbin_player( self ):
184         """ This is for situations where we have a normal (read: non-maemo)
185         version of gstreamer like on a regular linux distro. """
186         self.player = gst.element_factory_make('playbin2', 'player')
187         self.filesrc = self.player
188         self.filesrc_property = 'uri'
189         self.volume_control = self.player
190         self.volume_multiplier = 1.
191         self.volume_property = 'volume'
192
193     def _on_decoder_pad_added(self, decoder, src_pad, sink_pad):
194         # link the decoder's new "src_pad" to "sink_pad"
195         src_pad.link( sink_pad )
196
197     def _get_volume_level(self):
198         if self.volume_control is not None:
199             vol = self.volume_control.get_property( self.volume_property )
200             return  vol / float(self.volume_multiplier)
201
202     def _set_volume_level(self, value):
203         assert  0 <= value <= 1
204
205         if self.volume_control is not None:
206             vol = value * self.volume_multiplier
207             self.volume_control.set_property( self.volume_property, vol )
208
209     def _set_uri_to_be_played(self, uri):
210         # Sets the right property depending on the platform of self.filesrc
211         if self.player is not None:
212             self.filesrc.set_property(self.filesrc_property, uri)
213
214     def _on_message(self, bus, message):
215         t = message.type
216
217         if t == gst.MESSAGE_EOS:
218             self.eos_callback()
219
220         elif t == gst.MESSAGE_ERROR:
221             err, debug = message.parse_error()
222             log.critical( 'Error: %s %s', err, debug )
223             self.stop()
224
225     def set_eos_callback(self, cb):
226         self.eos_callback = cb
227
228 if util.platform == 'maemo':
229     class OssoPlayer(_Player):
230         """
231         A player which uses osso-media-player for playback (Maemo-specific)
232         """
233
234         SERVICE_NAME         = "com.nokia.osso_media_server"
235         OBJECT_PATH          = "/com/nokia/osso_media_server"
236         AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
237
238         def __init__(self):
239             self._on_eos = lambda: self.stop()
240             self._state = 'none'
241             self._audio = self._init_dbus()
242             self._init_signals()
243
244         def play_url(self, filetype, uri):
245             self._audio.play_media(uri)
246
247         def playing(self):
248             return self._state == 'playing'
249
250         def play_pause_toggle(self):
251             self.pause() if self.playing() else self.play()
252
253         def play(self):
254             self._audio.play()
255
256         def pause(self):
257             if self.playing():
258                 self._audio.pause()
259
260         def stop(self):
261             self._audio.stop()
262
263         def set_eos_callback(self, cb):
264             self._on_eos = cb
265
266
267         def _init_dbus(self):
268             session_bus = dbus.SessionBus()
269             oms_object = session_bus.get_object(self.SERVICE_NAME,
270                                                 self.OBJECT_PATH,
271                                                 introspect = False,
272                                                 follow_name_owner_changes = True)
273             return dbus.Interface(oms_object, self.AUDIO_INTERFACE_NAME)
274
275         def _init_signals(self):
276             error_signals = {
277                 "no_media_selected":            "No media selected",
278                 "file_not_found":               "File not found",
279                 "type_not_found":               "Type not found",
280                 "unsupported_type":             "Unsupported type",
281                 "gstreamer":                    "GStreamer Error",
282                 "dsp":                          "DSP Error",
283                 "device_unavailable":           "Device Unavailable",
284                 "corrupted_file":               "Corrupted File",
285                 "out_of_memory":                "Out of Memory",
286                 "audio_codec_not_supported":    "Audio codec not supported"
287             }
288
289             # Connect status signals
290             self._audio.connect_to_signal( "state_changed",
291                                                 self._on_state_changed )
292             self._audio.connect_to_signal( "end_of_stream",
293                                                 lambda x: self._call_eos() )
294
295             # Connect error signals
296             for error, msg in error_signals.iteritems():
297                 self._audio.connect_to_signal(error, lambda *x: self._error(msg))
298
299         def _error(self, msg):
300             log.error(msg)
301
302         def _call_eos(self):
303             self._on_eos()
304
305         def _on_state_changed(self, state):
306             states = ("playing", "paused", "stopped")
307             self.__state = state if state in states else 'none'
308
309 #    PlayerBackend = OssoPlayer
310 #else:
311 PlayerBackend = GStreamer
312
313 class Playlist(object):
314     class Entry(object):
315         def __init__(self, data):
316             if isinstance(data, dict):
317                 self.id = data['id']
318                 self.name = data['name']
319                 self.numalbum = int(data['numalbum'])
320                 self.url = data['mp3']
321                 self.type = 'mp3'
322             elif isinstance(data, basestring): # assume URI
323                 self.id = 0
324                 self.name = ''
325                 self.numalbum = 0
326                 self.url = data
327                 self.type = 'mp3'
328         def __str__(self):
329             return "{%s}" % (", ".join([str(self.name), str(self.numalbum), str(self.url)]))
330
331     def __init__(self, items = []):
332         if items is None:
333             items = []
334         self.items = [Playlist.Entry(item) for item in items]
335         self.current = -1
336
337     def add(self, item):
338         self.items.append(Playlist.Entry(item))
339
340     def next(self):
341         if self.has_next():
342             self.current = self.current + 1
343             return self.items[self.current]
344         return None
345
346     def has_next(self):
347         return self.current < (len(self.items)-1)
348
349 class Player(Playlist):
350     def __init__(self):
351         self.backend = PlayerBackend()
352         self.backend.set_eos_callback(self._on_eos)
353         self.playlist = None
354
355     def play(self, playlist = None):
356         if playlist:
357             self.playlist = playlist
358         if self.playlist is not None:
359             if self.playlist.has_next():
360                 entry = self.playlist.next()
361                 log.debug("playing %s", entry)
362                 self.backend.play_url(entry.type, entry.url)
363
364     def pause(self):
365         self.backend.pause()
366
367     def stop(self):
368         self.backend.stop()
369
370     def playing(self):
371         return self.backend.playing()
372
373     def next(self):
374         if self.playlist.has_next():
375             self.stop()
376             self.play()
377         else:
378             self.stop()
379
380     def prev(self):
381         pass
382
383     def _on_eos(self):
384         log.debug("EOS!")
385         self.next()