diff options
-rw-r--r-- | activity/activity-secuencia.svg | 103 | ||||
-rw-r--r-- | activity/activity.info | 9 | ||||
-rwxr-xr-x | multimedia.py | 252 | ||||
-rw-r--r-- | secuencia.py | 147 | ||||
-rwxr-xr-x | setup.py | 22 |
5 files changed, 533 insertions, 0 deletions
diff --git a/activity/activity-secuencia.svg b/activity/activity-secuencia.svg new file mode 100644 index 0000000..ec0bcdd --- /dev/null +++ b/activity/activity-secuencia.svg @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> + + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_svg "http://www.w3.org/2000/svg"> + <!ENTITY ns_xlink " http://www.w3.org/1999/xlink"> + <!ENTITY stroke_color "#010101"> + <!ENTITY fill_color "#FFFFFF"> + ]> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + enable-background="new 0 0 55 55" + height="55px" + version="1.1" + viewBox="0 0 55 55" + width="55px" + x="0px" + xml:space="preserve" + y="0px" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.45.1" + sodipodi:docname="activity-go.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:docbase="/home/gjpc/PlayGo/PlayGo/src/activity"><metadata + id="metadata26"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs24" /><sodipodi:namedview + inkscape:window-height="619" + inkscape:window-width="872" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + guidetolerance="10.0" + gridtolerance="10.0" + objecttolerance="10.0" + borderopacity="1.0" + bordercolor="#ffffff" + pagecolor="#ffffff" + id="base" + inkscape:zoom="7.3818182" + inkscape:cx="27.5" + inkscape:cy="27.5" + inkscape:window-x="26" + inkscape:window-y="59" + inkscape:current-layer="svg2" /><g + display="block" + id="activity-connect" + transform="translate(-0.5418719,-1.2192118)" + style="display:block"> + <g + display="inline" + id="g5" + style="display:inline"> + <path + d="M 14.118,9.535 C 14.119,11.967 12.148,13.94 9.716,13.941 C 7.283,13.942 5.311,11.971 5.31,9.539 C 5.31,9.538 5.31,9.536 5.31,9.535 C 5.308,7.103 7.279,5.13 9.711,5.128 C 12.143,5.127 14.116,7.098 14.117,9.53 C 14.118,9.532 14.118,9.534 14.118,9.535 z " + id="path2160" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 51.188,9.535 C 51.19,11.967 49.219,13.94 46.786,13.941 C 44.354,13.942 42.382,11.971 42.38,9.539 C 42.38,9.538 42.38,9.536 42.38,9.535 C 42.38,7.102 44.351,5.13 46.782,5.128 C 49.216,5.127 51.188,7.098 51.188,9.53 C 51.188,9.532 51.188,9.534 51.188,9.535 z " + id="path2162" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 26.475,9.535 C 26.475,11.967 24.504,13.94 22.072,13.941 C 19.639,13.942 17.667,11.971 17.665,9.539 C 17.665,9.538 17.665,9.536 17.665,9.535 C 17.664,7.102 19.635,5.13 22.067,5.128 C 24.5,5.127 26.472,7.098 26.474,9.53 C 26.475,9.532 26.475,9.534 26.475,9.535 z " + id="path3134" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 38.832,9.535 C 38.832,11.967 36.861,13.94 34.43,13.941 C 31.996,13.942 30.024,11.971 30.022,9.539 C 30.022,9.538 30.022,9.536 30.022,9.535 C 30.022,7.102 31.993,5.13 34.424,5.128 C 36.858,5.127 38.83,7.098 38.832,9.53 C 38.832,9.532 38.832,9.534 38.832,9.535 z " + id="path3136" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 26.475,21.892 C 26.475,24.325 24.504,26.297 22.072,26.298 C 19.639,26.299 17.667,24.328 17.665,21.896 C 17.665,21.895 17.665,21.893 17.665,21.892 C 17.664,19.459 19.635,17.487 22.067,17.485 C 24.5,17.484 26.472,19.455 26.474,21.887 C 26.475,21.889 26.475,21.891 26.475,21.892 z " + id="path3138" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 38.832,21.892 C 38.832,24.325 36.861,26.297 34.43,26.298 C 31.996,26.299 30.024,24.328 30.022,21.896 C 30.022,21.895 30.022,21.893 30.022,21.892 C 30.022,19.459 31.993,17.487 34.424,17.485 C 36.858,17.484 38.83,19.455 38.832,21.887 C 38.832,21.889 38.832,21.891 38.832,21.892 z " + id="path3140" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 14.118,21.892 C 14.119,24.325 12.148,26.297 9.716,26.298 C 7.284,26.299 5.311,24.328 5.31,21.896 C 5.31,21.895 5.31,21.893 5.31,21.892 C 5.309,19.459 7.28,17.487 9.712,17.485 C 12.144,17.484 14.117,19.455 14.118,21.887 C 14.118,21.889 14.118,21.891 14.118,21.892 z " + id="path3142" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 51.188,21.892 C 51.19,24.325 49.219,26.297 46.786,26.298 C 44.354,26.299 42.382,24.328 42.38,21.896 C 42.38,21.895 42.38,21.893 42.38,21.892 C 42.38,19.459 44.351,17.487 46.782,17.485 C 49.216,17.484 51.188,19.455 51.188,21.887 C 51.188,21.889 51.188,21.891 51.188,21.892 z " + id="path3144" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 14.118,34.248 C 14.119,36.68 12.148,38.652 9.716,38.654 C 7.283,38.655 5.311,36.684 5.31,34.252 C 5.31,34.25 5.31,34.249 5.31,34.248 C 5.309,31.815 7.28,29.843 9.712,29.842 C 12.144,29.84 14.117,31.811 14.118,34.244 C 14.118,34.245 14.118,34.246 14.118,34.248 z " + id="path3146" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 38.832,34.248 C 38.832,36.68 36.861,38.652 34.43,38.654 C 31.996,38.655 30.024,36.684 30.022,34.252 C 30.022,34.25 30.022,34.249 30.022,34.248 C 30.022,31.815 31.993,29.843 34.424,29.842 C 36.858,29.84 38.83,31.811 38.832,34.244 C 38.832,34.245 38.832,34.246 38.832,34.248 z " + id="path3152" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 38.832,46.604 C 38.832,49.038 36.861,51.01 34.43,51.011 C 31.996,51.012 30.024,49.041 30.022,46.609 C 30.022,46.608 30.022,46.606 30.022,46.604 C 30.022,44.172 31.993,42.2 34.424,42.198 C 36.858,42.198 38.83,44.169 38.832,46.6 C 38.832,46.602 38.832,46.603 38.832,46.604 z " + id="path3154" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 14.118,46.604 C 14.119,49.038 12.148,51.01 9.716,51.011 C 7.284,51.012 5.311,49.041 5.31,46.609 C 5.31,46.608 5.31,46.606 5.31,46.604 C 5.309,44.172 7.28,42.2 9.712,42.198 C 12.144,42.198 14.117,44.169 14.118,46.6 C 14.118,46.602 14.118,46.603 14.118,46.604 z " + id="path3156" fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 26.475,46.604 C 26.475,49.038 24.504,51.01 22.072,51.011 C 19.639,51.012 17.667,49.041 17.665,46.609 C 17.665,46.608 17.665,46.606 17.665,46.604 C 17.664,44.172 19.635,42.2 22.067,42.198 C 24.5,42.198 26.472,44.169 26.474,46.6 C 26.475,46.602 26.475,46.603 26.475,46.604 z " + id="path3158" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + <path + d="M 51.188,46.604 C 51.19,49.038 49.219,51.01 46.786,51.011 C 44.354,51.012 42.382,49.041 42.38,46.609 C 42.38,46.608 42.38,46.606 42.38,46.604 C 42.38,44.172 44.351,42.2 46.782,42.198 C 49.216,42.198 51.188,44.169 51.188,46.6 C 51.188,46.602 51.188,46.603 51.188,46.604 z " + id="path3160" fill="&stroke_color;" stroke="&stroke_color;" stroke-width="2.25;" /> + </g> +</g></svg> diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..f39b4c4 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,9 @@ +[Activity] +name = Secuencia +license = GPLv2+ +bundle_id = org.ceibaljam.Secuencia +exec = sugar-activity secuencia.Secuencia +icon = activity-secuencia +activity_version = 1 +host_version = 1 +show_launcher = no diff --git a/multimedia.py b/multimedia.py new file mode 100755 index 0000000..34ab734 --- /dev/null +++ b/multimedia.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +# Copyright (c) 2010, CeibalJam! +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gst +import gobject +import gtk + +import logging +logger = logging.getLogger('secuencia: ' + __file__) + +class AudioPlayer(object): + def __init__(self): + self.player = gst.element_factory_make('playbin') + fakesink = gst.element_factory_make('fakesink') + self.player.set_property("video-sink", fakesink) + + bus = self.player.get_bus() + bus.add_signal_watch() + bus.connect('message', self._gstmessage_cb) + + self.playing = False + + def play(self, file): + self.player.set_state(gst.STATE_NULL) + self.player.props.uri = 'file://' + file + self.player.set_state(gst.STATE_PLAYING) + self.playing = True + + def _gstmessage_cb(self, bus, message): + if message.type == gst.MESSAGE_EOS: + self.player.set_state(gst.STATE_NULL) + self.playing = False + elif message.type == gst.MESSAGE_ERROR: + err, debug = message.parse_error() + logger.error('play_pipe: %s %s' % (err, debug)) + self.player.set_state(gst.STATE_NULL) + self.playing = False + +class GstPlayer(gobject.GObject): + __gsignals__ = { + 'error': (gobject.SIGNAL_RUN_FIRST, None, [str, str]), + 'eos' : (gobject.SIGNAL_RUN_FIRST, None, []), + 'tag' : (gobject.SIGNAL_RUN_FIRST, None, [str, str]), + 'stream-info' : (gobject.SIGNAL_RUN_FIRST, None, [object]) + } + + def __init__(self, videowidget): + gobject.GObject.__init__(self) + + self.playing = False + self.error = False + + self.player = gst.element_factory_make("playbin", "player") + + r = gst.registry_get_default() + l = [x for x in r.get_feature_list(gst.ElementFactory) if (gst.ElementFactory.get_klass(x) == "Visualization")] + if len(l): + e = l.pop() # take latest plugin in the list + vis_plug = gst.element_factory_make(e.get_name()) + self.player.set_property('vis-plugin', vis_plug) + + self.overlay = None + self.videowidget = videowidget + self._init_video_sink() + + bus = self.player.get_bus() + bus.enable_sync_message_emission() + bus.add_signal_watch() + bus.connect('sync-message::element', self.on_sync_message) + bus.connect('message', self.on_message) + + def set_uri(self, uri): + self.player.set_property('uri', uri) + + def on_sync_message(self, bus, message): + if message.structure is None: + return + if message.structure.get_name() == 'prepare-xwindow-id': + self.videowidget.set_sink(message.src) + message.src.set_property('force-aspect-ratio', True) + + def on_message(self, bus, message): + t = message.type + if t == gst.MESSAGE_ERROR: + err, debug = message.parse_error() + logging.debug("Error: %s - %s" % (err, debug)) + self.error = True + self.emit("eos") + self.playing = False + self.emit("error", str(err), str(debug)) + elif t == gst.MESSAGE_EOS: + self.emit("eos") + self.playing = False + elif t == gst.MESSAGE_TAG: + tags = message.parse_tag() + for tag in tags.keys(): + self.emit('tag', str(tag), str(tags[tag])) + elif t == gst.MESSAGE_STATE_CHANGED: + old, new, pen = message.parse_state_changed() + if old == gst.STATE_READY and new == gst.STATE_PAUSED: + self.emit('stream-info', self.player.props.stream_info_value_array) + + def _init_video_sink(self): + self.bin = gst.Bin() + videoscale = gst.element_factory_make('videoscale') + self.bin.add(videoscale) + pad = videoscale.get_pad("sink") + ghostpad = gst.GhostPad("sink", pad) + self.bin.add_pad(ghostpad) + videoscale.set_property("method", 0) + + caps_string = "video/x-raw-yuv, " + r = self.videowidget.get_allocation() + if r.width > 500 and r.height > 500: + # Sigh... xvimagesink on the XOs will scale the video to fit + # but ximagesink in Xephyr does not. So we live with unscaled + # video in Xephyr so that the XO can work right. + w = 480 + h = float(w) / float(float(r.width) / float(r.height)) + caps_string += "width=%d, height=%d" % (w, h) + else: + caps_string += "width=480, height=360" + caps = gst.Caps(caps_string) + self.filter = gst.element_factory_make("capsfilter", "filter") + self.bin.add(self.filter) + self.filter.set_property("caps", caps) + + textoverlay = gst.element_factory_make('textoverlay') + self.overlay = textoverlay + self.bin.add(textoverlay) + conv = gst.element_factory_make ("ffmpegcolorspace", "conv"); + self.bin.add(conv) + videosink = gst.element_factory_make('autovideosink') + self.bin.add(videosink) + gst.element_link_many(videoscale, self.filter, textoverlay, conv, videosink) + self.player.set_property("video-sink", self.bin) + + def set_overlay(self, title, artist, album): + text = "%s\n%s" % (title, artist) + if album and len(album): + text += "\n%s" % album + self.overlay.set_property("text", text) + self.overlay.set_property("font-desc", "sans bold 14") + self.overlay.set_property("halignment", "right") + self.overlay.set_property("valignment", "bottom") + try: + # Only in OLPC versions of gstreamer-plugins-base for now + self.overlay.set_property("line-align", "left") + except: + pass + + def query_position(self): + "Returns a (position, duration) tuple" + try: + position, format = self.player.query_position(gst.FORMAT_TIME) + except: + position = gst.CLOCK_TIME_NONE + + try: + duration, format = self.player.query_duration(gst.FORMAT_TIME) + except: + duration = gst.CLOCK_TIME_NONE + + return (position, duration) + + def seek(self, location): + """ + @param location: time to seek to, in nanoseconds + """ + event = gst.event_new_seek(1.0, gst.FORMAT_TIME, + gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_ACCURATE, + gst.SEEK_TYPE_SET, location, + gst.SEEK_TYPE_NONE, 0) + + res = self.player.send_event(event) + if res: + self.player.set_new_stream_time(0L) + else: + logging.debug("seek to %r failed" % location) + + def pause(self): + logging.debug("pausing player") + self.player.set_state(gst.STATE_PAUSED) + self.playing = False + + def play(self): + logging.debug("playing player") + self.player.set_state(gst.STATE_PLAYING) + self.playing = True + self.error = False + + def stop(self): + self.player.set_state(gst.STATE_NULL) + logging.debug("stopped player") + + def get_state(self, timeout=1): + return self.player.get_state(timeout=timeout) + + def is_playing(self): + return self.playing + +class VideoWidget(gtk.DrawingArea): + def __init__(self): + gtk.DrawingArea.__init__(self) + self.set_events(gtk.gdk.POINTER_MOTION_MASK | + gtk.gdk.POINTER_MOTION_HINT_MASK | + gtk.gdk.EXPOSURE_MASK | + gtk.gdk.KEY_PRESS_MASK | + gtk.gdk.KEY_RELEASE_MASK) + self.imagesink = None + self.unset_flags(gtk.DOUBLE_BUFFERED) + self.set_flags(gtk.APP_PAINTABLE) + + def do_expose_event(self, event): + if self.imagesink: + self.imagesink.expose() + return False + else: + return True + + def set_sink(self, sink): + assert self.window.xid + self.imagesink = sink + self.imagesink.set_xwindow_id(self.window.xid) + +if __name__ == '__main__': + import sys + w = gtk.Window() + w.set_size_request(800,600) + view = VideoWidget() + + player = GstPlayer(view) + w.add(view) + w.connect('destroy', lambda w: gtk.main_quit()) + w.show_all() + print "Playing " + sys.argv[1] + player.set_uri(sys.argv[1]) + player.play() + gtk.main() diff --git a/secuencia.py b/secuencia.py new file mode 100644 index 0000000..04fdc7c --- /dev/null +++ b/secuencia.py @@ -0,0 +1,147 @@ +from sugar.activity import activity +from sugar.graphics.toolbarbox import ToolbarBox +from sugar.graphics.objectchooser import ObjectChooser +from sugar.datastore import datastore +from sugar import mime +from sugar.logger import logging +from gettext import gettext as _ +import gtk +import os +import statvfs +import zipfile +import gio + + +ACTION_ICONS = { + 'sound' : gtk.STOCK_MEDIA_PLAY, + 'video' : gtk.STOCK_MEDIA_PLAY, + 'next' : gtk.STOCK_GO_FORWARD, +} + +class Secuencia(activity.Activity): + def __init__(self, handle): + activity.Activity.__init__(self, handle) + + self.toolbox = activity.ActivityToolbox(self) + self.set_toolbox(self.toolbox) + self.toolbox.show() + + main_box = gtk.VBox() + + frame = gtk.Frame() + main_box.pack_start(frame) + frame.show() + + frame_vbox = gtk.VBox() + frame.add(frame_vbox) + frame_vbox.show() + + self.text = gtk.Label() + frame_vbox.pack_start(self.text) + self.text.show() + + self.image = gtk.Image() + frame_vbox.pack_start(self.image) + self.image.show() + + self.action_icon = gtk.Image() + main_box.pack_start(self.action_icon, False) + self.action_icon.show() + + self.set_canvas(main_box) + main_box.show() + + # A list of (filename, mimetype) tuples + self.list = [] + self.current_pos = -1 + self.executed_action = True + + if self.shared_activity: + pass + elif handle.object_id is not None: + self.list = self.metadata['files'] + else: + self._show_journal_object_picker() + + self.connect('button-press-event', lambda w, e: self._next()) + self.show_all() + self._next() + + def _next(self): + if not self.executed_action: + card = self.list[self.current_pos] + self._execute(card) + self.executed_action = True + else: + self.current_pos += 1 + try: + card = self.list[self.current_pos] + except IndexError: + self.current_pos = -1 + self._next() + else: + self._show_card(card) + self.executed_action = False + + def _execute(self, card): + if 'sound' in card: + # Play sound + pass + elif 'video' in card: + # Play video + pass + + def _show_card(self, card): + if 'image' in card: + self.image.set_from_file(os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], 'tmp', card['image'])) + else: + self.image.set_from_stock(gtk.STOCK_MISSING_IMAGE) + + if 'action' in card: + self.action_icon.set_from_stock(ACTION_ICONS[card['action']], gtk.ICON_SIZE_LARGE_TOOLBAR) + else: + self.action_icon.set_from_stock(ACTION_ICONS['next'], gtk.ICON_SIZE_LARGE_TOOLBAR) + + self.text.set_text(card['title']) + + def _show_journal_object_picker(self): + """Show the journal object picker to load a document. + + This is for if Read is launched without a document. + """ + chooser = ObjectChooser(_('Choose document'), self, + gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + what_filter=mime.GENERIC_TYPE_TEXT) #FIXME: Filter by this activity's type + try: + result = chooser.run() + if result == gtk.RESPONSE_ACCEPT: + logging.debug('ObjectChooser: %r' % + chooser.get_selected_object()) + jobject = chooser.get_selected_object() + if jobject and jobject.file_path: + self.list = jobject.metadata['files'] + self.read_file(jobject.file_path) + finally: + chooser.destroy() + del chooser + + def read_file(self, path): + zip = zipfile.ZipFile(path) + info = zip.infolist() + size = reduce(lambda s, i: s+i.file_size, info, 0) + stat = os.stavfs(os.environ['SUGAR_ACTIVITY_ROOT']) + if stat[statvfs.F_BSIZE]*stat[statvfs.F_BAVAIL] < size: + #TODO: Alert: Not enough free space + for member in zip.infolist(): + try: + data = member.read() + self.list.append((member.filename, gio.content_type_guess(None, data)) + f = open(os.path.join(os.environ['SUGAR_ACTIVITY_ROOT'], 'tmp', member.filename), 'w') + f.write(data) + del data + finally: + f.close() + + def write_file(self, path): + pass diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..382f317 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +# Copyright (C) 2006, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.activity import bundlebuilder + +if __name__ == "__main__": + bundlebuilder.start() |