make top_clip work, and tuning stitching scripts
[opo.git] / opo-launcher
blob16d398aad9e4c85d903d590bab48c2ef5738623d
1 #!/usr/bin/python
2 import pygtk
3 pygtk.require('2.0')
4 import gtk, gobject
5 import subprocess
6 import os, sys
8 from ConfigParser import SafeConfigParser, Error as CPError
10 #defaults, most can be over-ridden in opo.rc
11 OPO = './opo'
12 SCREENS = 4
13 SCREEN_WIDTH = 1024
14 SCREEN_HEIGHT = 768
15 X_SCREENS = 2
16 TIMEOUT = 20
17 CHOOSE_DIR = '.'
18 UNSTITCHED_DIR = '.'
19 ENCODER = 'mpeg4'
20 MUXER = None
21 #an approximate target for working out bps settings (for mpeg4/2-ish codecs)
22 #256 * 192 * 4 screens --> 192k * BYTES_PER_PIXEL_PER_SECOND bps
23 #1024 * 768 * 4 screens --> 3M * BYTES_PER_PIXEL_PER_SECOND bps
24 BYTES_PER_PIXEL_PER_SECOND = 2.0
26 RC_FILE = 'opo.rc'
27 AUTO_START = False
28 FULL_SCREEN = False
30 class OpoError(Exception):
31 pass
33 def log(*messages):
34 for m in messages:
35 print >> sys.stderr, m
37 def name_suggester(dir, base, suffix):
38 from os.path import join, exists
39 for i in range(1, 999):
40 name = "%s-%s.%s" % (base, i, suffix)
41 fullname = join(dir, name)
42 if not exists(fullname):
43 return fullname
44 raise OpoError("please: think up a name, or we'll be here forever")
46 def start_stitching_process(output_file, input_files, width, height,
47 audio_source=None, muxer=MUXER, encoder=ENCODER,
48 scale=1.0, clip_top=0, audio_codec='mp2',
49 progress_report=False):
50 import urllib
51 from urlparse import urlsplit
52 try:
53 for i, uri in enumerate(input_files):
54 if not uri.startswith('file://'):
55 fn = uri
56 uri = 'file://' + urllib.quote(os.path.abspath(uri))
57 else:
58 fn = '/' + urllib.unquote(urlsplit(uri).path)#.decode('utf-8')
59 #trigger exception if the filenamevdoesn't exist (so no http uris)
60 open(fn).close()
61 input_files[i] = uri
62 except AttributeError, e: #"None has no attribute startswith"
63 log(e)
64 raise OpoError("Not all input files are specified")
65 except IOError, e:
66 raise OpoError(e)
68 if muxer is None:
69 try:
70 muxer = output_file.rsplit('.', 1)[1]
71 except IndexError:
72 log('defaulting to avi muxer')
73 muxer = 'avi'
75 encoders = {
76 'vp8': ['vp8enc', 'quality=8'],
77 'mjpeg': ['jpegenc', 'idct-method=2', 'quality=85'],
78 'mpeg1': ['mpeg2enc',], #XXX need to set bitrate, etc
79 'theora': ['theoraenc',], #XXX settings
80 'x264': ['x264enc', 'tune=fastdecode', 'quantizer=22'],
81 'flv': ['ffenc_flv', "bitrate=%(bitrate)s"], #XXX settings
82 'mpeg4': ['ffenc_mpeg4', "bitrate=%(bitrate)s"], #XXX settings
83 'msmpeg4': ['ffenc_msmpeg4', "bitrate=%(bitrate)s"],#
86 details = {
87 'bitrate': int(BYTES_PER_PIXEL_PER_SECOND * width * height * len(input_files)),
89 audio_codecs = {
90 'vorbis': ['vorbisenc', 'bitrate=192', 'cbr=true', 'target=bitrate',],
91 'mp3': ['lamemp3enc', 'bitrate=192', 'cbr=true', 'target=bitrate',],
92 'mp2': ['twolame', 'bitrate=320'],
93 'wav':['wavenc'],
95 muxers = {
96 'avi': ['!', 'avimux', 'name=mux', ],
97 'flv': ['!', 'flvmux', 'name=mux', ],
98 'webm': ['!', 'webmmux', 'name=mux', ],
99 'mpeg': ['!', 'mplex', 'name=mux', ],
101 muxers['mpg'] = muxers['mpeg']
102 encoder_pipe = [s % details for s in encoders[encoder]]
103 mux_pipe = [s % details for s in muxers[muxer]]
104 if progress_report:
105 progress_pipe = ['progressreport', '!']
106 else:
107 progress_pipe = []
109 pipeline = (['gst-launch-0.10',
110 'videomixer',
111 'name=mix',
112 'background=1',
113 '!', 'ffmpegcolorspace',
114 '!',
116 progress_pipe +
117 encoder_pipe +
118 mux_pipe +
119 ['!', 'filesink', 'location=%s' % output_file,])
121 image_width = int(width * scale)
122 image_height = int(height * scale)
123 image_width_adj = (width - image_width) // 2
126 for i, fn in enumerate(input_files):
127 left = i * width + image_width_adj
128 right = (len(input_files) - 1 - i) * width + image_width_adj
129 top = clip_top
130 if i != audio_source:
131 pipeline.extend([
132 'uridecodebin',
133 'uri=%s' % fn,
135 else:
136 log("doing sound for %s" % i)
137 pipeline.extend([
138 'uridecodebin',
139 'uri=%s' % fn,
140 'name=demux',
141 'demux.',
142 '!', 'queue',
143 '!'])
144 pipeline.extend(audio_codecs[audio_codec])
145 pipeline.extend(['!', 'mux.', 'demux.'])
146 pipeline.extend([
147 '!', 'deinterlace',
148 '!', 'videoscale',
149 '!', 'video/x-raw-yuv,', 'width=%s,' % image_width, 'height=%s' % image_height,
150 ';', 'video/x-raw-rgb,', 'width=%s,' % image_width, 'height=%s' % image_height,
151 '!', 'videobox', 'border-alpha=0', 'alpha=1', 'left=-%s' % left, 'right=-%s' % right,
152 'top=%s' % top, 'bottom=%s' % (image_height - height - top),
153 '!', 'queue',
154 '!', 'mix.',
157 log(' '.join(pipeline).replace('!', ' \\\n!').replace('. ', '.\\\n '))
158 p = subprocess.Popen(pipeline)
159 return p
161 class Launcher:
162 is_auto = None
163 auto_tick_id = None
164 stitch_tick_id = None
165 tiemout = TIMEOUT
166 chooser = None
168 def play(self, logfile=None):
169 """Play the currently selected video"""
170 if logfile is not None:
171 import time
172 logfile = logfile.replace('$NOW', time.strftime('%Y-%m-%d+%H:%M:%S'))
173 f = open(logfile, 'w')
175 cmd = [OPO, '-s', str(self.screens), '-c', self.video,
176 '-w', str(self.display_width), '-h', str(self.display_height)]
177 if self.x_screens:
178 cmd.extend(['-x', str(self.x_screens)])
179 if self.force_multiscreen:
180 cmd.append('-m')
181 if self.full_screen:
182 cmd.append('-f')
183 log("Starting play: %r", ' '.join(cmd))
184 subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT)
185 if logfile is not None:
186 f.close()
189 def on_play_now(self, widget, data=None):
190 os.env['GST_DEBUG'] = '2'
191 self.play(logfile="logs/opo-$NOW.log")
193 def on_mode_switch(self, widget, data=None):
194 """Turning auto mode on or off, according to the widget's
195 state ('active' is auto). If the widget is toggled to the
196 current mode, ignore it."""
197 auto = widget.get_active()
198 if auto == self.is_auto:
199 log("spurious auto toggle")
200 return
201 self.is_auto = auto
202 for x in self.advanced_widgets:
203 x.set_sensitive(not auto)
204 if auto:
205 self.start_auto_countdown()
206 else:
207 self.stop_auto_countdown()
209 def start_auto_countdown(self):
210 self.countdown = self.timeout
211 if self.auto_tick_id is None: #lest, somehow, two ticks try going at once.
212 self.auto_tick_id = gobject.timeout_add(1000, self.auto_tick)
214 def stop_auto_countdown(self):
215 if self.auto_tick_id is not None:
216 gobject.source_remove(self.auto_tick_id)
217 self.auto_tick_id = None
218 self.mode_switch.set_label("Play _automatically in %s seconds" % self.timeout)
220 def auto_tick(self):
221 self.countdown -= 1
222 if self.countdown > 0:
223 if self.countdown == 1:
224 self.mode_switch.set_label("Play _automatically in one second!")
225 else:
226 self.mode_switch.set_label("Play _automatically in %s seconds" % self.countdown)
227 return True
228 self.auto_tick_id = None
229 self.play(logfile="logs/opo-auto-$NOW.log")
230 #returning False stops countdown, which is perhaps irrelevant
231 #as self.play should never return
232 return False
234 def on_chooser(self, widget, *data):
235 self.video = widget.get_uri()
236 self.update_heading()
237 self.update_choose_dir(widget.get_current_folder())
239 def stitch_video(self, widget, data=None):
240 """Launch the video joining gstreamer process, show a progress
241 bar/ spinner, and start a ticker that watches for its end."""
242 log("stitching video !")
243 output_file = self.stitch_target_field.get_text()
244 input_files = [x.get_uri() for x in self.stitch_choosers]
245 width = int(self.width_field.get_text())
246 height = int(self.width_field.get_text())
247 self.stitching_process = start_stitching_process(output_file, input_files,
248 width, height, self.stitch_audio_source)
249 #self.stitching_process = subprocess.Popen(['sleep', '10'])
250 self.progress_bar = gtk.ProgressBar()
251 self.progress_bar.set_pulse_step(0.02)
252 self.vbox.pack_start(self.progress_bar)
253 self.stitch_button.hide()
254 self.progress_bar.show()
255 self.stitch_tick_id = gobject.timeout_add(150, self.stitch_tick, ouptput_file)
256 self.currently_stitching = output_file
258 def stitch_tick(self, output_file):
259 """Spin the progress bar and wait for the finished video"""
260 r = self.stitching_process.poll()
261 if r is None:
262 self.progress_bar.pulse()
263 return True
264 if r != 0:
265 #XXX should catch and display gstreamer output
266 log("got result %s" % r)
267 self.progress_bar.hide()
268 self.stitch_button.show()
269 self.stitch_tick_id = None
270 #XXX make the new video the chosen one
272 v = self.currently_stitching = output_file
273 if v[:7] == 'file://':
274 v = v[7:]
275 self.chooser.set_filename(v)
276 self.update_choose_dir(dirname(v))
277 return False
279 def on_stitch_chooser(self, widget, n):
280 """Unify the stitch_choosers current folder, unless they have video set"""
281 d = widget.get_current_folder()
282 for i in range(self.screens):
283 if self.stitch_choosers[i].get_filename() is None:
284 self.stitch_choosers[i].set_current_folder(d)
285 return True
287 def on_choose_stitch_target(self, widget, data=None):
288 dialog = gtk.FileChooserDialog("Save as", action=gtk.FILE_CHOOSER_ACTION_SAVE,
289 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
290 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
292 dialog.set_do_overwrite_confirmation(True)
293 if self.choose_dir:
294 dialog.set_current_folder(self.choose_dir)
296 response = dialog.run()
297 filename = dialog.get_filename()
298 if response in (gtk.RESPONSE_ACCEPT, gtk.RESPONSE_OK):
299 directory, basename = os.path.split(filename)
300 self.update_choose_dir(directory)
301 self.stitch_target_field.set_text(dialog.get_filename())
302 log(response)
303 dialog.destroy()
305 def on_stitch_audio_source(self, widget, n):
306 self.stitch_audio_source = n
308 def read_rc(self):
309 rc = SafeConfigParser()
310 rc.read(RC_FILE)
311 def _get(section, item, default=None):
312 try:
313 return rc.get(section, item)
314 except CPError, e:
315 log(e)
316 return default
318 self.unstitched_dir = _get('Paths', 'unstitched_dir', UNSTITCHED_DIR)
319 self.update_choose_dir(_get('Paths', 'choose_dir', CHOOSE_DIR))
320 self.video = _get('Paths', 'last_played')
321 self.timeout = int(_get('Misc', 'timeout', TIMEOUT))
322 self.auto_start = _get('Misc', 'auto_start', '').lower() in ('true', '1', 'yes') or AUTO_START
323 self.full_screen = _get('Display', 'full_screen', '').lower() in ('true', '1', 'yes') or FULL_SCREEN
324 self.screens = int(_get('Display', 'screens', SCREENS))
325 self.import_width = int(_get('Import', 'screen_width', SCREEN_WIDTH))
326 self.import_height = int(_get('Import', 'screen_height', SCREEN_HEIGHT))
327 self.display_width = int(_get('Display', 'screen_width', SCREEN_WIDTH))
328 self.display_height = int(_get('Display', 'screen_height', SCREEN_HEIGHT))
329 self.x_screens = int(_get('Display', 'x_screens', X_SCREENS))
330 self.force_multiscreen = _get('Display', 'force_multiscreen', '').lower() in ('true', '1', 'yes')
332 def write_rc(self):
333 rc = SafeConfigParser()
334 rc.read(RC_FILE)
335 for section, key, value in (
336 ('Paths', 'unstitched_dir', self.unstitched_dir),
337 ('Paths', 'choose_dir', self.choose_dir),
338 ('Paths', 'last_played', self.video),
340 if value is not None:
341 if not rc.has_section(section):
342 rc.add_section(section)
343 rc.set(section, key, value)
345 with open(RC_FILE, 'wb') as configfile:
346 rc.write(configfile)
348 def update_heading(self):
349 if self.video is not None:
350 video_name = self.video.rsplit('/', 1)[1]
351 self.heading.set_markup('<big><b>%s</b> is ready to play</big>' %
352 video_name)
353 self.play_now.set_sensitive(True)
354 else:
355 self.heading.set_markup('<big>No video selected</big>')
356 self.play_now.set_sensitive(False)
358 def update_choose_dir(self, choosedir):
359 self.choose_dir = choosedir
360 if self.chooser:
361 self.chooser.set_current_folder(choosedir)
363 def make_window(self):
364 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
365 self.window.set_border_width(15)
366 self.vbox = gtk.VBox(False, 3)
367 self.advanced_widgets = []
369 _add = self.vbox.pack_start
370 def _add_advanced(widget):
371 self.vbox.pack_start(widget)
372 self.advanced_widgets.append(widget)
374 def _sep():
375 # add a separator with slightly more space than usual
376 _add(gtk.HSeparator(), True, True, 5)
378 # heading
379 h = gtk.Label()
380 h.set_line_wrap(True)
381 _add(h)
382 self.heading = h
384 self.play_now = gtk.Button("_Play now")
385 self.play_now.connect("clicked", self.on_play_now, None)
386 _add(self.play_now)
387 _sep()
389 #auto toggle
390 self.mode_switch = gtk.CheckButton("Play _automatically in %s seconds" % self.timeout)
391 self.mode_switch.connect("toggled", self.on_mode_switch, None)
392 _add(self.mode_switch)
393 _sep()
395 #choose another
396 #XXX file filters
397 chooser_lab = gtk.Label("Ch_oose another combined video (%s screens)" % self.screens)
398 chooser_lab.set_use_underline(True)
399 chooser_lab.set_alignment(0, 0.5)
400 self.chooser = gtk.FileChooserButton(title="video")
401 if self.choose_dir:
402 self.chooser.set_current_folder(self.choose_dir)
403 self.chooser.set_width_chars(40)
404 self.chooser.connect('file-set', self.on_chooser, None)
406 chooser_lab.set_mnemonic_widget(self.chooser)
408 _add_advanced(chooser_lab)
409 _add_advanced(self.chooser)
410 _sep()
412 #create another by stitching subvideos
413 nb = gtk.Label("Construct a _new combined video out of %s video files" % self.screens)
414 nb.set_use_underline(True)
416 nb.set_alignment(0, 0.5)
417 _add_advanced(nb)
419 sound = gtk.Label("Sound:")
420 sound.set_alignment(0.99, 0.5)
421 _add_advanced(sound)
423 #hb = gtk.HBox()
424 self.stitch_silent = gtk.RadioButton(None, "silent")
425 self.stitch_silent.connect("toggled", self.on_stitch_audio_source, None)
426 #hb.pack_start(self.stitch_silent, False)
428 #XXX file filters
429 self.stitch_choosers = []
430 self.stitch_audio_source = None
431 for i in range(self.screens):
432 fc = gtk.FileChooserButton(title="video %s" % i)
433 fcl = gtk.Label("Screen _%s" % (i + 1))
434 fcl.set_use_underline(True)
435 fcl.set_mnemonic_widget(fc)
436 fc_sound = gtk.RadioButton(self.stitch_silent, "audio %s" % (i + 1,))
437 fc_sound.set_active(False)
438 fc_sound.set_tooltip_text("use the sound from video %s" % (i + 1,))
439 fc_sound.connect("toggled", self.on_stitch_audio_source, i)
440 fc_set = gtk.HBox()
441 fc_set.pack_start(fcl, False)
442 fc_set.pack_start(fc)
443 fc_set.pack_start(fc_sound, False)
444 self.stitch_choosers.append(fc)
445 fc.connect('file-set', self.on_stitch_chooser, i)
446 _add_advanced(fc_set)
448 hb = gtk.HBox()
449 #self.stitch_silent = gtk.RadioButton(None, "no sound")
450 hb.pack_end(self.stitch_silent, False)
451 _add_advanced(hb)
454 #save_as box
455 #XXX file filters
456 self.stitch_target_field = gtk.Entry()
457 self.stitch_target_field.set_width_chars(40)
458 self.stitch_target_field.set_text(name_suggester(self.choose_dir, 'new', 'avi'))
460 self.choose_stitch_target = gtk.Button(label="choose")
461 self.choose_stitch_target.connect("clicked", self.on_choose_stitch_target, None)
463 hb = gtk.HBox()
464 name_label = gtk.Label("Save as")
465 hb.pack_start(name_label, False)
466 hb.pack_start(self.stitch_target_field)
467 hb.pack_start(self.choose_stitch_target)
468 nb.set_mnemonic_widget(self.stitch_choosers[0])
469 _add_advanced(hb)
471 #width/height boxes
472 hb = gtk.HBox()
473 label = gtk.Label("Pixel size of each screen: ")
474 hb.pack_start(label, False)
475 for name, default, label in (
476 ('width_field', self.import_width, "_width"),
477 ('height_field', self.import_height, "_height"),
479 e = gtk.Entry()
480 e.set_width_chars(5)
481 e.set_alignment(1)
482 e.set_text(str(default))
483 setattr(self, name, e)
484 lb = gtk.Label(label)
485 lb.set_use_underline(True)
486 lb.set_mnemonic_widget(e)
487 hb.pack_start(lb, False)
488 hb.pack_start(e, False)
489 _add_advanced(hb)
491 #stitch button
492 self.stitch_button = gtk.Button("Assemble the new _video")
493 self.stitch_button.connect("clicked", self.stitch_video, None)
494 _add_advanced(self.stitch_button)
496 self.window.add(self.vbox)
498 def place_window(self):
499 self.window.move(0, 0)
501 def __init__(self):
502 self.read_rc()
503 self.make_window()
504 self.place_window()
505 self.update_heading()
506 self.mode_switch.set_active(self.auto_start and self.video is not None)
507 self.window.connect("destroy", self.destroy)
508 self.window.show_all()
511 def destroy(self, widget, data=None):
512 log("bye")
513 self.write_rc()
514 gtk.main_quit()
516 quit_onclick = destroy
518 if __name__ == '__main__':
519 start = Launcher()
520 gtk.main()