Remove old nhew example files
[opo.git] / opo-launcher
blobdc5a4ce7f72695447ed3daa7a11af8ff6799142a
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.5
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=21'],
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 fn.endswith('Julia%20resize%204%2028th.mov'):
131 top += top * 2 // 3
133 if i != audio_source:
134 pipeline.extend([
135 'uridecodebin',
136 'uri=%s' % fn,
138 else:
139 log("doing sound for %s" % i)
140 pipeline.extend([
141 'uridecodebin',
142 'uri=%s' % fn,
143 'name=demux',
144 'demux.',
145 '!', 'queue',
146 '!'])
147 pipeline.extend(audio_codecs[audio_codec])
148 pipeline.extend(['!', 'mux.', 'demux.'])
149 pipeline.extend([
150 '!', 'deinterlace',
151 '!', 'videoscale',
152 '!', 'video/x-raw-yuv,', 'width=%s,' % image_width, 'height=%s' % image_height,
153 ';', 'video/x-raw-rgb,', 'width=%s,' % image_width, 'height=%s' % image_height,
154 '!', 'videobox', 'border-alpha=0', 'alpha=1', 'left=-%s' % left, 'right=-%s' % right,
155 'top=%s' % top, 'bottom=%s' % (image_height - height - top),
156 '!', 'queue',
157 '!', 'mix.',
160 log(' '.join(pipeline).replace('!', ' \\\n!').replace('. ', '.\\\n '))
161 p = subprocess.Popen(pipeline)
162 return p
164 class Launcher:
165 is_auto = None
166 auto_tick_id = None
167 stitch_tick_id = None
168 tiemout = TIMEOUT
169 chooser = None
171 def play(self, logfile=None):
172 """Play the currently selected video"""
173 os.environ['GST_DEBUG'] = '2'
174 if logfile is not None:
175 import time
176 logfile = logfile.replace('$NOW', time.strftime('%Y-%m-%d+%H:%M:%S'))
177 f = open(logfile, 'w')
179 cmd = [OPO, '-s', str(self.screens), '-c', self.video,
180 '-w', str(self.display_width), '-h', str(self.display_height)]
181 if self.x_screens:
182 cmd.extend(['-x', str(self.x_screens)])
183 if self.force_multiscreen:
184 cmd.append('-m')
185 if self.full_screen:
186 cmd.append('-f')
187 log("Starting play: %r" % ' '.join(cmd))
188 subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT)
189 if logfile is not None:
190 f.close()
193 def on_play_now(self, widget, data=None):
194 self.play(logfile="logs/opo-$NOW.log")
196 def on_mode_switch(self, widget, data=None):
197 """Turning auto mode on or off, according to the widget's
198 state ('active' is auto). If the widget is toggled to the
199 current mode, ignore it."""
200 auto = widget.get_active()
201 if auto == self.is_auto:
202 log("spurious auto toggle")
203 return
204 self.is_auto = auto
205 for x in self.advanced_widgets:
206 x.set_sensitive(not auto)
207 if auto:
208 self.start_auto_countdown()
209 else:
210 self.stop_auto_countdown()
212 def start_auto_countdown(self):
213 self.countdown = self.timeout
214 if self.auto_tick_id is None: #lest, somehow, two ticks try going at once.
215 self.auto_tick_id = gobject.timeout_add(1000, self.auto_tick)
217 def stop_auto_countdown(self):
218 if self.auto_tick_id is not None:
219 gobject.source_remove(self.auto_tick_id)
220 self.auto_tick_id = None
221 self.mode_switch.set_label("Play _automatically in %s seconds" % self.timeout)
223 def auto_tick(self):
224 self.countdown -= 1
225 if self.countdown > 0:
226 if self.countdown == 1:
227 self.mode_switch.set_label("Play _automatically in one second!")
228 else:
229 self.mode_switch.set_label("Play _automatically in %s seconds" % self.countdown)
230 return True
231 self.auto_tick_id = None
232 self.play(logfile="logs/opo-auto-$NOW.log")
233 #returning False stops countdown, which is perhaps irrelevant
234 #as self.play should never return
235 return False
237 def on_chooser(self, widget, *data):
238 self.video = widget.get_uri()
239 self.update_heading()
240 self.update_choose_dir(widget.get_current_folder())
242 def stitch_video(self, widget, data=None):
243 """Launch the video joining gstreamer process, show a progress
244 bar/ spinner, and start a ticker that watches for its end."""
245 log("stitching video !")
246 output_file = self.stitch_target_field.get_text()
247 input_files = [x.get_uri() for x in self.stitch_choosers]
248 width = int(self.width_field.get_text())
249 height = int(self.width_field.get_text())
250 self.stitching_process = start_stitching_process(output_file, input_files,
251 width, height, self.stitch_audio_source)
252 #self.stitching_process = subprocess.Popen(['sleep', '10'])
253 self.progress_bar = gtk.ProgressBar()
254 self.progress_bar.set_pulse_step(0.02)
255 self.vbox.pack_start(self.progress_bar)
256 self.stitch_button.hide()
257 self.progress_bar.show()
258 self.stitch_tick_id = gobject.timeout_add(150, self.stitch_tick, ouptput_file)
259 self.currently_stitching = output_file
261 def stitch_tick(self, output_file):
262 """Spin the progress bar and wait for the finished video"""
263 r = self.stitching_process.poll()
264 if r is None:
265 self.progress_bar.pulse()
266 return True
267 if r != 0:
268 #XXX should catch and display gstreamer output
269 log("got result %s" % r)
270 self.progress_bar.hide()
271 self.stitch_button.show()
272 self.stitch_tick_id = None
273 #XXX make the new video the chosen one
275 v = self.currently_stitching = output_file
276 if v[:7] == 'file://':
277 v = v[7:]
278 self.chooser.set_filename(v)
279 self.update_choose_dir(dirname(v))
280 return False
282 def on_stitch_chooser(self, widget, n):
283 """Unify the stitch_choosers current folder, unless they have video set"""
284 d = widget.get_current_folder()
285 for i in range(self.screens):
286 if self.stitch_choosers[i].get_filename() is None:
287 self.stitch_choosers[i].set_current_folder(d)
288 return True
290 def on_choose_stitch_target(self, widget, data=None):
291 dialog = gtk.FileChooserDialog("Save as", action=gtk.FILE_CHOOSER_ACTION_SAVE,
292 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
293 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
295 dialog.set_do_overwrite_confirmation(True)
296 if self.choose_dir:
297 dialog.set_current_folder(self.choose_dir)
299 response = dialog.run()
300 filename = dialog.get_filename()
301 if response in (gtk.RESPONSE_ACCEPT, gtk.RESPONSE_OK):
302 directory, basename = os.path.split(filename)
303 self.update_choose_dir(directory)
304 self.stitch_target_field.set_text(dialog.get_filename())
305 log(response)
306 dialog.destroy()
308 def on_stitch_audio_source(self, widget, n):
309 self.stitch_audio_source = n
311 def read_rc(self):
312 rc = SafeConfigParser()
313 rc.read(RC_FILE)
314 def _get(section, item, default=None):
315 try:
316 return rc.get(section, item)
317 except CPError, e:
318 log(e)
319 return default
321 self.unstitched_dir = _get('Paths', 'unstitched_dir', UNSTITCHED_DIR)
322 self.update_choose_dir(_get('Paths', 'choose_dir', CHOOSE_DIR))
323 self.video = _get('Paths', 'last_played')
324 self.timeout = int(_get('Misc', 'timeout', TIMEOUT))
325 self.auto_start = _get('Misc', 'auto_start', '').lower() in ('true', '1', 'yes') or AUTO_START
326 self.full_screen = _get('Display', 'full_screen', '').lower() in ('true', '1', 'yes') or FULL_SCREEN
327 self.screens = int(_get('Display', 'screens', SCREENS))
328 self.import_width = int(_get('Import', 'screen_width', SCREEN_WIDTH))
329 self.import_height = int(_get('Import', 'screen_height', SCREEN_HEIGHT))
330 self.display_width = int(_get('Display', 'screen_width', SCREEN_WIDTH))
331 self.display_height = int(_get('Display', 'screen_height', SCREEN_HEIGHT))
332 self.x_screens = int(_get('Display', 'x_screens', X_SCREENS))
333 self.force_multiscreen = _get('Display', 'force_multiscreen', '').lower() in ('true', '1', 'yes')
335 def write_rc(self):
336 rc = SafeConfigParser()
337 rc.read(RC_FILE)
338 for section, key, value in (
339 ('Paths', 'unstitched_dir', self.unstitched_dir),
340 ('Paths', 'choose_dir', self.choose_dir),
341 ('Paths', 'last_played', self.video),
343 if value is not None:
344 if not rc.has_section(section):
345 rc.add_section(section)
346 rc.set(section, key, value)
348 with open(RC_FILE, 'wb') as configfile:
349 rc.write(configfile)
351 def update_heading(self):
352 try:
353 video_name = self.video.rsplit('/', 1)[1]
354 self.heading.set_markup('<big><b>%s</b> is ready to play</big>' %
355 video_name)
356 self.play_now.set_sensitive(True)
357 except Exception, e:
358 log("Couldn't set heading", e)
359 self.heading.set_markup('<big>No video selected</big>')
360 self.play_now.set_sensitive(False)
362 def update_choose_dir(self, choosedir):
363 self.choose_dir = choosedir
364 if self.chooser:
365 self.chooser.set_current_folder(choosedir)
367 def make_window(self):
368 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
369 self.window.set_border_width(15)
370 self.vbox = gtk.VBox(False, 3)
371 self.advanced_widgets = []
373 _add = self.vbox.pack_start
374 def _add_advanced(widget):
375 self.vbox.pack_start(widget)
376 self.advanced_widgets.append(widget)
378 def _sep():
379 # add a separator with slightly more space than usual
380 _add(gtk.HSeparator(), True, True, 5)
382 # heading
383 h = gtk.Label()
384 h.set_line_wrap(True)
385 _add(h)
386 self.heading = h
388 self.play_now = gtk.Button("_Play now")
389 self.play_now.connect("clicked", self.on_play_now, None)
390 _add(self.play_now)
391 _sep()
393 #auto toggle
394 self.mode_switch = gtk.CheckButton("Play _automatically in %s seconds" % self.timeout)
395 self.mode_switch.connect("toggled", self.on_mode_switch, None)
396 _add(self.mode_switch)
397 _sep()
399 #choose another
400 #XXX file filters
401 chooser_lab = gtk.Label("Ch_oose another combined video (%s screens)" % self.screens)
402 chooser_lab.set_use_underline(True)
403 chooser_lab.set_alignment(0, 0.5)
404 self.chooser = gtk.FileChooserButton(title="video")
405 if self.choose_dir:
406 self.chooser.set_current_folder(self.choose_dir)
407 self.chooser.set_width_chars(40)
408 self.chooser.connect('file-set', self.on_chooser, None)
410 chooser_lab.set_mnemonic_widget(self.chooser)
412 _add_advanced(chooser_lab)
413 _add_advanced(self.chooser)
414 _sep()
416 #create another by stitching subvideos
417 nb = gtk.Label("Construct a _new combined video out of %s video files" % self.screens)
418 nb.set_use_underline(True)
420 nb.set_alignment(0, 0.5)
421 _add_advanced(nb)
423 sound = gtk.Label("Sound:")
424 sound.set_alignment(0.99, 0.5)
425 _add_advanced(sound)
427 #hb = gtk.HBox()
428 self.stitch_silent = gtk.RadioButton(None, "silent")
429 self.stitch_silent.connect("toggled", self.on_stitch_audio_source, None)
430 #hb.pack_start(self.stitch_silent, False)
432 #XXX file filters
433 self.stitch_choosers = []
434 self.stitch_audio_source = None
435 for i in range(self.screens):
436 fc = gtk.FileChooserButton(title="video %s" % i)
437 fcl = gtk.Label("Screen _%s" % (i + 1))
438 fcl.set_use_underline(True)
439 fcl.set_mnemonic_widget(fc)
440 fc_sound = gtk.RadioButton(self.stitch_silent, "audio %s" % (i + 1,))
441 fc_sound.set_active(False)
442 fc_sound.set_tooltip_text("use the sound from video %s" % (i + 1,))
443 fc_sound.connect("toggled", self.on_stitch_audio_source, i)
444 fc_set = gtk.HBox()
445 fc_set.pack_start(fcl, False)
446 fc_set.pack_start(fc)
447 fc_set.pack_start(fc_sound, False)
448 self.stitch_choosers.append(fc)
449 fc.connect('file-set', self.on_stitch_chooser, i)
450 _add_advanced(fc_set)
452 hb = gtk.HBox()
453 #self.stitch_silent = gtk.RadioButton(None, "no sound")
454 hb.pack_end(self.stitch_silent, False)
455 _add_advanced(hb)
458 #save_as box
459 #XXX file filters
460 self.stitch_target_field = gtk.Entry()
461 self.stitch_target_field.set_width_chars(40)
462 self.stitch_target_field.set_text(name_suggester(self.choose_dir, 'new', 'avi'))
464 self.choose_stitch_target = gtk.Button(label="choose")
465 self.choose_stitch_target.connect("clicked", self.on_choose_stitch_target, None)
467 hb = gtk.HBox()
468 name_label = gtk.Label("Save as")
469 hb.pack_start(name_label, False)
470 hb.pack_start(self.stitch_target_field)
471 hb.pack_start(self.choose_stitch_target)
472 nb.set_mnemonic_widget(self.stitch_choosers[0])
473 _add_advanced(hb)
475 #width/height boxes
476 hb = gtk.HBox()
477 label = gtk.Label("Pixel size of each screen: ")
478 hb.pack_start(label, False)
479 for name, default, label in (
480 ('width_field', self.import_width, "_width"),
481 ('height_field', self.import_height, "_height"),
483 e = gtk.Entry()
484 e.set_width_chars(5)
485 e.set_alignment(1)
486 e.set_text(str(default))
487 setattr(self, name, e)
488 lb = gtk.Label(label)
489 lb.set_use_underline(True)
490 lb.set_mnemonic_widget(e)
491 hb.pack_start(lb, False)
492 hb.pack_start(e, False)
493 _add_advanced(hb)
495 #stitch button
496 self.stitch_button = gtk.Button("Assemble the new _video")
497 self.stitch_button.connect("clicked", self.stitch_video, None)
498 _add_advanced(self.stitch_button)
500 self.window.add(self.vbox)
502 def place_window(self):
503 self.window.move(300, 50)
505 def __init__(self):
506 self.read_rc()
507 self.make_window()
508 self.place_window()
509 self.update_heading()
510 self.mode_switch.set_active(self.auto_start and self.video is not None)
511 self.window.connect("destroy", self.destroy)
512 self.window.show_all()
515 def destroy(self, widget, data=None):
516 log("bye")
517 self.write_rc()
518 gtk.main_quit()
520 quit_onclick = destroy
522 if __name__ == '__main__':
523 start = Launcher()
524 gtk.main()