9560934676905b1bde058e5093adf4a59c049db8
[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
27 log = logging.getLogger(__name__)
28
29
30 class GStreamer(object):
31     """Wraps GStreamer"""
32     STATES = { gst.STATE_NULL    : 'stopped',
33                gst.STATE_PAUSED  : 'paused',
34                gst.STATE_PLAYING : 'playing' }
35
36     def __init__(self):
37         self.time_format = gst.Format(gst.FORMAT_TIME)
38         self.player = None
39         self.filesrc = None
40         self.filesrc_property = None
41         self.volume_control = None
42         self.volume_multiplier = 1
43         self.volume_property = None
44         self.eos_callback = lambda: self.stop()
45
46     def setup(self, filetype, uri):
47         if None in (filetype, uri):
48             self.player = None
49             return False
50
51         log.debug("Setting up for %s : %s", filetype, uri)
52
53         # On maemo use software decoding to workaround some bugs their gst:
54         # 1. Weird volume bugs in playbin when playing ogg or wma files
55         # 2. When seeking the DSPs sometimes lie about the real position info
56         if util.platform == 'maemo':
57             if True or not self._maemo_setup_hardware_player(filetype):
58                 self._maemo_setup_software_player()
59                 log.debug( 'Using software decoding (maemo)' )
60             else:
61                 log.debug( 'Using hardware decoding (maemo)' )
62         else:
63             # This is for *ahem* "normal" versions of gstreamer
64             self._setup_playbin_player()
65             log.debug( 'Using playbin (non-maemo)' )
66
67         self._set_uri_to_be_played(uri)
68
69         bus = self.player.get_bus()
70         bus.add_signal_watch()
71         bus.connect('message', self._on_message)
72
73         self._set_volume_level( 1 )
74         return True
75
76     def get_state(self):
77         if self.player:
78             state = self.player.get_state()[1]
79             return self.STATES.get(state, 'none')
80         return 'none'
81
82     def playing(self):
83         return self.get_state() == 'playing'
84
85     def play(self):
86         if self.player:
87             log.debug("playing")
88             self.player.set_state(gst.STATE_PLAYING)
89
90     def pause(self):
91         if self.player:
92             self.player.set_state(gst.STATE_PAUSED)
93
94     def play_pause_toggle(self):
95         self.pause() if self.playing() else self.play()
96
97     def stop(self):
98         if self.player:
99             self.player.set_state(gst.STATE_NULL)
100             self.player = None
101
102     def _maemo_setup_hardware_player( self, filetype ):
103         """ Setup a hardware player for mp3 or aac audio using
104         dspaacsink or dspmp3sink """
105
106         if filetype in [ 'mp3', 'aac', 'mp4', 'm4a' ]:
107             self.player = gst.element_factory_make('playbin', 'player')
108             self.filesrc = self.player
109             self.filesrc_property = 'uri'
110             self.volume_control = self.player
111             self.volume_multiplier = 10.
112             self.volume_property = 'volume'
113             return True
114         else:
115             return False
116
117     def _maemo_setup_software_player( self ):
118         """
119         Setup a software decoding player for maemo, this is the only choice
120         for decoding wma and ogg or if audio is to be piped to a bluetooth
121         headset (this is because the audio must first be decoded only to be
122         re-encoded using sbcenc.
123         """
124
125         self.player = gst.Pipeline('player')
126         src = gst.element_factory_make('gnomevfssrc', 'src')
127         decoder = gst.element_factory_make('decodebin', 'decoder')
128         convert = gst.element_factory_make('audioconvert', 'convert')
129         resample = gst.element_factory_make('audioresample', 'resample')
130         sink = gst.element_factory_make('dsppcmsink', 'sink')
131
132         self.filesrc = src # pointer to the main source element
133         self.filesrc_property = 'location'
134         self.volume_control = sink
135         self.volume_multiplier = 1
136         self.volume_property = 'fvolume'
137
138         # Add the various elements to the player pipeline
139         self.player.add( src, decoder, convert, resample, sink )
140
141         # Link what can be linked now, the decoder->convert happens later
142         gst.element_link_many( src, decoder )
143         gst.element_link_many( convert, resample, sink )
144
145         # We can't link the two halves of the pipeline until it comes
146         # time to start playing, this singal lets us know when it's time.
147         # This is because the output from decoder can't be determined until
148         # decoder knows what it's decoding.
149         decoder.connect('pad-added',
150                         self._on_decoder_pad_added,
151                         convert.get_pad('sink') )
152
153     def _setup_playbin_player( self ):
154         """ This is for situations where we have a normal (read: non-maemo)
155         version of gstreamer like on a regular linux distro. """
156         self.player = gst.element_factory_make('playbin2', 'player')
157         self.filesrc = self.player
158         self.filesrc_property = 'uri'
159         self.volume_control = self.player
160         self.volume_multiplier = 1.
161         self.volume_property = 'volume'
162
163     def _on_decoder_pad_added(self, decoder, src_pad, sink_pad):
164         # link the decoder's new "src_pad" to "sink_pad"
165         src_pad.link( sink_pad )
166
167     def _get_volume_level(self):
168         if self.volume_control is not None:
169             vol = self.volume_control.get_property( self.volume_property )
170             return  vol / float(self.volume_multiplier)
171
172     def _set_volume_level(self, value):
173         assert  0 <= value <= 1
174
175         if self.volume_control is not None:
176             vol = value * self.volume_multiplier
177             self.volume_control.set_property( self.volume_property, vol )
178
179     def _set_uri_to_be_played(self, uri):
180         # Sets the right property depending on the platform of self.filesrc
181         if self.player is not None:
182             self.filesrc.set_property(self.filesrc_property, uri)
183
184     def _on_message(self, bus, message):
185         t = message.type
186
187         if t == gst.MESSAGE_EOS:
188             self.eos_callback()
189
190         elif t == gst.MESSAGE_ERROR:
191             err, debug = message.parse_error()
192             log.critical( 'Error: %s %s', err, debug )
193             self.stop()
194
195     def set_eos_callback(self, cb):
196         self.eos_callback = cb
197
198 class Playlist(object):
199     class Entry(object):
200         def __init__(self, data):
201             if isinstance(data, dict):
202                 self.id = data['id']
203                 self.name = data['name']
204                 self.numalbum = int(data['numalbum'])
205                 self.url = data['mp3']
206                 self.type = 'mp3'
207             elif isinstance(data, basestring): # assume URI
208                 self.id = 0
209                 self.name = ''
210                 self.numalbum = 0
211                 self.url = data
212                 self.type = 'mp3'
213         def __str__(self):
214             return "{%s}" % (", ".join([str(self.name), str(self.numalbum), str(self.url)]))
215
216     def __init__(self, items = []):
217         if items is None:
218             items = []
219         self.items = [Playlist.Entry(item) for item in items]
220         self.current = -1
221
222     def add(self, item):
223         self.items.append(Playlist.Entry(item))
224
225     def next(self):
226         if self.has_next():
227             self.current = self.current + 1
228             return self.items[self.current]
229         return None
230
231     def has_next(self):
232         return self.current < (len(self.items)-1)
233
234 class Player(Playlist):
235     def __init__(self):
236         self.gstreamer = GStreamer()
237         self.gstreamer.set_eos_callback(self._on_eos)
238         self.playlist = None
239
240     def play(self, playlist = None):
241         if playlist:
242             self.playlist = playlist
243         if self.playlist is not None:
244             if self.playlist.has_next():
245                 entry = self.playlist.next()
246                 log.debug("playing %s", entry)
247                 self.gstreamer.setup(entry.type, entry.url)
248                 self.gstreamer.play()
249
250     def pause(self):
251         self.gstreamer.pause()
252
253     def stop(self):
254         self.gstreamer.stop()
255
256     def playing(self):
257         return self.gstreamer.playing()
258
259     def next(self):
260         if self.playlist.has_next():
261             self.stop()
262             self.play()
263         else:
264             self.stop()
265
266     def prev(self):
267         pass
268
269     def _on_eos(self):
270         log.debug("EOS!")
271         self.next()