Ticket #3714: 0002-Code-improved-for-better-performance-and-robustness.patch
File 0002-Code-improved-for-better-performance-and-robustness.patch, 49.6 KB (added by humitos, 11 years ago) |
---|
-
activity.py
From 34f257acc510495ecb05a42b8e7669191d6a3d3f Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann <humitos@gmail.com> Date: Tue, 15 Jan 2013 17:55:47 -0300 Subject: [PATCH Jukebox 2/2] Code improved for better performance and robustness - Visualization on Audio streams removed This cause a 100% of CPU usage on XO-4 and XO-1.75. An empty widget is shown when an Audio stream is played and the Video stream is shown if a Video stream is played. - Better control over the playlist Use signals ('play-index' for example) to communicate between the main window and the playlist. Save and Load playlists and Add track methods are reduced in complexity. - Pause Video when lost focus If the user is playing a Video stream, this is paused if he changes the focus to another Activity. - Controls properly setted Show the correct icon (Play / Pause) depending on the status of the stream. - Old code removed Removed code to show some old notifications about missing tracks. - Put Gst.Pipeline in Gst.State.NULL at exit This is required by gstreamer to be able to clean up the Player and free its memory. JukeboxActivity.can_close method is used for this. - Do not initialize video sink (videoscale, videoconvert, autovideosink, etc) This was causing some problems on performance to reproduce .ogv Video streams on XO-4. Signed-off-by: Manuel Kaufmann <humitos@gmail.com> --- activity.py | 553 +++++++++++++----------------------------------------------- controls.py | 174 +++++++++++++++++-- player.py | 77 ++------- playlist.py | 136 ++++++++++++--- 4 files changed, 401 insertions(+), 539 deletions(-) diff --git a/activity.py b/activity.py index 14fcbe9..54d4fa3 100644
a b 21 21 22 22 import sys 23 23 import logging 24 import tempfile25 24 from gettext import gettext as _ 26 import os27 25 28 26 from sugar3.activity import activity 29 from sugar3.graphics.objectchooser import ObjectChooser30 27 from sugar3 import mime 31 28 from sugar3.datastore import datastore 32 29 … … gi.require_version('Gst', '1.0') 44 41 from gi.repository import GObject 45 42 from gi.repository import Gdk 46 43 from gi.repository import Gtk 47 from gi.repository import Gst48 44 from gi.repository import Gio 49 45 50 import urllib51 46 from viewtoolbar import ViewToolbar 52 47 from controls import Controls 53 48 from player import GstPlayer … … PLAYLIST_WIDTH_PROP = 1.0 / 3 58 53 59 54 60 55 class JukeboxActivity(activity.Activity): 61 UPDATE_INTERVAL = 500 56 57 __gsignals__ = { 58 'no-stream': (GObject.SignalFlags.RUN_FIRST, None, []), 59 } 62 60 63 61 def __init__(self, handle): 64 62 activity.Activity.__init__(self, handle) 65 self._object_id = handle.object_id 66 self.set_title(_('Jukebox Activity')) 63 67 64 self.player = None 68 self.max_participants = 169 65 self._playlist_jobject = None 70 66 67 self.set_title(_('Jukebox Activity')) 68 self.max_participants = 1 69 71 70 toolbar_box = ToolbarBox() 72 71 activity_button = ActivityToolbarButton(self) 73 72 activity_toolbar = activity_button.page 74 73 toolbar_box.toolbar.insert(activity_button, 0) 75 74 self.title_entry = activity_toolbar.title 76 75 77 # FIXME: I don't know what is the mission of this line 78 # activity_toolbar.stop.hide() 79 80 self.volume_monitor = Gio.VolumeMonitor.get() 81 self.volume_monitor.connect('mount-added', self._mount_added_cb) 82 self.volume_monitor.connect('mount-removed', self._mount_removed_cb) 83 84 _view_toolbar = ViewToolbar() 85 _view_toolbar.connect('go-fullscreen', 76 view_toolbar = ViewToolbar() 77 view_toolbar.connect('go-fullscreen', 86 78 self.__go_fullscreen_cb) 87 _view_toolbar.connect('toggle-playlist',79 view_toolbar.connect('toggle-playlist', 88 80 self.__toggle_playlist_cb) 89 81 view_toolbar_button = ToolbarButton( 90 page= _view_toolbar,82 page=view_toolbar, 91 83 icon_name='toolbar-view') 92 _view_toolbar.show()84 view_toolbar.show() 93 85 toolbar_box.toolbar.insert(view_toolbar_button, -1) 94 86 view_toolbar_button.show() 95 87 96 self.control = Controls(toolbar_box.toolbar, self) 88 self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 89 90 self.playlist_widget = PlayList() 91 self.playlist_widget.connect('play-index', self.__play_index) 92 self.playlist_widget.show() 93 self.canvas.pack_start(self.playlist_widget, False, True, 0) 94 95 # Create the player just once 96 self.player = GstPlayer() 97 self.player.connect('eos', self.__player_eos_cb) 98 self.player.connect('error', self.__player_error_cb) 99 self.player.connect('play', self.__player_play_cb) 100 101 self.control = Controls(self, toolbar_box.toolbar) 97 102 98 103 toolbar_box.toolbar.insert(StopButton(self), -1) 99 104 100 105 self.set_toolbar_box(toolbar_box) 101 106 toolbar_box.show_all() 102 107 103 self.connect( "key_press_event", self._key_press_event_cb)108 self.connect('key_press_event', self.__key_press_event_cb) 104 109 105 110 # We want to be notified when the activity gets the focus or 106 # loses it. 111 # loses it. When it is not active, we don't need to keep 107 112 # reproducing the video 108 self.connect("notify::active", self._notify_active_cb) 109 110 # FIXME: this is related with shared activity and it doesn't work 111 # if handle.uri: 112 # pass 113 # elif self._shared_activity: 114 # if self.get_shared(): 115 # pass 116 # else: 117 # # Wait for a successful join before trying to get the document 118 # self.connect("joined", self._joined_cb) 119 120 self.update_id = -1 121 self.changed_id = -1 122 self.seek_timeout_id = -1 123 self.player = None 124 self.uri = None 125 126 # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'} 127 self.playlist = [] 128 129 self.jobjectlist = [] 130 self.playpath = None 131 self.currentplaying = None 132 self.playflag = False 133 self._not_found_files = 0 134 135 # README: I changed this because I was getting an error when I 136 # tried to modify self.bin with something different than 137 # Gtk.Bin 113 self.connect('notify::active', self.__notify_active_cb) 138 114 139 # self.bin = Gtk.HBox() 140 # self.bin.show() 115 self._empty_widget = Gtk.Label(label='') 141 116 142 self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)143 self._alert = None144 145 self.playlist_widget = PlayList(self.play)146 self.playlist_widget.update(self.playlist)147 self.playlist_widget.show()148 self.canvas.pack_start(self.playlist_widget, False, True, 0)149 self._empty_widget = Gtk.Label(label="")150 117 self._empty_widget.show() 151 118 self.videowidget = VideoWidget() 152 119 self.set_canvas(self.canvas) … … class JukeboxActivity(activity.Activity): 154 121 self.show_all() 155 122 self.canvas.connect('size-allocate', self.__size_allocate_cb) 156 123 157 #From ImageViewer Activity 158 self._want_document = True 159 if self._object_id is None: 160 self._show_object_picker = GObject.timeout_add(1000, \ 161 self._show_picker_cb) 124 self.player.init_view_area(self.videowidget) 162 125 126 # Check if the activity was launched from a Journal Entry 163 127 if handle.uri: 164 self.uri = handle.uri 165 GObject.idle_add(self._start, self.uri, handle.title) 128 self._start(handle.uri, handle.title) 166 129 167 # Create the player just once 168 logging.debug('Instantiating GstPlayer') 169 self.player = GstPlayer(self.videowidget) 170 self.player.connect("eos", self._player_eos_cb) 171 self.player.connect("error", self._player_error_cb) 172 self.p_position = Gst.CLOCK_TIME_NONE 173 self.p_duration = Gst.CLOCK_TIME_NONE 174 175 def _notify_active_cb(self, widget, event): 130 self._volume_monitor = Gio.VolumeMonitor.get() 131 self._volume_monitor.connect('mount-added', self._mount_added_cb) 132 self._volume_monitor.connect('mount-removed', self._mount_removed_cb) 133 134 def __notify_active_cb(self, widget, event): 176 135 """Sugar notify us that the activity is becoming active or inactive. 177 136 When we are inactive, we stop the player if it is reproducing 178 137 a video. 179 138 """ 180 if self.player.player.props.uri is not None: 139 140 logging.debug('JukeboxActivity notify::active signal received') 141 142 if self.player.player.props.current_uri is not None and \ 143 self.player.playing_video(): 181 144 if not self.player.is_playing() and self.props.active: 182 145 self.player.play() 183 146 if self.player.is_playing() and not self.props.active: … … class JukeboxActivity(activity.Activity): 213 176 playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP) 214 177 self.playlist_widget.set_size_request(playlist_width, 0) 215 178 216 def open_button_clicked_cb(self, widget): 217 """ To open the dialog to select a new file""" 218 #self.player.seek(0L) 219 #self.player.stop() 220 #self.playlist = [] 221 #self.playpath = None 222 #self.currentplaying = None 223 #self.playflag = False 224 self._want_document = True 225 self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb) 226 227 def _key_press_event_cb(self, widget, event): 179 def __key_press_event_cb(self, widget, event): 228 180 keyname = Gdk.keyval_name(event.keyval) 229 logging.info("Keyname Press: %s, time: %s", keyname, event.time) 181 230 182 if self.title_entry.has_focus(): 231 183 return False 232 184 233 if keyname == "space":234 self. play_toggled()185 if keyname == 'space': 186 self.control._button_clicked_cb(None) 235 187 return True 236 188 237 def check_if_next_prev(self):238 if self.currentplaying == 0:239 self.control.prev_button.set_sensitive(False)240 else:241 self.control.prev_button.set_sensitive(True)242 if self.currentplaying == len(self.playlist) - 1:243 self.control.next_button.set_sensitive(False)244 else:245 self.control.next_button.set_sensitive(True)246 247 189 def songchange(self, direction): 248 #if self.playflag: 249 # self.playflag = False 250 # return 251 self.player.seek(0L) 252 if direction == "prev" and self.currentplaying > 0: 253 self.play(self.currentplaying - 1) 254 logging.info("prev: " + self.playlist[self.currentplaying]['url']) 255 #self.playflag = True 256 elif direction == "next" and \ 257 self.currentplaying < len(self.playlist) - 1: 258 self.play(self.currentplaying + 1) 259 logging.info("next: " + self.playlist[self.currentplaying]['url']) 260 #self.playflag = True 190 current_playing = self.playlist_widget._current_playing 191 if direction == 'prev' and current_playing > 0: 192 self.play_index(current_playing - 1) 193 elif direction == 'next' and \ 194 self.playlist_widget._current_playing < len(self.playlist_widget._items) - 1: 195 self.play_index(self.playlist_widget._current_playing + 1) 261 196 else: 262 self.play_toggled()263 self.player.stop()264 197 self._switch_canvas(show_video=False) 265 self.player.set_uri(None) 266 self.check_if_next_prev() 198 self.emit('no-stream') 267 199 268 def play (self, media_index):200 def play_index(self, index): 269 201 self._switch_canvas(show_video=True) 270 self.currentplaying = media_index 271 url = self.playlist[self.currentplaying]['url'] 272 error = None 273 if url.startswith('journal://'): 274 try: 275 jobject = datastore.get(url[len("journal://"):]) 276 url = 'file://' + jobject.file_path 277 except: 278 path = url[len("journal://"):] 279 error = _('The file %s was not found') % path 280 281 self.check_if_next_prev() 282 283 if error is None: 284 self.player.set_uri(url) 285 self.player.play() 286 else: 287 self.control.set_disabled() 288 self._show_error_alert(error) 202 self.playlist_widget._current_playing = index 289 203 290 self.playlist_widget.set_cursor(self.currentplaying) 204 path = self.playlist_widget._items[index]['path'] 205 self.control.check_if_next_prev() 291 206 292 def _player_eos_cb(self, widget):293 self. songchange('next')207 self.player.set_uri(path) 208 self.player.play() 294 209 295 def _show_error_alert(self, title, msg=None): 296 self._alert = ErrorAlert() 297 self._alert.props.title = title 298 if msg is not None: 299 self._alert.props.msg = msg 300 self.add_alert(self._alert) 301 self._alert.connect('response', self._alert_cancel_cb) 302 self._alert.show() 210 def __play_index(self, widget, index, path): 211 self._switch_canvas(show_video=True) 212 self.playlist_widget._current_playing = index 213 214 self.control.check_if_next_prev() 215 216 self.player.set_uri(path) 217 self.player.play() 218 219 def __player_eos_cb(self, widget): 220 self.songchange('next') 303 221 304 222 def _mount_added_cb(self, volume_monitor, device): 223 logging.debug('Mountpoint added. Checking...') 305 224 self.view_area.set_current_page(0) 306 self.remove_alert(self._alert) 307 self.playlist_widget.update(self.playlist) 225 self.playlist_widget.update() 308 226 309 227 def _mount_removed_cb(self, volume_monitor, device): 228 logging.debug('Mountpoint removed. Checking...') 310 229 self.view_area.set_current_page(0) 311 self.remove_alert(self._alert) 312 self.playlist_widget.update(self.playlist) 230 self.playlist_widget.update() 313 231 314 def _show_missing_tracks_alert(self, nro): 315 self._alert = Alert() 316 title = _('%s tracks not found.') % nro 232 def _show_error_alert(self, title, msg=None): 233 self._alert = ErrorAlert() 317 234 self._alert.props.title = title 318 self._alert.add_button(Gtk.ResponseType.APPLY, _('Details')) 235 if msg is not None: 236 self._alert.props.msg = msg 319 237 self.add_alert(self._alert) 320 self._alert.connect('response', 321 self.__missing_tracks_alert_response_cb) 322 323 def __missing_tracks_alert_response_cb(self, alert, response_id): 324 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 325 vbox.props.valign = Gtk.Align.CENTER 326 label = Gtk.Label(label='') 327 label.set_markup(_('<b>Missing tracks</b>')) 328 vbox.pack_start(label, False, False, 15) 329 330 for track in self.playlist_widget.get_missing_tracks(): 331 path = track['url'].replace('journal://', '')\ 332 .replace('file://', '') 333 label = Gtk.Label(label=path) 334 vbox.add(label) 238 self._alert.connect('response', self._alert_cancel_cb) 239 self._alert.show() 335 240 336 _missing_tracks = Gtk.ScrolledWindow() 337 _missing_tracks.add_with_viewport(vbox) 338 _missing_tracks.show_all() 241 def _alert_cancel_cb(self, alert, response_id): 242 self.remove_alert(alert) 339 243 340 self.view_area.append_page(_missing_tracks, None) 244 def __player_play_cb(self, widget): 245 # Do not show the visualization widget if we are playing just 246 # an audio stream 341 247 342 self.view_area.set_current_page(2) 343 self.remove_alert(alert) 248 def callback(): 249 if self.player.playing_video(): 250 self._switch_canvas(True) 251 else: 252 self._switch_canvas(False) 253 return False 344 254 345 def _alert_cancel_cb(self, alert, response_id): 346 self.remove_alert(alert) 255 # HACK: we need a timeout here because gstreamer returns 256 # n-video = 0 if we call it immediately 257 GObject.timeout_add(1000, callback) 347 258 348 def _ player_error_cb(self, widget, message, detail):259 def __player_error_cb(self, widget, message, detail): 349 260 self.player.stop() 350 self.player.set_uri(None)351 261 self.control.set_disabled() 352 262 353 file_path = self.playlist[self.currentplaying]['url']\ 354 .replace('journal://', 'file://') 263 logging.error('ERROR MESSAGE: %s', message) 264 logging.error('ERROR DETAIL: %s', detail) 265 266 file_path = self.playlist_widget._items[self.playlist_widget._current_playing]['path'] 355 267 mimetype = mime.get_for_file(file_path) 356 268 357 269 title = _('Error') … … class JukeboxActivity(activity.Activity): 359 271 self._switch_canvas(False) 360 272 self._show_error_alert(title, msg) 361 273 362 def _joined_cb(self, activity): 363 logging.debug("someone joined") 364 pass 365 366 def _shared_cb(self, activity): 367 logging.debug("shared start") 368 pass 369 370 def _show_picker_cb(self): 371 #From ImageViewer Activity 372 if not self._want_document: 373 return 374 375 # README: some arguments are deprecated so I avoid them 376 377 # chooser = ObjectChooser(_('Choose document'), self, 378 # Gtk.DialogFlags.MODAL | 379 # Gtk.DialogFlags.DESTROY_WITH_PARENT, 380 # what_filter=mime.GENERIC_TYPE_AUDIO) 381 382 chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO) 383 384 try: 385 result = chooser.run() 386 if result == Gtk.ResponseType.ACCEPT: 387 jobject = chooser.get_selected_object() 388 if jobject and jobject.file_path: 389 logging.error('Adding %s', jobject.file_path) 390 title = jobject.metadata.get('title', None) 391 self._load_file(jobject.file_path, title, 392 jobject.object_id) 393 finally: 394 #chooser.destroy() 395 #del chooser 396 pass 274 def can_close(self): 275 # We need to put the Gst.State in NULL so gstreamer can 276 # cleanup the pipeline 277 self.player.stop() 278 return True 397 279 398 280 def read_file(self, file_path): 399 281 """Load a file from the datastore on activity start.""" 400 282 logging.debug('JukeBoxAtivity.read_file: %s', file_path) 401 title = self.metadata.get('title', None) 402 self._load_file(file_path, title, self._object_id) 403 404 def _load_file(self, file_path, title, object_id): 405 self.uri = os.path.abspath(file_path) 406 if os.path.islink(self.uri): 407 self.uri = os.path.realpath(self.uri) 408 mimetype = mime.get_for_file('file://' + file_path) 409 logging.error('read_file mime %s', mimetype) 410 if mimetype == 'audio/x-mpegurl': 411 # is a M3U playlist: 412 for uri in self._read_m3u_playlist(file_path): 413 if not self.playlist_widget.check_available_media(uri['url']): 414 self._not_found_files += 1 415 416 GObject.idle_add(self._start, uri['url'], uri['title'], 417 uri['object_id']) 418 else: 419 # is another media file: 420 GObject.idle_add(self._start, self.uri, title, object_id) 421 422 if self._not_found_files > 0: 423 self._show_missing_tracks_alert(self._not_found_files) 424 425 def _create_playlist_jobject(self): 426 """Create an object in the Journal to store the playlist. 427 428 This is needed if the activity was not started from a playlist 429 or from scratch. 430 431 """ 432 jobject = datastore.create() 433 jobject.metadata['mime_type'] = "audio/x-mpegurl" 434 jobject.metadata['title'] = _('Jukebox playlist') 435 436 temp_path = os.path.join(activity.get_activity_root(), 437 'instance') 438 if not os.path.exists(temp_path): 439 os.makedirs(temp_path) 440 441 jobject.file_path = tempfile.mkstemp(dir=temp_path)[1] 442 self._playlist_jobject = jobject 283 title = self.metadata['title'] 284 self.playlist_widget.load_file(file_path, title) 443 285 444 286 def write_file(self, file_path): 445 287 … … class JukeboxActivity(activity.Activity): 447 289 """Open the file at file_path and write the playlist. 448 290 449 291 It is saved in audio/x-mpegurl format. 450 451 292 """ 293 452 294 list_file = open(file_path, 'w') 453 for uri in self.playlist :454 list_file.write('#EXTINF: 455 list_file.write('%s\n' % uri[' url'])295 for uri in self.playlist_widget._items: 296 list_file.write('#EXTINF:%s\n' % uri['title']) 297 list_file.write('%s\n' % uri['path']) 456 298 list_file.close() 457 299 458 300 if not self.metadata['mime_type']: … … class JukeboxActivity(activity.Activity): 463 305 464 306 else: 465 307 if self._playlist_jobject is None: 466 self._ create_playlist_jobject()308 self._playlist_jobject = self.playlist_widget.create_playlist_jobject() 467 309 468 310 # Add the playlist to the playlist jobject description. 469 311 # This is only done if the activity was not started from a 470 312 # playlist or from scratch: 471 313 description = '' 472 for uri in self.playlist :314 for uri in self.playlist_widget._items: 473 315 description += '%s\n' % uri['title'] 474 316 self._playlist_jobject.metadata['description'] = description 475 317 476 318 write_playlist_to_file(self._playlist_jobject.file_path) 477 319 datastore.write(self._playlist_jobject) 478 320 479 def _read_m3u_playlist(self, file_path):480 urls = []481 title = ''482 for line in open(file_path).readlines():483 line = line.strip()484 if line != '':485 if line.startswith('#EXTINF:'):486 # line with data487 #EXTINF: title488 title = line[len('#EXTINF:'):]489 else:490 uri = {}491 uri['url'] = line.strip()492 uri['title'] = title493 if uri['url'].startswith('journal://'):494 uri['object_id'] = uri['url'][len('journal://'):]495 else:496 uri['object_id'] = None497 urls.append(uri)498 title = ''499 return urls500 501 def _start(self, uri=None, title=None, object_id=None):502 self._want_document = False503 self.playpath = os.path.dirname(uri)504 if not uri:505 return False506 507 if title is not None:508 title = title.strip()509 if object_id is not None:510 self.playlist.append({'url': 'journal://' + object_id,511 'title': title})512 else:513 if uri.startswith("file://"):514 self.playlist.append({'url': uri, 'title': title})515 else:516 uri = "file://" + urllib.quote(os.path.abspath(uri))517 self.playlist.append({'url': uri, 'title': title})518 519 self.playlist_widget.update(self.playlist)520 521 try:522 if self.currentplaying is None:523 logging.info("Playing: " + self.playlist[0]['url'])524 url = self.playlist[0]['url']525 if url.startswith('journal://'):526 jobject = datastore.get(url[len("journal://"):])527 url = 'file://' + jobject.file_path528 529 self.player.set_uri(url)530 self.player.play()531 self.currentplaying = 0532 self.play_toggled()533 self.show_all()534 else:535 pass536 #self.player.seek(0L)537 #self.player.stop()538 #self.currentplaying += 1539 #self.player.set_uri(self.playlist[self.currentplaying])540 #self.play_toggled()541 except:542 pass543 self.check_if_next_prev()544 return False545 546 def play_toggled(self):547 self.control.set_enabled()548 549 if self.player.is_playing():550 self.player.pause()551 self.control.set_button_play()552 else:553 if self.player.error:554 self.control.set_disabled()555 else:556 self.player.play()557 if self.update_id == -1:558 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,559 self.update_scale_cb)560 self.control.set_button_pause()561 562 def volume_changed_cb(self, widget, value):563 if self.player:564 self.player.player.set_property('volume', value)565 566 def scale_button_press_cb(self, widget, event):567 self.control.button.set_sensitive(False)568 self.was_playing = self.player.is_playing()569 if self.was_playing:570 self.player.pause()571 572 # don't timeout-update position during seek573 if self.update_id != -1:574 GObject.source_remove(self.update_id)575 self.update_id = -1576 577 # make sure we get changed notifies578 if self.changed_id == -1:579 self.changed_id = self.control.hscale.connect('value-changed',580 self.scale_value_changed_cb)581 582 def scale_value_changed_cb(self, scale):583 # see seek.c:seek_cb584 real = long(scale.get_value() * self.p_duration / 100) # in ns585 self.player.seek(real)586 # allow for a preroll587 self.player.get_state(timeout=50 * Gst.MSECOND) # 50 ms588 589 def scale_button_release_cb(self, widget, event):590 # see seek.cstop_seek591 widget.disconnect(self.changed_id)592 self.changed_id = -1593 594 self.control.button.set_sensitive(True)595 if self.seek_timeout_id != -1:596 GObject.source_remove(self.seek_timeout_id)597 self.seek_timeout_id = -1598 else:599 if self.was_playing:600 self.player.play()601 602 if self.update_id != -1:603 self.error('Had a previous update timeout id')604 else:605 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,606 self.update_scale_cb)607 608 def update_scale_cb(self):609 success, self.p_position, self.p_duration = \610 self.player.query_position()611 612 if not success:613 return True614 615 if self.p_position != Gst.CLOCK_TIME_NONE:616 value = self.p_position * 100.0 / self.p_duration617 self.control.adjustment.set_value(value)618 619 # Update the current time620 seconds = self.p_position * 10 ** -9621 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))622 self.control.current_time_label.set_text(time)623 624 # FIXME: this should be updated just once when the file starts625 # the first time626 if self.p_duration != Gst.CLOCK_TIME_NONE:627 seconds = self.p_duration * 10 ** -9628 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))629 self.control.total_time_label.set_text(time)630 631 return True632 633 def _erase_playlist_entry_clicked_cb(self, widget):634 self.playlist_widget.delete_selected_items()635 636 321 def __go_fullscreen_cb(self, toolbar): 637 322 self.fullscreen() 638 323 … … class JukeboxActivity(activity.Activity): 647 332 class VideoWidget(Gtk.DrawingArea): 648 333 def __init__(self): 649 334 GObject.GObject.__init__(self) 650 self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |651 Gdk.EventMask.POINTER_MOTION_HINT_MASK |652 Gdk.EventMask.EXPOSURE_MASK |653 Gdk.EventMask.KEY_PRESS_MASK |654 Gdk.EventMask.KEY_RELEASE_MASK)335 # self.set_events(Gdk.EventMask.POINTER_MOTION_MASK | 336 # Gdk.EventMask.POINTER_MOTION_HINT_MASK | 337 # Gdk.EventMask.EXPOSURE_MASK | 338 # Gdk.EventMask.KEY_PRESS_MASK | 339 # Gdk.EventMask.KEY_RELEASE_MASK) 655 340 656 341 self.set_app_paintable(True) 657 342 self.set_double_buffered(False) -
controls.py
diff --git a/controls.py b/controls.py index b84a55a..a5922e8 100644
a b 18 18 import logging 19 19 20 20 from gi.repository import Gtk 21 from gi.repository import Gst 21 22 from gi.repository import GObject 22 23 23 24 from gettext import gettext as _ 24 25 26 from sugar3 import mime 25 27 from sugar3.graphics.toolbutton import ToolButton 28 from sugar3.graphics.objectchooser import ObjectChooser 26 29 27 30 28 31 class Controls(GObject.GObject): 29 32 """Class to create the Control (play, back, forward, 30 33 add, remove, etc) toolbar""" 31 34 32 def __init__(self, toolbar, jukebox): 35 SCALE_UPDATE_INTERVAL = 1000 36 37 def __init__(self, activity, toolbar): 33 38 GObject.GObject.__init__(self) 34 39 40 self.activity = activity 35 41 self.toolbar = toolbar 36 self.jukebox = jukebox 42 43 self._scale_update_id = -1 44 self._scale_changed_id = -1 45 self._seek_timeout_id = -1 37 46 38 47 self.open_button = ToolButton('list-add') 39 48 self.open_button.set_tooltip(_('Add track')) 40 49 self.open_button.show() 41 self.open_button.connect('clicked', jukebox.open_button_clicked_cb)50 self.open_button.connect('clicked', self.__open_button_clicked_cb) 42 51 self.toolbar.insert(self.open_button, -1) 43 52 44 53 erase_playlist_entry_btn = ToolButton(icon_name='list-remove') 45 54 erase_playlist_entry_btn.set_tooltip(_('Remove track')) 46 55 erase_playlist_entry_btn.connect('clicked', 47 jukebox._erase_playlist_entry_clicked_cb)56 self.__erase_playlist_entry_clicked_cb) 48 57 self.toolbar.insert(erase_playlist_entry_btn, -1) 49 58 50 59 spacer = Gtk.SeparatorToolItem() … … class Controls(GObject.GObject): 54 63 self.prev_button = ToolButton('player_rew') 55 64 self.prev_button.set_tooltip(_('Previous')) 56 65 self.prev_button.show() 57 self.prev_button.connect('clicked', self. prev_button_clicked_cb)66 self.prev_button.connect('clicked', self.__prev_button_clicked_cb) 58 67 self.toolbar.insert(self.prev_button, -1) 59 68 60 69 self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE, … … class Controls(GObject.GObject): 75 84 self.next_button = ToolButton('player_fwd') 76 85 self.next_button.set_tooltip(_('Next')) 77 86 self.next_button.show() 78 self.next_button.connect('clicked', self. next_button_clicked_cb)87 self.next_button.connect('clicked', self.__next_button_clicked_cb) 79 88 self.toolbar.insert(self.next_button, -1) 80 89 81 90 current_time = Gtk.ToolItem() 82 91 self.current_time_label = Gtk.Label(label='') 83 92 current_time.add(self.current_time_label) 84 93 current_time.show() 85 toolbar.insert(current_time, -1)94 self.toolbar.insert(current_time, -1) 86 95 87 96 self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) 88 97 self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, … … class Controls(GObject.GObject): 93 102 logging.debug("FIXME: AttributeError: 'Scale' object has no " 94 103 "attribute 'set_update_policy'") 95 104 self.hscale.connect('button-press-event', 96 jukebox.scale_button_press_cb)105 self.__scale_button_press_cb) 97 106 self.hscale.connect('button-release-event', 98 jukebox.scale_button_release_cb)107 self.__scale_button_release_cb) 99 108 100 109 self.scale_item = Gtk.ToolItem() 101 110 self.scale_item.set_expand(True) … … class Controls(GObject.GObject): 106 115 self.total_time_label = Gtk.Label(label='') 107 116 total_time.add(self.total_time_label) 108 117 total_time.show() 109 toolbar.insert(total_time, -1) 118 self.toolbar.insert(total_time, -1) 119 120 self.activity.connect('no-stream', self.__no_stream_cb) 121 self.activity.player.connect('play', self.__player_play) 122 123 def __player_play(self, widget): 124 if self._scale_update_id == -1: 125 self._scale_update_id = GObject.timeout_add( 126 self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb) 127 self.set_enabled() 128 self.set_button_pause() 129 130 def __open_button_clicked_cb(self, widget): 131 self.__show_picker_cb() 132 133 def __erase_playlist_entry_clicked_cb(self, widget): 134 self.activity.playlist_widget.delete_selected_items() 135 136 def __show_picker_cb(self): 137 jobject = None 138 chooser = ObjectChooser(self.activity, 139 what_filter=mime.GENERIC_TYPE_AUDIO) 110 140 111 def prev_button_clicked_cb(self, widget): 112 self.jukebox.songchange('prev') 141 try: 142 result = chooser.run() 143 if result == Gtk.ResponseType.ACCEPT: 144 jobject = chooser.get_selected_object() 145 if jobject and jobject.file_path: 146 logging.info('Adding %s', jobject.file_path) 147 self.activity.playlist_widget.load_file(jobject) 148 self.check_if_next_prev() 149 finally: 150 if jobject is not None: 151 jobject.destroy() 113 152 114 def next_button_clicked_cb(self, widget): 115 self.jukebox.songchange('next') 153 def __prev_button_clicked_cb(self, widget): 154 self.activity.songchange('prev') 155 156 def __next_button_clicked_cb(self, widget): 157 self.activity.songchange('next') 158 159 def check_if_next_prev(self): 160 current_playing = self.activity.playlist_widget._current_playing 161 if current_playing == 0: 162 self.prev_button.set_sensitive(False) 163 else: 164 self.prev_button.set_sensitive(True) 165 166 if current_playing == len(self.activity.playlist_widget._items) - 1: 167 self.next_button.set_sensitive(False) 168 else: 169 self.next_button.set_sensitive(True) 116 170 117 171 def _button_clicked_cb(self, widget): 118 self.jukebox.play_toggled() 172 self.set_enabled() 173 174 if self.activity.player.is_playing(): 175 self.activity.player.pause() 176 self.set_button_play() 177 GObject.source_remove(self._scale_update_id) 178 else: 179 if self.activity.player.error: 180 self.set_disabled() 181 else: 182 self.activity.player.play() 183 self.activity._switch_canvas(True) 184 self._scale_update_id = GObject.timeout_add( 185 self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb) 119 186 120 187 def set_button_play(self): 121 188 self.button.set_icon_widget(self.play_image) … … class Controls(GObject.GObject): 132 199 self.button.set_sensitive(True) 133 200 self.scale_item.set_sensitive(True) 134 201 self.hscale.set_sensitive(True) 202 203 def __scale_button_press_cb(self, widget, event): 204 self.button.set_sensitive(False) 205 self.was_playing = self.activity.player.is_playing() 206 if self.was_playing: 207 self.activity.player.pause() 208 209 # don't timeout-update position during seek 210 if self._scale_update_id != -1: 211 GObject.source_remove(self._scale_update_id) 212 self._scale_update_id = -1 213 214 # make sure we get changed notifies 215 if self._scale_changed_id == -1: 216 self._scale_changed_id = self.hscale.connect('value-changed', 217 self.__scale_value_changed_cb) 218 219 def __scale_value_changed_cb(self, scale): 220 # see seek.c:seek_cb 221 real = long(scale.get_value() * self.p_duration / 100) # in ns 222 self.activity.player.seek(real) 223 # allow for a preroll 224 self.activity.player.get_state(timeout=50 * Gst.MSECOND) # 50 ms 225 226 def __scale_button_release_cb(self, widget, event): 227 # see seek.cstop_seek 228 widget.disconnect(self._scale_changed_id) 229 self._scale_changed_id = -1 230 231 self.button.set_sensitive(True) 232 if self._seek_timeout_id != -1: 233 GObject.source_remove(self._seek_timeout_id) 234 self._seek_timeout_id = -1 235 else: 236 if self.was_playing: 237 self.activity.player.play() 238 239 if self._scale_update_id != -1: 240 # self.error('Had a previous update timeout id') 241 pass 242 else: 243 self._scale_update_id = GObject.timeout_add( 244 self.SCALE_UPDATE_INTERVAL, self.__update_scale_cb) 245 246 def __update_scale_cb(self): 247 success, self.p_position, self.p_duration = \ 248 self.activity.player.query_position() 249 250 if not success: 251 return True 252 253 if self.p_position != Gst.CLOCK_TIME_NONE: 254 value = self.p_position * 100.0 / self.p_duration 255 self.adjustment.set_value(value) 256 257 # Update the current time 258 seconds = self.p_position * 10 ** -9 259 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) 260 self.current_time_label.set_text(time) 261 262 # FIXME: this should be updated just once when the file starts 263 # the first time 264 if self.p_duration != Gst.CLOCK_TIME_NONE: 265 seconds = self.p_duration * 10 ** -9 266 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) 267 self.total_time_label.set_text(time) 268 269 return True 270 271 def __no_stream_cb(self, widget): 272 self.activity.player.stop() 273 self.set_button_play() 274 self.check_if_next_prev() 275 276 self.adjustment.set_value(0) 277 self.current_time_label.set_text('') 278 self.total_time_label.set_text('') -
player.py
diff --git a/player.py b/player.py index b5b03da..7ddaeaa 100644
a b class GstPlayer(GObject.GObject): 37 37 __gsignals__ = { 38 38 'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]), 39 39 'eos': (GObject.SignalFlags.RUN_FIRST, None, []), 40 'play': (GObject.SignalFlags.RUN_FIRST, None, []), 40 41 } 41 42 42 def __init__(self , videowidget):43 def __init__(self): 43 44 GObject.GObject.__init__(self) 44 45 45 46 self.playing = False … … class GstPlayer(GObject.GObject): 62 63 self.player = Gst.ElementFactory.make('playbin', None) 63 64 self.pipeline.add(self.player) 64 65 65 # Set the proper flags to render the vis-plugin 66 GST_PLAY_FLAG_VIS = 1 << 3 67 GST_PLAY_FLAG_TEXT = 1 << 2 68 self.player.props.flags |= GST_PLAY_FLAG_VIS 69 self.player.props.flags |= GST_PLAY_FLAG_TEXT 70 71 r = Gst.Registry.get() 72 l = [x for x in r.get_feature_list(Gst.ElementFactory) 73 if (x.get_metadata('klass') == "Visualization")] 74 if len(l): 75 e = l.pop() # take latest plugin in the list 76 vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name()) 77 self.player.set_property('vis-plugin', vis_plug) 78 79 self.overlay = None 66 def init_view_area(self, videowidget): 80 67 videowidget.realize() 81 68 self.videowidget = videowidget 82 69 self.videowidget_xid = videowidget.get_window().get_xid() 83 self._init_video_sink()84 70 85 71 def __on_error_message(self, bus, msg): 86 72 self.stop() … … class GstPlayer(GObject.GObject): 100 86 101 87 def set_uri(self, uri): 102 88 self.pipeline.set_state(Gst.State.READY) 103 logging.debug('### Setting URI: %s', uri) 89 # gstreamer needs the 'file://' prefix 90 uri = 'file://' + uri 91 logging.debug('URI: %s', uri) 104 92 self.player.set_property('uri', uri) 105 93 106 def _init_video_sink(self):107 self.bin = Gst.Bin()108 videoscale = Gst.ElementFactory.make('videoscale', 'videoscale')109 self.bin.add(videoscale)110 pad = videoscale.get_static_pad("sink")111 ghostpad = Gst.GhostPad.new("sink", pad)112 self.bin.add_pad(ghostpad)113 videoscale.set_property("method", 0)114 115 textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')116 self.overlay = textoverlay117 self.bin.add(textoverlay)118 conv = Gst.ElementFactory.make("videoconvert", "conv")119 self.bin.add(conv)120 videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink')121 self.bin.add(videosink)122 123 videoscale.link(textoverlay)124 textoverlay.link(conv)125 conv.link(videosink)126 127 self.player.set_property("video-sink", self.bin)128 129 def set_overlay(self, title, artist, album):130 text = "%s\n%s" % (title, artist)131 if album and len(album):132 text += "\n%s" % album133 self.overlay.set_property("text", text)134 self.overlay.set_property("font-desc", "sans bold 14")135 self.overlay.set_property("halignment", "right")136 self.overlay.set_property("valignment", "bottom")137 try:138 # Only in OLPC versions of gstreamer-plugins-base for now139 self.overlay.set_property("line-align", "left")140 except:141 pass142 143 94 def query_position(self): 144 95 "Returns a (position, duration) tuple" 145 96 … … class GstPlayer(GObject.GObject): 149 100 return (p_success and d_success, position, duration) 150 101 151 102 def seek(self, location): 152 """ 153 @param location: time to seek to, in nanoseconds 154 """ 103 # Time to seek to, in nanoseconds 155 104 156 logging.debug('S eek: %s ns', location)105 logging.debug('SEEK: %s ns', location) 157 106 158 107 self.pipeline.seek_simple(Gst.Format.TIME, 159 108 Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 160 109 location) 161 110 162 111 def pause(self): 163 logging.debug( "pausing player")112 logging.debug('Pausing player') 164 113 self.pipeline.set_state(Gst.State.PAUSED) 165 114 self.playing = False 166 115 167 116 def play(self): 168 logging.debug( "playing player")117 logging.debug('Playing player') 169 118 self.pipeline.set_state(Gst.State.PLAYING) 170 119 self.playing = True 171 120 self.error = False 121 self.emit('play') 172 122 173 123 def stop(self): 174 124 self.playing = False 175 125 self.pipeline.set_state(Gst.State.NULL) 176 logging.debug( "stopped player")126 logging.debug('Stopped player') 177 127 178 128 def get_state(self, timeout=1): 179 129 return self.player.get_state(timeout=timeout) 180 130 181 131 def is_playing(self): 182 132 return self.playing 133 134 def playing_video(self): 135 return self.player.props.n_video > 0 -
playlist.py
diff --git a/playlist.py b/playlist.py index 4563d47..0587ab2 100644
a b 13 13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 14 14 # USA 15 15 16 import logging17 16 import os 17 import logging 18 import tempfile 18 19 from gettext import gettext as _ 19 20 20 import gi21 gi.require_version('Gtk', '3.0')22 23 21 from gi.repository import GObject 24 22 from gi.repository import Gtk 25 23 from gi.repository import Pango 26 24 25 from sugar3 import mime 26 from sugar3.datastore import datastore 27 from sugar3.activity import activity 27 28 from sugar3.graphics.icon import CellRendererIcon 28 29 29 30 30 COLUMNS_NAME = ('index', ' media', 'available')31 COLUMNS_NAME = ('index', 'title', 'available') 31 32 COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME)) 32 33 33 34 34 35 class PlayList(Gtk.ScrolledWindow): 35 def __init__(self, play_callback): 36 self._playlist = None 37 self._play_callback = play_callback 36 __gsignals__ = { 37 'play-index': (GObject.SignalFlags.RUN_FIRST, None, [int, str]), 38 } 39 40 def __init__(self): 41 self._not_found_files = 0 42 self._current_playing = None 43 self._items = [] 38 44 39 45 GObject.GObject.__init__(self, hadjustment=None, 40 46 vadjustment=None) 41 47 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 42 48 self.listview = Gtk.TreeView() 43 49 self.treemodel = Gtk.ListStore(int, object, bool) … … class PlayList(Gtk.ScrolledWindow): 63 69 64 70 renderer_title = Gtk.CellRendererText() 65 71 renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) 66 treecol_title = Gtk.TreeViewColumn(_(' Play List'))72 treecol_title = Gtk.TreeViewColumn(_('Track')) 67 73 treecol_title.pack_start(renderer_title, True) 68 74 treecol_title.set_cell_data_func(renderer_title, self._set_title) 69 75 self.listview.append_column(treecol_title) … … class PlayList(Gtk.ScrolledWindow): 79 85 model = treeview.get_model() 80 86 81 87 treeiter = model.get_iter(path) 82 media_idx = model.get_value(treeiter, COLUMNS['index']) 83 self._play_callback(media_idx) 88 index = model.get_value(treeiter, COLUMNS['index']) 89 # TODO: put the path inside the ListStore 90 path = self._items[index]['path'] 91 # TODO: check if the stream is available before emitting the signal 92 self._current_playing = index 93 self.set_cursor(index) 94 self.emit('play-index', index, path) 84 95 85 96 def _set_number(self, column, cell, model, it, data): 86 97 idx = model.get_value(it, COLUMNS['index']) 87 98 cell.set_property('text', idx + 1) 88 99 89 100 def _set_title(self, column, cell, model, it, data): 90 playlist_item = model.get_value(it, COLUMNS['media'])101 title = model.get_value(it, COLUMNS['title']) 91 102 available = model.get_value(it, COLUMNS['available']) 92 103 93 cell.set_property('text', playlist_item['title'])104 cell.set_property('text', title) 94 105 sensitive = True 95 106 if not available: 96 107 sensitive = False … … class PlayList(Gtk.ScrolledWindow): 100 111 available = model.get_value(it, COLUMNS['available']) 101 112 cell.set_property('visible', not available) 102 113 103 def update(self, playlist):104 self.treemodel.clear()105 self._playlist = playlist106 pl = list(enumerate(playlist))107 for i, media in pl:108 available = self.check_available_media(media['url'])109 media['available'] = available110 self.treemodel.append((i, media, available))111 #self.set_cursor(0)112 113 114 def set_cursor(self, index): 114 115 self.listview.set_cursor((index,)) 115 116 … … class PlayList(Gtk.ScrolledWindow): 118 119 sel_model, sel_rows = self.listview.get_selection().get_selected_rows() 119 120 for row in sel_rows: 120 121 index = sel_model.get_value(sel_model.get_iter(row), 0) 121 self._ playlist.pop(index)122 self._items.pop(index) 122 123 self.treemodel.remove(self.treemodel.get_iter(row)) 123 self.update(self._playlist)124 124 125 def check_available_media(self, uri): 126 path = uri.replace('journal://', '').replace('file://', '') 125 def check_available_media(self, path): 127 126 if os.path.exists(path): 128 127 return True 129 128 else: … … class PlayList(Gtk.ScrolledWindow): 135 134 if not track['available']: 136 135 missing_tracks.append(track) 137 136 return missing_tracks 137 138 def _load_m3u_playlist(self, file_path): 139 for uri in self._read_m3u_playlist(file_path): 140 if not self.check_available_media(uri['path']): 141 self._not_found_files += 1 142 143 self._add_track(uri['path'], uri['title']) 144 145 def _load_stream(self, file_path, title=None): 146 # TODO: read id3 here 147 if os.path.islink(file_path): 148 file_path = os.path.realpath(file_path) 149 self._add_track(file_path, title) 150 151 def load_file(self, jobject, title=None): 152 logging.debug('#### jobject: %s', type(jobject)) 153 if isinstance(jobject, datastore.RawObject) or \ 154 isinstance(jobject, datastore.DSObject): 155 file_path = jobject.file_path 156 title = jobject.metadata['title'] 157 else: 158 file_path = jobject 159 160 mimetype = mime.get_for_file('file://' + file_path) 161 logging.info('read_file mime %s', mimetype) 162 if mimetype == 'audio/x-mpegurl': 163 # is a M3U playlist: 164 self._load_m3u_playlist(file_path) 165 else: 166 # is not a M3U playlist 167 self._load_stream(file_path, title) 168 169 def update(self): 170 for tree_item, playlist_item in zip(self.treemodel, self._items): 171 tree_item[2] = self.check_available_media(playlist_item['path']) 172 173 def _add_track(self, file_path, title): 174 available = self.check_available_media(file_path) 175 item = {'path': file_path, 176 'title': title, 177 'available': available} 178 self._items.append(item) 179 index = len(self._items) - 1 180 self.treemodel.append((index, item['title'], available)) 181 182 def _read_m3u_playlist(self, file_path): 183 urls = [] 184 title = '' 185 for line in open(file_path).readlines(): 186 line = line.strip() 187 if line != '': 188 if line.startswith('#EXTINF:'): 189 # line with data 190 #EXTINF: title 191 title = line[len('#EXTINF:'):] 192 else: 193 uri = {} 194 uri['path'] = line.strip() 195 uri['title'] = title 196 urls.append(uri) 197 title = '' 198 return urls 199 200 def create_playlist_jobject(self): 201 """Create an object in the Journal to store the playlist. 202 203 This is needed if the activity was not started from a playlist 204 or from scratch. 205 """ 206 207 jobject = datastore.create() 208 jobject.metadata['mime_type'] = "audio/x-mpegurl" 209 jobject.metadata['title'] = _('Jukebox playlist') 210 211 temp_path = os.path.join(activity.get_activity_root(), 212 'instance') 213 if not os.path.exists(temp_path): 214 os.makedirs(temp_path) 215 216 jobject.file_path = tempfile.mkstemp(dir=temp_path)[1] 217 return jobject