Download links
[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.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.on_settings_changed)
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             log.debug("playing")
126             self.player.set_state(gst.STATE_PLAYING)
127
128     def pause(self):
129         if self.player:
130             self.player.set_state(gst.STATE_PAUSED)
131
132     def stop(self):
133         if self.player:
134             self.player.set_state(gst.STATE_NULL)
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             self.volume_control.set_property( self.volume_property, vol )
179
180     def _set_uri_to_be_played(self, uri):
181         # Sets the right property depending on the platform of self.filesrc
182         if self.player is not None:
183             self.filesrc.set_property(self.filesrc_property, uri)
184
185     def _on_message(self, bus, message):
186         t = message.type
187
188         if t == gst.MESSAGE_EOS:
189             log.info("End of stream")
190             self.eos_callback()
191         elif t == gst.MESSAGE_STATE_CHANGED:
192             if (message.src == self.player and
193                 message.structure['new-state'] == gst.STATE_PLAYING):
194                 log.info("State changed to playing")
195         elif t == gst.MESSAGE_ERROR:
196             err, debug = message.parse_error()
197             log.critical( 'Error: %s %s', err, debug )
198             self.stop()
199
200     def set_eos_callback(self, cb):
201         self.eos_callback = cb
202
203 if util.platform == 'maemo':
204     class OssoPlayer(_Player):
205         """
206         A player which uses osso-media-player for playback (Maemo-specific)
207         """
208
209         SERVICE_NAME         = "com.nokia.osso_media_server"
210         OBJECT_PATH          = "/com/nokia/osso_media_server"
211         AUDIO_INTERFACE_NAME = "com.nokia.osso_media_server.music"
212
213         def __init__(self):
214             self._on_eos = lambda: self.stop()
215             self._state = 'none'
216             self._audio = self._init_dbus()
217             self._init_signals()
218
219         def play_url(self, filetype, uri):
220             self._audio.play_media(uri)
221
222         def playing(self):
223             return self._state == 'playing'
224
225         def play_pause_toggle(self):
226             self.pause() if self.playing() else self.play()
227
228         def play(self):
229             self._audio.play()
230
231         def pause(self):
232             if self.playing():
233                 self._audio.pause()
234
235         def stop(self):
236             self._audio.stop()
237
238         def set_eos_callback(self, cb):
239             self._on_eos = cb
240
241
242         def _init_dbus(self):
243             session_bus = dbus.SessionBus()
244             oms_object = session_bus.get_object(self.SERVICE_NAME,
245                                                 self.OBJECT_PATH,
246                                                 introspect = False,
247                                                 follow_name_owner_changes = True)
248             return dbus.Interface(oms_object, self.AUDIO_INTERFACE_NAME)
249
250         def _init_signals(self):
251             error_signals = {
252                 "no_media_selected":            "No media selected",
253                 "file_not_found":               "File not found",
254                 "type_not_found":               "Type not found",
255                 "unsupported_type":             "Unsupported type",
256                 "gstreamer":                    "GStreamer Error",
257                 "dsp":                          "DSP Error",
258                 "device_unavailable":           "Device Unavailable",
259                 "corrupted_file":               "Corrupted File",
260                 "out_of_memory":                "Out of Memory",
261                 "audio_codec_not_supported":    "Audio codec not supported"
262             }
263
264             # Connect status signals
265             self._audio.connect_to_signal( "state_changed",
266                                                 self._on_state_changed )
267             self._audio.connect_to_signal( "end_of_stream",
268                                                 lambda x: self._call_eos() )
269
270             # Connect error signals
271             for error, msg in error_signals.iteritems():
272                 self._audio.connect_to_signal(error, lambda *x: self._error(msg))
273
274         def _error(self, msg):
275             log.error(msg)
276
277         def _call_eos(self):
278             self._on_eos()
279
280         def _on_state_changed(self, state):
281             states = ("playing", "paused", "stopped")
282             self.__state = state if state in states else 'none'
283
284 #    PlayerBackend = OssoPlayer
285 #else:
286 PlayerBackend = GStreamer
287
288 class Playlist(object):
289     def __init__(self, items = []):
290         if items is None:
291             items = []
292         for item in items:
293             assert(isinstance(item, jamaendo.Track))
294         self.items = items
295         self._current = -1
296
297     def add(self, item):
298         if isinstance(item, list):
299             for i in item:
300                 assert(isinstance(i, jamaendo.Track))
301             self.items.extend(item)
302         else:
303             self.items.append(item)
304
305     def next(self):
306         if self.has_next():
307             self._current = self._current + 1
308             return self.items[self._current]
309         return None
310
311     def prev(self):
312         if self.has_prev():
313             self._current = self._current - 1
314             return self.items[self._current]
315         return None
316
317     def has_next(self):
318         return self._current < (len(self.items)-1)
319
320     def has_prev(self):
321         return self._current > 0
322
323     def current(self):
324         if self._current >= 0:
325             return self.items[self._current]
326         return None
327
328     def current_index(self):
329         return self._current
330
331     def size(self):
332         return len(self.items)
333
334     def __repr__(self):
335         return "Playlist(%s)"%(", ".join([str(item.ID) for item in self.items]))
336
337 class Player(object):
338     def __init__(self):
339         self.backend = PlayerBackend()
340         self.backend.set_eos_callback(self._on_eos)
341         self.playlist = Playlist()
342
343     def play(self, playlist = None):
344         if playlist:
345             self.playlist = playlist
346         elif self.playlist is None:
347             self.playlist = Playlist()
348         if self.playlist.size():
349             if self.playlist.has_next():
350                 entry = self.playlist.next()
351                 log.debug("playing %s", entry)
352                 self.backend.play_url('mp3', entry.mp3_url())
353
354     def pause(self):
355         self.backend.pause()
356
357     def stop(self):
358         self.backend.stop()
359
360     def playing(self):
361         return self.backend.playing()
362
363     def next(self):
364         if self.playlist.has_next():
365             self.stop()
366             self.play()
367         else:
368             self.stop()
369
370     def prev(self):
371         if self.playlist.has_prev():
372             entry = self.playlist.prev()
373             log.debug("playing %s", entry)
374             self.backend.play_url('mp3', entry.mp3_url())
375
376     def _on_eos(self):
377         log.debug("EOS!")
378         self.next()
379
380 the_player = Player() # the player instance