#!/usr/bin/env python # -*- Mode: Python -*- # vi:si:et:sw=4:sts=4:ts=4 import pygtk pygtk.require('2.0') import sys import gobject gobject.threads_init() import pygst pygst.require('0.10') import gst import gst.interfaces import gtk gtk.gdk.threads_init() class GstPlayer: def __init__(self, videowidget): self.playing = False self.player = gst.element_factory_make("playbin", "player") self.videowidget = videowidget 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 on_sync_message(self, bus, message): if message.structure is None: return if message.structure.get_name() == 'prepare-xwindow-id': # Sync with the X server before giving the X-id to the sink gtk.gdk.threads_enter() gtk.gdk.display_get_default().sync() self.videowidget.set_sink(message.src) message.src.set_property('force-aspect-ratio', True) gtk.gdk.threads_leave() def on_message(self, bus, message): t = message.type if t == gst.MESSAGE_ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug if self.on_eos: self.on_eos() self.playing = False elif t == gst.MESSAGE_EOS: if self.on_eos: self.on_eos() self.playing = False def set_location(self, location): self.player.set_state(gst.STATE_NULL) self.player.set_property('uri', location) def get_location(self): return self.player.get_property('uri') 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 """ gst.debug("seeking to %r" % location) event = gst.event_new_seek(1.0, gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, gst.SEEK_TYPE_SET, location, gst.SEEK_TYPE_NONE, 0) res = self.player.send_event(event) if res: gst.info("setting new stream time to 0") self.player.set_new_stream_time(0L) else: gst.error("seek to %r failed" % location) def pause(self): gst.info("pausing player") self.player.set_state(gst.STATE_PAUSED) self.playing = False def play(self): gst.info("playing player") self.player.set_state(gst.STATE_PLAYING) self.playing = True def stop(self): self.player.set_state(gst.STATE_NULL) gst.info("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) 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) class TimeControl(gtk.HBox): # all labels same size sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) __gproperties__ = {'time': (gobject.TYPE_UINT64, 'Time', 'Time', # not actually usable: see #335854 # kept for .notify() usage 0L, (1<<63)-1, 0L, gobject.PARAM_READABLE)} def __init__(self, window, label): gtk.HBox.__init__(self) self.pwindow = window self.label = label self.create_ui() def get_property(self, param, pspec): if param == 'time': return self.get_time() else: assert param in self.__gproperties__, \ 'Unknown property: %s' % param def create_ui(self): label = gtk.Label(self.label + ": ") label.show() a = gtk.Alignment(1.0, 0.5) a.add(label) a.set_padding(0, 0, 12, 0) a.show() self.sizegroup.add_widget(a) self.pack_start(a, True, False, 0) self.minutes = minutes = gtk.Entry(5) minutes.set_width_chars(5) minutes.set_alignment(1.0) minutes.connect('changed', lambda *x: self.notify('time')) minutes.connect_after('activate', lambda *x: self.activated()) label2 = gtk.Label(":") self.seconds = seconds = gtk.Entry(2) seconds.set_width_chars(2) seconds.set_alignment(1.0) seconds.connect('changed', lambda *x: self.notify('time')) seconds.connect_after('activate', lambda *x: self.activated()) label3 = gtk.Label(".") self.milliseconds = milliseconds = gtk.Entry(3) milliseconds.set_width_chars(3) milliseconds.set_alignment(0.0) milliseconds.connect('changed', lambda *x: self.notify('time')) milliseconds.connect_after('activate', lambda *x: self.activated()) set = gtk.Button('Set') goto = gtk.Button('Go') goto.set_property('image', gtk.image_new_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_BUTTON)) for w in minutes, label2, seconds, label3, milliseconds: w.show() self.pack_start(w, False) set.show() self.pack_start(set, False, False, 6) goto.show() self.pack_start(goto, False, False, 0) set.connect('clicked', lambda *x: self.set_now()) goto.connect('clicked', lambda *x: self.activated()) pad = gtk.Label("") pad.show() self.pack_start(pad, True, False, 0) def get_time(self): time = 0 for w, multiplier in ((self.minutes, gst.SECOND*60), (self.seconds, gst.SECOND), (self.milliseconds, gst.MSECOND)): text = w.get_text() try: val = int(text) except ValueError: val = 0 w.set_text(val and str(val) or '0') time += val * multiplier return time def set_time(self, time): if time == gst.CLOCK_TIME_NONE: print "Can't set '%s' (invalid time)" % self.label return self.freeze_notify() for w, multiplier in ((self.minutes, gst.SECOND*60), (self.seconds, gst.SECOND), (self.milliseconds, gst.MSECOND)): val = time // multiplier w.set_text(str(val)) time -= val * multiplier self.thaw_notify() def set_now(self): time, dur = self.pwindow.player.query_position() self.set_time(time) def activated(self): time = self.get_time() if self.pwindow.player.is_playing(): self.pwindow.play_toggled() self.pwindow.player.seek(time) self.pwindow.player.get_state(timeout=gst.MSECOND * 200) class ProgressDialog(gtk.Dialog): def __init__(self, title, description, task, parent, flags, buttons): gtk.Dialog.__init__(self, title, parent, flags, buttons) self._create_ui(title, description, task) def _create_ui(self, title, description, task): self.set_border_width(6) self.set_resizable(False) self.set_has_separator(False) vbox = gtk.VBox() vbox.set_border_width(6) vbox.show() self.vbox.pack_start(vbox, False) label = gtk.Label('%s' % title) label.set_use_markup(True) label.set_alignment(0.0, 0.0) label.show() vbox.pack_start(label, False) label = gtk.Label(description) label.set_use_markup(True) label.set_alignment(0.0, 0.0) label.set_line_wrap(True) label.set_padding(0, 12) label.show() vbox.pack_start(label, False) self.progress = progress = gtk.ProgressBar() progress.show() vbox.pack_start(progress, False) self.progresstext = label = gtk.Label('') label.set_line_wrap(True) label.set_use_markup(True) label.set_alignment(0.0, 0.0) label.show() vbox.pack_start(label) self.set_task(task) def set_task(self, task): self.progresstext.set_markup('%s' % task) UNKNOWN = 0 SUCCESS = 1 FAILURE = 2 CANCELLED = 3 class RemuxProgressDialog(ProgressDialog): def __init__(self, parent, start, stop, fromname, toname): ProgressDialog.__init__(self, "Writing to disk", ('Writing the selected segment of %s ' 'to %s. This may take some time.' % (fromname, toname)), 'Starting media pipeline', parent, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, CANCELLED, gtk.STOCK_CLOSE, SUCCESS)) self.start = start self.stop = stop self.update_position(start) self.set_completed(False) def update_position(self, pos): pos = min(max(pos, self.start), self.stop) remaining = self.stop - pos minutes = remaining // (gst.SECOND * 60) seconds = (remaining - minutes * gst.SECOND * 60) // gst.SECOND self.progress.set_text('%d:%02d of video remaining' % (minutes, seconds)) self.progress.set_fraction(1.0 - float(remaining) / (self.stop - self.start)) def set_completed(self, completed): self.set_response_sensitive(CANCELLED, not completed) self.set_response_sensitive(SUCCESS, completed) def set_connection_blocked_async_marshalled(pads, proc, *args, **kwargs): def clear_list(l): while l: l.pop() to_block = list(pads) to_relink = [(x, x.get_peer()) for x in pads] def on_pad_blocked_sync(pad, is_blocked): if pad not in to_block: # can happen after the seek and before unblocking -- racy, # but no prob, bob. return to_block.remove(pad) if not to_block: # marshal to main thread gobject.idle_add(on_pads_blocked) def on_pads_blocked(): for src, sink in to_relink: src.link(sink) proc(*args, **kwargs) for src, sink in to_relink: src.set_blocked_async(False, lambda *x: None) clear_list(to_relink) for src, sink in to_relink: src.unlink(sink) src.set_blocked_async(True, on_pad_blocked_sync) class Remuxer(gst.Pipeline): __gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))} def __init__(self, fromuri, touri, start, stop): # HACK: should do Pipeline.__init__, but that doesn't do what we # want; there's a bug open aboooot that self.__gobject_init__() assert start >= 0 assert stop > start self.fromuri = fromuri self.touri = None self.start_time = start self.stop_time = stop self.src = self.remuxbin = self.sink = None self.resolution = UNKNOWN self.window = None self.pdialog = None self._query_id = -1 def do_setup_pipeline(self): self.src = gst.element_make_from_uri(gst.URI_SRC, self.fromuri) self.remuxbin = RemuxBin(self.start_time, self.stop_time) self.sink = gst.element_make_from_uri(gst.URI_SINK, self.touri) self.resolution = UNKNOWN if gobject.signal_lookup('allow-overwrite', self.sink.__class__): self.sink.connect('allow-overwrite', lambda *x: True) self.add(self.src, self.remuxbin, self.sink) self.src.link(self.remuxbin) self.remuxbin.link(self.sink) def do_get_touri(self): chooser = gtk.FileChooserDialog('Save as...', self.window, action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL, CANCELLED, gtk.STOCK_SAVE, SUCCESS)) chooser.set_uri(self.fromuri) # to select the folder chooser.unselect_all() chooser.set_do_overwrite_confirmation(True) name = self.fromuri.split('/')[-1][:-4] + '-remuxed.ogg' chooser.set_current_name(name) resp = chooser.run() uri = chooser.get_uri() chooser.destroy() if resp == SUCCESS: return uri else: return None def _start_queries(self): def do_query(): try: # HACK: self.remuxbin.query() should do the same # (requires implementing a vmethod, dunno how to do that # although i think it's possible) # HACK: why does self.query_position(..) not give useful # answers? pad = self.remuxbin.get_pad('src') pos, duration = pad.query_position(gst.FORMAT_TIME) if pos != gst.CLOCK_TIME_NONE: self.pdialog.update_position(pos) except: # print 'query failed' pass return True if self._query_id == -1: self._query_id = gobject.timeout_add(100, # 10 Hz do_query) def _stop_queries(self): if self._query_id != -1: gobject.source_remove(self._query_id) self._query_id = -1 def _bus_watch(self, bus, message): if message.type == gst.MESSAGE_ERROR: print 'error', message self._stop_queries() m = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, "Error processing file") gerror, debug = message.parse_error() txt = ('There was an error processing your file: %s\n\n' 'Debug information:\n%s' % (gerror, debug)) m.format_secondary_text(txt) m.run() m.destroy() self.response(FAILURE) elif message.type == gst.MESSAGE_WARNING: print 'warning', message elif message.type == gst.MESSAGE_EOS: # print 'eos, woot', message.src name = self.touri if name.startswith('file://'): name = name[7:] self.pdialog.set_task('Finished writing %s' % name) self.pdialog.update_position(self.stop_time) self._stop_queries() self.pdialog.set_completed(True) elif message.type == gst.MESSAGE_STATE_CHANGED: if message.src == self: old, new, pending = message.parse_state_changed() if ((old, new, pending) == (gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_VOID_PENDING)): self.pdialog.set_task('Processing file') self.pdialog.update_position(self.start_time) self._start_queries() self.set_state(gst.STATE_PLAYING) def response(self, response): assert self.resolution == UNKNOWN self.resolution = response self.set_state(gst.STATE_NULL) self.pdialog.destroy() self.pdialog = None self.window.set_sensitive(True) self.emit('done', response) def start(self, main_window): self.window = main_window self.touri = self.do_get_touri() if not self.touri: return False self.do_setup_pipeline() bus = self.get_bus() bus.add_signal_watch() bus.connect('message', self._bus_watch) if self.window: # can be None if we are debugging... self.window.set_sensitive(False) fromname = self.fromuri.split('/')[-1] toname = self.touri.split('/')[-1] self.pdialog = RemuxProgressDialog(main_window, self.start_time, self.stop_time, fromname, toname) self.pdialog.show() self.pdialog.connect('response', lambda w, r: self.response(r)) self.set_state(gst.STATE_PAUSED) return True def run(self, main_window): if self.start(main_window): loop = gobject.MainLoop() self.connect('done', lambda *x: gobject.idle_add(loop.quit)) loop.run() else: self.resolution = CANCELLED return self.resolution class RemuxBin(gst.Bin): def __init__(self, start_time, stop_time): self.__gobject_init__() self.parsefactories = self._find_parsers() self.parsers = [] self.demux = gst.element_factory_make('oggdemux') self.mux = gst.element_factory_make('oggmux') self.add(self.demux, self.mux) self.add_pad(gst.GhostPad('sink', self.demux.get_pad('sink'))) self.add_pad(gst.GhostPad('src', self.mux.get_pad('src'))) self.demux.connect('pad-added', self._new_demuxed_pad) self.demux.connect('no-more-pads', self._no_more_pads) self.start_time = start_time self.stop_time = stop_time def _find_parsers(self): registry = gst.registry_get_default() ret = {} for f in registry.get_feature_list(gst.ElementFactory): if f.get_klass().find('Parser') >= 0: for t in f.get_static_pad_templates(): if t.direction == gst.PAD_SINK: for s in t.get_caps(): ret[s.get_name()] = f.get_name() break return ret def _new_demuxed_pad(self, element, pad): format = pad.get_caps()[0].get_name() if format not in self.parsefactories: self.async_error("Unsupported media type: %s", format) return queue = gst.element_factory_make('queue', 'queue_' + format) queue.set_property('max-size-buffers', 1000) parser = gst.element_factory_make(self.parsefactories[format]) self.add(queue) self.add(parser) queue.set_state(gst.STATE_PAUSED) parser.set_state(gst.STATE_PAUSED) pad.link(queue.get_compatible_pad(pad)) queue.link(parser) parser.link(self.mux) self.parsers.append(parser) def _do_seek(self): flags = gst.SEEK_FLAG_FLUSH # HACK: self.seek should work, should try that at some point return self.demux.seek(1.0, gst.FORMAT_TIME, flags, gst.SEEK_TYPE_SET, self.start_time, gst.SEEK_TYPE_SET, self.stop_time) def _no_more_pads(self, element): pads = [x.get_pad('src') for x in self.parsers] set_connection_blocked_async_marshalled(pads, self._do_seek) class PlayerWindow(gtk.Window): UPDATE_INTERVAL = 500 def __init__(self): gtk.Window.__init__(self) self.set_default_size(600, 425) self.create_ui() self.player = GstPlayer(self.videowidget) def on_eos(): self.player.seek(0L) self.play_toggled() self.player.on_eos = lambda *x: on_eos() self.update_id = -1 self.changed_id = -1 self.seek_timeout_id = -1 self.p_position = gst.CLOCK_TIME_NONE self.p_duration = gst.CLOCK_TIME_NONE def on_delete_event(): self.player.stop() gtk.main_quit() self.connect('delete-event', lambda *x: on_delete_event()) def load_file(self, location): filename = location.split('/')[-1] self.set_title('%s munger' % filename) self.player.set_location(location) if self.videowidget.flags() & gtk.REALIZED: self.play_toggled() else: self.videowidget.connect_after('realize', lambda *x: self.play_toggled()) def create_ui(self): vbox = gtk.VBox() vbox.show() self.add(vbox) self.videowidget = VideoWidget() self.videowidget.show() vbox.pack_start(self.videowidget) hbox = gtk.HBox() hbox.show() vbox.pack_start(hbox, fill=False, expand=False) self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) hscale = gtk.HScale(self.adjustment) hscale.set_digits(2) hscale.set_update_policy(gtk.UPDATE_CONTINUOUS) hscale.connect('button-press-event', self.scale_button_press_cb) hscale.connect('button-release-event', self.scale_button_release_cb) hscale.connect('format-value', self.scale_format_value_cb) hbox.pack_start(hscale) hscale.show() self.hscale = hscale table = gtk.Table(2,3) table.show() vbox.pack_start(table, fill=False, expand=False, padding=6) self.button = button = gtk.Button(stock=gtk.STOCK_MEDIA_PLAY) button.set_property('can-default', True) button.set_focus_on_click(False) button.show() # problem: play and paused are of different widths and cause the # window to re-layout # "solution": add more buttons to a vbox so that the horizontal # width is enough bvbox = gtk.VBox() bvbox.add(button) bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PLAY)) bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PAUSE)) sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) for kid in bvbox.get_children(): sizegroup.add_widget(kid) bvbox.show() table.attach(bvbox, 0, 1, 0, 2, gtk.FILL, gtk.FILL) # can't set this property before the button has a window button.set_property('has-default', True) button.connect('clicked', lambda *args: self.play_toggled()) self.cutin = cut = TimeControl(self, "Cut in time") cut.show() table.attach(cut, 1, 2, 0, 1, gtk.EXPAND, 0, 12) self.cutout = cut = TimeControl(self, "Cut out time") cut.show() table.attach(cut, 1, 2, 1, 2, gtk.EXPAND, 0, 12) button = gtk.Button("_Open other movie...") button.show() button.connect('clicked', lambda *x: self.do_choose_file()) table.attach(button, 2, 3, 0, 1, gtk.FILL, gtk.FILL) button = gtk.Button("_Write to disk") button.set_property('image', gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_BUTTON)) button.connect('clicked', lambda *x: self.do_remux()) button.show() table.attach(button, 2, 3, 1, 2, gtk.FILL, gtk.FILL) #self.cutin.connect('notify::time', lambda *x: self.check_cutout()) #self.cutout.connect('notify::time', lambda *x: self.check_cutin()) def do_remux(self): if self.player.is_playing(): self.play_toggled() in_uri = self.player.get_location() out_uri = in_uri[:-4] + '-remuxed.ogg' r = Remuxer(in_uri, out_uri, self.cutin.get_time(), self.cutout.get_time()) r.run(self) def do_choose_file(self): if self.player.is_playing(): self.play_toggled() chooser = gtk.FileChooserDialog('Choose a movie to cut cut cut', self, buttons=(gtk.STOCK_CANCEL, CANCELLED, gtk.STOCK_OPEN, SUCCESS)) chooser.set_local_only(False) chooser.set_select_multiple(False) f = gtk.FileFilter() f.set_name("All files") f.add_pattern("*") chooser.add_filter(f) f = gtk.FileFilter() f.set_name("Ogg files") f.add_pattern("*.ogg") # as long as this is the only thing we # support... chooser.add_filter(f) chooser.set_filter(f) prev = self.player.get_location() if prev: chooser.set_uri(prev) resp = chooser.run() uri = chooser.get_uri() chooser.destroy() if resp == SUCCESS: self.load_file(uri) return True else: return False def check_cutout(self): if self.cutout.get_time() <= self.cutin.get_time(): pos, dur = self.player.query_position() self.cutout.set_time(dur) def check_cutin(self): if self.cutin.get_time() >= self.cutout.get_time(): self.cutin.set_time(0) def play_toggled(self): if self.player.is_playing(): self.player.pause() self.button.set_label(gtk.STOCK_MEDIA_PLAY) else: self.player.play() if self.update_id == -1: self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL, self.update_scale_cb) self.button.set_label(gtk.STOCK_MEDIA_PAUSE) def scale_format_value_cb(self, scale, value): if self.p_duration == -1: real = 0 else: real = value * self.p_duration / 100 seconds = real / gst.SECOND return "%02d:%02d" % (seconds / 60, seconds % 60) def scale_button_press_cb(self, widget, event): # see seek.c:start_seek gst.debug('starting seek') self.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.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 gst.debug('value changed, perform seek to %r' % real) 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.button.set_sensitive(True) if self.seek_timeout_id != -1: gobject.source_remove(self.seek_timeout_id) self.seek_timeout_id = -1 else: gst.debug('released slider, setting back to playing') 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): had_duration = self.p_duration != gst.CLOCK_TIME_NONE 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.adjustment.set_value(value) if not had_duration: self.cutin.set_time(0) return True def main(args): def usage(): sys.stderr.write("usage: %s [URI-OF-MEDIA-FILE]\n" % args[0]) return 1 w = PlayerWindow() w.show() if len(args) == 1: if not w.do_choose_file(): return 1 elif len(args) == 2: if not gst.uri_is_valid(args[1]): sys.stderr.write("Error: Invalid URI: %s\n" % args[1]) return 1 w.load_file(args[1]) else: return usage() gtk.main() if __name__ == '__main__': sys.exit(main(sys.argv))