root/branches/0.3/src/audio_control.py

Revision 801, 31.4 kB (checked in by nicfit, 1 year ago)

Fixed TB

Line 
1 ################################################################################
2 #  Copyright (C) 2006  Travis Shirk <travis@pobox.com>
3 #
4 #  This program is free software; you can redistribute it and/or modify
5 #  it under the terms of the GNU General Public License as published by
6 #  the Free Software Foundation; either version 2 of the License, or
7 #  (at your option) any later version.
8 #
9 #  This program is distributed in the hope that it will be useful,
10 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #  GNU General Public License for more details.
13 #
14 #  You should have received a copy of the GNU General Public License
15 #  along with this program; if not, write to the Free Software
16 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 #
18 #  $Id$
19 ################################################################################
20 import gtk, gtk.glade
21 import os, gobject
22
23 import mesk, mesk.audio
24 import gst
25
26 import mesk, mesk.audio, mesk.playlist, mesk.plugin
27 from mesk.plugin.interfaces import AudioControlListener
28 from mesk.i18n import _
29
30 import control
31 import mesk.gtk_utils
32
33 class AudioControl(control.Control):
34     # States
35     (STOP,
36      PLAY,
37      PAUSE) = range(3)
38
39     # Internal clock interval
40     TICK_MS = 500
41
42     # Tooltip strings
43     PLAY_TOOLTIP  = _('Play')
44     PAUSE_TOOLTIP = _('Pause')
45
46     # When set_playlist is called with None, this is used. MUST have something
47     NULL_PLAYLIST = mesk.playlist.Playlist()
48
49     def __init__(self, parent_xml, parent_win, playlist=None):
50         control.Control.__init__(self)
51
52         self._parent_xml = parent_xml
53         self._parent_win = parent_win
54         self._playlist = playlist
55         self._current_audio_src = None
56         self._last_src_started = None
57         self._volume_button_tip = gtk.Tooltips()
58         self._volume_scale_tip = gtk.Tooltips()
59
60         # Volume slider window
61         vol_xml = mesk.gtk_utils.get_glade('volume_slider_window',
62                                            'audio_control.glade')
63         vol_xml.signal_autoconnect(self)
64         self._volume_window = vol_xml.get_widget('volume_slider_window')
65         self._volume_scale = vol_xml.get_widget('volume_scale')
66         self._volume_scale_ebox = vol_xml.get_widget('volume_scale_eventbox')
67
68         self._state = self.STOP
69         self._tick_stopped = True
70         self._duration = gst.CLOCK_TIME_NONE
71
72         # Set up volume widgets
73         self._volume_ebox = parent_xml.get_widget('volume_eventbox')
74         self._volume_togglebutton = parent_xml.get_widget('volume_togglebutton')
75         self._volume_mute_img = None
76         self._volume_low_img = None
77         self._volume_medium_img = None
78         self._volume_high_img = None
79
80         # Control widgets
81         self._prev_button = parent_xml.get_widget('prev_button')
82         self._play_pause_button = parent_xml.get_widget('play_pause_button')
83         self._stop_button = parent_xml.get_widget('stop_button')
84         self._next_button = parent_xml.get_widget('next_button')
85
86         self._track_time_label = parent_xml.get_widget('track_time_label')
87         self._track_scale = parent_xml.get_widget('track_scale')
88         self._track_scale.set_sensitive(False)
89         self._track_scale_adj = gtk.Adjustment(value = 0.0, lower = 0.0,
90                                                upper = 100.0,
91                                                step_incr = 0.1, page_incr = 1.0,
92                                                page_size = 1.0)
93         self._track_scale.set_adjustment(self._track_scale_adj)
94
95         # Play/Pause button images
96         self._play_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY,
97                                                     gtk.ICON_SIZE_BUTTON)
98         self._pause_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE,
99                                                      gtk.ICON_SIZE_BUTTON)
100         self._play_pause_state = self.PLAY
101         self._play_pause_button.remove(self._play_pause_button.get_child())
102         self._play_pause_button.add(self._play_image)
103         self._play_image.show()
104
105         # Define custom signals
106         gobject.signal_new('play', AudioControl, gobject.SIGNAL_RUN_LAST,
107                            gobject.TYPE_NONE, [])
108         gobject.signal_new('pause', AudioControl, gobject.SIGNAL_RUN_LAST,
109                            gobject.TYPE_NONE, [])
110         gobject.signal_new('stopped', AudioControl, gobject.SIGNAL_RUN_LAST,
111                            gobject.TYPE_NONE, [])
112         gobject.signal_new('next', AudioControl, gobject.SIGNAL_RUN_LAST,
113                            gobject.TYPE_NONE, [])
114         gobject.signal_new('prev', AudioControl, gobject.SIGNAL_RUN_LAST,
115                            gobject.TYPE_NONE, [])
116         gobject.signal_new('source-changed', AudioControl,
117                            gobject.SIGNAL_RUN_LAST,
118                            gobject.TYPE_NONE,
119                            [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])
120         gobject.signal_new('error', AudioControl, gobject.SIGNAL_RUN_LAST,
121                            gobject.TYPE_NONE, [gobject.TYPE_STRING,
122                                                gobject.TYPE_PYOBJECT])
123         # Callback takes: widget, audio_src
124         gobject.signal_new('tag-update', AudioControl, gobject.SIGNAL_RUN_LAST,
125                            gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT])
126         gobject.signal_new('playlist-reset', AudioControl,
127                            gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
128
129         # Hook up audio control events
130         self._prev_button.connect('clicked', self._on_prev_button_clicked)
131         self._play_pause_button.connect('clicked',
132                                         self._on_play_pause_button_clicked)
133         self._stop_button.connect('clicked', self._on_stop_button_clicked)
134         self._next_button.connect('clicked', self._on_next_button_clicked)
135
136         self.set_widget_sensitivites()
137         mesk.log.verbose(_('Gstreamer version %d.%d.%d audio control '
138                            'initialized') % (gst.gst_version[0],
139                                              gst.gst_version[1],
140                                              gst.gst_version[2]))
141         # Initialize Gstreamer
142         self._gst_sink = self._gst_bin = None
143         self._create_gst_pipeline()
144
145         # Set initial volume from config
146         vol = mesk.config.getfloat(mesk.CONFIG_AUDIO, 'volume')
147         vol = max(vol, 0.0)
148         vol = min(vol, 1.0)
149         self._unmute_volume = None
150         self._volume = None
151         self.set_volume(vol)
152
153         self._parent_win.connect('configure-event',
154                                  self._on_main_window_configure_event)
155         self._parent_xml.signal_autoconnect(self)
156
157         import urllib2
158         self._http_creds = urllib2.HTTPPasswordMgr()
159
160     def set_playlist(self, pl):
161         if pl is None:
162             self._playlist = self.NULL_PLAYLIST
163         else:
164             self._playlist = pl
165
166         self.set_widget_sensitivites()
167         curr = self._playlist.get_curr_index()
168         if curr < 0 and len(self._playlist):
169             self.enqueue_source(next=True, start_playing=False)
170         elif len(self._playlist):
171             self.enqueue_source(absolute=curr, start_playing=False)
172
173     def set_widget_sensitivites(self):
174         if not self._playlist or not len(self._playlist):
175             # No playlist, nothing to do
176             self._prev_button.set_sensitive(False)
177             self._play_pause_button.set_sensitive(False)
178             self._stop_button.set_sensitive(False)
179             self._next_button.set_sensitive(False)
180             self._track_scale.set_sensitive(False)
181         else:
182             self._play_pause_button.set_sensitive(True)
183
184             if self.is_playing():
185                 self._stop_button.set_sensitive(True)
186                 self._track_scale.set_sensitive(True)
187             elif self.is_stopped():
188                 self._stop_button.set_sensitive(False)
189                 self._track_scale.set_sensitive(False)
190
191             # Next/previous buttons
192             self._prev_button.set_sensitive(self._playlist.has_prev())
193             self._next_button.set_sensitive(self._playlist.has_next())
194
195     def _set_track_scale(self, pos, lower = None, upper = None):
196         if lower is not None:
197             self._track_scale_adj.lower = lower
198         if upper is not None:
199             self._track_scale_adj.upper = upper
200         self._track_scale_adj.value = pos
201
202     def is_playing(self):
203         return self._state == self.PLAY
204     def is_paused(self):
205         return self._state == self.PAUSE
206     def is_stopped(self):
207         return self._state == self.STOP
208
209     def play(self):
210         if self.is_playing():
211             return
212
213         self._adjusting_scale_timeout_id = None
214         self._scale_mouse_event = False
215         self._set_play_pause_state(self.PAUSE)
216
217         self._gst_error_set = False
218         self._state = self.PLAY
219         self._gst_bin.set_state(gst.STATE_PLAYING)
220         if self._gst_error_set:
221             self._gst_error_set = False
222             return
223
224         self._start_play_tick()
225
226         self.emit('play')
227         mesk.plugin.emit(AudioControlListener, 'on_plugin_audio_play',
228                          self._current_audio_src)
229         if self._last_src_started is None:
230             self._last_src_started = self._current_audio_src
231             mesk.plugin.emit(AudioControlListener, 'on_plugin_source_started',
232                              self._current_audio_src)
233
234     def pause(self):
235         if self.is_paused():
236             return
237
238         self._set_play_pause_state(self.PLAY)
239         self._stop_play_tick()
240         self._state = self.PAUSE
241         self._gst_bin.set_state(gst.STATE_PAUSED)
242
243         self.emit('pause')
244         mesk.plugin.emit(AudioControlListener, 'on_plugin_audio_pause',
245                          self._current_audio_src)
246
247     def stop(self):
248         if self.is_stopped():
249             return
250
251         self._set_play_pause_state(self.PLAY)
252         self._state = self.STOP
253         self._gst_bin.set_state(gst.STATE_NULL)
254         self._gst_bin.set_state(gst.STATE_READY)
255         self._stop_play_tick()
256         self._set_track_scale(0.0)
257
258         self.emit('stopped')
259         self._last_src_started = None
260         mesk.plugin.emit(AudioControlListener, 'on_plugin_audio_stop',
261                          self._current_audio_src)
262
263     def seek(self, location):
264         '''location is the position to seek to in nanoseconds'''
265         mesk.plugin.emit(AudioControlListener, 'on_plugin_audio_seek',
266                          self._current_audio_src)
267         unpause = False
268         if self.is_playing():
269             self.pause()
270             unpause = True
271
272         event = gst.event_new_seek(1.0, gst.FORMAT_TIME,
273                                    gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE,
274                                    gst.SEEK_TYPE_SET, location,
275                                    gst.SEEK_TYPE_NONE, 0)
276         if self._gst_bin.send_event(event):
277             mesk.log.debug('Seeking to location %r' % location)
278             self._gst_bin.set_new_stream_time(0L)
279         else:
280             mesk.log.error('Seeking to location %r failed' % location)
281
282         if unpause:
283             self.play()
284
285     def next(self, play=None):
286         is_playing = self.is_playing() or self.is_paused()
287         if is_playing:
288             self.stop()
289         self.enqueue_source(next=True, start_playing=(is_playing or play))
290         self.set_widget_sensitivites()
291         self.emit('next')
292
293     def prev(self):
294         is_playing = self.is_playing() or self.is_paused()
295         if is_playing:
296             self.stop()
297         self.enqueue_source(prev=True, start_playing=is_playing)
298         self.set_widget_sensitivites()
299         self.emit('prev')
300
301     def get_position(self):
302         try:
303             position, format = self._gst_bin.query_position(gst.FORMAT_TIME)
304         except:
305             position = gst.CLOCK_TIME_NONE
306         try:
307             duration, format = self._gst_bin.query_duration(gst.FORMAT_TIME)
308         except:
309             duration = gst.CLOCK_TIME_NONE
310         return (position, duration)
311
312     def get_volume(self):
313         return self._volume
314     def set_volume(self, v):
315         '''Volume is value between 0.0 and 1.0'''
316         if v < 0.0:
317             v = 0.0
318         elif v > 1.0:
319             v = 1.0
320         elif v == self._volume:
321             return
322
323         self._volume = v
324         self._gst_bin.set_property('volume', v)
325
326         self._set_volume_image()
327         self._volume_scale.set_value(v)
328         tip_str = _('Volume %d%%') % int(self._volume * 100.0)
329         self._volume_button_tip.set_tip(self._volume_ebox, tip_str)
330         self._volume_scale_tip.set_tip(self._volume_scale_ebox, tip_str)
331
332         # Update config
333         mesk.config.set(mesk.CONFIG_AUDIO, 'volume', str(self._volume))
334
335     def _set_play_pause_state(self, state):
336         assert(state == self.PLAY or state == self.PAUSE)
337
338         def update_pp_button():
339             self._play_pause_button.remove(self._play_pause_button.get_child())
340             if state == self.PAUSE:
341                 self._play_pause_state = self.PAUSE
342                 old_image = self._play_image
343                 new_image = self._pause_image
344                 new_tip = self.PAUSE_TOOLTIP
345             else:
346                 self._play_pause_state = self.PLAY
347                 old_image = self._pause_image
348                 new_image = self._play_image
349                 new_tip = self.PLAY_TOOLTIP
350             old_image.hide()
351             self._play_pause_button.add(new_image)
352             new_image.show()
353
354             tips = gtk.tooltips_data_get(self._play_pause_button)[0]
355             tips.set_tip(self._play_pause_button, new_tip)
356
357         # Doing the button updates from this method would often lead to a
358         # blank button laggy (no image) update
359         gobject.timeout_add(250, update_pp_button)
360
361
362     def enqueue_source(self, args=None, **keywords):
363         '''
364         args is a dictionary supporting the following keys:
365         Mutally exclusive:
366         next: increment playlst (bool)
367         prev: decrement playlist (bool)
368         absolute: absolute list position (int)
369
370         start_playing:  start playing the enqueued source (bool)
371
372         Note, keyword arguments may be used in place of the args dict.
373         '''
374
375         if not args:
376             args = keywords
377
378         mesk.log.debug('AudioControl.enqueue_source: %s' % str(args))
379         def arg_value(key):
380             try:
381                 return args[key]
382             except:
383                 return None
384
385         if self._playlist is None:
386             return
387
388         self.stop()
389
390         # Determine previous source
391         old = self._playlist.get_curr_index()
392         if old is None:
393             old = -1
394         elif old == -1:
395             old = 0
396         old_src = None
397         if old is not None:
398             try:
399                 old_src = self._playlist[old]
400             except IndexError:
401                 pass
402
403         # Determine new source
404         src = None
405         if arg_value('next'):
406             src = self._playlist.get_next()
407         elif arg_value('prev'):
408             src = self._playlist.get_prev()
409         elif arg_value('absolute') is not None:
410             i = arg_value('absolute')
411             try:
412                 src = self._playlist[i]
413             except IndexError:
414                 i = 0
415                 src = self._playlist[0]
416             self._playlist.set_curr_index(i)
417
418         new = self._playlist.get_curr_index()
419         if new is None or new < 0:
420             new = -1
421
422         self._last_src_started = None
423         if src:
424             try:
425                 self._set_source(src)
426             except Exception, ex:
427                 mesk.log.error('enqueue_source error: %s' % str(ex))
428                 self.emit('error', ex, src)
429                 return
430
431             if arg_value('start_playing'):
432                 self.play()
433         else:
434             # Emitting before reset allows listener to capture pre-reset state
435             self.emit('playlist-reset')
436             self._playlist.reset()
437         self.emit('source-changed', (old, old_src), (new, src))
438
439     def _set_source(self, src):
440         mesk.log.debug("AudioControl._set_source %s %s" % (str(src),
441                                                            str(src.uri)))
442         if src and src.uri.scheme.startswith('http'):
443             base_uri = os.path.dirname(str(src.uri))
444
445             # Test http access to the URI
446             import gnomevfs
447             done = False
448             while not done:
449                 try:
450                     info = gnomevfs.get_file_info(src.uri)
451                     done = True
452                 except gnomevfs.AccessDeniedError, ex:
453                     # See if we have credentials cached
454                     (user, pw) = self._http_creds.find_user_password('mesk',
455                                                                      base_uri)
456                     if not user or not pw:
457                         # Handle HTTP auth
458                         (resp, user, pw) = self._handle_http_auth(src.uri)
459                         if resp != 0:
460                             raise ex
461
462                     # Set credentials
463                     src.uri.user_name = user
464                     src.uri.password = pw
465                     # Try again with new creds ....
466                 except (gnomevfs.GenericError, gnomevfs.NotSupportedError), ex:
467                     # gnomevfs does not like streams, hope for the best..
468                     done = True
469
470             if src.uri.user_name and src.uri.password:
471                 # Cache credentials
472                 self._http_creds.add_password('mesk', base_uri,
473                                               src.uri.user_name,
474                                               src.uri.password)
475
476         # Initialize sink with fresh source
477         self._set_track_scale(0.0)
478         if src:
479             self._gst_bin.set_property('uri', str(src.uri))
480         else:
481             self._gst_bin.set_property('uri', '')
482         self._current_audio_src = src
483
484     def _handle_http_auth(self, uri):
485         auth_xml = mesk.gtk_utils.get_glade('http_auth_dialog',
486                                             'audio_control.glade')
487         auth_dialog = auth_xml.get_widget('http_auth_dialog')
488         auth_xml.get_widget('url_label').set_text(str(uri))
489
490         username = ''
491         passwd = ''
492         resp = auth_dialog.run()
493         if resp == 0:
494             username = auth_xml.get_widget('username_entry').get_text()
495             passwd = auth_xml.get_widget('password_entry').get_text()
496
497         auth_dialog.destroy()
498         return (resp, username, passwd)
499
500     ### Button event handlers ###
501     def _on_play_pause_button_clicked(self, button):
502         if self._play_pause_state == self.PLAY:
503             self.play()
504         else:
505             self.pause()
506
507     def _on_stop_button_clicked(self, button):
508         self.stop()
509
510     def _on_prev_button_clicked(self, button):
511         self.prev()
512
513     def _on_next_button_clicked(self, button):
514         self.next()
515
516     ### Gstreamer Pipeline Creation ###
517     def _create_gst_pipeline(self):
518         (sink_name, self._gst_sink) = self._create_gst_sink()
519         if not self._gst_sink:
520             msg = 'Unable to locate a valid Gstreamer sync'
521             mesk.log.error(msg)
522             raise mesk.MeskException(msg)
523
524         if self._gst_bin:
525             # Cleanup existing bin
526             self._gst_bin.set_state(gst.STATE_NULL)
527             del