From a94b05add51c9e01af0ae625f681863a36e0ed7a Mon Sep 17 00:00:00 2001 From: Andy Wingo Date: Fri, 31 Mar 2006 17:12:50 +0000 Subject: [PATCH] examples/remuxer.py: Example GUI for a remuxer, unfinished -- dropping it here while I hack on it. Based on player.py. Original commit message from CVS: 2006-03-31 Andy Wingo * examples/remuxer.py: Example GUI for a remuxer, unfinished -- dropping it here while I hack on it. Based on player.py. * examples/Makefile.am (examples_DATA): Add remuxer.py, reorder list. --- ChangeLog | 8 + common | 2 +- examples/Makefile.am | 17 +- examples/remuxer.py | 525 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 543 insertions(+), 9 deletions(-) create mode 100644 examples/remuxer.py diff --git a/ChangeLog b/ChangeLog index e501f43148..daa533cdb2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2006-03-31 Andy Wingo + + * examples/remuxer.py: Example GUI for a remuxer, unfinished -- + dropping it here while I hack on it. Based on player.py. + + * examples/Makefile.am (examples_DATA): Add remuxer.py, reorder + list. + 2006-03-29 David I. Lehn * configure.ac: diff --git a/common b/common index 45cc64e522..c18b429f2a 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 45cc64e522d61410eb8d1a3e7ef67569851cd77a +Subproject commit c18b429f2a3698b6fc5e849a637aa0c8b91e82b9 diff --git a/examples/Makefile.am b/examples/Makefile.am index 4fa3e83b85..86dfd5ed10 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -1,19 +1,20 @@ examplesdir = $(pkgdatadir)/$(GST_MAJORMINOR)/examples examples_DATA = \ + audioconcat.py \ audio-controller.py \ bps.py \ cp.py \ debugslider.py \ f2f.py \ filesrc.py \ - gst123 \ - play.py \ - vorbisplay.py \ - gstfile.py \ - audioconcat.py \ - pipeline-tester \ - vumeter.py \ fvumeter.py \ - sinkelement.py + gst123 \ + gstfile.py \ + play.py \ + pipeline-tester \ + remuxer.py \ + sinkelement.py \ + vorbisplay.py \ + vumeter.py \ EXTRA_DIST = $(examples_DATA) diff --git a/examples/remuxer.py b/examples/remuxer.py new file mode 100644 index 0000000000..18bccb41ad --- /dev/null +++ b/examples/remuxer.py @@ -0,0 +1,525 @@ +#!/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 + +import pygst +pygst.require('0.10') +import gst +import gst.interfaces +import gtk + +class GstPlayer: + def __init__(self, videowidget): + self.playing = False + self.player = gst.element_factory_make("playbin", "player") + self.videowidget = videowidget + self.on_eos = False + + 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': + 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() + 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_property('uri', location) + + 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_FLAG_ACCURATE, + 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()) + + 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 '') + 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_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('%s' % task) + label.set_use_markup(True) + label.set_alignment(0.0, 0.0) + label.show() + vbox.pack_start(label) + +class RemuxProgressDialog(ProgressDialog): + def __init__(self, parent, start, stop): + ProgressDialog.__init__(self, + "Writing to disk", + ('Your ears need to be periodically cleaned to ' + 'maintain a decent level of hearing. The ' + 'process will be quick and painless.'), + 'Removing excess ear wax with a golf pencil', + parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)) + 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(gtk.RESPONSE_ACCEPT, completed) + +class Remuxer(gst.Pipeline): + def __init__(self, fromuri, touri, start, stop): + gst.Pipeline.__init__(self) + self.src = gst.element_make_from_uri(gst.URI_SRC, fromuri) + self.demux = gst.element_factory_make('oggdemux') + self.mux = gst.element_factory_make('oggmux') + self.sink = gst.element_make_from_uri(gst.URI_SINK, touri) + + self.add(self.src, self.demux, self.mux, self.sink) + + self.src.link(self.demux) + self.mux.link(self.sink) + + self.demux.connect('pad-added', self._new_demuxed_pad) + self.demux.connect('no-more-pads', self._no_more_pads) + + def _new_demuxed_pad(self, element, pad): + pad.link(self.mux.get_pad('sink_%d')) + + def _no_more_pads(self, element): + pass + + def run(self): + self.set_state(gst.STATE_PLAYING) + + +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): + self.player.set_location(location) + + 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 + + self.videowidget.connect_after('realize', + lambda *x: self.play_toggled()) + + table = gtk.Table(2,3) + table.show() + vbox.pack_start(table, fill=False, expand=False, padding=6) + + self.pause_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, + gtk.ICON_SIZE_LARGE_TOOLBAR) + self.pause_image.show() + self.play_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, + gtk.ICON_SIZE_LARGE_TOOLBAR) + self.play_image.show() + self.button = button = gtk.Button() + button.add(self.play_image) + button.set_property('can-default', True) + button.set_focus_on_click(False) + button.show() + aspect = gtk.AspectFrame(obey_child=False, xalign=0.0) + aspect.set_property('shadow_type', gtk.SHADOW_NONE) + aspect.show() + aspect.add(button) + table.attach(aspect, 0, 1, 0, 2, gtk.EXPAND|gtk.FILL, gtk.EXPAND|gtk.FILL) + 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) + + buttonbox = gtk.HButtonBox() + buttonbox.set_layout(gtk.BUTTONBOX_END) + buttonbox.show() + table.attach(buttonbox, 2, 3, 1, 2, 0, 0) + + button = gtk.Button("_Render to disk") + button.set_property('image', + gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, + gtk.ICON_SIZE_BUTTON)) + button.show() + buttonbox.pack_start(button, False) + + self.cutin.connect('notify::time', lambda *x: self.check_cutout()) + self.cutout.connect('notify::time', lambda *x: self.check_cutin()) + + 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): + self.button.remove(self.button.child) + if self.player.is_playing(): + self.player.pause() + self.button.add(self.play_image) + else: + self.player.play() + if self.update_id == -1: + self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL, + self.update_scale_cb) + self.button.add(self.pause_image) + + 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): + 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) + + return True + +def main(args): + def usage(): + sys.stderr.write("usage: %s URI-OF-MEDIA-FILE\n" % args[0]) + sys.exit(1) + + w = PlayerWindow() + + if len(args) != 2: + usage() + + if not gst.uri_is_valid(args[1]): + sys.stderr.write("Error: Invalid URI: %s\n" % args[1]) + sys.exit(1) + + w.load_file(args[1]) + w.show() + + gtk.main() + +if __name__ == '__main__': + sys.exit(main(sys.argv))