8 from ConfigParser
import SafeConfigParser
, Error
as CPError
10 #defaults, most can be over-ridden in opo.rc
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
30 class OpoError(Exception):
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
):
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):
51 from urlparse
import urlsplit
53 for i
, uri
in enumerate(input_files
):
54 if not uri
.startswith('file://'):
56 uri
= 'file://' + urllib
.quote(os
.path
.abspath(uri
))
58 fn
= '/' + urllib
.unquote(urlsplit(uri
).path
)#.decode('utf-8')
59 #trigger exception if the filenamevdoesn't exist (so no http uris)
62 except AttributeError, e
: #"None has no attribute startswith"
64 raise OpoError("Not all input files are specified")
70 muxer
= output_file
.rsplit('.', 1)[1]
72 log('defaulting to avi muxer')
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"],#
87 'bitrate': int(BYTES_PER_PIXEL_PER_SECOND
* width
* height
* len(input_files
)),
90 'vorbis': ['vorbisenc', 'bitrate=192', 'cbr=true', 'target=bitrate',],
91 'mp3': ['lamemp3enc', 'bitrate=192', 'cbr=true', 'target=bitrate',],
92 'mp2': ['twolame', 'bitrate=320'],
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
]]
105 progress_pipe
= ['progressreport', '!']
109 pipeline
= (['gst-launch-0.10',
113 '!', 'ffmpegcolorspace',
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
130 if fn
.endswith('Julia%20resize%204%2028th.mov'):
133 if i
!= audio_source
:
139 log("doing sound for %s" % i
)
147 pipeline
.extend(audio_codecs
[audio_codec
])
148 pipeline
.extend(['!', 'mux.', 'demux.'])
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
),
160 log(' '.join(pipeline
).replace('!', ' \\\n!').replace('. ', '.\\\n '))
161 p
= subprocess
.Popen(pipeline
)
167 stitch_tick_id
= 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:
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
)]
182 cmd
.extend(['-x', str(self
.x_screens
)])
183 if self
.force_multiscreen
:
187 log("Starting play: %r" % ' '.join(cmd
))
188 subprocess
.call(cmd
, stdout
=f
, stderr
=subprocess
.STDOUT
)
189 if logfile
is not None:
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")
205 for x
in self
.advanced_widgets
:
206 x
.set_sensitive(not auto
)
208 self
.start_auto_countdown()
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
)
225 if self
.countdown
> 0:
226 if self
.countdown
== 1:
227 self
.mode_switch
.set_label("Play _automatically in one second!")
229 self
.mode_switch
.set_label("Play _automatically in %s seconds" % self
.countdown
)
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
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()
265 self
.progress_bar
.pulse()
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://':
278 self
.chooser
.set_filename(v
)
279 self
.update_choose_dir(dirname(v
))
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
)
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)
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())
308 def on_stitch_audio_source(self
, widget
, n
):
309 self
.stitch_audio_source
= n
312 rc
= SafeConfigParser()
314 def _get(section
, item
, default
=None):
316 return rc
.get(section
, item
)
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')
336 rc
= SafeConfigParser()
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
:
351 def update_heading(self
):
353 video_name
= self
.video
.rsplit('/', 1)[1]
354 self
.heading
.set_markup('<big><b>%s</b> is ready to play</big>' %
356 self
.play_now
.set_sensitive(True)
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
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
)
379 # add a separator with slightly more space than usual
380 _add(gtk
.HSeparator(), True, True, 5)
384 h
.set_line_wrap(True)
388 self
.play_now
= gtk
.Button("_Play now")
389 self
.play_now
.connect("clicked", self
.on_play_now
, None)
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
)
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")
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
)
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)
423 sound
= gtk
.Label("Sound:")
424 sound
.set_alignment(0.99, 0.5)
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)
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
)
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
)
453 #self.stitch_silent = gtk.RadioButton(None, "no sound")
454 hb
.pack_end(self
.stitch_silent
, False)
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)
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])
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"),
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)
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)
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):
520 quit_onclick
= destroy
522 if __name__
== '__main__':