gstreamer/examples/remuxer.py

663 lines
22 KiB
Python
Raw Normal View History

#!/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 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_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('<big><b>%s</b></big>' % 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('')
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('<i>%s</i>' % task)
UNKNOWN = 0
SUCCESS = 1
FAILURE = 2
CANCELLED = 3
class RemuxProgressDialog(ProgressDialog):
def __init__(self, parent, start, stop):
ProgressDialog.__init__(self,
"Writing to disk",
('Writing the selected segment of your '
'media file to disk. This may take some '
'time depending on the file size.'),
'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)
class Remuxer(gst.Pipeline):
__gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))}
def __init__(self, fromuri, touri, start, stop):
self.__gobject_init__()
assert start >= 0
assert stop > start
self.src = gst.element_make_from_uri(gst.URI_SRC, fromuri)
self.remuxbin = RemuxBin()
self.sink = gst.element_make_from_uri(gst.URI_SINK, touri)
self.resolution = UNKNOWN
self.add(self.src, self.remuxbin, self.sink)
self.src.link(self.remuxbin)
self.remuxbin.link(self.sink)
self.window = None
self.pdialog = None
self.start_time = start
self.stop_time = stop
def _start_queries(self):
pass
def _stop_queries(self):
pass
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")
txt = 'There was an error processing your file: %r' % message
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'
self.pdialog.set_task('Finished')
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:
print message
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.emit('done', response)
def start(self, main_window):
bus = self.get_bus()
bus.add_signal_watch()
bus.connect('message', self._bus_watch)
self.window = main_window
if self.window:
# can be None if we are debugging...
self.window.set_sensitive(False)
self.pdialog = RemuxProgressDialog(main_window, self.start_time,
self.stop_time)
self.pdialog.show()
self.pdialog.connect('response', lambda w, r: self.response(r))
self.set_state(gst.STATE_PAUSED)
def run(self, main_window):
self.start(None)
loop = gobject.MainLoop()
self.connect('done', lambda *x: gobject.idle_add(loop.quit))
loop.run()
return self.resolution
class RemuxBin(gst.Bin):
def __init__(self):
self.__gobject_init__()
self.parsers = self._find_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)
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.parsers:
self.async_error("Unsupported media type: %s", format)
return
parser = gst.element_factory_make(self.parsers[format])
self.add(parser)
parser.set_state(gst.STATE_PAUSED)
pad.link(parser.get_compatible_pad(pad))
parser.link(self.mux)
def _no_more_pads(self, element):
# this is when we should commit to paused or something
pass
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("_Write 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())
button.connect('clicked', lambda *x: self.do_remux())
def do_remux(self):
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 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))