3
# vi:si:et:sw=4:sts=4:ts=4
11
gobject.threads_init()
20
def __init__(self, videowidget):
22
self.player = gst.element_factory_make("playbin", "player")
23
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
self.videowidget.set_sink(message.src)
37
message.src.set_property('force-aspect-ratio', True)
39
def on_message(self, bus, message):
41
if t == gst.MESSAGE_ERROR:
42
err, debug = message.parse_error()
43
print "Error: %s" % err, debug
47
elif t == gst.MESSAGE_EOS:
52
def set_location(self, location):
53
self.player.set_property('uri', location)
55
def get_location(self):
56
return self.player.get_property('uri')
58
def query_position(self):
59
"Returns a (position, duration) tuple"
61
position, format = self.player.query_position(gst.FORMAT_TIME)
63
position = gst.CLOCK_TIME_NONE
66
duration, format = self.player.query_duration(gst.FORMAT_TIME)
68
duration = gst.CLOCK_TIME_NONE
70
return (position, duration)
72
def seek(self, location):
74
@param location: time to seek to, in nanoseconds
76
gst.debug("seeking to %r" % location)
77
event = gst.event_new_seek(1.0, gst.FORMAT_TIME,
79
gst.SEEK_TYPE_SET, location,
80
gst.SEEK_TYPE_NONE, 0)
82
res = self.player.send_event(event)
84
gst.info("setting new stream time to 0")
85
self.player.set_new_stream_time(0L)
87
gst.error("seek to %r failed" % location)
90
gst.info("pausing player")
91
self.player.set_state(gst.STATE_PAUSED)
95
gst.info("playing player")
96
self.player.set_state(gst.STATE_PLAYING)
100
self.player.set_state(gst.STATE_NULL)
101
gst.info("stopped player")
103
def get_state(self, timeout=1):
104
return self.player.get_state(timeout=timeout)
106
def is_playing(self):
109
class VideoWidget(gtk.DrawingArea):
111
gtk.DrawingArea.__init__(self)
112
self.imagesink = None
113
self.unset_flags(gtk.DOUBLE_BUFFERED)
115
def do_expose_event(self, event):
117
self.imagesink.expose()
122
def set_sink(self, sink):
123
assert self.window.xid
124
self.imagesink = sink
125
self.imagesink.set_xwindow_id(self.window.xid)
127
class TimeControl(gtk.HBox):
128
# all labels same size
129
sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
130
__gproperties__ = {'time': (gobject.TYPE_UINT64, 'Time', 'Time',
131
# not actually usable: see #335854
132
# kept for .notify() usage
134
gobject.PARAM_READABLE)}
136
def __init__(self, window, label):
137
gtk.HBox.__init__(self)
138
self.pwindow = window
142
def get_property(self, param, pspec):
144
return self.get_time()
146
assert param in self.__gproperties__, \
147
'Unknown property: %s' % param
150
label = gtk.Label(self.label + ": ")
152
a = gtk.Alignment(1.0, 0.5)
154
a.set_padding(0, 0, 12, 0)
156
self.sizegroup.add_widget(a)
157
self.pack_start(a, True, False, 0)
159
self.minutes = minutes = gtk.Entry(5)
160
minutes.set_width_chars(5)
161
minutes.set_alignment(1.0)
162
minutes.connect('changed', lambda *x: self.notify('time'))
163
minutes.connect_after('activate', lambda *x: self.activated())
164
label2 = gtk.Label(":")
165
self.seconds = seconds = gtk.Entry(2)
166
seconds.set_width_chars(2)
167
seconds.set_alignment(1.0)
168
seconds.connect('changed', lambda *x: self.notify('time'))
169
seconds.connect_after('activate', lambda *x: self.activated())
170
label3 = gtk.Label(".")
171
self.milliseconds = milliseconds = gtk.Entry(3)
172
milliseconds.set_width_chars(3)
173
milliseconds.set_alignment(0.0)
174
milliseconds.connect('changed', lambda *x: self.notify('time'))
175
milliseconds.connect_after('activate', lambda *x: self.activated())
176
set = gtk.Button('Set')
177
goto = gtk.Button('Go')
178
goto.set_property('image',
179
gtk.image_new_from_stock(gtk.STOCK_JUMP_TO,
180
gtk.ICON_SIZE_BUTTON))
181
for w in minutes, label2, seconds, label3, milliseconds:
183
self.pack_start(w, False)
185
self.pack_start(set, False, False, 6)
187
self.pack_start(goto, False, False, 0)
188
set.connect('clicked', lambda *x: self.set_now())
189
goto.connect('clicked', lambda *x: self.activated())
193
for w, multiplier in ((self.minutes, gst.SECOND*60),
194
(self.seconds, gst.SECOND),
195
(self.milliseconds, gst.MSECOND)):
201
w.set_text(val and str(val) or '')
202
time += val * multiplier
205
def set_time(self, time):
206
if time == gst.CLOCK_TIME_NONE:
207
print "Can't set '%s' (invalid time)" % self.label
210
for w, multiplier in ((self.minutes, gst.SECOND*60),
211
(self.seconds, gst.SECOND),
212
(self.milliseconds, gst.MSECOND)):
213
val = time // multiplier
215
time -= val * multiplier
219
time, dur = self.pwindow.player.query_position()
223
time = self.get_time()
224
if self.pwindow.player.is_playing():
225
self.pwindow.play_toggled()
226
self.pwindow.player.seek(time)
227
self.pwindow.player.get_state(timeout=gst.MSECOND * 200)
229
class ProgressDialog(gtk.Dialog):
230
def __init__(self, title, description, task, parent, flags, buttons):
231
gtk.Dialog.__init__(self, title, parent, flags, buttons)
232
self._create_ui(title, description, task)
234
def _create_ui(self, title, description, task):
235
self.set_border_width(6)
236
self.set_resizable(False)
237
self.set_has_separator(False)
240
vbox.set_border_width(6)
242
self.vbox.pack_start(vbox, False)
244
label = gtk.Label('<big><b>%s</b></big>' % title)
245
label.set_use_markup(True)
246
label.set_alignment(0.0, 0.0)
248
vbox.pack_start(label, False)
250
label = gtk.Label(description)
251
label.set_line_wrap(True)
252
label.set_padding(0, 12)
254
vbox.pack_start(label, False)
256
self.progress = progress = gtk.ProgressBar()
258
vbox.pack_start(progress, False)
260
self.progresstext = label = gtk.Label('')
261
label.set_use_markup(True)
262
label.set_alignment(0.0, 0.0)
264
vbox.pack_start(label)
267
def set_task(self, task):
268
self.progresstext.set_markup('<i>%s</i>' % task)
275
class RemuxProgressDialog(ProgressDialog):
276
def __init__(self, parent, start, stop):
277
ProgressDialog.__init__(self,
279
('Writing the selected segment of your '
280
'media file to disk. This may take some '
281
'time depending on the file size.'),
282
'Starting media pipeline',
284
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
285
(gtk.STOCK_CANCEL, CANCELLED,
286
gtk.STOCK_CLOSE, SUCCESS))
289
self.update_position(start)
290
self.set_completed(False)
292
def update_position(self, pos):
294
pos = min(max(pos, self.start), self.stop)
295
remaining = self.stop - pos
296
minutes = remaining // (gst.SECOND * 60)
297
seconds = (remaining - minutes * gst.SECOND * 60) // gst.SECOND
298
self.progress.set_text('%d:%02d of video remaining' % (minutes, seconds))
299
self.progress.set_fraction(1.0 - float(remaining) / (self.stop - self.start))
301
def set_completed(self, completed):
302
self.set_response_sensitive(CANCELLED, not completed)
303
self.set_response_sensitive(SUCCESS, completed)
305
def set_connection_blocked_async_marshalled(pads, proc, *args, **kwargs):
310
to_block = list(pads)
311
to_relink = [(x, x.get_peer()) for x in pads]
313
def on_pad_blocked_sync(pad, is_blocked):
314
if pad not in to_block:
315
# can happen after the seek and before unblocking -- racy,
320
# marshal to main thread
321
gobject.idle_add(on_pads_blocked)
323
def on_pads_blocked():
324
for src, sink in to_relink:
326
proc(*args, **kwargs)
327
for src, sink in to_relink:
328
src.set_blocked_async(False, lambda *x: None)
329
clear_list(to_relink)
331
for src, sink in to_relink:
333
src.set_blocked_async(True, on_pad_blocked_sync)
335
class Remuxer(gst.Pipeline):
337
__gsignals__ = {'done': (gobject.SIGNAL_RUN_LAST, None, (int,))}
339
def __init__(self, fromuri, touri, start, stop):
340
# HACK: should do Pipeline.__init__, but that doesn't do what we
341
# want; there's a bug open aboooot that
342
self.__gobject_init__()
347
self.src = gst.element_make_from_uri(gst.URI_SRC, fromuri)
348
self.remuxbin = RemuxBin(start, stop)
349
self.sink = gst.element_make_from_uri(gst.URI_SINK, touri)
350
self.resolution = UNKNOWN
352
if gobject.signal_lookup('allow-overwrite', self.sink.__class__):
353
self.sink.connect('allow-overwrite', self._allow_overwrite)
355
self.add(self.src, self.remuxbin, self.sink)
357
self.src.link(self.remuxbin)
358
self.remuxbin.link(self.sink)
363
self.start_time = start
364
self.stop_time = stop
368
def _allow_overwrite(self, sink, uri):
369
name = self.sink.get_uri()
370
name = (gst.uri_has_protocol(name, 'file')
371
and gst.uri_get_location(name)
373
m = gtk.MessageDialog(self.window,
374
gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
375
gtk.MESSAGE_QUESTION,
377
("The file %s already exists. Would you "
378
"like to replace it?") % name)
379
b = gtk.Button(stock=gtk.STOCK_CANCEL)
381
m.add_action_widget(b, CANCELLED)
382
b = gtk.Button('Replace')
384
m.add_action_widget(b, SUCCESS)
385
txt = ('If you replace an existing file, its contents will be '
387
m.format_secondary_text(txt)
390
return resp == SUCCESS
392
def _start_queries(self):
395
# HACK: self.remuxbin.query() should do the same
396
# (requires implementing a vmethod, dunno how to do that
397
# although i think it's possible)
398
# HACK: why does self.query_position(..) not give useful
400
pad = self.remuxbin.get_pad('src')
401
pos, duration = pad.query_position(gst.FORMAT_TIME)
402
if pos != gst.CLOCK_TIME_NONE:
403
self.pdialog.update_position(pos)
404
except gst.QueryError:
408
if self._query_id == -1:
409
self._query_id = gobject.timeout_add(100, # 10 Hz
412
def _stop_queries(self):
413
if self._query_id != -1:
414
gobject.source_remove(self._query_id)
417
def _bus_watch(self, bus, message):
418
if message.type == gst.MESSAGE_ERROR:
419
print 'error', message
421
m = gtk.MessageDialog(self.window,
422
gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
425
"Error processing file")
426
gerror, debug = message.parse_error()
427
txt = ('There was an error processing your file: %s\n\n'
428
'Debug information:\n%s' % (gerror, debug))
429
m.format_secondary_text(txt)
432
self.response(FAILURE)
433
elif message.type == gst.MESSAGE_WARNING:
434
print 'warning', message
435
elif message.type == gst.MESSAGE_SEGMENT_DONE:
436
print 'eos, woot', message.src
437
self.pdialog.set_task('Finished')
438
self.pdialog.update_position(self.stop_time)
440
self.pdialog.set_completed(True)
441
elif message.type == gst.MESSAGE_STATE_CHANGED:
442
if message.src == self:
444
old, new, pending = message.parse_state_changed()
445
if ((old, new, pending) ==
446
(gst.STATE_READY, gst.STATE_PAUSED,
447
gst.STATE_VOID_PENDING)):
448
self.pdialog.set_task('Processing file')
449
self.pdialog.update_position(self.start_time)
450
self._start_queries()
451
self.set_state(gst.STATE_PLAYING)
453
def response(self, response):
454
assert self.resolution == UNKNOWN
455
self.resolution = response
456
self.set_state(gst.STATE_NULL)
457
self.pdialog.destroy()
459
self.window.set_sensitive(True)
460
self.emit('done', response)
462
def start(self, main_window):
464
bus.add_signal_watch()
465
bus.connect('message', self._bus_watch)
466
self.window = main_window
468
# can be None if we are debugging...
469
self.window.set_sensitive(False)
470
self.pdialog = RemuxProgressDialog(main_window, self.start_time,
473
self.pdialog.connect('response', lambda w, r: self.response(r))
475
self.set_state(gst.STATE_PAUSED)
477
def run(self, main_window):
478
self.start(main_window)
479
loop = gobject.MainLoop()
480
self.connect('done', lambda *x: gobject.idle_add(loop.quit))
482
return self.resolution
484
class RemuxBin(gst.Bin):
485
def __init__(self, start_time, stop_time):
486
self.__gobject_init__()
488
self.parsefactories = self._find_parsers()
491
self.demux = gst.element_factory_make('oggdemux')
492
self.mux = gst.element_factory_make('oggmux')
494
self.add(self.demux, self.mux)
496
self.add_pad(gst.GhostPad('sink', self.demux.get_pad('sink')))
497
self.add_pad(gst.GhostPad('src', self.mux.get_pad('src')))
499
self.demux.connect('pad-added', self._new_demuxed_pad)
500
self.demux.connect('no-more-pads', self._no_more_pads)
502
self.start_time = start_time
503
self.stop_time = stop_time
505
def _find_parsers(self):
506
registry = gst.registry_get_default()
508
for f in registry.get_feature_list(gst.ElementFactory):
509
if f.get_klass().find('Parser') >= 0:
510
for t in f.get_static_pad_templates():
511
if t.direction == gst.PAD_SINK:
512
for s in t.get_caps():
513
ret[s.get_name()] = f.get_name()
517
def _new_demuxed_pad(self, element, pad):
518
format = pad.get_caps()[0].get_name()
520
if format not in self.parsefactories:
521
self.async_error("Unsupported media type: %s", format)
524
queue = gst.element_factory_make('queue', 'queue_' + format)
525
parser = gst.element_factory_make(self.parsefactories[format])
528
queue.set_state(gst.STATE_PAUSED)
529
parser.set_state(gst.STATE_PAUSED)
530
pad.link(queue.get_compatible_pad(pad))
532
parser.link(self.mux)
533
self.parsers.append(parser)
535
def _do_segment_seek(self):
536
flags = gst.SEEK_FLAG_SEGMENT | gst.SEEK_FLAG_FLUSH
537
# HACK: self.seek should work, should try that at some point
538
return self.demux.seek(1.0, gst.FORMAT_TIME, flags,
539
gst.SEEK_TYPE_SET, self.start_time,
540
gst.SEEK_TYPE_SET, self.stop_time)
542
def _no_more_pads(self, element):
543
pads = [x.get_pad('src') for x in self.parsers]
544
set_connection_blocked_async_marshalled(pads,
545
self._do_segment_seek)
548
class PlayerWindow(gtk.Window):
549
UPDATE_INTERVAL = 500
551
gtk.Window.__init__(self)
552
self.set_default_size(600, 425)
556
self.player = GstPlayer(self.videowidget)
561
self.player.on_eos = lambda *x: on_eos()
565
self.seek_timeout_id = -1
567
self.p_position = gst.CLOCK_TIME_NONE
568
self.p_duration = gst.CLOCK_TIME_NONE
570
def on_delete_event():
573
self.connect('delete-event', lambda *x: on_delete_event())
575
def load_file(self, location):
576
self.player.set_location(location)
583
self.videowidget = VideoWidget()
584
self.videowidget.show()
585
vbox.pack_start(self.videowidget)
589
vbox.pack_start(hbox, fill=False, expand=False)
591
self.adjustment = gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)
592
hscale = gtk.HScale(self.adjustment)
594
hscale.set_update_policy(gtk.UPDATE_CONTINUOUS)
595
hscale.connect('button-press-event', self.scale_button_press_cb)
596
hscale.connect('button-release-event', self.scale_button_release_cb)
597
hscale.connect('format-value', self.scale_format_value_cb)
598
hbox.pack_start(hscale)
602
self.videowidget.connect_after('realize',
603
lambda *x: self.play_toggled())
605
table = gtk.Table(2,3)
607
vbox.pack_start(table, fill=False, expand=False, padding=6)
609
self.pause_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE,
610
gtk.ICON_SIZE_LARGE_TOOLBAR)
611
self.pause_image.show()
612
self.play_image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY,
613
gtk.ICON_SIZE_LARGE_TOOLBAR)
614
self.play_image.show()
615
self.button = button = gtk.Button()
616
button.add(self.play_image)
617
button.set_property('can-default', True)
618
button.set_focus_on_click(False)
620
aspect = gtk.AspectFrame(obey_child=False, xalign=0.0)
621
aspect.set_property('shadow_type', gtk.SHADOW_NONE)
624
table.attach(aspect, 0, 1, 0, 2, gtk.EXPAND|gtk.FILL, gtk.EXPAND|gtk.FILL)
625
button.set_property('has-default', True)
626
button.connect('clicked', lambda *args: self.play_toggled())
628
self.cutin = cut = TimeControl(self, "Cut in time")
630
table.attach(cut, 1, 2, 0, 1, gtk.EXPAND, 0, 12)
632
self.cutout = cut = TimeControl(self, "Cut out time")
634
table.attach(cut, 1, 2, 1, 2, gtk.EXPAND, 0, 12)
636
buttonbox = gtk.HButtonBox()
637
buttonbox.set_layout(gtk.BUTTONBOX_END)
639
table.attach(buttonbox, 2, 3, 1, 2, 0, 0)
641
button = gtk.Button("_Write to disk")
642
button.set_property('image',
643
gtk.image_new_from_stock(gtk.STOCK_SAVE_AS,
644
gtk.ICON_SIZE_BUTTON))
646
buttonbox.pack_start(button, False)
648
self.cutin.connect('notify::time', lambda *x: self.check_cutout())
649
self.cutout.connect('notify::time', lambda *x: self.check_cutin())
650
button.connect('clicked', lambda *x: self.do_remux())
653
if self.player.is_playing():
655
in_uri = self.player.get_location()
656
out_uri = in_uri[:-4] + '-remuxed.ogg'
657
r = Remuxer(in_uri, out_uri,
658
self.cutin.get_time(), self.cutout.get_time())
661
def check_cutout(self):
662
if self.cutout.get_time() <= self.cutin.get_time():
663
pos, dur = self.player.query_position()
664
self.cutout.set_time(dur)
666
def check_cutin(self):
667
if self.cutin.get_time() >= self.cutout.get_time():
668
self.cutin.set_time(0)
670
def play_toggled(self):
671
self.button.remove(self.button.child)
672
if self.player.is_playing():
674
self.button.add(self.play_image)
677
if self.update_id == -1:
678
self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
679
self.update_scale_cb)
680
self.button.add(self.pause_image)
682
def scale_format_value_cb(self, scale, value):
683
if self.p_duration == -1:
686
real = value * self.p_duration / 100
688
seconds = real / gst.SECOND
690
return "%02d:%02d" % (seconds / 60, seconds % 60)
692
def scale_button_press_cb(self, widget, event):
693
# see seek.c:start_seek
694
gst.debug('starting seek')
696
self.button.set_sensitive(False)
697
self.was_playing = self.player.is_playing()
701
# don't timeout-update position during seek
702
if self.update_id != -1:
703
gobject.source_remove(self.update_id)
706
# make sure we get changed notifies
707
if self.changed_id == -1:
708
self.changed_id = self.hscale.connect('value-changed',
709
self.scale_value_changed_cb)
711
def scale_value_changed_cb(self, scale):
713
real = long(scale.get_value() * self.p_duration / 100) # in ns
714
gst.debug('value changed, perform seek to %r' % real)
715
self.player.seek(real)
716
# allow for a preroll
717
self.player.get_state(timeout=50*gst.MSECOND) # 50 ms
719
def scale_button_release_cb(self, widget, event):
720
# see seek.cstop_seek
721
widget.disconnect(self.changed_id)
724
self.button.set_sensitive(True)
725
if self.seek_timeout_id != -1:
726
gobject.source_remove(self.seek_timeout_id)
727
self.seek_timeout_id = -1
729
gst.debug('released slider, setting back to playing')
733
if self.update_id != -1:
734
self.error('Had a previous update timeout id')
736
self.update_id = gobject.timeout_add(self.UPDATE_INTERVAL,
737
self.update_scale_cb)
739
def update_scale_cb(self):
740
self.p_position, self.p_duration = self.player.query_position()
741
if self.p_position != gst.CLOCK_TIME_NONE:
742
value = self.p_position * 100.0 / self.p_duration
743
self.adjustment.set_value(value)
749
sys.stderr.write("usage: %s URI-OF-MEDIA-FILE\n" % args[0])
757
if not gst.uri_is_valid(args[1]):
758
sys.stderr.write("Error: Invalid URI: %s\n" % args[1])
766
if __name__ == '__main__':
767
sys.exit(main(sys.argv))