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 SyncPoints(gtk.VBox):
133 def __init__(self, window):
134 gtk.VBox.__init__(self)
135 self.pwindow = window
138 def get_time_as_str(self, iter, i):
139 value = self.model.get_value(iter, i)
141 for div, sep, mod, pad in ((gst.SECOND*60, '', 0, 0),
142 (gst.SECOND, ':', 60, 2),
143 (gst.MSECOND, '.', 1000, 3)):
147 ret += sep + ('%%0%dd' % pad) % n
151 self.model = model = gtk.ListStore(gobject.TYPE_UINT64,
153 self.view = view = gtk.TreeView(self.model)
155 renderer = gtk.CellRendererText()
156 column = gtk.TreeViewColumn("Audio time", renderer)
157 def time_to_text(column, cell, method, iter, i):
158 cell.set_property('text', self.get_time_as_str(iter, i))
159 column.set_cell_data_func(renderer, time_to_text, 0)
160 column.set_expand(True)
161 column.set_clickable(True)
162 view.append_column(column)
164 renderer = gtk.CellRendererText()
165 column = gtk.TreeViewColumn("Video time", renderer)
166 column.set_cell_data_func(renderer, time_to_text, 1)
167 column.set_expand(True)
168 view.append_column(column)
171 self.pack_start(view, True, True, 6)
173 hbox = gtk.HBox(False, 0)
175 self.pack_start(hbox, False, False, 0)
177 add = gtk.Button(stock=gtk.STOCK_ADD)
179 def add_and_select(*x):
180 iter = model.append()
181 self.view.get_selection().select_iter(iter)
183 add.connect("clicked", add_and_select)
184 hbox.pack_end(add, False, False, 0)
186 remove = gtk.Button(stock=gtk.STOCK_REMOVE)
188 def remove_selected(*x):
189 model, iter = self.view.get_selection().get_selected()
192 remove.connect("clicked", remove_selected)
193 hbox.pack_end(remove, False, False, 0)
199 label = gtk.Label("Set: ")
201 hbox.pack_start(label)
203 a = gtk.Button("A_udio")
205 a.connect("clicked", lambda *x: self.set_selected_audio_now())
212 v = gtk.Button("_Video")
214 v.connect("clicked", lambda *x: self.set_selected_video_now())
217 def get_sync_points(self):
218 def get_value(row, i):
219 return self.model.get_value(row.iter, i)
220 pairs = [(get_value(row, 1), get_value(row, 0)) for row in self.model]
225 maxdiff = max(maxdiff, abs(pair[1] - pair[0]))
230 print 'Sync times now:'
231 for index, row in enumerate(self.model):
232 print 'A/V %d: %s -- %s' % (index,
233 self.get_time_as_str(row.iter, 0),
234 self.get_time_as_str(row.iter, 1))
237 def set_selected_audio(self, time):
238 sel = self.view.get_selection()
239 model, iter = sel.get_selected()
241 model.set_value(iter, 0, time)
244 def set_selected_video(self, time):
245 sel = self.view.get_selection()
246 model, iter = sel.get_selected()
248 model.set_value(iter, 1, time)
251 def set_selected_audio_now(self):
252 time, dur = self.pwindow.player.query_position()
253 self.set_selected_audio(time)
255 def set_selected_video_now(self):
256 # pause and preroll first
257 if self.pwindow.player.is_playing():
258 self.pwindow.play_toggled()
259 self.pwindow.player.get_state(timeout=gst.MSECOND * 200)
261 time, dur = self.pwindow.player.query_position()
262 self.set_selected_video(time)
264 def seek_and_pause(self, time):
265 if self.pwindow.player.is_playing():
266 self.pwindow.play_toggled()
267 self.pwindow.player.seek(time)
268 if self.pwindow.player.is_playing():
269 self.pwindow.play_toggled()
270 self.pwindow.player.get_state(timeout=gst.MSECOND * 200)
272 class ProgressDialog(gtk.Dialog):
273 def __init__(self, title, description, task, parent, flags, buttons):
274 gtk.Dialog.__init__(self, title, parent, flags, buttons)
275 self._create_ui(title, description, task)
277 def _create_ui(self, title, description, task):
278 self.set_border_width(6)
279 self.set_resizable(False)
280 self.set_has_separator(False)
283 vbox.set_border_width(6)
285 self.vbox.pack_start(vbox, False)
287 label = gtk.Label('<big><b>%s</b></big>' % title)
288 label.set_use_markup(True)
289 label.set_alignment(0.0, 0.0)
291 vbox.pack_start(label, False)
293 label = gtk.Label(description)
294 label.set_use_markup(True)
295 label.set_alignment(0.0, 0.0)
296 label.set_line_wrap(True)
297 label.set_padding(0, 12)
299 vbox.pack_start(label, False)
301 self.progress = progress = gtk.ProgressBar()
303 vbox.pack_start(progress, False)
305 self.progresstext = label = gtk.Label('')
306 label.set_line_wrap(True)
307 label.set_use_markup(True)
308 label.set_alignment(0.0, 0.0)
310 vbox.pack_start(label)
313 def set_task(self, task):
314 self.progresstext.set_markup('<i>%s</i>' % task)
321 class RemuxProgressDialog(ProgressDialog):
322 def __init__(self, parent, fromname, toname):
323 ProgressDialog.__init__(self,
325 ('Writing the newly synchronized <b>%s</b> '
326 'to <b>%s</b>. This may take some time.'
327 % (fromname, toname)),
328 'Starting media pipeline',
330 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
331 (gtk.STOCK_CANCEL, CANCELLED,
332 gtk.STOCK_CLOSE, SUCCESS))
333 self.set_completed(False)
335 def update_position(self, pos, dur):
336 remaining = dur - pos
337 minutes = remaining // (gst.SECOND * 60)
338 seconds = (remaining - minutes * gst.SECOND * 60) // gst.SECOND
339 self.progress.set_text('%d:%02d of video remaining' % (minutes, seconds))
340 self.progress.set_fraction(1.0 - float(remaining) / dur)
342 def set_completed(self, completed):
343 self.set_response_sensitive(CANCELLED, not completed)
344 self.set_response_sensitive(SUCCESS, completed)
346 class Resynchronizer(gst.Pipeline):
348 __gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))}
350 def __init__(self, fromuri, touri, (syncpoints, maxdiff)):
351 # HACK: should do Pipeline.__init__, but that doesn't do what we
352 # want; there's a bug open aboooot that
353 self.__gobject_init__()
355 self.fromuri = fromuri
357 self.syncpoints = syncpoints
358 self.maxdiff = maxdiff
360 self.src = self.resyncbin = self.sink = None
361 self.resolution = UNKNOWN
368 def do_setup_pipeline(self):
369 self.src = gst.element_make_from_uri(gst.URI_SRC, self.fromuri)
370 self.resyncbin = ResyncBin(self.syncpoints, self.maxdiff)
371 self.sink = gst.element_make_from_uri(gst.URI_SINK, self.touri)
372 self.resolution = UNKNOWN
374 if gobject.signal_lookup('allow-overwrite', self.sink.__class__):
375 self.sink.connect('allow-overwrite', lambda *x: True)
377 self.add(self.src, self.resyncbin, self.sink)
379 self.src.link(self.resyncbin)
380 self.resyncbin.link(self.sink)
382 def do_get_touri(self):
383 chooser = gtk.FileChooserDialog('Save as...',
385 action=gtk.FILE_CHOOSER_ACTION_SAVE,
386 buttons=(gtk.STOCK_CANCEL,
390 chooser.set_uri(self.fromuri) # to select the folder
391 chooser.unselect_all()
392 chooser.set_do_overwrite_confirmation(True)
393 name = self.fromuri.split('/')[-1][:-4] + '-remuxed.ogg'
394 chooser.set_current_name(name)
396 uri = chooser.get_uri()
404 def _start_queries(self):
407 # HACK: self.remuxbin.query() should do the same
408 # (requires implementing a vmethod, dunno how to do that
409 # although i think it's possible)
410 # HACK: why does self.query_position(..) not give useful
412 pad = self.resyncbin.get_pad('src')
413 pos, format = pad.query_position(gst.FORMAT_TIME)
414 dur, format = pad.query_duration(gst.FORMAT_TIME)
415 if pos != gst.CLOCK_TIME_NONE:
416 self.pdialog.update_position(pos, duration)
418 # print 'query failed'
421 if self._query_id == -1:
422 self._query_id = gobject.timeout_add(100, # 10 Hz
425 def _stop_queries(self):
426 if self._query_id != -1:
427 gobject.source_remove(self._query_id)
430 def _bus_watch(self, bus, message):
431 if message.type == gst.MESSAGE_ERROR:
432 print 'error', message
434 m = gtk.MessageDialog(self.window,
435 gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
438 "Error processing file")
439 gerror, debug = message.parse_error()
440 txt = ('There was an error processing your file: %s\n\n'
441 'Debug information:\n%s' % (gerror, debug))
442 m.format_secondary_text(txt)
445 self.response(FAILURE)
446 elif message.type == gst.MESSAGE_WARNING:
447 print 'warning', message
448 elif message.type == gst.MESSAGE_EOS:
449 # print 'eos, woot', message.src
451 if name.startswith('file://'):
453 self.pdialog.set_task('Finished writing %s' % name)
454 self.pdialog.update_position(1,1)
456 self.pdialog.set_completed(True)
457 elif message.type == gst.MESSAGE_STATE_CHANGED:
458 if message.src == self:
459 old, new, pending = message.parse_state_changed()
460 if ((old, new, pending) ==
461 (gst.STATE_READY, gst.STATE_PAUSED,
462 gst.STATE_VOID_PENDING)):
463 self.pdialog.set_task('Processing file')
464 self._start_queries()
465 self.set_state(gst.STATE_PLAYING)
467 def response(self, response):
468 assert self.resolution == UNKNOWN
469 self.resolution = response
470 self.set_state(gst.STATE_NULL)
471 self.pdialog.destroy()
473 self.window.set_sensitive(True)
474 self.emit('done', response)
476 def start(self, main_window):
477 self.window = main_window
478 self.touri = self.do_get_touri()
481 self.do_setup_pipeline()
483 bus.add_signal_watch()
484 bus.connect('message', self._bus_watch)
486 # can be None if we are debugging...
487 self.window.set_sensitive(False)
488 fromname = self.fromuri.split('/')[-1]
489 toname = self.touri.split('/')[-1]
490 self.pdialog = RemuxProgressDialog(main_window, fromname, toname)
492 self.pdialog.connect('response', lambda w, r: self.response(r))
494 self.set_state(gst.STATE_PAUSED)
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))
503 self.resolution = CANCELLED
504 return self.resolution
506 class ResyncBin(gst.Bin):
507 def __init__(self, sync_points, maxdiff):
508 self.__gobject_init__()
510 self.parsefactories = self._find_parsers()
513 self.demux = gst.element_factory_make('oggdemux')
514 self.mux = gst.element_factory_make('oggmux')
516 self.add(self.demux, self.mux)
518 self.add_pad(gst.GhostPad('sink', self.demux.get_pad('sink')))
519 self.add_pad(gst.GhostPad('src', self.mux.get_pad('src')))
521 self.demux.connect('pad-added', self._new_demuxed_pad)
523 self.sync_points = sync_points
524 self.maxdiff = maxdiff
526 def _find_parsers(self):
527 registry = gst.registry_get_default()
529 for f in registry.get_feature_list(gst.ElementFactory):
530 if f.get_klass().find('Parser') >= 0:
531 for t in f.get_static_pad_templates():
532 if t.direction == gst.PAD_SINK:
533 for s in t.get_caps():
534 ret[s.get_name()] = f.get_name()
538 def _new_demuxed_pad(self, element, pad):
539 format = pad.get_caps()[0].get_name()
541 if format not in self.parsefactories:
542 self.async_error("Unsupported media type: %s", format)
545 queue = gst.element_factory_make('queue', 'queue_' + format)
546 queue.set_property('max-size-buffers', 0)
547 queue.set_property('max-size-bytes', 0)
549 queue.set_property('max-size-time', int(self.maxdiff * 1.5))
550 parser = gst.element_factory_make(self.parsefactories[format])
553 queue.set_state(gst.STATE_PAUSED)
554 parser.set_state(gst.STATE_PAUSED)
555 pad.link(queue.get_compatible_pad(pad))
557 parser.link(self.mux)
558 self.parsers.append(parser)
560 print repr(self.sync_points)
562 if 'video' in format:
563 parser.set_property('synchronization-points',
566 class PlayerWindow(gtk.Window):
567 UPDATE_INTERVAL = 500
569 gtk.Window.__init__(self)
570 self.set_default_size(600, 500)
574 self.player = GstPlayer(self.videowidget)
579 self.player.on_eos = lambda *x: on_eos()
583 self.seek_timeout_id = -1
585 self.p_position = gst.CLOCK_TIME_NONE
586 self.p_duration = gst.CLOCK_TIME_NONE
588 def on_delete_event():
591 self.connect('delete-event', lambda *x: on_delete_event())
593 def load_file(self, location):
594 filename = location.split('/')[-1]
595 self.set_title('%s munger' % filename)
596 self.player.set_location(location)
597 if self.videowidget.flags() & gtk.REALIZED:
600 self.videowidget.connect_after('realize',
601 lambda *x: self.play_toggled())
608 self.videowidget = VideoWidget()
609 self.videowidget.show()
610 vbox.pack_start(self.videowidget)
614 vbox.pack_start(hbox, fill=False, expand=False)
616 self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
617 hscale = gtk.HScale(self.adjustment)
619 hscale.set_update_policy(gtk.UPDATE_CONTINUOUS)
620 hscale.connect('button-press-event', self.scale_button_press_cb)
621 hscale.connect('button-release-event', self.scale_button_release_cb)
622 hscale.connect('format-value', self.scale_format_value_cb)
623 hbox.pack_start(hscale)
627 table = gtk.Table(3,3)
629 vbox.pack_start(table, fill=False, expand=False, padding=6)
631 self.button = button = gtk.Button(stock=gtk.STOCK_MEDIA_PLAY)
632 button.set_property('can-default', True)
633 button.set_focus_on_click(False)
636 # problem: play and paused are of different widths and cause the
637 # window to re-layout
638 # "solution": add more buttons to a vbox so that the horizontal
642 bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PLAY))
643 bvbox.add(gtk.Button(stock=gtk.STOCK_MEDIA_PAUSE))
644 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
645 for kid in bvbox.get_children():
646 sizegroup.add_widget(kid)
648 table.attach(bvbox, 0, 1, 1, 3, gtk.FILL, gtk.FILL)
650 # can't set this property before the button has a window
651 button.set_property('has-default', True)
652 button.connect('clicked', lambda *args: self.play_toggled())
654 self.sync = sync = SyncPoints(self)
656 table.attach(sync, 1, 2, 0, 3, gtk.EXPAND, gtk.EXPAND|gtk.FILL, 12)
657 # nasty things to get sizes
658 l = gtk.Label('\n\n\n')
660 table.attach(l, 0, 1, 0, 1, 0, 0, 0)
661 l = gtk.Label('\n\n\n')
663 table.attach(l, 2, 3, 0, 1, 0, 0, 0)
665 button = gtk.Button("_Open other movie...")
667 button.connect('clicked', lambda *x: self.do_choose_file())
668 table.attach(button, 2, 3, 1, 2, gtk.FILL, gtk.FILL)
670 button = gtk.Button("_Write to disk")
671 button.set_property('image',
672 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS,
673 gtk.ICON_SIZE_BUTTON))
674 button.connect('clicked', lambda *x: self.do_remux())
676 table.attach(button, 2, 3, 2, 3, gtk.FILL, gtk.FILL)
679 if self.player.is_playing():
681 in_uri = self.player.get_location()
682 out_uri = in_uri[:-4] + '-remuxed.ogg'
683 r = Resynchronizer(in_uri, out_uri, self.sync.get_sync_points())
686 def do_choose_file(self):
687 if self.player.is_playing():
689 chooser = gtk.FileChooserDialog('Choose a movie to bork bork bork',
691 buttons=(gtk.STOCK_CANCEL,
695 chooser.set_local_only(False)
696 chooser.set_select_multiple(False)
698 f.set_name("All files")
700 chooser.add_filter(f)
702 f.set_name("Ogg files")
703 f.add_pattern("*.ogg") # as long as this is the only thing we
705 chooser.add_filter(f)
706 chooser.set_filter(f)
708 prev = self.player.get_location()
710 chooser.set_uri(prev)
713 uri = chooser.get_uri()
722 def play_toggled(self):
723 if self.player.is_playing():
725 self.button.set_label(gtk.STOCK_MEDIA_PLAY)
728 if self.update_id == -1:
729 self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
730 self.update_scale_cb)
731 self.button.set_label(gtk.STOCK_MEDIA_PAUSE)
733 def scale_format_value_cb(self, scale, value):
734 if self.p_duration == -1:
737 real = value * self.p_duration / 100
739 seconds = real / gst.SECOND
741 return "%02d:%02d" % (seconds / 60, seconds % 60)
743 def scale_button_press_cb(self, widget, event):
744 # see seek.c:start_seek
745 gst.debug('starting seek')
747 self.button.set_sensitive(False)
748 self.was_playing = self.player.is_playing()
752 # don't timeout-update position during seek
753 if self.update_id != -1:
754 gobject.source_remove(self.update_id)
757 # make sure we get changed notifies
758 if self.changed_id == -1:
759 self.changed_id = self.hscale.connect('value-changed',
760 self.scale_value_changed_cb)
762 def scale_value_changed_cb(self, scale):
764 real = long(scale.get_value() * self.p_duration / 100) # in ns
765 gst.debug('value changed, perform seek to %r' % real)
766 self.player.seek(real)
767 # allow for a preroll
768 self.player.get_state(timeout=50*gst.MSECOND) # 50 ms
770 def scale_button_release_cb(self, widget, event):
771 # see seek.cstop_seek
772 widget.disconnect(self.changed_id)
775 self.button.set_sensitive(True)
776 if self.seek_timeout_id != -1:
777 gobject.source_remove(self.seek_timeout_id)
778 self.seek_timeout_id = -1
780 gst.debug('released slider, setting back to playing')
784 if self.update_id != -1:
785 self.error('Had a previous update timeout id')
787 self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
788 self.update_scale_cb)
790 def update_scale_cb(self):
791 had_duration = self.p_duration != gst.CLOCK_TIME_NONE
792 self.p_position, self.p_duration = self.player.query_position()
793 if self.p_position != gst.CLOCK_TIME_NONE:
794 value = self.p_position * 100.0 / self.p_duration
795 self.adjustment.set_value(value)
800 sys.stderr.write("usage: %s [URI-OF-MEDIA-FILE]\n" % args[0])
807 if not w.do_choose_file():
810 if not gst.uri_is_valid(args[1]):
811 sys.stderr.write("Error: Invalid URI: %s\n" % args[1])
819 if __name__ == '__main__':
820 sys.exit(main(sys.argv))