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