| #! /ufs/guido/bin/sgi/python |
| |
| # Video bag of tricks: record video(+audio) in various formats and modes |
| |
| # XXX To do: |
| # - audio |
| # - improve user interface |
| # - help button? |
| # - command line options to set initial settings |
| # - save settings in a file |
| # - ...? |
| |
| import sys |
| import time |
| import getopt |
| import string |
| import os |
| sts = os.system('makemap') # Must be before "import fl" to work |
| import sgi |
| import gl |
| import GL |
| import DEVICE |
| import fl |
| import FL |
| import flp |
| import watchcursor |
| import sv |
| import SV |
| import VFile |
| import VGrabber |
| import imageop |
| sys.path.append('/ufs/jack/src/av/vcr') |
| |
| ARROW = 0 |
| WATCH = 1 |
| watchcursor.defwatch(WATCH) |
| |
| def main(): |
| ## fl.set_graphics_mode(0, 1) |
| vb = VideoBagOfTricks().init() |
| while 1: |
| dummy = fl.do_forms() |
| [dummy] |
| |
| StopCapture = 'StopCapture' |
| |
| VideoFormatLabels = ['Video off', 'rgb8', 'grey8', 'grey4', 'grey2', \ |
| 'grey2 dith', 'mono dith', 'mono thresh'] |
| VideoFormats = ['', 'rgb8', 'grey', 'grey4', 'grey2', \ |
| 'grey2', 'mono', 'mono'] |
| |
| VideoModeLabels = ['Continuous', 'Burst', 'Single frame', 'VCR sync'] |
| [VM_CONT, VM_BURST, VM_SINGLE, VM_VCR] = range(1, 5) |
| |
| AudioFormatLabels = ['Audio off', \ |
| '16 bit mono', '16 bit stereo', '8 bit mono', '8 bit stereo'] |
| [A_OFF, A_16_MONO, A_16_STEREO, A_8_MONO, A_8_STEREO] = range(1, 6) |
| |
| class VideoBagOfTricks: |
| |
| # Init/close stuff |
| |
| def init(self): |
| formdef = flp.parse_form('VbForm', 'form') |
| flp.create_full_form(self, formdef) |
| self.g_cont.hide_object() |
| self.g_burst.hide_object() |
| self.g_single.hide_object() |
| self.g_vcr.hide_object() |
| self.setdefaults() |
| self.openvideo() |
| self.makewindow() |
| self.bindvideo() |
| self.showform() |
| fl.set_event_call_back(self.do_event) |
| return self |
| |
| def close(self): |
| self.close_video() |
| self.close_audio() |
| raise SystemExit, 0 |
| |
| def showform(self): |
| # Get position of video window |
| gl.winset(self.window) |
| x, y = gl.getorigin() |
| width, height = gl.getsize() |
| # Calculate position of form window |
| x1 = x + width + 10 |
| x2 = x1 + int(self.form.w) - 1 |
| y2 = y + height - 1 |
| y1 = y2 - int(self.form.h) + 1 |
| # Position and show form window |
| gl.prefposition(x1, x2, y1, y2) |
| self.form.show_form(FL.PLACE_FREE, FL.TRUE, \ |
| 'Video Bag Of Tricks') |
| |
| def setdefaults(self): |
| self.vcr = None |
| self.vout = None |
| self.capturing = 0 |
| # Video defaults |
| self.vfile = 'film.video' |
| self.vmode = VM_CONT |
| self.mono_thresh = 128 |
| self.vformat = 'rgb8' |
| self.c_vformat.clear_choice() |
| for label in VideoFormatLabels: |
| self.c_vformat.addto_choice(label) |
| self.c_vformat.set_choice(1 + VideoFormats.index(self.vformat)) |
| self.c_vmode.clear_choice() |
| for label in VideoModeLabels: |
| self.c_vmode.addto_choice(label) |
| self.c_vmode.set_choice(self.vmode) |
| self.get_vformat() |
| self.b_drop.set_button(1) |
| self.in_rate.set_input('2') |
| self.in_maxmem.set_input('1.0') |
| self.in_nframes.set_input('0') |
| self.in_nframes_vcr.set_input('1') |
| self.in_sleeptime.set_input('1.0') |
| # Audio defaults |
| self.aout = None |
| self.aport = None |
| self.afile = 'film.aiff' |
| self.aformat = A_OFF |
| self.c_aformat.clear_choice() |
| for label in AudioFormatLabels: |
| self.c_aformat.addto_choice(label) |
| self.c_aformat.set_choice(self.aformat) |
| self.get_aformat() |
| |
| def openvideo(self): |
| try: |
| self.video = sv.OpenVideo() |
| except sv.error, msg: |
| print 'Error opening video:', msg |
| self.video = None |
| param = [SV.BROADCAST, SV.PAL] |
| if self.video: self.video.GetParam(param) |
| if param[1] == SV.PAL: |
| x = SV.PAL_XMAX |
| y = SV.PAL_YMAX |
| elif param[1] == SV.NTSC: |
| x = SV.NTSC_XMAX |
| y = SV.NTSC_YMAX |
| else: |
| print 'Unknown video standard:', param[1] |
| sys.exit(1) |
| self.maxx, self.maxy = x, y |
| |
| def makewindow(self): |
| x, y = self.maxx, self.maxy |
| gl.foreground() |
| gl.maxsize(x, y) |
| gl.keepaspect(x, y) |
| gl.stepunit(8, 6) |
| width = 256 # XXX |
| if width: |
| height = width*3/4 |
| x1 = 150 |
| x2 = x1 + width-1 |
| y2 = 768-150 |
| y1 = y2-height+1 |
| gl.prefposition(x1, x2, y1, y2) |
| self.window = gl.winopen('Vb: initializing') |
| self.settitle() |
| if width: |
| gl.maxsize(x, y) |
| gl.keepaspect(x, y) |
| gl.stepunit(8, 6) |
| gl.winconstraints() |
| gl.qdevice(DEVICE.LEFTMOUSE) |
| gl.qdevice(DEVICE.WINQUIT) |
| gl.qdevice(DEVICE.WINSHUT) |
| |
| def bindvideo(self): |
| if not self.video: return |
| x, y = gl.getsize() |
| self.video.SetSize(x, y) |
| drop = self.b_drop.get_button() |
| if drop: |
| param = [SV.FIELDDROP, 1, SV.GENLOCK, SV.GENLOCK_OFF] |
| else: |
| param = [SV.FIELDDROP, 0, SV.GENLOCK, SV.GENLOCK_ON] |
| if self.rgb: |
| param = param+[SV.COLOR, SV.DEFAULT_COLOR, \ |
| SV.DITHER, 1, \ |
| SV.INPUT_BYPASS, 0] |
| else: |
| param = param+[SV.COLOR, SV.MONO, SV.DITHER, 0, \ |
| SV.INPUT_BYPASS, 1] |
| self.video.BindGLWindow(self.window, SV.IN_REPLACE) |
| self.video.SetParam(param) |
| |
| def rebindvideo(self): |
| gl.winset(self.window) |
| self.bindvideo() |
| |
| def reset(self): |
| self.close_video() |
| self.close_audio() |
| |
| # Event handler (catches resize of video window) |
| |
| def do_event(self, dev, val): |
| #print 'Event:', dev, val |
| if dev in (DEVICE.WINSHUT, DEVICE.WINQUIT): |
| self.close() |
| if dev == DEVICE.REDRAW and val == self.window: |
| self.rebindvideo() |
| self.settitle() |
| |
| # Video controls: format, mode, file |
| |
| def cb_vformat(self, *args): |
| self.reset() |
| self.get_vformat() |
| if self.mono_use_thresh: |
| s = `self.mono_thresh` |
| s = fl.show_input('Please enter mono threshold', s) |
| if s: |
| try: |
| self.mono_thresh = string.atoi(s) |
| except string.atoi_error: |
| fl.show_message('Bad input, using', \ |
| `self.mono_thresh`, '') |
| self.rebindvideo() |
| |
| def cb_vmode(self, *args): |
| if self.vcr: |
| self.vcr = None |
| self.vmode = self.c_vmode.get_choice() |
| self.form.freeze_form() |
| self.g_cont.hide_object() |
| self.g_burst.hide_object() |
| self.g_single.hide_object() |
| self.g_vcr.hide_object() |
| if self.vmode == VM_CONT: |
| self.g_cont.show_object() |
| elif self.vmode == VM_BURST: |
| self.g_burst.show_object() |
| elif self.vmode == VM_SINGLE: |
| self.g_single.show_object() |
| elif self.vmode == VM_VCR: |
| self.g_vcr.show_object() |
| self.form.unfreeze_form() |
| |
| def cb_vfile(self, *args): |
| filename = self.vfile |
| hd, tl = os.path.split(filename) |
| filename = fl.file_selector('Video save file:', hd, '', tl) |
| if filename: |
| self.reset() |
| hd, tl = os.path.split(filename) |
| if hd == os.getcwd(): |
| filename = tl |
| self.vfile = filename |
| |
| # Video mode specific video controls |
| |
| def cb_rate(self, *args): |
| pass |
| |
| def cb_drop(self, *args): |
| self.rebindvideo() |
| |
| def cb_maxmem(self, *args): |
| pass |
| |
| def cb_nframes(self, *args): |
| pass |
| |
| def cb_fps(self, *args): |
| pass |
| |
| def cb_nframes_vcr(self, *args): |
| pass |
| |
| def cb_sleeptime(self, *args): |
| pass |
| |
| # Audio controls: format, file |
| |
| def cb_aformat(self, *args): |
| self.get_aformat() |
| |
| def cb_afile(self, *args): |
| filename = self.afile |
| hd, tl = os.path.split(filename) |
| filename = fl.file_selector('Audio save file:', hd, '', tl) |
| if filename: |
| self.reset() |
| hd, tl = os.path.split(filename) |
| if hd == os.getcwd(): |
| filename = tl |
| self.afile = filename |
| |
| # General controls: capture, reset, play, quit |
| |
| def cb_capture(self, *args): |
| if self.capturing: |
| raise StopCapture |
| if not self.b_capture.get_button(): |
| return |
| if not self.video or not self.vformat: |
| gl.ringbell() |
| return |
| if self.vmode == VM_CONT: |
| self.cont_capture() |
| elif self.vmode == VM_BURST: |
| self.burst_capture() |
| elif self.vmode == VM_SINGLE: |
| self.single_capture() |
| elif self.vmode == VM_VCR: |
| self.vcr_capture() |
| |
| def cb_reset(self, *args): |
| self.reset() |
| |
| def cb_play(self, *args): |
| sts = os.system('Vplay -q ' + self.vfile + ' &') |
| |
| def cb_quit(self, *args): |
| self.close() |
| |
| # Capture routines |
| |
| def burst_capture(self): |
| self.setwatch() |
| gl.winset(self.window) |
| x, y = gl.getsize() |
| vformat = SV.RGB8_FRAMES |
| nframes = self.getint(self.in_nframes, 0) |
| if nframes == 0: |
| maxmem = self.getint(self.in_maxmem, 1.0) |
| memsize = int(maxmem * 1024 * 1024) |
| nframes = self.calcnframes() |
| info = (vformat, x, y, nframes, 1) |
| try: |
| info2, data, bitvec = self.video.CaptureBurst(info) |
| except sv.error, msg: |
| self.b_capture.set_button(0) |
| self.setarrow() |
| fl.show_message('Capture error:', str(msg), '') |
| return |
| if info <> info2: print info, '<>', info2 |
| self.save_burst(info2, data, bitvec) |
| self.setarrow() |
| |
| def calcnframes(self): |
| gl.winset(self.window) |
| x, y = gl.getsize() |
| pixels = x*y |
| pixels = pixels/2 # XXX always assume fields |
| if self.mono or self.grey: |
| n = memsize/pixels |
| else: |
| n = memsize/(4*pixels) |
| return max(1, n) |
| |
| def save_burst(self, info, data, bitvec): |
| (vformat, x, y, nframes, rate) = info |
| self.open_if_closed() |
| fieldsize = x*y/2 |
| nskipped = 0 |
| realframeno = 0 |
| tpf = 1000 / 50.0 # XXX |
| for frameno in range(0, nframes*2): |
| if frameno <> 0 and \ |
| bitvec[frameno] == bitvec[frameno-1]: |
| nskipped = nskipped + 1 |
| continue |
| # |
| # Save field. |
| # XXX Works only for fields and top-to-bottom |
| # |
| start = frameno*fieldsize |
| field = data[start:start+fieldsize] |
| realframeno = realframeno + 1 |
| fn = int(realframeno*tpf) |
| if not self.write_frame(fn, field): |
| break |
| |
| def cont_capture(self): |
| saved_label = self.b_capture.label |
| self.b_capture.label = 'Stop\n' + saved_label |
| self.open_if_closed() |
| self.init_cont() |
| fps = 59.64 # Fields per second |
| # XXX (fps of Indigo monitor, not of PAL or NTSC!) |
| tpf = 1000.0 / fps # Time per field in msec |
| self.capturing = 1 |
| self.start_audio() |
| while 1: |
| try: |
| void = fl.check_forms() |
| except StopCapture: |
| break |
| try: |
| cd, id = self.video.GetCaptureData() |
| except sv.error: |
| sgi.nap(1) |
| continue |
| id = id + 2*self.rate |
| data = cd.InterleaveFields(1) |
| cd.UnlockCaptureData() |
| t = id*tpf |
| if not self.write_frame(t, data): |
| break |
| self.stop_audio() |
| self.capturing = 0 |
| self.end_cont() |
| self.reset() |
| self.b_capture.label = saved_label |
| |
| def single_capture(self): |
| self.open_if_closed() |
| self.init_cont() |
| while 1: |
| try: |
| cd, id = self.video.GetCaptureData() |
| break |
| except sv.error: |
| pass |
| sgi.nap(1) |
| data = cd.InterleaveFields(1) |
| cd.UnlockCaptureData() |
| self.end_cont() |
| t = (self.nframes+1) * (1000/25) |
| return self.write_frame(t, data) |
| |
| def vcr_capture(self): |
| if not self.vcr: |
| import VCR |
| try: |
| self.vcr = VCR.VCR().init() |
| self.vcr.wait() |
| self.vcr.fmmode('dnr') |
| except VCR.error, msg: |
| self.vcr = None |
| self.b_capture.set_button(0) |
| fl.show_message('VCR error', str(msg), '') |
| return |
| count = self.getint(self.in_nframes_vcr, 1) |
| if count <= 0: count = 1 |
| sleeptime = self.getfloat(self.in_sleeptime, 1.0) |
| for i in range(count): |
| if i > 0: |
| time.sleep(sleeptime) |
| if not self.single_capture(): |
| break |
| if not self.vcr.step(): |
| break |
| |
| # Init/end continuous capture mode |
| |
| def init_cont(self): |
| qsize = 1 |
| if self.vmode == VM_CONT: |
| self.rate = self.getint(self.in_rate, 2) |
| else: |
| self.rate = 2 |
| x, y = self.vout.getsize() |
| info = (SV.RGB8_FRAMES, x, y, qsize, self.rate) |
| info2 = self.video.InitContinuousCapture(info) |
| if info2 <> info: |
| # XXX This is really only debug info |
| print 'Info mismatch: requested', info, 'got', info2 |
| |
| def end_cont(self): |
| self.video.EndContinuousCapture() |
| |
| # Misc stuff |
| |
| def settitle(self): |
| gl.winset(self.window) |
| x, y = gl.getsize() |
| title = 'Vb:' + self.vfile + ' (%dx%d)' % (x, y) |
| gl.wintitle(title) |
| |
| def get_vformat(self): |
| i = self.c_vformat.get_choice() |
| label = VideoFormatLabels[i-1] |
| format = VideoFormats[i-1] |
| self.vformat = format |
| if self.vformat == '': |
| self.form.freeze_form() |
| self.g_video.hide_object() |
| self.g_cont.hide_object() |
| self.g_burst.hide_object() |
| self.g_single.hide_object() |
| self.form.unfreeze_form() |
| return |
| else: |
| self.g_video.show_object() |
| if self.vmode == VM_CONT: |
| self.g_cont.show_object() |
| elif self.vmode == VM_BURST: |
| self.g_burst.show_object() |
| elif self.vmode == VM_SINGLE: |
| self.g_single.show_object() |
| # |
| self.rgb = (format[:3] == 'rgb') |
| self.mono = (format == 'mono') |
| self.grey = (format[:4] == 'grey') |
| self.mono_use_thresh = (label == 'mono thresh') |
| s = format[4:] |
| if s: |
| self.greybits = string.atoi(s) |
| else: |
| self.greybits = 8 |
| if label == 'grey2 dith': |
| self.greybits = -2 |
| # |
| convertor = None |
| if self.grey: |
| if self.greybits == 2: |
| convertor = imageop.grey2grey2 |
| elif self.greybits == 4: |
| convertor = imageop.grey2grey4 |
| elif self.greybits == -2: |
| convertor = imageop.dither2grey2 |
| self.convertor = convertor |
| |
| def get_aformat(self): |
| self.reset() |
| self.aformat = self.c_aformat.get_choice() |
| if self.aformat == A_OFF: |
| self.g_audio.hide_object() |
| else: |
| self.g_audio.show_object() |
| |
| def open_if_closed(self): |
| if not self.vout: |
| self.open_video() |
| if not self.aout: |
| self.open_audio() |
| |
| # File I/O handling |
| |
| def open_video(self): |
| self.close_video() |
| gl.winset(self.window) |
| x, y = gl.getsize() |
| vout = VFile.VoutFile().init(self.vfile) |
| vout.setformat(self.vformat) |
| vout.setsize(x, y) |
| if self.vmode == VM_BURST: |
| vout.setpf((1, -2)) |
| vout.writeheader() |
| self.vout = vout |
| self.nframes = 0 |
| self.t_nframes.label = `self.nframes` |
| |
| def write_frame(self, t, data): |
| if not self.vout: |
| gl.ringbell() |
| return |
| if self.convertor: |
| data = self.convertor(data, len(data), 1) |
| elif self.mono: |
| if self.mono_use_thresh: |
| data = imageop.grey2mono(data, \ |
| len(data), 1,\ |
| self.mono_thresh) |
| else: |
| data = imageop.dither2mono(data, \ |
| len(data), 1) |
| try: |
| self.vout.writeframe(int(t), data, None) |
| except IOError, msg: |
| if msg == (0, 'Error 0'): |
| msg = 'disk full??' |
| fl.show_message('IOError', str(msg), '') |
| return 0 |
| self.nframes = self.nframes + 1 |
| self.t_nframes.label = `self.nframes` |
| return 1 |
| |
| def close_video(self): |
| if not self.vout: |
| return |
| self.nframes = 0 |
| self.t_nframes.label = '' |
| try: |
| self.vout.close() |
| except IOError, msg: |
| if msg == (0, 'Error 0'): |
| msg = 'disk full??' |
| fl.show_message('IOError', str(msg), '') |
| self.vout = None |
| |
| # Watch cursor handling |
| |
| def setwatch(self): |
| gl.winset(self.form.window) |
| gl.setcursor(WATCH, 0, 0) |
| gl.winset(self.window) |
| gl.setcursor(WATCH, 0, 0) |
| |
| def setarrow(self): |
| gl.winset(self.form.window) |
| gl.setcursor(ARROW, 0, 0) |
| gl.winset(self.window) |
| gl.setcursor(ARROW, 0, 0) |
| |
| # Numeric field handling |
| |
| def getint(self, field, default): |
| try: |
| value = string.atoi(field.get_input()) |
| except string.atoi_error: |
| value = default |
| field.set_input(`value`) |
| return value |
| |
| def getfloat(self, field, default): |
| try: |
| value = float(eval(field.get_input())) |
| except: |
| value = float(default) |
| field.set_input(`value`) |
| return value |
| |
| # Audio stuff |
| |
| def open_audio(self): |
| if self.aformat == A_OFF: |
| return |
| import aifc |
| import al |
| import AL |
| import thread |
| self.close_audio() |
| params = [AL.INPUT_RATE, 0] |
| al.getparams(AL.DEFAULT_DEVICE, params) |
| rate = params[1] |
| self.aout = aifc.open(self.afile, 'w') |
| if self.aformat in (A_16_STEREO, A_8_STEREO): |
| nch = AL.STEREO |
| else: |
| nch = AL.MONO |
| if self.aformat in (A_16_STEREO, A_16_MONO): |
| width = AL.SAMPLE_16 |
| else: |
| width = AL.SAMPLE_8 |
| self.aout.setnchannels(nch) |
| self.aout.setsampwidth(width) |
| self.aout.setframerate(rate) |
| self.aout.writeframes('') |
| c = al.newconfig() |
| c.setqueuesize(8000) |
| c.setchannels(nch) |
| c.setwidth(width) |
| self.aport = al.openport('Vb audio record', 'r', c) |
| self.audio_stop = 0 |
| self.audio_ok = 0 |
| self.audio_busy = 1 |
| thread.start_new_thread(self.record_audio, ()) |
| |
| def start_audio(self): |
| if self.aformat == A_OFF: |
| return |
| self.audio_ok = 1 |
| |
| def record_audio(self, *args): |
| # This function runs in a separate thread |
| # Currently no semaphores are used |
| while not self.audio_stop: |
| data = self.aport.readsamps(4000) |
| if self.audio_ok: |
| self.aout.writeframesraw(data) |
| data = None |
| self.audio_busy = 0 |
| |
| def stop_audio(self): |
| self.audio_ok = 0 |
| |
| def close_audio(self): |
| if self.aout: |
| self.audio_ok = 0 |
| self.audio_stop = 1 |
| while self.audio_busy: |
| time.sleep(0.1) |
| self.aout.close() |
| self.aout = None |
| if self.aport: |
| self.aport.closeport() |
| self.aport = None |
| |
| |
| try: |
| main() |
| except KeyboardInterrupt: |
| print '[Interrupt]' |
| sys.exit(1) |