3 # Video bag of tricks: record video(+audio) in various formats and modes
7 # - improve user interface
9 # - command line options to set initial settings
10 # - save settings in a file
18 sts
= os
.system('makemap') # Must be before "import fl" to work
32 sys
.path
.append('/ufs/jack/src/av/vcr')
41 watchcursor
.defwatch(WATCH
)
44 ## fl.set_graphics_mode(0, 1)
45 vb
= VideoBagOfTricks()
50 StopCapture
= 'StopCapture'
52 VideoFormatLabels
= ['Video off', 'rgb8', 'grey8', 'grey4', 'grey2', \
53 'grey2 dith', 'mono dith', 'mono thresh', 'rgb24', 'rgb24-jpeg', \
55 VideoFormats
= ['', 'rgb8', 'grey', 'grey4', 'grey2', \
56 'grey2', 'mono', 'mono', 'rgb', 'jpeg', 'compress']
58 VideoModeLabels
= ['Continuous', 'Burst', 'Single frame', 'VCR sync']
59 [VM_CONT
, VM_BURST
, VM_SINGLE
, VM_VCR
] = range(1, 5)
61 AudioFormatLabels
= ['Audio off', \
62 '16 bit mono', '16 bit stereo', '8 bit mono', '8 bit stereo']
63 [A_OFF
, A_16_MONO
, A_16_STEREO
, A_8_MONO
, A_8_STEREO
] = range(1, 6)
65 VcrSpeedLabels
= ['normal', '1/3', '1/5', '1/10', '1/30', 'single-step']
66 VcrSpeeds
= [None, 5, 4, 3, 2, 1, 0]
68 RgbSizeLabels
= ['full', 'quarter', 'sixteenth']
71 if os
.environ
.has_key('HOME'):
72 HOME
=os
.environ
['HOME']
75 VB_INIT_FILE
=HOME
+ '/.Vb_init'
77 VB_INIT_KEYS
=['vfile', 'vmode', 'mono_thresh', 'vformat', 'comp_scheme', \
78 'rgb24_size', 'afile', 'aformat']
80 class VideoBagOfTricks
:
86 formdef
= flp
.parse_form('VbForm', 'form')
87 flp
.create_full_form(self
, formdef
)
89 if self
.vmode
<> VM_CONT
:
90 self
.g_cont
.hide_object()
91 if self
.vmode
<> VM_BURST
:
92 self
.g_burst
.hide_object()
93 if self
.vmode
<> VM_SINGLE
:
94 self
.g_single
.hide_object()
95 if self
.vmode
<> VM_VCR
:
96 self
.g_vcr
.hide_object()
97 if self
.vformat
<> 'compress':
98 self
.g_compress
.hide_object()
104 self
.optfullsizewindow()
106 fl
.set_event_call_back(self
.do_event
)
115 # Get position of video window
116 gl
.winset(self
.window
)
117 x
, y
= gl
.getorigin()
118 width
, height
= gl
.getsize()
119 # Calculate position of form window
121 x2
= x1
+ int(self
.form
.w
) - 1
123 y1
= y2
- int(self
.form
.h
) + 1
124 # Position and show form window
125 gl
.prefposition(x1
, x2
, y1
, y2
)
126 self
.form
.show_form(FL
.PLACE_FREE
, FL
.TRUE
, 'Vb Control')
128 def getdefaultdefaults(self
):
130 self
.vfile
= 'film.video'
132 self
.mono_thresh
= 128
133 self
.vformat
= 'rgb8'
134 self
.comp_scheme
= 'Uncompressed'
136 # Missing: drop, rate, maxmem, nframes, rate, vcrspeed
138 self
.afile
= 'film.aiff'
141 def getdefaults(self
):
142 self
.getdefaultdefaults()
143 # XXXX Read defaults file and override.
145 fp
= open(VB_INIT_FILE
, 'r')
147 print 'Vb: no init file'
150 data
= fp
.read(1000000)
152 self
.initcont
= eval(data
)
154 print 'Vb: Ill-formatted init file'
156 for k
in self
.initcont
.keys():
158 setattr(self
, k
, self
.initcont
[k
])
160 def savedefaults(self
):
162 for k
in VB_INIT_KEYS
:
163 newdb
[k
] = getattr(self
, k
)
164 if newdb
<> self
.initcont
:
166 fp
= open(VB_INIT_FILE
, 'w')
168 print 'Vb: Cannot create', VB_INIT_FILE
173 def setdefaults(self
):
178 self
.c_vformat
.clear_choice()
179 for label
in VideoFormatLabels
:
180 self
.c_vformat
.addto_choice(label
)
181 self
.c_vformat
.set_choice(1 + VideoFormats
.index(self
.vformat
))
182 self
.c_vmode
.clear_choice()
183 for label
in VideoModeLabels
:
184 self
.c_vmode
.addto_choice(label
)
185 self
.c_vmode
.set_choice(self
.vmode
)
187 self
.b_drop
.set_button(1)
188 self
.in_rate
.set_input('2')
189 self
.in_maxmem
.set_input('1.0')
190 self
.in_nframes
.set_input('0')
191 self
.in_nframes_vcr
.set_input('1')
192 self
.in_rate_vcr
.set_input('1')
193 self
.c_vcrspeed
.clear_choice()
194 for label
in VcrSpeedLabels
:
195 self
.c_vcrspeed
.addto_choice(label
)
196 self
.c_vcrspeed
.set_choice(4)
197 self
.c_rgb24_size
.clear_choice()
198 for label
in RgbSizeLabels
:
199 self
.c_rgb24_size
.addto_choice(label
)
200 self
.c_rgb24_size
.set_choice(self
.rgb24_size
)
202 algs
= cl
.QueryAlgorithms(cl
.VIDEO
)
203 self
.all_comp_schemes
= []
204 for i
in range(0, len(algs
), 2):
205 if algs
[i
+1] in (cl
.COMPRESSOR
, cl
.CODEC
):
206 self
.all_comp_schemes
.append(algs
[i
])
207 self
.c_cformat
.clear_choice()
208 for label
in self
.all_comp_schemes
:
209 self
.c_cformat
.addto_choice(label
)
210 i
= self
.all_comp_schemes
.index(self
.comp_scheme
)
211 self
.c_cformat
.set_choice(i
+1)
215 self
.c_aformat
.clear_choice()
216 for label
in AudioFormatLabels
:
217 self
.c_aformat
.addto_choice(label
)
218 self
.c_aformat
.set_choice(self
.aformat
)
223 self
.video
= sv
.OpenVideo()
224 except sv
.error
, msg
:
225 print 'Error opening video:', msg
227 param
= [SV
.BROADCAST
, SV
.PAL
]
228 if self
.video
: self
.video
.GetParam(param
)
229 if param
[1] == SV
.PAL
:
232 elif param
[1] == SV
.NTSC
:
236 print 'Unknown video standard:', param
[1]
238 self
.maxx
, self
.maxy
= x
, y
242 def makewindow(self
):
243 x
, y
= self
.maxx
, self
.maxy
251 # Place the window at (150, 150) from top left
252 # (the video board likes this location...)
258 gl
.prefposition(x1
, x2
, y1
, y2
)
259 self
.window
= gl
.winopen('Vb video')
266 gl
.qdevice(DEVICE
.LEFTMOUSE
)
267 gl
.qdevice(DEVICE
.WINQUIT
)
268 gl
.qdevice(DEVICE
.WINSHUT
)
270 def optfullsizewindow(self
):
273 gl
.winset(self
.window
)
275 x
, y
= self
.maxx
, self
.maxy
277 x
, y
= self
.curx
, self
.cury
278 left
, bottom
= gl
.getorigin()
279 width
, height
= gl
.getsize()
280 bottom
= bottom
+height
-y
281 gl
.prefposition(left
, left
+x
-1, bottom
, bottom
+y
-1)
286 gl
.maxsize(self
.maxx
, self
.maxy
)
291 if not self
.video
: return
294 self
.curx
, self
.cury
= x
, y
295 self
.video
.SetSize(x
, y
)
296 drop
= self
.b_drop
.get_button()
298 param
= [SV
.FIELDDROP
, 1, SV
.GENLOCK
, SV
.GENLOCK_OFF
]
300 param
= [SV
.FIELDDROP
, 0, SV
.GENLOCK
, SV
.GENLOCK_ON
]
302 param
= param
+[SV
.COLOR
, SV
.DEFAULT_COLOR
, \
306 param
= param
+[SV
.COLOR
, SV
.MONO
, SV
.DITHER
, 0, \
308 self
.video
.BindGLWindow(self
.window
, SV
.IN_REPLACE
)
309 self
.video
.SetParam(param
)
311 def rebindvideo(self
):
312 gl
.winset(self
.window
)
320 ok
= self
.vcr
.still()
324 self
.b_capture
.set_button(0)
326 # Event handler (catches resize of video window)
328 def do_event(self
, dev
, val
):
329 #print 'Event:', dev, val
330 if dev
in (DEVICE
.WINSHUT
, DEVICE
.WINQUIT
):
332 if dev
== DEVICE
.REDRAW
and val
== self
.window
:
336 # Video controls: format, mode, file
338 def cb_vformat(self
, *args
):
341 if self
.mono_use_thresh
:
342 s
= `self
.mono_thresh`
343 s
= fl
.show_input('Please enter mono threshold', s
)
346 self
.mono_thresh
= string
.atoi(s
)
347 except string
.atoi_error
:
348 fl
.show_message('Bad input, using', \
349 `self
.mono_thresh`
, '')
352 def cb_cformat(self
, *args
):
353 i
= self
.c_cformat
.get_choice()
354 self
.comp_scheme
= self
.all_comp_schemes
[i
-1]
357 def cb_vmode(self
, *args
):
360 self
.vmode
= self
.c_vmode
.get_choice()
361 self
.form
.freeze_form()
362 self
.g_cont
.hide_object()
363 self
.g_burst
.hide_object()
364 self
.g_single
.hide_object()
365 self
.g_vcr
.hide_object()
366 if self
.vmode
== VM_CONT
:
367 self
.g_cont
.show_object()
368 elif self
.vmode
== VM_BURST
:
369 self
.g_burst
.show_object()
370 elif self
.vmode
== VM_SINGLE
:
371 self
.g_single
.show_object()
372 elif self
.vmode
== VM_VCR
:
373 self
.g_vcr
.show_object()
374 self
.form
.unfreeze_form()
376 def cb_vfile(self
, *args
):
377 filename
= self
.vfile
378 hd
, tl
= os
.path
.split(filename
)
379 filename
= fl
.file_selector('Video save file:', hd
, '', tl
)
382 hd
, tl
= os
.path
.split(filename
)
383 if hd
== os
.getcwd():
385 self
.vfile
= filename
387 # Video mode specific video controls
389 def cb_rate(self
, *args
):
392 def cb_drop(self
, *args
):
395 def cb_maxmem(self
, *args
):
398 def cb_nframes(self
, *args
):
401 def cb_fps(self
, *args
):
404 def cb_nframes_vcr(self
, *args
):
407 def cb_rate_vcr(self
, *args
):
410 def cb_vcrspeed(self
, *args
):
413 def cb_rgb24_size(self
, *args
):
414 i
= self
.c_rgb24_size
.get_choice()
418 # Audio controls: format, file
420 def cb_aformat(self
, *args
):
423 def cb_afile(self
, *args
):
424 filename
= self
.afile
425 hd
, tl
= os
.path
.split(filename
)
426 filename
= fl
.file_selector('Audio save file:', hd
, '', tl
)
429 hd
, tl
= os
.path
.split(filename
)
430 if hd
== os
.getcwd():
432 self
.afile
= filename
434 # General controls: capture, reset, play, quit
436 def cb_capture(self
, *args
):
439 if not self
.b_capture
.get_button():
441 if not self
.video
or not self
.vformat
:
444 if self
.vmode
== VM_CONT
:
446 elif self
.vmode
== VM_BURST
:
448 elif self
.vmode
== VM_SINGLE
:
449 self
.single_capture(None, None)
450 elif self
.vmode
== VM_VCR
:
453 def cb_reset(self
, *args
):
456 def cb_play(self
, *args
):
458 sts
= os
.system('Vplay -q ' + self
.vfile
+ ' &')
460 def cb_quit(self
, *args
):
465 def burst_capture(self
):
467 gl
.winset(self
.window
)
470 fl
.show_message('Sorry, no 24 bit continuous capture yet', '', '')
472 vformat
= SV
.RGB8_FRAMES
473 nframes
= self
.getint(self
.in_nframes
, 0)
475 maxmem
= self
.getint(self
.in_maxmem
, 1.0)
476 memsize
= int(maxmem
* 1024 * 1024)
477 nframes
= self
.calcnframes(memsize
)
478 info
= (vformat
, x
, y
, nframes
, 1)
480 info2
, data
, bitvec
= self
.video
.CaptureBurst(info
)
481 except sv
.error
, msg
:
482 self
.b_capture
.set_button(0)
484 fl
.show_message('Capture error:', str(msg
), '')
486 if info
<> info2
: print info
, '<>', info2
487 self
.save_burst(info2
, data
, bitvec
)
490 def calcnframes(self
, memsize
):
491 gl
.winset(self
.window
)
494 pixels
= pixels
/2 # XXX always assume fields
495 if self
.mono
or self
.grey
:
498 n
= memsize
/(4*pixels
)
501 def save_burst(self
, info
, data
, bitvec
):
502 (vformat
, x
, y
, nframes
, rate
) = info
503 self
.open_if_closed()
507 tpf
= 1000 / 50.0 # XXX
508 for frameno
in range(0, nframes
*2):
509 if frameno
<> 0 and \
510 bitvec
[frameno
] == bitvec
[frameno
-1]:
511 nskipped
= nskipped
+ 1
515 # XXX Works only for fields and top-to-bottom
517 start
= frameno
*fieldsize
518 field
= data
[start
:start
+fieldsize
]
519 realframeno
= realframeno
+ 1
520 fn
= int(realframeno
*tpf
)
521 if not self
.write_frame(fn
, field
):
524 def cont_capture(self
):
525 saved_label
= self
.b_capture
.label
526 self
.b_capture
.label
= 'Stop\n' + saved_label
527 self
.open_if_closed()
529 fps
= 59.64 # Fields per second
530 # XXX (fps of Indigo monitor, not of PAL or NTSC!)
531 tpf
= 1000.0 / fps
# Time per field in msec
536 void
= fl
.check_forms()
540 cd
, id = self
.video
.GetCaptureData()
544 id = id + 2*self
.rate
545 data
= cd
.InterleaveFields(1)
546 cd
.UnlockCaptureData()
548 if not self
.write_frame(t
, data
):
554 # If recording audio, can't capture multiple sequences
556 self
.b_capture
.label
= saved_label
558 def single_capture(self
, stepfunc
, timecode
):
559 self
.open_if_closed()
563 cd
, id = self
.video
.GetCaptureData()
568 if stepfunc
: # This might step the video
569 d
=stepfunc() # to the next frame
571 data
= cd
.InterleaveFields(1)
573 x
, y
= self
.vout
.getsize()
574 if self
.use_compress
:
575 if self
.rgb24_size
== 1:
576 data
= cd
.YUVtoYUV422DC(0)
577 elif self
.rgb24_size
== 2:
578 data
= cd
.YUVtoYUV422DC_quarter(1)
581 elif self
.rgb24_size
== 3:
582 data
= cd
.YUVtoYUV422DC_sixteenth(1)
586 data
= cd
.YUVtoRGB(1)
587 if self
.maxx
*self
.maxy
*4 <> len(data
):
588 print 'maxx,maxy,exp,got=', self
.maxx
,
589 print self
.maxy
,self
.maxx
*self
.maxy
*4,
591 fl
.showmessage('Wrong sized data')
593 if self
.rgb24_size
<> 1:
594 data
= imageop
.scale(data
, 4, \
595 self
.maxx
, self
.maxy
, x
, y
)
598 data
= jpeg
.compress(data
, x
, y
, 4)
599 if self
.use_compress
:
600 data
= self
.compressor
.Compress(1, data
)
601 cd
.UnlockCaptureData()
604 timecode
= (self
.nframes
+1) * (1000/25)
605 return self
.write_frame(timecode
, data
)
607 def vcr_capture(self
):
610 print 'Connecting to VCR ...'
612 print 'Waiting for VCR to come online ...'
614 print 'Preparing VCR ...'
615 if not (self
.vcr
.fmmode('dnr') and \
616 self
.vcr
.dmcontrol('digital slow')):
617 self
.vcr_error('digital slow failed')
620 except VCR
.error
, msg
:
624 if not self
.vcr
.still():
625 self
.vcr_error('still failed')
627 self
.open_if_closed()
628 rate
= self
.getint(self
.in_rate_vcr
, 1)
630 vcrspeed
= self
.c_vcrspeed
.get_choice()
631 vcrspeed
= VcrSpeeds
[vcrspeed
]
633 stepfunc
= self
.vcr
.step
636 self
.speed_factor
= rate
637 addr
= start_addr
= self
.vcr
.sense()
638 if not self
.single_capture(None, 0):
640 print 'captured %02d:%02d:%02d:%02d' % self
.vcr
.addr2tc(addr
)
641 count
= self
.getint(self
.in_nframes_vcr
, 1) - 1
644 if not self
.vcr
.step():
645 self
.vcr_error('step failed')
646 here
= self
.vcr
.sense()
648 rate
= rate
- (here
- addr
)
651 if not self
.vcr
.fwdshuttle(vcrspeed
):
652 self
.vcr_error('fwd shuttle failed')
657 here
= self
.vcr
.sense()
658 except VCR
.error
, msg
:
663 print 'Missed', here
-addr
-1,
664 print 'frame' + 's'*(here
-addr
-1 <> 1)
665 cycle
= (cycle
+1) % rate
667 tc
= (here
-start_addr
)*40
668 if not self
.single_capture(stepfunc
, \
671 print 'captured %02d:%02d:%02d:%02d' \
672 % self
.vcr
.addr2tc(here
)
675 if self
.vcr
and not self
.vcr
.still():
676 self
.vcr_error('still failed')
678 def vcr_error(self
, msg
):
680 fl
.show_message('VCR error:', str(msg
), '')
682 # Init/end continuous capture mode
686 if self
.vmode
== VM_CONT
:
687 self
.rate
= self
.getint(self
.in_rate
, 2)
690 x
, y
= self
.vout
.getsize()
692 info
= (SV
.YUV411_FRAMES
, x
, y
, qsize
, self
.rate
)
694 info
= (SV
.RGB8_FRAMES
, x
, y
, qsize
, self
.rate
)
695 info2
= self
.video
.InitContinuousCapture(info
)
697 # XXX This is really only debug info
698 print 'Info mismatch: requested', info
, 'got', info2
701 self
.video
.EndContinuousCapture()
706 gl
.winset(self
.window
)
708 title
= 'Vb ' + self
.vfile
+ ' (%dx%d)' % (x
, y
)
711 def get_vformat(self
):
712 i
= self
.c_vformat
.get_choice()
713 label
= VideoFormatLabels
[i
-1]
714 format
= VideoFormats
[i
-1]
715 if format
== 'compress' and cl
== None:
716 fl
.show_message('Sorry, no compression library support')
719 self
.vformat
= format
720 if self
.vformat
== '':
721 self
.form
.freeze_form()
722 self
.g_video
.hide_object()
723 self
.g_cont
.hide_object()
724 self
.g_burst
.hide_object()
725 self
.g_single
.hide_object()
726 self
.form
.unfreeze_form()
728 self
.g_video
.show_object()
729 if self
.vmode
== VM_CONT
:
730 self
.g_cont
.show_object()
731 elif self
.vmode
== VM_BURST
:
732 self
.g_burst
.show_object()
733 elif self
.vmode
== VM_SINGLE
:
734 self
.g_single
.show_object()
736 self
.rgb
= (format
[:3] == 'rgb' or format
== 'compress')
737 self
.mono
= (format
== 'mono')
738 self
.grey
= (format
[:4] == 'grey')
739 self
.use_24
= (format
in ('rgb', 'jpeg', 'compress'))
741 self
.g_rgb24
.show_object()
743 self
.g_rgb24
.hide_object()
744 self
.use_jpeg
= (format
== 'jpeg')
745 self
.mono_use_thresh
= (label
== 'mono thresh')
746 self
.use_compress
= (format
== 'compress')
747 if self
.use_compress
:
748 self
.g_compress
.show_object()
750 self
.g_compress
.hide_object()
753 self
.greybits
= string
.atoi(s
)
756 if label
== 'grey2 dith':
761 if self
.greybits
== 2:
762 convertor
= imageop
.grey2grey2
763 elif self
.greybits
== 4:
764 convertor
= imageop
.grey2grey4
765 elif self
.greybits
== -2:
766 convertor
= imageop
.dither2grey2
767 self
.convertor
= convertor
768 self
.optfullsizewindow()
770 def get_aformat(self
):
772 self
.aformat
= self
.c_aformat
.get_choice()
773 if self
.aformat
== A_OFF
:
774 self
.g_audio
.hide_object()
776 self
.g_audio
.show_object()
778 def init_compressor(self
, w
, h
):
779 self
.compressor
= None
780 scheme
= cl
.QuerySchemeFromName(cl
.VIDEO
, self
.comp_scheme
)
781 self
.compressor
= cl
.OpenCompressor(scheme
)
782 parambuf
= [cl
.IMAGE_WIDTH
, w
, \
783 cl
.IMAGE_HEIGHT
, h
, \
784 cl
.ORIGINAL_FORMAT
, cl
.YUV422DC
]
785 self
.compressor
.SetParams(parambuf
)
786 return self
.compressor
.Compress(0, '')
788 def open_if_closed(self
):
796 def open_video(self
):
798 gl
.winset(self
.window
)
801 if self
.rgb24_size
== 2:
803 elif self
.rgb24_size
== 3:
805 vout
= VFile
.VoutFile(self
.vfile
)
806 vout
.setformat(self
.vformat
)
807 if self
.vformat
== 'compress':
808 cheader
= self
.init_compressor(x
, y
)
809 vout
.setcompressheader(cheader
)
811 if self
.vmode
== VM_BURST
:
816 self
.speed_factor
= 1
817 self
.t_nframes
.label
= `self
.nframes`
819 def write_frame(self
, t
, data
):
820 t
= t
* self
.speed_factor
825 data
= self
.convertor(data
, len(data
), 1)
827 if self
.mono_use_thresh
:
828 data
= imageop
.grey2mono(data
, \
832 data
= imageop
.dither2mono(data
, \
835 self
.vout
.writeframe(int(t
), data
, None)
838 if msg
== (0, 'Error 0'):
840 fl
.show_message('IOError', str(msg
), '')
842 self
.nframes
= self
.nframes
+ 1
843 self
.t_nframes
.label
= `self
.nframes`
846 def close_video(self
):
850 self
.t_nframes
.label
= ''
854 if msg
== (0, 'Error 0'):
856 fl
.show_message('IOError', str(msg
), '')
858 self
.compressor
= None
860 # Watch cursor handling
863 gl
.winset(self
.form
.window
)
864 gl
.setcursor(WATCH
, 0, 0)
865 gl
.winset(self
.window
)
866 gl
.setcursor(WATCH
, 0, 0)
869 gl
.winset(self
.form
.window
)
870 gl
.setcursor(ARROW
, 0, 0)
871 gl
.winset(self
.window
)
872 gl
.setcursor(ARROW
, 0, 0)
874 # Numeric field handling
876 def getint(self
, field
, default
):
878 value
= string
.atoi(field
.get_input())
879 except string
.atoi_error
:
881 field
.set_input(`value`
)
884 def getfloat(self
, field
, default
):
886 value
= float(eval(field
.get_input()))
888 value
= float(default
)
889 field
.set_input(`value`
)
894 def open_audio(self
):
895 if self
.aformat
== A_OFF
:
902 params
= [AL
.INPUT_RATE
, 0]
903 al
.getparams(AL
.DEFAULT_DEVICE
, params
)
905 self
.aout
= aifc
.open(self
.afile
, 'w')
906 if self
.aformat
in (A_16_STEREO
, A_8_STEREO
):
910 if self
.aformat
in (A_16_STEREO
, A_16_MONO
):
914 self
.aout
.setnchannels(nch
)
915 self
.aout
.setsampwidth(width
)
916 self
.aout
.setframerate(rate
)
921 self
.aport
= al
.openport('Vb audio record', 'r', c
)
925 thread
.start_new_thread(self
.record_audio
, ())
927 def start_audio(self
):
928 if self
.aformat
== A_OFF
:
932 def record_audio(self
, *args
):
933 # This function runs in a separate thread
934 # Currently no semaphores are used
935 while not self
.audio_stop
:
936 data
= self
.aport
.readsamps(4000)
938 self
.aout
.writeframes(data
)
942 def stop_audio(self
):
945 def close_audio(self
):
949 while self
.audio_busy
:
954 self
.aport
.closeport()
960 except KeyboardInterrupt: