""" jukeboxactivity.py Activity that plays media. Copyright (C) 2007 Andy Wingo Copyright (C) 2007 Red Hat, Inc. """ # 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 import logging from gettext import gettext as _ import os from sugar.activity import activity import pygtk pygtk.require('2.0') import sys import gobject import pygst pygst.require('0.10') import gst import gst.interfaces import gtk import urllib class JukeboxActivity(activity.Activity): UPDATE_INTERVAL = 500 def __init__(self, handle): activity.Activity.__init__(self, handle) self.set_title(_('Jukebox Activity')) toolbox = activity.ActivityToolbox(self) self.set_toolbox(toolbox) self.toolbar = toolbar = ControlToolbar(toolbox, self) toolbox.add_toolbar(_('Play'), toolbar) toolbar.show() toolbox.show() self.connect("shared", self._shared_cb) 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 self.tags = {} self.only_audio = False self.got_stream_info = False self.p_position = gst.CLOCK_TIME_NONE self.p_duration = gst.CLOCK_TIME_NONE self.player = None self.bin = gtk.HBox() self.videowidget = VideoWidget() self.bin.add(self.videowidget) self.set_canvas(self.bin) self.show_all() if handle.uri: self.uri = handle.uri gobject.idle_add(self._start, self.uri) def _player_eos_cb(self, widget): self.player.seek(0L) self.play_toggled() def _player_error_cb(self, widget, message, detail): self.player.stop() self.player.set_uri(None) self.toolbar.set_disabled() self.bin.remove(self.videowidget) text = gtk.Label("Error: %s - %s" % (message, detail)) text.show_all() self.bin.add(text) def _player_new_tag_cb(self, widget, tag, value): if not tag in [gst.TAG_TITLE, gst.TAG_ARTIST, gst.TAG_ALBUM]: return self.tags[tag] = value self._update_overlay() def _update_overlay(self): if self.only_audio == False: return if not self.tags.has_key(gst.TAG_TITLE) or not self.tags.has_key(gst.TAG_ARTIST): return album = None if self.tags.has_key(gst.TAG_ALBUM): album = self.tags[gst.TAG_ALBUM] self.player.set_overlay(self.tags[gst.TAG_TITLE], self.tags[gst.TAG_ARTIST], album) def _player_stream_info_cb(self, widget, stream_info): if not len(stream_info) or self.got_stream_info: return GST_STREAM_TYPE_UNKNOWN = 0 GST_STREAM_TYPE_AUDIO = 1 GST_STREAM_TYPE_VIDEO = 2 GST_STREAM_TYPE_TEXT = 3 only_audio = True for item in stream_info: if item.props.type == GST_STREAM_TYPE_VIDEO: only_audio = False self.only_audio = only_audio self.got_stream_info = True self._update_overlay() def _joined_cb(self, activity): logging.debug("someone joined") pass def _shared_cb(self, activity): logging.debug("shared start") pass def read_file(self, file_path): self.uri = "file://" + urllib.quote(os.path.abspath(file_path)) gobject.idle_add(self._start, self.uri) def _start(self, uri=None): if not uri: return False # FIXME: parse m3u files and extract actual URL if not self.player: # lazy init the player so that videowidget is realized # and has a valid widget allocation self.player = GstPlayer(self.videowidget) self.player.connect("eos", self._player_eos_cb) self.player.connect("error", self._player_error_cb) self.player.connect("tag", self._player_new_tag_cb) self.player.connect("stream-info", self._player_stream_info_cb) self.player.set_uri(uri) self.play_toggled() self.show_all() return False def play_toggled(self): if self.player.is_playing(): self.player.pause() self.toolbar.set_button_play() else: if self.player.error: self.toolbar.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.toolbar.set_button_pause() def scale_button_press_cb(self, widget, event): self.toolbar.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.toolbar.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.toolbar.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): self.p_position, self.p_duration = self.player.query_position() if self.p_position != gst.CLOCK_TIME_NONE: value = self.p_position * 100.0 / self.p_duration self.toolbar.adjustment.set_value(value) return True class ControlToolbar(gtk.Toolbar): def __init__(self, toolbox, jukebox): gtk.Toolbar.__init__(self) self.toolbox = toolbox self.jukebox = jukebox self.pause_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON) self.pause_image.show() self.play_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON) self.play_image.show() self.button = gtk.ToolButton() self.button.set_icon_widget(self.play_image) self.button.set_property('can-default', True) self.button.show() self.button.connect('clicked', self._button_clicked_cb) self.insert(self.button, -1) self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) self.hscale = gtk.HScale(self.adjustment) self.hscale.set_draw_value(False) self.hscale.set_update_policy(gtk.UPDATE_CONTINUOUS) self.hscale.connect('button-press-event', jukebox.scale_button_press_cb) self.hscale.connect('button-release-event', jukebox.scale_button_release_cb) self.scale_item = gtk.ToolItem() self.scale_item.set_expand(True) self.scale_item.add(self.hscale) self.insert(self.scale_item, -1) def _button_clicked_cb(self, widget): self.jukebox.play_toggled() def set_button_play(self): self.button.set_icon_widget(self.play_image) def set_button_pause(self): self.button.set_icon_widget(self.pause_image) def set_disabled(self): self.button.set_sensitive(False) self.scale_item.set_sensitive(False) self.hscale.set_sensitive(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.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)