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