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/>.
26 from playlist
import Playlist
27 from settings
import settings
28 from services
import ObservableService
29 from dbusinterface
import interface
33 PLAYING
, PAUSED
, STOPPED
, NULL
= range(4)
35 class panucciPlayer(ObservableService
):
37 signals
= [ 'playing', 'paused', 'stopped' ]
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?
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
):
68 self
.playlist
.load_last_played()
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
)
78 # should something happen here? perhaps self.stop()?
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
)
101 return self
.get_state() == PLAYING
104 if self
.__player
is None:
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'
127 def __maemo_setup_software_player( self
):
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
]:
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)' )
191 self
.__log
.info( 'Using hardware decoding (maemo)' )
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
)
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):
236 (pos
, dur
) = self
.get_position_duration()
238 pos
= self
.playlist
.get_current_position()
239 text
= util
.convert_ns(pos
)
242 def get_position_duration(self
):
243 """ returns [ current position, total duration ] """
245 pos_int
= self
.__player
.query_position(self
.time_format
, None)[0]
246 dur_int
= self
.__player
.query_duration(self
.time_format
, None)[0]
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
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:
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
)
279 self
.__log
.warning('No seek parameters specified.')
283 self
.__log
.debug('do_seek: Seeking to: %d', position
)
284 self
.__seek
(position
)
285 return position
, duration
287 self
.__log
.debug('do_seek: Could not seek.')
291 def __seek(self
, position
):
292 # Don't use this, use self.do_seek instead
297 self
.__player
.seek_simple(
298 self
.time_format
, gst
.SEEK_FLAG_FLUSH
, position
)
300 self
.__log
.exception( 'Error seeking' )
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
)
310 def on_new_track(self
):
311 self
.stop(save_resume_point
=False)
314 def __on_message(self
, bus
, message
):
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
)
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
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
343 """ Called when the application exits """
347 # there should only ever be one panucciPlayer object
348 player
= panucciPlayer()