3 # vi:si:et:sw=4:sts=4:ts=4
11 gobject.threads_init()
18 gtk.gdk.threads_init()
21 def __init__(self, videowidget):
23 self.player = gst.element_factory_make("playbin", "player")
24 self.videowidget = videowidget
26 bus = self.player.get_bus()
27 bus.enable_sync_message_emission()
28 bus.add_signal_watch()
29 bus.connect('sync-message::element', self.on_sync_message)
30 bus.connect('message', self.on_message)
32 def on_sync_message(self, bus, message):
33 if message.structure is None:
35 if message.structure.get_name() == 'prepare-xwindow-id':
36 # Sync with the X server before giving the X-id to the sink
37 gtk.gdk.threads_enter()
38 gtk.gdk.display_get_default().sync()
39 self.videowidget.set_sink(message.src)
40 message.src.set_property('force-aspect-ratio', True)
41 gtk.gdk.threads_leave()
43 def on_message(self, bus, message):
45 if t == gst.MESSAGE_ERROR:
46 err, debug = message.parse_error()
47 print "Error: %s" % err, debug
51 elif t == gst.MESSAGE_EOS:
56 def set_location(self, location):
57 self.player.set_state(gst.STATE_NULL)
58 self.player.set_property('uri', location)
60 def get_location(self):
61 return self.player.get_property('uri')
63 def query_position(self):
64 "Returns a (position, duration) tuple"
66 position, format = self.player.query_position(gst.FORMAT_TIME)
68 position = gst.CLOCK_TIME_NONE
71 duration, format = self.player.query_duration(gst.FORMAT_TIME)
73 duration = gst.CLOCK_TIME_NONE
75 return (position, duration)
77 def seek(self, location):
79 @param location: time to seek to, in nanoseconds
81 gst.debug("seeking to %r" % location)
82 event = gst.event_new_seek(1.0, gst.FORMAT_TIME,
84 gst.SEEK_TYPE_SET, location,
85 gst.SEEK_TYPE_NONE, 0)
87 res = self.player.send_event(event)
89 gst.info("setting new stream time to 0")
90 self.player.set_new_stream_time(0L)
92 gst.error("seek to %r failed" % location)
95 gst.info("pausing player")
96 self.player.set_state(gst.STATE_PAUSED)
100 gst.info("playing player")
101 self.player.set_state(gst.STATE_PLAYING)
105 self.player.set_state(gst.STATE_NULL)
106 gst.info("stopped player")
108 def get_state(self, timeout=1):
109 return self.player.get_state(timeout=timeout)
111 def is_playing(self):
114 class VideoWidget(gtk.DrawingArea):
116 gtk.DrawingArea.__init__(self)
117 self.imagesink = None
118 self.unset_flags(gtk.DOUBLE_BUFFERED)
120 def do_expose_event(self, event):
122 self.imagesink.expose()
127 def set_sink(self, sink):
128 assert self.window.xid
129 self.imagesink = sink
130 self.imagesink.set_xwindow_id(self.window.xid)
132 class TimeControl(gtk.HBox):
133 # all labels same size
134 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
135 __gproperties__ = {'time': (gobject.TYPE_UINT64, 'Time', 'Time',
136 # not actually usable: see #335854
137 # kept for .notify() usage
139 gobject.PARAM_READABLE)}
141 def __init__(self, window, label):
142 gtk.HBox.__init__(self)
143 self.pwindow = window
147 def get_property(self, param, pspec):
149 return self.get_time()
151 assert param in self.__gproperties__, \
152 'Unknown property: %s' % param
155 label = gtk.Label(self.label + ": ")
157 a = gtk.Alignment(1.0, 0.5)
159 a.set_padding(0, 0, 12, 0)
161 self.sizegroup.add_widget(a)
162 self.pack_start(a, True, False, 0)
164 self.minutes = minutes = gtk.Entry(5)
165 minutes.set_width_chars(5)
166 minutes.set_alignment(1.0)
167 minutes.connect('changed', lambda *x: self.notify('time'))
168 minutes.connect_after('activate', lambda *x: self.activated())
169 label2 = gtk.Label(":")
170 self.seconds = seconds = gtk.Entry(2)
171 seconds.set_width_chars(2)
172 seconds.set_alignment(1.0)
173 seconds.connect('changed', lambda *x: self.notify('time'))
174 seconds.connect_after('activate', lambda *x: self.activated())
175 label3 = gtk.Label(".")
176 self.milliseconds = milliseconds = gtk.Entry(3)
177 milliseconds.set_width_chars(3)
178 milliseconds.set_alignment(0.0)
179 milliseconds.connect('changed', lambda *x: self.notify('time'))
180 milliseconds.connect_after('activate', lambda *x: self.activated())
181 set = gtk.Button('Set')
182 goto = gtk.Button('Go')
183 goto.set_property('image',
184 gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
185 gtk.ICON_SIZE_BUTTON))
186 for w in minutes, label2, seconds, label3, milliseconds:
188 self.pack_start(w, False)
190 self.pack_start(set, False, False, 6)
192 self.pack_start(goto, False, False, 0)
193 set.connect('clicked', lambda *x: self.set_now())
194 goto.connect('clicked', lambda *x: self.activated())
197 self.pack_start(pad, True, False, 0)
201 for w, multiplier in ((self.minutes, gst.SECOND*60),
202 (self.seconds, gst.SECOND),
203 (self.milliseconds, gst.MSECOND)):
209 w.set_text(val and str(val) or '0')
210 time += val * multiplier
213 def set_time(self, time):
214 if time == gst.CLOCK_TIME_NONE:
215 print "Can't set '%s' (invalid time)" % self.label
218 for w, multiplier in ((self.minutes, gst.SECOND*60),
219 (self.seconds, gst.SECOND),
220 (self.milliseconds, gst.MSECOND)):
221 val = time // multiplier
223 time -= val * multiplier
227 time, dur = self.pwindow.player.query_position()
231 time = self.get_time()
232 if self.pwindow.player.is_playing():
233 self.pwindow.play_toggled()
234 self.pwindow.player.seek(time)
235 self.pwindow.player.get_state(timeout=gst.MSECOND * 200)
237 class ProgressDialog(gtk.Dialog):
238 def __init__(self, title, description, task, parent, flags, buttons):
239 gtk.Dialog.__init__(self, title, parent, flags, buttons)
240 self._create_ui(title, description, task)
242 def _create_ui(self, title, description, task):
243 self.set_border_width(6)
244 self.set_resizable(False)
245 self.set_has_separator(False)
248 vbox.set_border_width(6)
250 self.vbox.pack_start(vbox, False)
252 label = gtk.Label('<big><b>%s</b></big>' % title)
253 label.set_use_markup(True)
254 label.set_alignment(0.0, 0.0)
256 vbox.pack_start(label, False)
258 label = gtk.Label(description)
259 label.set_use_markup(True)
260 label.set_alignment(0.0, 0.0)
261 label.set_line_wrap(True)
262 label.set_padding(0, 12)
264 vbox.pack_start(label, False)
266 self.progress = progress = gtk.ProgressBar()
268 vbox.pack_start(progress, False)
270 self.progresstext = label = gtk.Label('')
271 label.set_line_wrap(True)
272 label.set_use_markup(True)
273 label.set_alignment(0.0, 0.0)
275 vbox.pack_start(label)
278 def set_task(self, task):
279 self.progresstext.set_markup('<i>%s</i>' % task)
286 class RemuxProgressDialog(ProgressDialog):
287 def __init__(self, parent, start, stop, fromname, toname):
288 ProgressDialog.__init__(self,
290 ('Writing the selected segment of <b>%s</b> '
291 'to <b>%s</b>. This may take some time.'
292 % (fromname, toname)),
293 'Starting media pipeline',
295 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
296 (gtk.STOCK_CANCEL, CANCELLED,
297 gtk.STOCK_CLOSE, SUCCESS))
300 self.update_position(start)
301 self.set_completed(False)
303 def update_position(self, pos):
304 pos = min(max(pos, self.start), self.stop)
305 remaining = self.stop - pos
306 minutes = remaining // (gst.SECOND * 60)
307 seconds = (remaining - minutes * gst.SECOND * 60) // gst.SECOND
308 self.progress.set_text('%d:%02d of video remaining' % (minutes, seconds))
309 self.progress.set_fraction(1.0 - float(remaining) / (self.stop - self.start))
311 def set_completed(self, completed):
312 self.set_response_sensitive(CANCELLED, not completed)
313 self.set_response_sensitive(SUCCESS, completed)
315 def set_connection_blocked_async_marshalled(pads, proc, *args, **kwargs):
320 to_block = list(pads)
321 to_relink = [(x, x.get_peer()) for x in pads]
323 def on_pad_blocked_sync(pad, is_blocked):
324 if pad not in to_block:
325 # can happen after the seek and before unblocking -- racy,
330 # marshal to main thread
331 gobject.idle_add(on_pads_blocked)
333 def on_pads_blocked():
334 for src, sink in to_relink:
336 proc(*args, **kwargs)
337 for src, sink in to_relink:
338 src.set_blocked_async(False, lambda *x: None)
339 clear_list(to_relink)
341 for src, sink in to_relink:
343 src.set_blocked_async(True, on_pad_blocked_sync)
345 class Remuxer(gst.Pipeline):
347 __gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))}
349 def __init__(self, fromuri, touri, start, stop):
350 # HACK: should do Pipeline.__init__, but that doesn't do what we
351 # want; there's a bug open aboooot that
352 self.__gobject_init__()
357 self.fromuri = fromuri
359 self.start_time = start
360 self.stop_time = stop
362 self.src = self.remuxbin = self.sink = None
363 self.resolution = UNKNOWN
370 def do_setup_pipeline(self):
371 self.src = gst.element_make_from_uri(gst.URI_SRC, self.fromuri)
372 self.remuxbin = RemuxBin(self.start_time, self.stop_time)
373 self.sink = gst.element_make_from_uri(gst.URI_SINK, self.touri)
374 self.resolution = UNKNOWN
376 if gobject.signal_lookup('allow-overwrite', self.sink.__class__):
377 self.sink.connect('allow-overwrite', lambda *x: True)
379 self.add(self.src, self.remuxbin, self.sink)
381 self.src.link(self.remuxbin)
382 self.remuxbin.link(self.sink)
384 def do_get_touri(self):
385 chooser = gtk.FileChooserDialog('Save as...',
387 action=gtk.FILE_CHOOSER_ACTION_SAVE,
388 buttons=(gtk.STOCK_CANCEL,
392 chooser.set_uri(self.fromuri) # to select the folder
393 chooser.unselect_all()
394 chooser.set_do_overwrite_confirmation(True)
395 name = self.fromuri.split('/')[-1][:-4] + '-remuxed.ogg'
396 chooser.set_current_name(name)
398 uri = chooser.get_uri()
406 def _start_queries(self):
409 # HACK: self.remuxbin.query() should do the same
410 # (requires implementing a vmethod, dunno how to do that
411 # although i think it's possible)
412 # HACK: why does self.query_position(..) not give useful
414 pad = self.remuxbin.get_pad('src')
415 pos, duration = pad.query_position(gst.FORMAT_TIME)
416 if pos != gst.CLOCK_TIME_NONE:
417 self.pdialog.update_position(pos)
419 # print 'query failed'
422 if self._query_id == -1:
423 self._query_id = gobject.timeout_add(100, # 10 Hz
426 def _stop_queries(self):
427 if self._query_id != -1:
428 gobject.source_remove(self._query_id)
431 def _bus_watch(self, bus, message):
432 if message.type == gst.MESSAGE_ERROR:
433 print 'error', message
435 m = gtk.MessageDialog(self.window,
436 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
439 "Error processing file")
440 gerror, debug = message.parse_error()
441 txt = ('There was an error processing your file: %s\n\n'
442 'Debug information:\n%s' % (gerror, debug))
443 m.format_secondary_text(txt)
446 self.response(FAILURE)
447 elif message.type == gst.MESSAGE_WARNING:
448 print 'warning', message
449 elif message.type == gst.MESSAGE_EOS:
450 # print 'eos, woot', message.src
452 if name.startswith('file://'):
454 self.pdialog.set_task('Finished writing %s' % name)
455 self.pdialog.update_position(self.stop_time)
457 self.pdialog.set_completed(True)
458 elif message.type == gst.MESSAGE_STATE_CHANGED:
459 if message.src == self:
460 old, new, pending = message.parse_state_changed()
461 if ((old, new, pending) ==
462 (gst.STATE_READY, gst.STATE_PAUSED,
463 gst.STATE_VOID_PENDING)):
464 self.pdialog.set_task('Processing file')
465 self.pdialog.update_position(self.start_time)
466 self._start_queries()
467 self.set_state(gst.STATE_PLAYING)
469 def response(self, response):
470 assert self.resolution == UNKNOWN
471 self.resolution = response
472 self.set_state(gst.STATE_NULL)
473 self.pdialog.destroy()
475 self.window.set_sensitive(True)
476 self.emit('done', response)
478 def start(self, main_window):
479 self.window = main_window
480 self.touri = self.do_get_touri()
483 self.do_setup_pipeline()
485 bus.add_signal_watch()
486 bus.connect('message', self._bus_watch)
488 # can be None if we are debugging...
489 self.window.set_sensitive(False)
490 fromname = self.fromuri.split('/')[-1]
491 toname = self.touri.split('/')[-1]
492 self.pdialog = RemuxProgressDialog(main_window, self.start_time,
493 self.stop_time, fromname, toname)
495 self.pdialog.connect('response', lambda w, r: self.response(r))
497 self.set_state(gst.STATE_PAUSED)
500 def run(self, main_window):
501 if self.start(main_window):
502 loop = gobject.MainLoop()
503 self.connect('done', lambda *x: gobject.idle_add(loop.quit))
506 self.resolution = CANCELLED
507 return self.resolution
509 class RemuxBin(gst.Bin):
510 def __init__(self, start_time, stop_time):
511 self.__gobject_init__()
513 self.parsefactories = self._find_parsers()
516 self.demux = gst.element_factory_make('oggdemux')
517 self.mux = gst.element_factory_make('oggmux')
519 self.add(self.demux, self.mux)
521 self.add_pad(gst.GhostPad('sink', self.demux.get_pad('sink')))
522 self.add_pad(gst.GhostPad('src', self.mux.get_pad('src')))
524 self.demux.connect('pad-added', self._new_demuxed_pad)
525 self.demux.connect('no-more-pads', self._no_more_pads)
527 self.start_time = start_time
528 self.stop_time = stop_time
530 def _find_parsers(self):
531 registry = gst.registry_get_default()
533 for f in registry.get_feature_list(gst.ElementFactory):
534 if f.get_klass().find('Parser') >= 0:
535 for t in f.get_static_pad_templates():
536 if t.direction == gst.PAD_SINK:
537 for s in t.get_caps():
538 ret[s.get_name()] = f.get_name()
542 def _new_demuxed_pad(self, element, pad):
543 format = pad.get_caps()[0].get_name()
545 if format not in self.parsefactories:
546 self.async_error("Unsupported media type: %s", format)
549 queue = gst.element_factory_make('queue', None);
550 queue.set_property('max-size-buffers', 1000)
551 parser = gst.element_factory_make(self.parsefactories[format])
554 queue.set_state(gst.STATE_PAUSED)
555 parser.set_state(gst.STATE_PAUSED)
556 pad.link(queue.get_compatible_pad(pad))
558 parser.link(self.mux)
559 self.parsers.append(parser)
562 flags = gst.SEEK_FLAG_FLUSH
563 # HACK: self.seek should work, should try that at some point
564 return self.demux.seek(1.0, gst.FORMAT_TIME, flags,
565 gst.SEEK_TYPE_SET, self.start_time,
566 gst.SEEK_TYPE_SET, self.stop_time)
568 def _no_more_pads(self, element):
569 pads = [x.get_pad('src') for x in self.parsers]
570 set_connection_blocked_async_marshalled(pads,
574 class PlayerWindow(gtk.Window):
575 UPDATE_INTERVAL = 500
577 gtk.Window.__init__(self)
578 self.set_default_size(600, 425)
582 self.player = GstPlayer(self.videowidget)
587 self.player.on_eos = lambda *x: on_eos()
591 self.seek_timeout_id = -1
593 self.p_position = gst.CLOCK_TIME_NONE
594 self.p_duration = gst.CLOCK_TIME_NONE
596 def on_delete_event():
599 self.connect('delete-event', lambda *x: on_delete_event())
601 def load_file(self, location):
602 filename = location.split('/')[-1]
603 self.set_title('%s munger' % filename)
604 self.player.set_location(location)
605 if self.videowidget.flags() & gtk.REALIZED:
608 self.videowidget.connect_after('realize',
609 lambda *x: self.play_toggled())
616 self.videowidget = VideoWidget()
617 self.videowidget.show()
618 vbox.pack_start(self.videowidget)
622 vbox.pack_start(hbox, fill=False, expand=False)
624 self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
625 hscale = gtk.HScale(self.adjustment)
627 hscale.set_update_policy(gtk.UPDATE_CONTINUOUS)
628 hscale.connect('button-press-event', self.scale_button_press_cb)
629 hscale.connect('button-release-event', self.scale_button_release_cb)
630 hscale.connect('format-value', self.scale_format_value_cb)
631 hbox.pack_start(hscale)
635 table = gtk.Table(2,3)
637 vbox.pack_start(table, fill=False, expand=False, padding=6)
639 self.button = button = gtk.Button(stock=gtk.STOCK_MEDIA_PLAY)
640 button.set_property('can-default', True)
641 button.set_focus_on_click(False)
644 # problem: play and paused are of different widths and cause the
645 # window to re-layout
646 # "solution": add more buttons to a vbox so that the horizontal
650 bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PLAY))
651 bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PAUSE))
652 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
653 for kid in bvbox.get_children():
654 sizegroup.add_widget(kid)
656 table.attach(bvbox, 0, 1, 0, 2, gtk.FILL, gtk.FILL)
658 # can't set this property before the button has a window
659 button.set_property('has-default', True)
660 button.connect('clicked', lambda *args: self.play_toggled())
662 self.cutin = cut = TimeControl(self, "Cut in time")
664 table.attach(cut, 1, 2, 0, 1, gtk.EXPAND, 0, 12)
666 self.cutout = cut = TimeControl(self, "Cut out time")
668 table.attach(cut, 1, 2, 1, 2, gtk.EXPAND, 0, 12)
670 button = gtk.Button("_Open other movie...")
672 button.connect('clicked', lambda *x: self.do_choose_file())
673 table.attach(button, 2, 3, 0, 1, gtk.FILL, gtk.FILL)
675 button = gtk.Button("_Write to disk")
676 button.set_property('image',
677 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS,
678 gtk.ICON_SIZE_BUTTON))
679 button.connect('clicked', lambda *x: self.do_remux())
681 table.attach(button, 2, 3, 1, 2, gtk.FILL, gtk.FILL)
683 #self.cutin.connect('notify::time', lambda *x: self.check_cutout())
684 #self.cutout.connect('notify::time', lambda *x: self.check_cutin())
687 if self.player.is_playing():
689 in_uri = self.player.get_location()
690 out_uri = in_uri[:-4] + '-remuxed.ogg'
691 r = Remuxer(in_uri, out_uri,
692 self.cutin.get_time(), self.cutout.get_time())
695 def do_choose_file(self):
696 if self.player.is_playing():
698 chooser = gtk.FileChooserDialog('Choose a movie to cut cut cut',
700 buttons=(gtk.STOCK_CANCEL,
704 chooser.set_local_only(False)
705 chooser.set_select_multiple(False)
707 f.set_name("All files")
709 chooser.add_filter(f)
711 f.set_name("Ogg files")
712 f.add_pattern("*.og[gvax]") # as long as this is the only thing we
714 chooser.add_filter(f)
715 chooser.set_filter(f)
717 prev = self.player.get_location()
719 chooser.set_uri(prev)
722 uri = chooser.get_uri()
725 if resp == SUCCESS and uri != None:
731 def check_cutout(self):
732 if self.cutout.get_time() <= self.cutin.get_time():
733 pos, dur = self.player.query_position()
734 self.cutout.set_time(dur)
736 def check_cutin(self):
737 if self.cutin.get_time() >= self.cutout.get_time():
738 self.cutin.set_time(0)
740 def play_toggled(self):
741 if self.player.is_playing():
743 self.button.set_label(gtk.STOCK_MEDIA_PLAY)
746 if self.update_id == -1:
747 self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
748 self.update_scale_cb)
749 self.button.set_label(gtk.STOCK_MEDIA_PAUSE)
751 def scale_format_value_cb(self, scale, value):
752 if self.p_duration == -1:
755 real = value * self.p_duration / 100
757 seconds = real / gst.SECOND
759 return "%02d:%02d" % (seconds / 60, seconds % 60)
761 def scale_button_press_cb(self, widget, event):
762 # see seek.c:start_seek
763 gst.debug('starting seek')
765 self.button.set_sensitive(False)
766 self.was_playing = self.player.is_playing()
770 # don't timeout-update position during seek
771 if self.update_id != -1:
772 gobject.source_remove(self.update_id)
775 # make sure we get changed notifies
776 if self.changed_id == -1:
777 self.changed_id = self.hscale.connect('value-changed',
778 self.scale_value_changed_cb)
780 def scale_value_changed_cb(self, scale):
782 real = long(scale.get_value() * self.p_duration / 100) # in ns
783 gst.debug('value changed, perform seek to %r' % real)
784 self.player.seek(real)
785 # allow for a preroll
786 self.player.get_state(timeout=50*gst.MSECOND) # 50 ms
788 def scale_button_release_cb(self, widget, event):
789 # see seek.cstop_seek
790 widget.disconnect(self.changed_id)
793 self.button.set_sensitive(True)
794 if self.seek_timeout_id != -1:
795 gobject.source_remove(self.seek_timeout_id)
796 self.seek_timeout_id = -1
798 gst.debug('released slider, setting back to playing')
802 if self.update_id != -1:
803 self.error('Had a previous update timeout id')
805 self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
806 self.update_scale_cb)
808 def update_scale_cb(self):
809 had_duration = self.p_duration != gst.CLOCK_TIME_NONE
810 self.p_position, self.p_duration = self.player.query_position()
811 if self.p_position != gst.CLOCK_TIME_NONE:
812 value = self.p_position * 100.0 / self.p_duration
813 self.adjustment.set_value(value)
815 self.cutin.set_time(0)
820 sys.stderr.write("usage: %s [URI-OF-MEDIA-FILE]\n" % args[0])
827 if not w.do_choose_file():
830 if not gst.uri_is_valid(args[1]):
831 sys.stderr.write("Error: Invalid URI: %s\n" % args[1])
839 if __name__ == '__main__':
840 sys.exit(main(sys.argv))