Use playbin2 instead of the old playbin
[panucci.git] / src / panucci / player.py
blobcb64d9893f907d18bdc2a1017685d5844096ae39
1 #!/usr/bin/env python
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
20 import logging
22 import pygst
23 pygst.require('0.10')
24 import gst
26 from playlist import Playlist
27 from settings import settings
28 from services import ObservableService
29 from dbusinterface import interface
31 import util
33 PLAYING, PAUSED, STOPPED, NULL = range(4)
35 class panucciPlayer(ObservableService):
36 """ """
37 signals = [ 'playing', 'paused', 'stopped' ]
39 def __init__(self):
40 self.__log = logging.getLogger('panucci.player.panucciPlayer')
41 ObservableService.__init__(self, self.signals, self.__log)
42 interface.register_player(self)
44 self.playlist = Playlist()
45 self.playlist.register( 'new-track-playing', self.on_new_track )
46 self.playlist.register( 'seek-requested', self.do_seek )
47 self.playlist.register( 'stop-requested', self.stop )
48 settings.register( 'volume_changed', self.__set_volume_level )
50 # have we preformed the initial seek?
51 self.__initial_seek_completed = False
52 self.seeking = False # are we seeking?
54 self.__player = None
55 self.__filesrc = None
56 self.__filesrc_property = None
57 self.__volume_control = None
58 self.__volume_multiplier = 1
59 self.__volume_property = None
61 self.time_format = gst.Format(gst.FORMAT_TIME)
63 def init(self, filepath=None):
64 """ This should be called by the UI once it has initialized """
65 if filepath is not None and self.playlist.load( filepath ):
66 self.play()
67 else:
68 self.playlist.load_last_played()
70 def play(self):
71 have_player = self.__player is not None
72 if have_player or self.__setup_player():
73 self.notify('playing', caller=self.play)
74 self.__initial_seek_completed = have_player
75 self.__player.set_state(gst.STATE_PLAYING)
76 return True
77 else:
78 # should something happen here? perhaps self.stop()?
79 return False
81 def pause(self):
82 pos, dur = self.get_position_duration()
83 self.notify('paused', pos, dur, caller=self.pause)
84 self.__player.set_state(gst.STATE_PAUSED)
85 self.playlist.pause(pos)
87 def play_pause_toggle(self):
88 self.pause() if self.playing else self.play()
90 def stop(self, save_resume_point=True):
91 self.notify('stopped', caller=self.stop)
93 if self.__player is not None:
94 position_string, position = self.get_formatted_position()
95 self.playlist.stop(position, save_resume_point)
96 self.__player.set_state(gst.STATE_NULL)
97 self.__player = None
99 @property
100 def playing(self):
101 return self.get_state() == PLAYING
103 def get_state(self):
104 if self.__player is None:
105 return NULL
106 else:
107 state = self.__player.get_state()[1]
108 return { gst.STATE_NULL : STOPPED,
109 gst.STATE_PAUSED : PAUSED,
110 gst.STATE_PLAYING : PLAYING }.get( state, NULL )
112 def __maemo_setup_hardware_player( self, filetype ):
113 """ Setup a hardware player for mp3 or aac audio using
114 dspaacsink or dspmp3sink """
116 if filetype in [ 'mp3', 'aac', 'mp4', 'm4a' ]:
117 self.__player = gst.element_factory_make('playbin', 'player')
118 self.__filesrc = self.__player
119 self.__filesrc_property = 'uri'
120 self.__volume_control = self.__player
121 self.__volume_multiplier = 10.
122 self.__volume_property = 'volume'
123 return True
124 else:
125 return False
127 def __maemo_setup_software_player( self ):
128 """
129 Setup a software decoding player for maemo, this is the only choice
130 for decoding wma and ogg or if audio is to be piped to a bluetooth
131 headset (this is because the audio must first be decoded only to be
132 re-encoded using sbcenc.
135 self.__player = gst.Pipeline('player')
136 src = gst.element_factory_make('gnomevfssrc', 'src')
137 decoder = gst.element_factory_make('decodebin', 'decoder')
138 convert = gst.element_factory_make('audioconvert', 'convert')
139 resample = gst.element_factory_make('audioresample', 'resample')
140 sink = gst.element_factory_make('dsppcmsink', 'sink')
142 self.__filesrc = src # pointer to the main source element
143 self.__filesrc_property = 'location'
144 self.__volume_control = sink
145 self.__volume_multiplier = 1
146 self.__volume_property = 'fvolume'
148 # Add the various elements to the player pipeline
149 self.__player.add( src, decoder, convert, resample, sink )
151 # Link what can be linked now, the decoder->convert happens later
152 gst.element_link_many( src, decoder )
153 gst.element_link_many( convert, resample, sink )
155 # We can't link the two halves of the pipeline until it comes
156 # time to start playing, this singal lets us know when it's time.
157 # This is because the output from decoder can't be determined until
158 # decoder knows what it's decoding.
159 decoder.connect( 'pad-added',
160 self.__on_decoder_pad_added,
161 convert.get_pad('sink') )
163 def __setup_playbin_player( self ):
164 """ This is for situations where we have a normal (read: non-maemo)
165 version of gstreamer like on a regular linux distro. """
167 self.__player = gst.element_factory_make('playbin2', 'player')
168 self.__filesrc = self.__player
169 self.__filesrc_property = 'uri'
170 self.__volume_control = self.__player
171 self.__volume_multiplier = 1.
172 self.__volume_property = 'volume'
174 def __setup_player(self):
175 filetype = self.playlist.get_current_filetype()
176 filepath = self.playlist.get_current_filepath()
178 if None in [ filetype, filepath ]:
179 self.__player = None
180 return False
182 # On maemo use software decoding to workaround some bugs their gst:
183 # 1. Weird volume bugs in playbin when playing ogg or wma files
184 # 2. When seeking the DSPs sometimes lie about the real position info
185 if util.platform == util.MAEMO:
186 if not settings.enable_hardware_decoding or not \
187 self.__maemo_setup_hardware_player( filetype ):
188 self.__maemo_setup_software_player()
189 self.__log.info( 'Using software decoding (maemo)' )
190 else:
191 self.__log.info( 'Using hardware decoding (maemo)' )
192 else:
193 # This is for *ahem* "normal" versions of gstreamer
194 self.__setup_playbin_player()
195 self.__log.info( 'Using playbin (non-maemo)' )
197 self.__set_uri_to_be_played( 'file://' + filepath )
199 bus = self.__player.get_bus()
200 bus.add_signal_watch()
201 bus.connect('message', self.__on_message)
202 self.__set_volume_level( settings.volume )
204 return True
206 def __on_decoder_pad_added(self, decoder, src_pad, sink_pad):
207 # link the decoder's new "src_pad" to "sink_pad"
208 src_pad.link( sink_pad )
210 def __get_volume_level(self):
211 if self.__volume_control is not None:
212 vol = self.__volume_control.get_property( self.__volume_property )
213 return vol / float(self.__volume_multiplier)
215 def __set_volume_level(self, value):
216 assert 0 <= value <= 1
218 if self.__volume_control is not None:
219 vol = value * self.__volume_multiplier
220 self.__volume_control.set_property( self.__volume_property, vol )
222 def __set_uri_to_be_played( self, uri ):
223 # Sets the right property depending on the platform of self.__filesrc
224 if self.__player is not None:
225 self.__filesrc.set_property( self.__filesrc_property, uri )
227 def add_bookmark_at_current_position( self ):
228 label, position = player.get_formatted_position()
229 self.playlist.save_bookmark( label, position )
230 self.__log.info('Added bookmark: %s - %d', label, position)
231 return label, position
233 def get_formatted_position(self, pos=None):
234 if pos is None:
235 if self.playing:
236 (pos, dur) = self.get_position_duration()
237 else:
238 pos = self.playlist.get_current_position()
239 text = util.convert_ns(pos)
240 return (text, pos)
242 def get_position_duration(self):
243 """ returns [ current position, total duration ] """
244 try:
245 pos_int = self.__player.query_position(self.time_format, None)[0]
246 dur_int = self.__player.query_duration(self.time_format, None)[0]
247 except Exception, e:
248 #self.__log.exception('Error getting position...')
249 pos_int = dur_int = 0
250 return pos_int, dur_int
252 def do_seek(self, from_beginning=None, from_current=None, percent=None ):
253 """ Takes one of the following keyword arguments:
254 from_beginning=n: seek n nanoseconds from the start of the file
255 from_current=n: seek n nanoseconds from the current position
256 percent=n: seek n percent from the beginning of the file
258 error = False
259 position, duration = self.get_position_duration()
261 # if we're on maemo and paused the position is wrong (go figure...)
262 if util.platform == util.MAEMO and not self.playing:
263 position = self.playlist.get_current_position()
265 # if position and duration are 0 then player_get_position caught an
266 # exception. Therefore self.__player isn't ready to be seeking.
267 if not ( position or duration ) or self.__player is None:
268 error = True
269 else:
270 if from_beginning is not None:
271 assert from_beginning >= 0
272 position = min( from_beginning, duration )
273 elif from_current is not None:
274 position = max( 0, min( position+from_current, duration ))
275 elif percent is not None:
276 assert 0 <= percent <= 1
277 position = int(duration*percent)
278 else:
279 self.__log.warning('No seek parameters specified.')
280 error = True
282 if not error:
283 self.__log.debug('do_seek: Seeking to: %d', position)
284 self.__seek(position)
285 return position, duration
286 else:
287 self.__log.debug('do_seek: Could not seek.')
289 return False
291 def __seek(self, position):
292 # Don't use this, use self.do_seek instead
293 self.seeking = True
294 error = False
296 try:
297 self.__player.seek_simple(
298 self.time_format, gst.SEEK_FLAG_FLUSH, position )
299 except Exception, e:
300 self.__log.exception( 'Error seeking' )
301 error = True
303 # (see above) if we're paused and on maemo update the PlaylistItem
304 if not error and util.platform == util.MAEMO and not self.playing:
305 self.playlist.pause(position)
307 self.seeking = False
308 return not error
310 def on_new_track(self):
311 self.stop(save_resume_point=False)
312 self.play()
314 def __on_message(self, bus, message):
315 t = message.type
316 # self.__log.debug('Got message of type %s', t)
318 if t == gst.MESSAGE_EOS and not self.playlist.next():
319 self.stop(save_resume_point=False)
321 elif t == gst.MESSAGE_ERROR:
322 err, debug = message.parse_error()
323 self.__log.critical( 'Error: %s %s', err, debug )
324 self.stop()
326 elif t == gst.MESSAGE_STATE_CHANGED:
327 if ( message.src == self.__player and
328 message.structure['new-state'] == gst.STATE_PLAYING ):
330 if not self.__initial_seek_completed:
331 # This only gets called when the file is first loaded
332 pause_time = self.playlist.play()
333 # don't seek if position is 0
334 if pause_time > 0:
335 self.__log.info('Seeking to %d' % pause_time)
336 # seek manually; on maemo it is sometimes impossible
337 # to query the player this early in the process
338 self.__seek(pause_time)
340 self.__initial_seek_completed = True
342 def quit(self):
343 """ Called when the application exits """
344 self.stop()
345 self.playlist.quit()
347 # there should only ever be one panucciPlayer object
348 player = panucciPlayer()