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