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.0
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=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"],#
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 i
!= audio_source
:
136 log("doing sound for %s" % i
)
144 pipeline
.extend(audio_codecs
[audio_codec
])
145 pipeline
.extend(['!', 'mux.', 'demux.'])
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
),
157 log(' '.join(pipeline
).replace('!', ' \\\n!').replace('. ', '.\\\n '))
158 p
= subprocess
.Popen(pipeline
)
164 stitch_tick_id
= None
168 def play(self
, logfile
=None):
169 """Play the currently selected video"""
170 if logfile
is not None:
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
)]
178 cmd
.extend(['-x', str(self
.x_screens
)])
179 if self
.force_multiscreen
:
183 log("Starting play: %r", ' '.join(cmd
))
184 subprocess
.call(cmd
, stdout
=f
, stderr
=subprocess
.STDOUT
)
185 if logfile
is not None:
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")
202 for x
in self
.advanced_widgets
:
203 x
.set_sensitive(not auto
)
205 self
.start_auto_countdown()
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
)
222 if self
.countdown
> 0:
223 if self
.countdown
== 1:
224 self
.mode_switch
.set_label("Play _automatically in one second!")
226 self
.mode_switch
.set_label("Play _automatically in %s seconds" % self
.countdown
)
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
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()
262 self
.progress_bar
.pulse()
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://':
275 self
.chooser
.set_filename(v
)
276 self
.update_choose_dir(dirname(v
))
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
)
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)
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())
305 def on_stitch_audio_source(self
, widget
, n
):
306 self
.stitch_audio_source
= n
309 rc
= SafeConfigParser()
311 def _get(section
, item
, default
=None):
313 return rc
.get(section
, item
)
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')
333 rc
= SafeConfigParser()
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
:
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>' %
353 self
.play_now
.set_sensitive(True)
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
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
)
375 # add a separator with slightly more space than usual
376 _add(gtk
.HSeparator(), True, True, 5)
380 h
.set_line_wrap(True)
384 self
.play_now
= gtk
.Button("_Play now")
385 self
.play_now
.connect("clicked", self
.on_play_now
, None)
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
)
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")
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
)
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)
419 sound
= gtk
.Label("Sound:")
420 sound
.set_alignment(0.99, 0.5)
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)
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
)
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
)
449 #self.stitch_silent = gtk.RadioButton(None, "no sound")
450 hb
.pack_end(self
.stitch_silent
, False)
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)
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])
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"),
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)
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)
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):
516 quit_onclick
= destroy
518 if __name__
== '__main__':