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