diff options
author | Manuel Kaufmann <humitos@gmail.com> | 2013-01-14 15:42:35 (GMT) |
---|---|---|
committer | Gonzalo Odiard <godiard@gmail.com> | 2013-01-25 12:38:40 (GMT) |
commit | 0bf46d8bf132609bab93a93360e78e09f01d7b88 (patch) | |
tree | 45cf4f0892622a61b3a06caf8365920dadc7f0b1 /activity.py | |
parent | f4ac3ea1126a571bd0b8e60bcced451066853b9c (diff) |
Modularization of classes
- Put each class in a different .py file
- Rename jukeboxactivity.py to activity.py to make this more standard
Signed-off-by: Manuel Kaufmann <humitos@gmail.com>
Reviewed-by: Gonzalo Odiard <gonzalo@laptop.org>
Diffstat (limited to 'activity.py')
-rw-r--r-- | activity.py | 678 |
1 files changed, 678 insertions, 0 deletions
diff --git a/activity.py b/activity.py new file mode 100644 index 0000000..14fcbe9 --- /dev/null +++ b/activity.py @@ -0,0 +1,678 @@ +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +# Activity that plays media. +# Copyright (C) 2007 Andy Wingo <wingo@pobox.com> +# Copyright (C) 2007 Red Hat, Inc. +# Copyright (C) 2008-2010 Kushal Das <kushal@fedoraproject.org> +# Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com> + +import sys +import logging +import tempfile +from gettext import gettext as _ +import os + +from sugar3.activity import activity +from sugar3.graphics.objectchooser import ObjectChooser +from sugar3 import mime +from sugar3.datastore import datastore + +from sugar3.graphics.toolbarbox import ToolbarBox +from sugar3.graphics.toolbarbox import ToolbarButton +from sugar3.activity.widgets import StopButton +from sugar3.activity.widgets import ActivityToolbarButton +from sugar3.graphics.alert import ErrorAlert +from sugar3.graphics.alert import Alert + +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gst', '1.0') + +from gi.repository import GObject +from gi.repository import Gdk +from gi.repository import Gtk +from gi.repository import Gst +from gi.repository import Gio + +import urllib +from viewtoolbar import ViewToolbar +from controls import Controls +from player import GstPlayer + +from playlist import PlayList + +PLAYLIST_WIDTH_PROP = 1.0 / 3 + + +class JukeboxActivity(activity.Activity): + UPDATE_INTERVAL = 500 + + def __init__(self, handle): + activity.Activity.__init__(self, handle) + self._object_id = handle.object_id + self.set_title(_('Jukebox Activity')) + self.player = None + self.max_participants = 1 + self._playlist_jobject = None + + toolbar_box = ToolbarBox() + activity_button = ActivityToolbarButton(self) + activity_toolbar = activity_button.page + toolbar_box.toolbar.insert(activity_button, 0) + self.title_entry = activity_toolbar.title + + # FIXME: I don't know what is the mission of this line + # activity_toolbar.stop.hide() + + self.volume_monitor = Gio.VolumeMonitor.get() + self.volume_monitor.connect('mount-added', self._mount_added_cb) + self.volume_monitor.connect('mount-removed', self._mount_removed_cb) + + _view_toolbar = ViewToolbar() + _view_toolbar.connect('go-fullscreen', + self.__go_fullscreen_cb) + _view_toolbar.connect('toggle-playlist', + self.__toggle_playlist_cb) + view_toolbar_button = ToolbarButton( + page=_view_toolbar, + icon_name='toolbar-view') + _view_toolbar.show() + toolbar_box.toolbar.insert(view_toolbar_button, -1) + view_toolbar_button.show() + + self.control = Controls(toolbar_box.toolbar, self) + + toolbar_box.toolbar.insert(StopButton(self), -1) + + self.set_toolbar_box(toolbar_box) + toolbar_box.show_all() + + self.connect("key_press_event", self._key_press_event_cb) + + # We want to be notified when the activity gets the focus or + # loses it. When it is not active, we don't need to keep + # reproducing the video + self.connect("notify::active", self._notify_active_cb) + + # FIXME: this is related with shared activity and it doesn't work + # if handle.uri: + # pass + # elif self._shared_activity: + # if self.get_shared(): + # pass + # else: + # # Wait for a successful join before trying to get the document + # self.connect("joined", self._joined_cb) + + self.update_id = -1 + self.changed_id = -1 + self.seek_timeout_id = -1 + self.player = None + self.uri = None + + # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'} + self.playlist = [] + + self.jobjectlist = [] + self.playpath = None + self.currentplaying = None + self.playflag = False + self._not_found_files = 0 + + # README: I changed this because I was getting an error when I + # tried to modify self.bin with something different than + # Gtk.Bin + + # self.bin = Gtk.HBox() + # self.bin.show() + + self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self._alert = None + + self.playlist_widget = PlayList(self.play) + self.playlist_widget.update(self.playlist) + self.playlist_widget.show() + self.canvas.pack_start(self.playlist_widget, False, True, 0) + self._empty_widget = Gtk.Label(label="") + self._empty_widget.show() + self.videowidget = VideoWidget() + self.set_canvas(self.canvas) + self._init_view_area() + self.show_all() + self.canvas.connect('size-allocate', self.__size_allocate_cb) + + #From ImageViewer Activity + self._want_document = True + if self._object_id is None: + self._show_object_picker = GObject.timeout_add(1000, \ + self._show_picker_cb) + + if handle.uri: + self.uri = handle.uri + GObject.idle_add(self._start, self.uri, handle.title) + + # Create the player just once + logging.debug('Instantiating GstPlayer') + self.player = GstPlayer(self.videowidget) + self.player.connect("eos", self._player_eos_cb) + self.player.connect("error", self._player_error_cb) + self.p_position = Gst.CLOCK_TIME_NONE + self.p_duration = Gst.CLOCK_TIME_NONE + + def _notify_active_cb(self, widget, event): + """Sugar notify us that the activity is becoming active or inactive. + When we are inactive, we stop the player if it is reproducing + a video. + """ + if self.player.player.props.uri is not None: + if not self.player.is_playing() and self.props.active: + self.player.play() + if self.player.is_playing() and not self.props.active: + self.player.pause() + + def _init_view_area(self): + """ + Use a notebook with two pages, one empty an another + with the videowidget + """ + self.view_area = Gtk.Notebook() + self.view_area.set_show_tabs(False) + self.view_area.append_page(self._empty_widget, None) + self.view_area.append_page(self.videowidget, None) + self.canvas.pack_end(self.view_area, expand=True, + fill=True, padding=0) + + def _switch_canvas(self, show_video): + """Show or hide the video visualization in the canvas. + + When hidden, the canvas is filled with an empty widget to + ensure redrawing. + + """ + if show_video: + self.view_area.set_current_page(1) + else: + self.view_area.set_current_page(0) + self.canvas.queue_draw() + + def __size_allocate_cb(self, widget, allocation): + canvas_size = self.canvas.get_allocation() + playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP) + self.playlist_widget.set_size_request(playlist_width, 0) + + def open_button_clicked_cb(self, widget): + """ To open the dialog to select a new file""" + #self.player.seek(0L) + #self.player.stop() + #self.playlist = [] + #self.playpath = None + #self.currentplaying = None + #self.playflag = False + self._want_document = True + self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb) + + def _key_press_event_cb(self, widget, event): + keyname = Gdk.keyval_name(event.keyval) + logging.info("Keyname Press: %s, time: %s", keyname, event.time) + if self.title_entry.has_focus(): + return False + + if keyname == "space": + self.play_toggled() + return True + + def check_if_next_prev(self): + if self.currentplaying == 0: + self.control.prev_button.set_sensitive(False) + else: + self.control.prev_button.set_sensitive(True) + if self.currentplaying == len(self.playlist) - 1: + self.control.next_button.set_sensitive(False) + else: + self.control.next_button.set_sensitive(True) + + def songchange(self, direction): + #if self.playflag: + # self.playflag = False + # return + self.player.seek(0L) + if direction == "prev" and self.currentplaying > 0: + self.play(self.currentplaying - 1) + logging.info("prev: " + self.playlist[self.currentplaying]['url']) + #self.playflag = True + elif direction == "next" and \ + self.currentplaying < len(self.playlist) - 1: + self.play(self.currentplaying + 1) + logging.info("next: " + self.playlist[self.currentplaying]['url']) + #self.playflag = True + else: + self.play_toggled() + self.player.stop() + self._switch_canvas(show_video=False) + self.player.set_uri(None) + self.check_if_next_prev() + + def play(self, media_index): + self._switch_canvas(show_video=True) + self.currentplaying = media_index + url = self.playlist[self.currentplaying]['url'] + error = None + if url.startswith('journal://'): + try: + jobject = datastore.get(url[len("journal://"):]) + url = 'file://' + jobject.file_path + except: + path = url[len("journal://"):] + error = _('The file %s was not found') % path + + self.check_if_next_prev() + + if error is None: + self.player.set_uri(url) + self.player.play() + else: + self.control.set_disabled() + self._show_error_alert(error) + + self.playlist_widget.set_cursor(self.currentplaying) + + def _player_eos_cb(self, widget): + self.songchange('next') + + def _show_error_alert(self, title, msg=None): + self._alert = ErrorAlert() + self._alert.props.title = title + if msg is not None: + self._alert.props.msg = msg + self.add_alert(self._alert) + self._alert.connect('response', self._alert_cancel_cb) + self._alert.show() + + def _mount_added_cb(self, volume_monitor, device): + self.view_area.set_current_page(0) + self.remove_alert(self._alert) + self.playlist_widget.update(self.playlist) + + def _mount_removed_cb(self, volume_monitor, device): + self.view_area.set_current_page(0) + self.remove_alert(self._alert) + self.playlist_widget.update(self.playlist) + + def _show_missing_tracks_alert(self, nro): + self._alert = Alert() + title = _('%s tracks not found.') % nro + self._alert.props.title = title + self._alert.add_button(Gtk.ResponseType.APPLY, _('Details')) + self.add_alert(self._alert) + self._alert.connect('response', + self.__missing_tracks_alert_response_cb) + + def __missing_tracks_alert_response_cb(self, alert, response_id): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + vbox.props.valign = Gtk.Align.CENTER + label = Gtk.Label(label='') + label.set_markup(_('<b>Missing tracks</b>')) + vbox.pack_start(label, False, False, 15) + + for track in self.playlist_widget.get_missing_tracks(): + path = track['url'].replace('journal://', '')\ + .replace('file://', '') + label = Gtk.Label(label=path) + vbox.add(label) + + _missing_tracks = Gtk.ScrolledWindow() + _missing_tracks.add_with_viewport(vbox) + _missing_tracks.show_all() + + self.view_area.append_page(_missing_tracks, None) + + self.view_area.set_current_page(2) + self.remove_alert(alert) + + def _alert_cancel_cb(self, alert, response_id): + self.remove_alert(alert) + + def _player_error_cb(self, widget, message, detail): + self.player.stop() + self.player.set_uri(None) + self.control.set_disabled() + + file_path = self.playlist[self.currentplaying]['url']\ + .replace('journal://', 'file://') + mimetype = mime.get_for_file(file_path) + + title = _('Error') + msg = _('This "%s" file can\'t be played') % mimetype + self._switch_canvas(False) + self._show_error_alert(title, msg) + + def _joined_cb(self, activity): + logging.debug("someone joined") + pass + + def _shared_cb(self, activity): + logging.debug("shared start") + pass + + def _show_picker_cb(self): + #From ImageViewer Activity + if not self._want_document: + return + + # README: some arguments are deprecated so I avoid them + + # chooser = ObjectChooser(_('Choose document'), self, + # Gtk.DialogFlags.MODAL | + # Gtk.DialogFlags.DESTROY_WITH_PARENT, + # what_filter=mime.GENERIC_TYPE_AUDIO) + + chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO) + + try: + result = chooser.run() + if result == Gtk.ResponseType.ACCEPT: + jobject = chooser.get_selected_object() + if jobject and jobject.file_path: + logging.error('Adding %s', jobject.file_path) + title = jobject.metadata.get('title', None) + self._load_file(jobject.file_path, title, + jobject.object_id) + finally: + #chooser.destroy() + #del chooser + pass + + def read_file(self, file_path): + """Load a file from the datastore on activity start.""" + logging.debug('JukeBoxAtivity.read_file: %s', file_path) + title = self.metadata.get('title', None) + self._load_file(file_path, title, self._object_id) + + def _load_file(self, file_path, title, object_id): + self.uri = os.path.abspath(file_path) + if os.path.islink(self.uri): + self.uri = os.path.realpath(self.uri) + mimetype = mime.get_for_file('file://' + file_path) + logging.error('read_file mime %s', mimetype) + if mimetype == 'audio/x-mpegurl': + # is a M3U playlist: + for uri in self._read_m3u_playlist(file_path): + if not self.playlist_widget.check_available_media(uri['url']): + self._not_found_files += 1 + + GObject.idle_add(self._start, uri['url'], uri['title'], + uri['object_id']) + else: + # is another media file: + GObject.idle_add(self._start, self.uri, title, object_id) + + if self._not_found_files > 0: + self._show_missing_tracks_alert(self._not_found_files) + + def _create_playlist_jobject(self): + """Create an object in the Journal to store the playlist. + + This is needed if the activity was not started from a playlist + or from scratch. + + """ + jobject = datastore.create() + jobject.metadata['mime_type'] = "audio/x-mpegurl" + jobject.metadata['title'] = _('Jukebox playlist') + + temp_path = os.path.join(activity.get_activity_root(), + 'instance') + if not os.path.exists(temp_path): + os.makedirs(temp_path) + + jobject.file_path = tempfile.mkstemp(dir=temp_path)[1] + self._playlist_jobject = jobject + + def write_file(self, file_path): + + def write_playlist_to_file(file_path): + """Open the file at file_path and write the playlist. + + It is saved in audio/x-mpegurl format. + + """ + list_file = open(file_path, 'w') + for uri in self.playlist: + list_file.write('#EXTINF: %s\n' % uri['title']) + list_file.write('%s\n' % uri['url']) + list_file.close() + + if not self.metadata['mime_type']: + self.metadata['mime_type'] = 'audio/x-mpegurl' + + if self.metadata['mime_type'] == 'audio/x-mpegurl': + write_playlist_to_file(file_path) + + else: + if self._playlist_jobject is None: + self._create_playlist_jobject() + + # Add the playlist to the playlist jobject description. + # This is only done if the activity was not started from a + # playlist or from scratch: + description = '' + for uri in self.playlist: + description += '%s\n' % uri['title'] + self._playlist_jobject.metadata['description'] = description + + write_playlist_to_file(self._playlist_jobject.file_path) + datastore.write(self._playlist_jobject) + + def _read_m3u_playlist(self, file_path): + urls = [] + title = '' + for line in open(file_path).readlines(): + line = line.strip() + if line != '': + if line.startswith('#EXTINF:'): + # line with data + #EXTINF: title + title = line[len('#EXTINF:'):] + else: + uri = {} + uri['url'] = line.strip() + uri['title'] = title + if uri['url'].startswith('journal://'): + uri['object_id'] = uri['url'][len('journal://'):] + else: + uri['object_id'] = None + urls.append(uri) + title = '' + return urls + + def _start(self, uri=None, title=None, object_id=None): + self._want_document = False + self.playpath = os.path.dirname(uri) + if not uri: + return False + + if title is not None: + title = title.strip() + if object_id is not None: + self.playlist.append({'url': 'journal://' + object_id, + 'title': title}) + else: + if uri.startswith("file://"): + self.playlist.append({'url': uri, 'title': title}) + else: + uri = "file://" + urllib.quote(os.path.abspath(uri)) + self.playlist.append({'url': uri, 'title': title}) + + self.playlist_widget.update(self.playlist) + + try: + if self.currentplaying is None: + logging.info("Playing: " + self.playlist[0]['url']) + url = self.playlist[0]['url'] + if url.startswith('journal://'): + jobject = datastore.get(url[len("journal://"):]) + url = 'file://' + jobject.file_path + + self.player.set_uri(url) + self.player.play() + self.currentplaying = 0 + self.play_toggled() + self.show_all() + else: + pass + #self.player.seek(0L) + #self.player.stop() + #self.currentplaying += 1 + #self.player.set_uri(self.playlist[self.currentplaying]) + #self.play_toggled() + except: + pass + self.check_if_next_prev() + return False + + def play_toggled(self): + self.control.set_enabled() + + if self.player.is_playing(): + self.player.pause() + self.control.set_button_play() + else: + if self.player.error: + self.control.set_disabled() + else: + self.player.play() + if self.update_id == -1: + self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL, + self.update_scale_cb) + self.control.set_button_pause() + + def volume_changed_cb(self, widget, value): + if self.player: + self.player.player.set_property('volume', value) + + def scale_button_press_cb(self, widget, event): + self.control.button.set_sensitive(False) + self.was_playing = self.player.is_playing() + if self.was_playing: + self.player.pause() + + # don't timeout-update position during seek + if self.update_id != -1: + GObject.source_remove(self.update_id) + self.update_id = -1 + + # make sure we get changed notifies + if self.changed_id == -1: + self.changed_id = self.control.hscale.connect('value-changed', + self.scale_value_changed_cb) + + def scale_value_changed_cb(self, scale): + # see seek.c:seek_cb + real = long(scale.get_value() * self.p_duration / 100) # in ns + self.player.seek(real) + # allow for a preroll + self.player.get_state(timeout=50 * Gst.MSECOND) # 50 ms + + def scale_button_release_cb(self, widget, event): + # see seek.cstop_seek + widget.disconnect(self.changed_id) + self.changed_id = -1 + + self.control.button.set_sensitive(True) + if self.seek_timeout_id != -1: + GObject.source_remove(self.seek_timeout_id) + self.seek_timeout_id = -1 + else: + if self.was_playing: + self.player.play() + + if self.update_id != -1: + self.error('Had a previous update timeout id') + else: + self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL, + self.update_scale_cb) + + def update_scale_cb(self): + success, self.p_position, self.p_duration = \ + self.player.query_position() + + if not success: + return True + + if self.p_position != Gst.CLOCK_TIME_NONE: + value = self.p_position * 100.0 / self.p_duration + self.control.adjustment.set_value(value) + + # Update the current time + seconds = self.p_position * 10 ** -9 + time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) + self.control.current_time_label.set_text(time) + + # FIXME: this should be updated just once when the file starts + # the first time + if self.p_duration != Gst.CLOCK_TIME_NONE: + seconds = self.p_duration * 10 ** -9 + time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) + self.control.total_time_label.set_text(time) + + return True + + def _erase_playlist_entry_clicked_cb(self, widget): + self.playlist_widget.delete_selected_items() + + def __go_fullscreen_cb(self, toolbar): + self.fullscreen() + + def __toggle_playlist_cb(self, toolbar): + if self.playlist_widget.get_visible(): + self.playlist_widget.hide() + else: + self.playlist_widget.show_all() + self.canvas.queue_draw() + + +class VideoWidget(Gtk.DrawingArea): + def __init__(self): + GObject.GObject.__init__(self) + self.set_events(Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.POINTER_MOTION_HINT_MASK | + Gdk.EventMask.EXPOSURE_MASK | + Gdk.EventMask.KEY_PRESS_MASK | + Gdk.EventMask.KEY_RELEASE_MASK) + + self.set_app_paintable(True) + self.set_double_buffered(False) + + +if __name__ == '__main__': + window = Gtk.Window() + view = VideoWidget() + + #player.connect("eos", self._player_eos_cb) + #player.connect("error", self._player_error_cb) + view.show() + window.add(view) + + def map_cb(widget): + player = GstPlayer(view) + player.set_uri(sys.argv[1]) + player.play() + + window.connect('map', map_cb) + window.maximize() + window.show_all() + window.connect("destroy", Gtk.main_quit) + Gtk.main() |