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