home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / doc / python-gst0.10 / examples / remuxer.py < prev    next >
Encoding:
Python Source  |  2009-02-21  |  28.6 KB  |  838 lines

  1. #!/usr/bin/env python
  2. # -*- Mode: Python -*-
  3. # vi:si:et:sw=4:sts=4:ts=4
  4.  
  5. import pygtk
  6. pygtk.require('2.0')
  7.  
  8. import sys
  9.  
  10. import gobject
  11. gobject.threads_init()
  12.  
  13. import pygst
  14. pygst.require('0.10')
  15. import gst
  16. import gst.interfaces
  17. import gtk
  18.  
  19. class GstPlayer:
  20.     def __init__(self, videowidget):
  21.         self.playing = False
  22.         self.player = gst.element_factory_make("playbin", "player")
  23.         self.videowidget = videowidget
  24.  
  25.         bus = self.player.get_bus()
  26.         bus.enable_sync_message_emission()
  27.         bus.add_signal_watch()
  28.         bus.connect('sync-message::element', self.on_sync_message)
  29.         bus.connect('message', self.on_message)
  30.  
  31.     def on_sync_message(self, bus, message):
  32.         if message.structure is None:
  33.             return
  34.         if message.structure.get_name() == 'prepare-xwindow-id':
  35.             # Sync with the X server before giving the X-id to the sink
  36.             gtk.gdk.display_get_default().sync()
  37.             self.videowidget.set_sink(message.src)
  38.             message.src.set_property('force-aspect-ratio', True)
  39.             
  40.     def on_message(self, bus, message):
  41.         t = message.type
  42.         if t == gst.MESSAGE_ERROR:
  43.             err, debug = message.parse_error()
  44.             print "Error: %s" % err, debug
  45.             if self.on_eos:
  46.                 self.on_eos()
  47.             self.playing = False
  48.         elif t == gst.MESSAGE_EOS:
  49.             if self.on_eos:
  50.                 self.on_eos()
  51.             self.playing = False
  52.  
  53.     def set_location(self, location):
  54.         self.player.set_state(gst.STATE_NULL)
  55.         self.player.set_property('uri', location)
  56.  
  57.     def get_location(self):
  58.         return self.player.get_property('uri')
  59.  
  60.     def query_position(self):
  61.         "Returns a (position, duration) tuple"
  62.         try:
  63.             position, format = self.player.query_position(gst.FORMAT_TIME)
  64.         except:
  65.             position = gst.CLOCK_TIME_NONE
  66.  
  67.         try:
  68.             duration, format = self.player.query_duration(gst.FORMAT_TIME)
  69.         except:
  70.             duration = gst.CLOCK_TIME_NONE
  71.  
  72.         return (position, duration)
  73.  
  74.     def seek(self, location):
  75.         """
  76.         @param location: time to seek to, in nanoseconds
  77.         """
  78.         gst.debug("seeking to %r" % location)
  79.         event = gst.event_new_seek(1.0, gst.FORMAT_TIME,
  80.             gst.SEEK_FLAG_FLUSH,
  81.             gst.SEEK_TYPE_SET, location,
  82.             gst.SEEK_TYPE_NONE, 0)
  83.  
  84.         res = self.player.send_event(event)
  85.         if res:
  86.             gst.info("setting new stream time to 0")
  87.             self.player.set_new_stream_time(0L)
  88.         else:
  89.             gst.error("seek to %r failed" % location)
  90.  
  91.     def pause(self):
  92.         gst.info("pausing player")
  93.         self.player.set_state(gst.STATE_PAUSED)
  94.         self.playing = False
  95.  
  96.     def play(self):
  97.         gst.info("playing player")
  98.         self.player.set_state(gst.STATE_PLAYING)
  99.         self.playing = True
  100.         
  101.     def stop(self):
  102.         self.player.set_state(gst.STATE_NULL)
  103.         gst.info("stopped player")
  104.  
  105.     def get_state(self, timeout=1):
  106.         return self.player.get_state(timeout=timeout)
  107.  
  108.     def is_playing(self):
  109.         return self.playing
  110.     
  111. class VideoWidget(gtk.DrawingArea):
  112.     def __init__(self):
  113.         gtk.DrawingArea.__init__(self)
  114.         self.imagesink = None
  115.         self.unset_flags(gtk.DOUBLE_BUFFERED)
  116.  
  117.     def do_expose_event(self, event):
  118.         if self.imagesink:
  119.             self.imagesink.expose()
  120.             return False
  121.         else:
  122.             return True
  123.  
  124.     def set_sink(self, sink):
  125.         assert self.window.xid
  126.         self.imagesink = sink
  127.         self.imagesink.set_xwindow_id(self.window.xid)
  128.  
  129. class TimeControl(gtk.HBox):
  130.     # all labels same size
  131.     sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
  132.     __gproperties__ = {'time': (gobject.TYPE_UINT64, 'Time', 'Time',
  133.                                 # not actually usable: see #335854
  134.                                 # kept for .notify() usage
  135.                                 0L, (1<<63)-1, 0L,
  136.                                 gobject.PARAM_READABLE)}
  137.  
  138.     def __init__(self, window, label):
  139.         gtk.HBox.__init__(self)
  140.         self.pwindow = window
  141.         self.label = label
  142.         self.create_ui()
  143.  
  144.     def get_property(self, param, pspec):
  145.         if param == 'time':
  146.             return self.get_time()
  147.         else:
  148.             assert param in self.__gproperties__, \
  149.                    'Unknown property: %s' % param
  150.  
  151.     def create_ui(self):
  152.         label = gtk.Label(self.label + ": ")
  153.         label.show()
  154.         a = gtk.Alignment(1.0, 0.5)
  155.         a.add(label)
  156.         a.set_padding(0, 0, 12, 0)
  157.         a.show()
  158.         self.sizegroup.add_widget(a)
  159.         self.pack_start(a, True, False, 0)
  160.  
  161.         self.minutes = minutes = gtk.Entry(5)
  162.         minutes.set_width_chars(5)
  163.         minutes.set_alignment(1.0)
  164.         minutes.connect('changed', lambda *x: self.notify('time'))
  165.         minutes.connect_after('activate', lambda *x: self.activated())
  166.         label2 = gtk.Label(":")
  167.         self.seconds = seconds = gtk.Entry(2)
  168.         seconds.set_width_chars(2)
  169.         seconds.set_alignment(1.0)
  170.         seconds.connect('changed', lambda *x: self.notify('time'))
  171.         seconds.connect_after('activate', lambda *x: self.activated())
  172.         label3 = gtk.Label(".")
  173.         self.milliseconds = milliseconds = gtk.Entry(3)
  174.         milliseconds.set_width_chars(3)
  175.         milliseconds.set_alignment(0.0)
  176.         milliseconds.connect('changed', lambda *x: self.notify('time'))
  177.         milliseconds.connect_after('activate', lambda *x: self.activated())
  178.         set = gtk.Button('Set')
  179.         goto = gtk.Button('Go')
  180.         goto.set_property('image',
  181.                           gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
  182.                                                    gtk.ICON_SIZE_BUTTON))
  183.         for w in minutes, label2, seconds, label3, milliseconds:
  184.             w.show()
  185.             self.pack_start(w, False)
  186.         set.show()
  187.         self.pack_start(set, False, False, 6)
  188.         goto.show()
  189.         self.pack_start(goto, False, False, 0)
  190.         set.connect('clicked', lambda *x: self.set_now())
  191.         goto.connect('clicked', lambda *x: self.activated())
  192.         pad = gtk.Label("")
  193.         pad.show()
  194.         self.pack_start(pad, True, False, 0)
  195.  
  196.     def get_time(self):
  197.         time = 0
  198.         for w, multiplier in ((self.minutes, gst.SECOND*60),
  199.                               (self.seconds, gst.SECOND),
  200.                               (self.milliseconds, gst.MSECOND)):
  201.             text = w.get_text()
  202.             try:
  203.                 val = int(text)
  204.             except ValueError:
  205.                 val = 0
  206.             w.set_text(val and str(val) or '0')
  207.             time += val * multiplier
  208.         return time
  209.  
  210.     def set_time(self, time):
  211.         if time == gst.CLOCK_TIME_NONE:
  212.             print "Can't set '%s' (invalid time)" % self.label
  213.             return
  214.         self.freeze_notify()
  215.         for w, multiplier in ((self.minutes, gst.SECOND*60),
  216.                               (self.seconds, gst.SECOND),
  217.                               (self.milliseconds, gst.MSECOND)):
  218.             val = time // multiplier
  219.             w.set_text(str(val))
  220.             time -= val * multiplier
  221.         self.thaw_notify()
  222.  
  223.     def set_now(self):
  224.         time, dur = self.pwindow.player.query_position()
  225.         self.set_time(time)
  226.  
  227.     def activated(self):
  228.         time = self.get_time()
  229.         if self.pwindow.player.is_playing():
  230.             self.pwindow.play_toggled()
  231.         self.pwindow.player.seek(time)
  232.         self.pwindow.player.get_state(timeout=gst.MSECOND * 200)
  233.  
  234. class ProgressDialog(gtk.Dialog):
  235.     def __init__(self, title, description, task, parent, flags, buttons):
  236.         gtk.Dialog.__init__(self, title, parent, flags, buttons)
  237.         self._create_ui(title, description, task)
  238.  
  239.     def _create_ui(self, title, description, task):
  240.         self.set_border_width(6)
  241.         self.set_resizable(False)
  242.         self.set_has_separator(False)
  243.  
  244.         vbox = gtk.VBox()
  245.         vbox.set_border_width(6)
  246.         vbox.show()
  247.         self.vbox.pack_start(vbox, False)
  248.  
  249.         label = gtk.Label('<big><b>%s</b></big>' % title)
  250.         label.set_use_markup(True)
  251.         label.set_alignment(0.0, 0.0)
  252.         label.show()
  253.         vbox.pack_start(label, False)
  254.         
  255.         label = gtk.Label(description)
  256.         label.set_use_markup(True)
  257.         label.set_alignment(0.0, 0.0)
  258.         label.set_line_wrap(True)
  259.         label.set_padding(0, 12)
  260.         label.show()
  261.         vbox.pack_start(label, False)
  262.  
  263.         self.progress = progress = gtk.ProgressBar()
  264.         progress.show()
  265.         vbox.pack_start(progress, False)
  266.  
  267.         self.progresstext = label = gtk.Label('')
  268.         label.set_line_wrap(True)
  269.         label.set_use_markup(True)
  270.         label.set_alignment(0.0, 0.0)
  271.         label.show()
  272.         vbox.pack_start(label)
  273.         self.set_task(task)
  274.  
  275.     def set_task(self, task):
  276.         self.progresstext.set_markup('<i>%s</i>' % task)
  277.  
  278. UNKNOWN = 0
  279. SUCCESS = 1
  280. FAILURE = 2
  281. CANCELLED = 3
  282.  
  283. class RemuxProgressDialog(ProgressDialog):
  284.     def __init__(self, parent, start, stop, fromname, toname):
  285.         ProgressDialog.__init__(self,
  286.                                 "Writing to disk",
  287.                                 ('Writing the selected segment of <b>%s</b> '
  288.                                  'to <b>%s</b>. This may take some time.'
  289.                                  % (fromname, toname)),
  290.                                 'Starting media pipeline',
  291.                                 parent,
  292.                                 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
  293.                                 (gtk.STOCK_CANCEL, CANCELLED,
  294.                                  gtk.STOCK_CLOSE, SUCCESS))
  295.         self.start = start
  296.         self.stop = stop
  297.         self.update_position(start)
  298.         self.set_completed(False)
  299.         
  300.     def update_position(self, pos):
  301.         pos = min(max(pos, self.start), self.stop)
  302.         remaining = self.stop - pos
  303.         minutes = remaining // (gst.SECOND * 60)
  304.         seconds = (remaining - minutes * gst.SECOND * 60) // gst.SECOND
  305.         self.progress.set_text('%d:%02d of video remaining' % (minutes, seconds))
  306.         self.progress.set_fraction(1.0 - float(remaining) / (self.stop - self.start))
  307.  
  308.     def set_completed(self, completed):
  309.         self.set_response_sensitive(CANCELLED, not completed)
  310.         self.set_response_sensitive(SUCCESS, completed)
  311.  
  312. def set_connection_blocked_async_marshalled(pads, proc, *args, **kwargs):
  313.     def clear_list(l):
  314.         while l:
  315.             l.pop()
  316.  
  317.     to_block = list(pads)
  318.     to_relink = [(x, x.get_peer()) for x in pads]
  319.  
  320.     def on_pad_blocked_sync(pad, is_blocked):
  321.         if pad not in to_block:
  322.             # can happen after the seek and before unblocking -- racy,
  323.             # but no prob, bob.
  324.             return
  325.         to_block.remove(pad)
  326.         if not to_block:
  327.             # marshal to main thread
  328.             gobject.idle_add(on_pads_blocked)
  329.  
  330.     def on_pads_blocked():
  331.         for src, sink in to_relink:
  332.             src.link(sink)
  333.         proc(*args, **kwargs)
  334.         for src, sink in to_relink:
  335.             src.set_blocked_async(False, lambda *x: None)
  336.         clear_list(to_relink)
  337.  
  338.     for src, sink in to_relink:
  339.         src.unlink(sink)
  340.         src.set_blocked_async(True, on_pad_blocked_sync)
  341.  
  342. class Remuxer(gst.Pipeline):
  343.  
  344.     __gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))}
  345.  
  346.     def __init__(self, fromuri, touri, start, stop):
  347.         # HACK: should do Pipeline.__init__, but that doesn't do what we
  348.         # want; there's a bug open aboooot that
  349.         self.__gobject_init__()
  350.  
  351.         assert start >= 0
  352.         assert stop > start
  353.  
  354.         self.fromuri = fromuri
  355.         self.touri = None
  356.         self.start_time = start
  357.         self.stop_time = stop
  358.  
  359.         self.src = self.remuxbin = self.sink = None
  360.         self.resolution = UNKNOWN
  361.  
  362.         self.window = None
  363.         self.pdialog = None
  364.  
  365.         self._query_id = -1
  366.  
  367.     def do_setup_pipeline(self):
  368.         self.src = gst.element_make_from_uri(gst.URI_SRC, self.fromuri)
  369.         self.remuxbin = RemuxBin(self.start_time, self.stop_time)
  370.         self.sink = gst.element_make_from_uri(gst.URI_SINK, self.touri)
  371.         self.resolution = UNKNOWN
  372.  
  373.         if gobject.signal_lookup('allow-overwrite', self.sink.__class__):
  374.             self.sink.connect('allow-overwrite', lambda *x: True)
  375.  
  376.         self.add(self.src, self.remuxbin, self.sink)
  377.  
  378.         self.src.link(self.remuxbin)
  379.         self.remuxbin.link(self.sink)
  380.  
  381.     def do_get_touri(self):
  382.         chooser = gtk.FileChooserDialog('Save as...',
  383.                                         self.window,
  384.                                         action=gtk.FILE_CHOOSER_ACTION_SAVE,
  385.                                         buttons=(gtk.STOCK_CANCEL,
  386.                                                  CANCELLED,
  387.                                                  gtk.STOCK_SAVE,
  388.                                                  SUCCESS))
  389.         chooser.set_uri(self.fromuri) # to select the folder
  390.         chooser.unselect_all()
  391.         chooser.set_do_overwrite_confirmation(True)
  392.         name = self.fromuri.split('/')[-1][:-4] + '-remuxed.ogg'
  393.         chooser.set_current_name(name)
  394.         resp = chooser.run()
  395.         uri = chooser.get_uri()
  396.         chooser.destroy()
  397.  
  398.         if resp == SUCCESS:
  399.             return uri
  400.         else:
  401.             return None
  402.  
  403.     def _start_queries(self):
  404.         def do_query():
  405.             try:
  406.                 # HACK: self.remuxbin.query() should do the same
  407.                 # (requires implementing a vmethod, dunno how to do that
  408.                 # although i think it's possible)
  409.                 # HACK: why does self.query_position(..) not give useful
  410.                 # answers? 
  411.                 pad = self.remuxbin.get_pad('src')
  412.                 pos, duration = pad.query_position(gst.FORMAT_TIME)
  413.                 if pos != gst.CLOCK_TIME_NONE:
  414.                     self.pdialog.update_position(pos)
  415.             except:
  416.                 # print 'query failed'
  417.                 pass
  418.             return True
  419.         if self._query_id == -1:
  420.             self._query_id = gobject.timeout_add(100, # 10 Hz
  421.                                                  do_query)
  422.  
  423.     def _stop_queries(self):
  424.         if self._query_id != -1:
  425.             gobject.source_remove(self._query_id)
  426.             self._query_id = -1
  427.  
  428.     def _bus_watch(self, bus, message):
  429.         if message.type == gst.MESSAGE_ERROR:
  430.             print 'error', message
  431.             self._stop_queries()
  432.             m = gtk.MessageDialog(self.window,
  433.                                   gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
  434.                                   gtk.MESSAGE_ERROR,
  435.                                   gtk.BUTTONS_CLOSE,
  436.                                   "Error processing file")
  437.             gerror, debug = message.parse_error()
  438.             txt = ('There was an error processing your file: %s\n\n'
  439.                    'Debug information:\n%s' % (gerror, debug))
  440.             m.format_secondary_text(txt)
  441.             m.run()
  442.             m.destroy()
  443.             self.response(FAILURE)
  444.         elif message.type == gst.MESSAGE_WARNING:
  445.             print 'warning', message
  446.         elif message.type == gst.MESSAGE_EOS:
  447.             # print 'eos, woot', message.src
  448.             name = self.touri
  449.             if name.startswith('file://'):
  450.                 name = name[7:]
  451.             self.pdialog.set_task('Finished writing %s' % name)
  452.             self.pdialog.update_position(self.stop_time)
  453.             self._stop_queries()
  454.             self.pdialog.set_completed(True)
  455.         elif message.type == gst.MESSAGE_STATE_CHANGED:
  456.             if message.src == self:
  457.                 old, new, pending = message.parse_state_changed()
  458.                 if ((old, new, pending) ==
  459.                     (gst.STATE_READY, gst.STATE_PAUSED,
  460.                      gst.STATE_VOID_PENDING)):
  461.                     self.pdialog.set_task('Processing file')
  462.                     self.pdialog.update_position(self.start_time)
  463.                     self._start_queries()
  464.                     self.set_state(gst.STATE_PLAYING)
  465.  
  466.     def response(self, response):
  467.         assert self.resolution == UNKNOWN
  468.         self.resolution = response
  469.         self.set_state(gst.STATE_NULL)
  470.         self.pdialog.destroy()
  471.         self.pdialog = None
  472.         self.window.set_sensitive(True)
  473.         self.emit('done', response)
  474.  
  475.     def start(self, main_window):
  476.         self.window = main_window
  477.         self.touri = self.do_get_touri()
  478.         if not self.touri:
  479.             return False
  480.         self.do_setup_pipeline()
  481.         bus = self.get_bus()
  482.         bus.add_signal_watch()
  483.         bus.connect('message', self._bus_watch)
  484.         if self.window:
  485.             # can be None if we are debugging...
  486.             self.window.set_sensitive(False)
  487.         fromname = self.fromuri.split('/')[-1]
  488.         toname = self.touri.split('/')[-1]
  489.         self.pdialog = RemuxProgressDialog(main_window, self.start_time,
  490.                                            self.stop_time, fromname, toname)
  491.         self.pdialog.show()
  492.         self.pdialog.connect('response', lambda w, r: self.response(r))
  493.  
  494.         self.set_state(gst.STATE_PAUSED)
  495.         return True
  496.         
  497.     def run(self, main_window):
  498.         if self.start(main_window):
  499.             loop = gobject.MainLoop()
  500.             self.connect('done', lambda *x: gobject.idle_add(loop.quit))
  501.             loop.run()
  502.         else:
  503.             self.resolution = CANCELLED
  504.         return self.resolution
  505.         
  506. class RemuxBin(gst.Bin):
  507.     def __init__(self, start_time, stop_time):
  508.         self.__gobject_init__()
  509.  
  510.         self.parsefactories = self._find_parsers()
  511.         self.parsers = []
  512.  
  513.         self.demux = gst.element_factory_make('oggdemux')
  514.         self.mux = gst.element_factory_make('oggmux')
  515.  
  516.         self.add(self.demux, self.mux)
  517.  
  518.         self.add_pad(gst.GhostPad('sink', self.demux.get_pad('sink')))
  519.         self.add_pad(gst.GhostPad('src', self.mux.get_pad('src')))
  520.  
  521.         self.demux.connect('pad-added', self._new_demuxed_pad)
  522.         self.demux.connect('no-more-pads', self._no_more_pads)
  523.  
  524.         self.start_time = start_time
  525.         self.stop_time = stop_time
  526.  
  527.     def _find_parsers(self):
  528.         registry = gst.registry_get_default()
  529.         ret = {}
  530.         for f in registry.get_feature_list(gst.ElementFactory):
  531.             if f.get_klass().find('Parser') >= 0:
  532.                 for t in f.get_static_pad_templates():
  533.                     if t.direction == gst.PAD_SINK:
  534.                         for s in t.get_caps():
  535.                             ret[s.get_name()] = f.get_name()
  536.                         break
  537.         return ret
  538.  
  539.     def _new_demuxed_pad(self, element, pad):
  540.         format = pad.get_caps()[0].get_name()
  541.  
  542.         if format not in self.parsefactories:
  543.             self.async_error("Unsupported media type: %s", format)
  544.             return
  545.  
  546.         queue = gst.element_factory_make('queue', 'queue_' + format)
  547.         queue.set_property('max-size-buffers', 1000)
  548.         parser = gst.element_factory_make(self.parsefactories[format])
  549.         self.add(queue)
  550.         self.add(parser)
  551.         queue.set_state(gst.STATE_PAUSED)
  552.         parser.set_state(gst.STATE_PAUSED)
  553.         pad.link(queue.get_compatible_pad(pad))
  554.         queue.link(parser)
  555.         parser.link(self.mux)
  556.         self.parsers.append(parser)
  557.  
  558.     def _do_seek(self):
  559.         flags = gst.SEEK_FLAG_FLUSH
  560.         # HACK: self.seek should work, should try that at some point
  561.         return self.demux.seek(1.0, gst.FORMAT_TIME, flags,
  562.                                gst.SEEK_TYPE_SET, self.start_time,
  563.                                gst.SEEK_TYPE_SET, self.stop_time)
  564.  
  565.     def _no_more_pads(self, element):
  566.         pads = [x.get_pad('src') for x in self.parsers]
  567.         set_connection_blocked_async_marshalled(pads,
  568.                                                 self._do_seek)
  569.  
  570.  
  571. class PlayerWindow(gtk.Window):
  572.     UPDATE_INTERVAL = 500
  573.     def __init__(self):
  574.         gtk.Window.__init__(self)
  575.         self.set_default_size(600, 425)
  576.  
  577.         self.create_ui()
  578.  
  579.         self.player = GstPlayer(self.videowidget)
  580.  
  581.         def on_eos():
  582.             self.player.seek(0L)
  583.             self.play_toggled()
  584.         self.player.on_eos = lambda *x: on_eos()
  585.         
  586.         self.update_id = -1
  587.         self.changed_id = -1
  588.         self.seek_timeout_id = -1
  589.  
  590.         self.p_position = gst.CLOCK_TIME_NONE
  591.         self.p_duration = gst.CLOCK_TIME_NONE
  592.  
  593.         def on_delete_event():
  594.             self.player.stop()
  595.             gtk.main_quit()
  596.         self.connect('delete-event', lambda *x: on_delete_event())
  597.  
  598.     def load_file(self, location):
  599.         filename = location.split('/')[-1]
  600.         self.set_title('%s munger' % filename)
  601.         self.player.set_location(location)
  602.         if self.videowidget.flags() & gtk.REALIZED:
  603.             self.play_toggled()
  604.         else:
  605.             self.videowidget.connect_after('realize',
  606.                                            lambda *x: self.play_toggled())
  607.                                   
  608.     def create_ui(self):
  609.         vbox = gtk.VBox()
  610.         vbox.show()
  611.         self.add(vbox)
  612.  
  613.         self.videowidget = VideoWidget()
  614.         self.videowidget.show()
  615.         vbox.pack_start(self.videowidget)
  616.         
  617.         hbox = gtk.HBox()
  618.         hbox.show()
  619.         vbox.pack_start(hbox, fill=False, expand=False)
  620.         
  621.         self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
  622.         hscale = gtk.HScale(self.adjustment)
  623.         hscale.set_digits(2)
  624.         hscale.set_update_policy(gtk.UPDATE_CONTINUOUS)
  625.         hscale.connect('button-press-event', self.scale_button_press_cb)
  626.         hscale.connect('button-release-event', self.scale_button_release_cb)
  627.         hscale.connect('format-value', self.scale_format_value_cb)
  628.         hbox.pack_start(hscale)
  629.         hscale.show()
  630.         self.hscale = hscale
  631.  
  632.         table = gtk.Table(2,3)
  633.         table.show()
  634.         vbox.pack_start(table, fill=False, expand=False, padding=6)
  635.  
  636.         self.button = button = gtk.Button(stock=gtk.STOCK_MEDIA_PLAY)
  637.         button.set_property('can-default', True)
  638.         button.set_focus_on_click(False)
  639.         button.show()
  640.  
  641.         # problem: play and paused are of different widths and cause the
  642.         # window to re-layout
  643.         # "solution": add more buttons to a vbox so that the horizontal
  644.         # width is enough
  645.         bvbox = gtk.VBox()
  646.         bvbox.add(button)
  647.         bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PLAY))
  648.         bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PAUSE))
  649.         sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
  650.         for kid in bvbox.get_children():
  651.             sizegroup.add_widget(kid)
  652.         bvbox.show()
  653.         table.attach(bvbox, 0, 1, 0, 2, gtk.FILL, gtk.FILL)
  654.         
  655.         # can't set this property before the button has a window
  656.         button.set_property('has-default', True)
  657.         button.connect('clicked', lambda *args: self.play_toggled())
  658.  
  659.         self.cutin = cut = TimeControl(self, "Cut in time")
  660.         cut.show()
  661.         table.attach(cut, 1, 2, 0, 1, gtk.EXPAND, 0, 12)
  662.  
  663.         self.cutout = cut = TimeControl(self, "Cut out time")
  664.         cut.show()
  665.         table.attach(cut, 1, 2, 1, 2, gtk.EXPAND, 0, 12)
  666.  
  667.         button = gtk.Button("_Open other movie...")
  668.         button.show()
  669.         button.connect('clicked', lambda *x: self.do_choose_file())
  670.         table.attach(button, 2, 3, 0, 1, gtk.FILL, gtk.FILL)
  671.  
  672.         button = gtk.Button("_Write to disk")
  673.         button.set_property('image',
  674.                             gtk.image_new_from_stock(gtk.STOCK_SAVE_AS,
  675.                                                      gtk.ICON_SIZE_BUTTON))
  676.         button.connect('clicked', lambda *x: self.do_remux())
  677.         button.show()
  678.         table.attach(button, 2, 3, 1, 2, gtk.FILL, gtk.FILL)
  679.  
  680.         #self.cutin.connect('notify::time', lambda *x: self.check_cutout())
  681.         #self.cutout.connect('notify::time', lambda *x: self.check_cutin())
  682.  
  683.     def do_remux(self):
  684.         if self.player.is_playing():
  685.             self.play_toggled()
  686.         in_uri = self.player.get_location()
  687.         out_uri = in_uri[:-4] + '-remuxed.ogg'
  688.         r = Remuxer(in_uri, out_uri,
  689.                     self.cutin.get_time(), self.cutout.get_time())
  690.         r.run(self)
  691.  
  692.     def do_choose_file(self):
  693.         if self.player.is_playing():
  694.             self.play_toggled()
  695.         chooser = gtk.FileChooserDialog('Choose a movie to cut cut cut',
  696.                                         self,
  697.                                         buttons=(gtk.STOCK_CANCEL,
  698.                                                  CANCELLED,
  699.                                                  gtk.STOCK_OPEN,
  700.                                                  SUCCESS))
  701.         chooser.set_local_only(False)
  702.         chooser.set_select_multiple(False)
  703.         f = gtk.FileFilter()
  704.         f.set_name("All files")
  705.         f.add_pattern("*")
  706.         chooser.add_filter(f)
  707.         f = gtk.FileFilter()
  708.         f.set_name("Ogg files")
  709.         f.add_pattern("*.ogg") # as long as this is the only thing we
  710.                                # support...
  711.         chooser.add_filter(f)
  712.         chooser.set_filter(f)
  713.         
  714.         prev = self.player.get_location()
  715.         if prev:
  716.             chooser.set_uri(prev)
  717.  
  718.         resp = chooser.run()
  719.         uri = chooser.get_uri()
  720.         chooser.destroy()
  721.  
  722.         if resp == SUCCESS:
  723.             self.load_file(uri)
  724.             return True
  725.         else:
  726.             return False
  727.         
  728.     def check_cutout(self):
  729.         if self.cutout.get_time() <= self.cutin.get_time():
  730.             pos, dur = self.player.query_position()
  731.             self.cutout.set_time(dur)
  732.  
  733.     def check_cutin(self):
  734.         if self.cutin.get_time() >= self.cutout.get_time():
  735.             self.cutin.set_time(0)
  736.  
  737.     def play_toggled(self):
  738.         if self.player.is_playing():
  739.             self.player.pause()
  740.             self.button.set_label(gtk.STOCK_MEDIA_PLAY)
  741.         else:
  742.             self.player.play()
  743.             if self.update_id == -1:
  744.                 self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
  745.                                                      self.update_scale_cb)
  746.             self.button.set_label(gtk.STOCK_MEDIA_PAUSE)
  747.  
  748.     def scale_format_value_cb(self, scale, value):
  749.         if self.p_duration == -1:
  750.             real = 0
  751.         else:
  752.             real = value * self.p_duration / 100
  753.         
  754.         seconds = real / gst.SECOND
  755.  
  756.         return "%02d:%02d" % (seconds / 60, seconds % 60)
  757.  
  758.     def scale_button_press_cb(self, widget, event):
  759.         # see seek.c:start_seek
  760.         gst.debug('starting seek')
  761.         
  762.         self.button.set_sensitive(False)
  763.         self.was_playing = self.player.is_playing()
  764.         if self.was_playing:
  765.             self.player.pause()
  766.  
  767.         # don't timeout-update position during seek
  768.         if self.update_id != -1:
  769.             gobject.source_remove(self.update_id)
  770.             self.update_id = -1
  771.  
  772.         # make sure we get changed notifies
  773.         if self.changed_id == -1:
  774.             self.changed_id = self.hscale.connect('value-changed',
  775.                 self.scale_value_changed_cb)
  776.             
  777.     def scale_value_changed_cb(self, scale):
  778.         # see seek.c:seek_cb
  779.         real = long(scale.get_value() * self.p_duration / 100) # in ns
  780.         gst.debug('value changed, perform seek to %r' % real)
  781.         self.player.seek(real)
  782.         # allow for a preroll
  783.         self.player.get_state(timeout=50*gst.MSECOND) # 50 ms
  784.  
  785.     def scale_button_release_cb(self, widget, event):
  786.         # see seek.cstop_seek
  787.         widget.disconnect(self.changed_id)
  788.         self.changed_id = -1
  789.  
  790.         self.button.set_sensitive(True)
  791.         if self.seek_timeout_id != -1:
  792.             gobject.source_remove(self.seek_timeout_id)
  793.             self.seek_timeout_id = -1
  794.         else:
  795.             gst.debug('released slider, setting back to playing')
  796.             if self.was_playing:
  797.                 self.player.play()
  798.  
  799.         if self.update_id != -1:
  800.             self.error('Had a previous update timeout id')
  801.         else:
  802.             self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
  803.                 self.update_scale_cb)
  804.  
  805.     def update_scale_cb(self):
  806.         had_duration = self.p_duration != gst.CLOCK_TIME_NONE
  807.         self.p_position, self.p_duration = self.player.query_position()
  808.         if self.p_position != gst.CLOCK_TIME_NONE:
  809.             value = self.p_position * 100.0 / self.p_duration
  810.             self.adjustment.set_value(value)
  811.             if not had_duration:
  812.                 self.cutin.set_time(0)
  813.         return True
  814.  
  815. def main(args):
  816.     def usage():
  817.         sys.stderr.write("usage: %s [URI-OF-MEDIA-FILE]\n" % args[0])
  818.         return 1
  819.  
  820.     w = PlayerWindow()
  821.     w.show()
  822.  
  823.     if len(args) == 1:
  824.         if not w.do_choose_file():
  825.             return 1
  826.     elif len(args) == 2:
  827.         if not gst.uri_is_valid(args[1]):
  828.             sys.stderr.write("Error: Invalid URI: %s\n" % args[1])
  829.             return 1
  830.         w.load_file(args[1])
  831.     else:
  832.         return usage()
  833.  
  834.     gtk.main()
  835.  
  836. if __name__ == '__main__':
  837.     sys.exit(main(sys.argv))
  838.