#!/usr/bin/python import os, sys, threading, time # Dimensions for recommended povray rendering recommended_width, recommended_height = 751, 459 povray_aspect_ratio = (1. * recommended_width) / recommended_height def set_resolution(w, h): global mpeg_width, mpeg_height, povray_width, povray_height # mpeg_height and mpeg_width must both be even to make mpeg2encode # happy. The aspect ratio for video should be 4:3. def even(x): return int(x) & -2 mpeg_width = even(w) mpeg_height = even(h) povray_height = mpeg_height povray_width = int(povray_aspect_ratio * povray_height) def set_width(w): set_resolution(w, (3.0 / 4.0) * w) def set_height(h): set_resolution((4.0 / 3.0) * h, h) set_resolution(600, 450) worker_list = [ ('localhost', '/tmp/mpeg'), ('server', '/tmp/mpeg'), ('laptop', '/tmp/mpeg'), ('mac', '/Users/wware/tmp') ] bitrate = 6.0e6 framelimit = None povray_pretty = True border = None #################### # # # DEBUG STUFF # # # #################### DEBUG = False def linenum(*args): try: raise Exception except: tb = sys.exc_info()[2] f = tb.tb_frame.f_back print f.f_code.co_filename, f.f_code.co_name, f.f_lineno, if len(args) > 0: print ' --> ', for x in args: print x, print def do(cmd, howfarback=0): if DEBUG: if False: try: raise Exception except: tb = sys.exc_info()[2] f = tb.tb_frame.f_back for i in range(howfarback): f = f.f_back print f.f_code.co_filename, f.f_code.co_name, f.f_lineno print cmd if os.system(cmd) != 0: raise Exception(cmd) ############################ # # # DISTRIBUTED POVRAY # # # ############################ _which_povray_job = 0 class PovrayJob: def __init__(self, srcdir, dstdir, povfmt, povmin, povmax_plus_one, yuv, pwidth, pheight, ywidth, yheight, textlist): assert povfmt[-4:] == '.pov' assert yuv[-4:] == '.yuv' self.srcdir = srcdir self.dstdir = dstdir self.povfmt = povfmt self.povmin = povmin self.povmax_plus_one = povmax_plus_one self.yuv = yuv self.pwidth = pwidth self.pheight = pheight self.ywidth = ywidth self.yheight = yheight self.textlist = textlist def go(self, machine, workdir): local = machine in ('localhost', '127.0.0.1') def worker_do(cmd): if DEBUG: print '[[%s]]' % machine, if local: # do stuff on this machine do(cmd, howfarback=1) else: # do stuff on a remote machine do('ssh %s "%s"' % (machine, cmd), howfarback=1) if povray_pretty: povray_options = '+A -V -D +X' else: povray_options = '-A +Q0 -V -D +X' worker_do('mkdir -p ' + workdir) # worker_do('find %s -type f -exec rm -f {} \;' % workdir) # # Create a shell script to run on the worker machine # global _which_povray_job self.scriptname = 'povray_job_%08d.sh' % _which_povray_job _which_povray_job += 1 video_aspect_ratio = 4.0 / 3.0 w2 = int(video_aspect_ratio * self.pheight) jpg = (self.povfmt % self.povmin)[:-4] + '.jpg' tgalist = '' povlist = '' scriptlines = [ ] scriptlines.append('cd %s' % workdir) # Worker machine renders a bunch of pov files to tga files for i in range(self.povmin, self.povmax_plus_one): pov = self.povfmt % i povlist += ' ' + pov tga = pov[:-4] + '.tga' tgalist += ' ' + tga scriptlines.append('povray +I%s +O%s +FT %s +W%d +H%d 2>/dev/null' % (pov, tga, povray_options, self.pwidth, self.pheight)) # Worker machine averages the tga files into one jpeg file scriptlines.append('convert -average %s -crop %dx%d+%d+0 -geometry %dx%d! %s' % (tgalist, w2, self.pheight, (self.pwidth - w2) / 2, self.ywidth, self.yheight, jpg)) # Worker cleans up the pov and tga files, no longer needed scriptlines.append('rm -f %s %s' % (povlist, tgalist)) if DEBUG: for line in scriptlines: print machine + '>>> ' + line shellscript = open(os.path.join(self.srcdir, self.scriptname), 'w') for line in scriptlines: shellscript.write(line + '\n') shellscript.close() # # Copy shell script and pov files to worker # if local: cmd = ('(cd %s; tar cf - %s %s) | (cd %s; tar xf -)' % (self.srcdir, self.scriptname, povlist, workdir)) else: cmd = ('(cd %s; tar cf - %s %s) | gzip | ssh %s "(cd %s; gunzip | tar xf -)"' % (self.srcdir, self.scriptname, povlist, machine, workdir)) do(cmd) do('rm -f ' + os.path.join(self.srcdir, self.scriptname)) worker_do('chmod +x ' + os.path.join(workdir, self.scriptname)) # # Run the shell script on the worker # worker_do(os.path.join(workdir, self.scriptname)) # # Retrieve finished image file back from worker # if DEBUG: print '[[%s]]' % machine, if local: do('cp %s %s' % (os.path.join(workdir, jpg), os.path.join(self.dstdir, jpg))) else: do('scp %s:%s %s' % (machine, os.path.join(workdir, jpg), os.path.join(self.dstdir, jpg))) # # Put text on finished image, apply border, and convert to YUV # if self.textlist: cmd = ('convert %s -font times-roman -pointsize 30' % (os.path.join(self.dstdir, jpg))) for i in range(len(self.textlist)): cmd += ' -annotate +10+%d "%s"' % (30 * (i + 1), self.textlist[i]) if border is not None: cmd += ' -bordercolor black -border %dx%d' % border cmd += ' ' + os.path.join(self.dstdir, self.yuv) do(cmd) else: do('convert %s %s' % (os.path.join(self.dstdir, jpg), os.path.join(self.dstdir, self.yuv))) # # Clean up remaining files on the worker machine # worker_do('rm -f %s %s' % (os.path.join(workdir, self.scriptname), os.path.join(workdir, jpg))) all_workers_stop = False class Worker(threading.Thread): def __init__(self, jobqueue, machine, workdir): threading.Thread.__init__(self) self.machine = machine self.jobqueue = jobqueue self.workdir = workdir self.busy = True # # Each worker grabs a new jobs as soon as he finishes the previous # one. This allows mixing of slower and faster worker machines; each # works at capacity. # def run(self): global all_workers_stop while not all_workers_stop: job = self.jobqueue.get() if job is None: # no jobs left in the queue, we're finished self.busy = False return try: job.go(self.machine, self.workdir) except: all_workers_stop = True raise class PovrayJobQueue: def __init__(self): self.worker_pool = [ ] self.jobqueue = [ ] self._lock = threading.Lock() for machine, workdir in worker_list: self.worker_pool.append(Worker(self, machine, workdir)) def append(self, job): self._lock.acquire() # thread safety self.jobqueue.append(job) self._lock.release() def get(self): self._lock.acquire() # thread safety try: r = self.jobqueue.pop(0) except IndexError: r = None self._lock.release() return r def start(self): for worker in self.worker_pool: worker.start() def wait(self): busy_workers = 1 while busy_workers > 0: time.sleep(0.5) busy_workers = 0 for worker in self.worker_pool: if worker.busy: busy_workers += 1 if all_workers_stop: raise Exception #################### # # # MPEG STUFF # # # #################### params = """MPEG-2 Test Sequence, 30 frames/sec %(sourcefileformat)s /* name of source files */ - /* name of reconstructed images ("-": don't store) */ - /* name of intra quant matrix file ("-": default matrix) */ - /* name of non intra quant matrix file ("-": default matrix) */ stat.out /* name of statistics file ("-": stdout ) */ 1 /* input picture file format: 0=*.Y,*.U,*.V, 1=*.yuv, 2=*.ppm */ %(frames)d /* number of frames */ 0 /* number of first frame */ 00:00:00:00 /* timecode of first frame */ 15 /* N (# of frames in GOP) */ 3 /* M (I/P frame distance) */ 0 /* ISO/IEC 11172-2 stream */ 0 /* 0:frame pictures, 1:field pictures */ %(width)d /* horizontal_size */ %(height)d /* vertical_size */ 2 /* aspect_ratio_information 1=square pel, 2=4:3, 3=16:9, 4=2.11:1 */ 5 /* frame_rate_code 1=23.976, 2=24, 3=25, 4=29.97, 5=30 frames/sec. */ %(bitrate)f /* bit_rate (bits/s) */ 112 /* vbv_buffer_size (in multiples of 16 kbit) */ 0 /* low_delay */ 0 /* constrained_parameters_flag */ 4 /* Profile ID: Simple = 5, Main = 4, SNR = 3, Spatial = 2, High = 1 */ 8 /* Level ID: Low = 10, Main = 8, High 1440 = 6, High = 4 */ 0 /* progressive_sequence */ 1 /* chroma_format: 1=4:2:0, 2=4:2:2, 3=4:4:4 */ 2 /* video_format: 0=comp., 1=PAL, 2=NTSC, 3=SECAM, 4=MAC, 5=unspec. */ 5 /* color_primaries */ 5 /* transfer_characteristics */ 4 /* matrix_coefficients */ %(width)d /* display_horizontal_size */ %(height)d /* display_vertical_size */ 0 /* intra_dc_precision (0: 8 bit, 1: 9 bit, 2: 10 bit, 3: 11 bit */ 1 /* top_field_first */ 0 0 0 /* frame_pred_frame_dct (I P B) */ 0 0 0 /* concealment_motion_vectors (I P B) */ 1 1 1 /* q_scale_type (I P B) */ 1 0 0 /* intra_vlc_format (I P B)*/ 0 0 0 /* alternate_scan (I P B) */ 0 /* repeat_first_field */ 0 /* progressive_frame */ 0 /* P distance between complete intra slice refresh */ 0 /* rate control: r (reaction parameter) */ 0 /* rate control: avg_act (initial average activity) */ 0 /* rate control: Xi (initial I frame global complexity measure) */ 0 /* rate control: Xp (initial P frame global complexity measure) */ 0 /* rate control: Xb (initial B frame global complexity measure) */ 0 /* rate control: d0i (initial I frame virtual buffer fullness) */ 0 /* rate control: d0p (initial P frame virtual buffer fullness) */ 0 /* rate control: d0b (initial B frame virtual buffer fullness) */ 2 2 11 11 /* P: forw_hor_f_code forw_vert_f_code search_width/height */ 1 1 3 3 /* B1: forw_hor_f_code forw_vert_f_code search_width/height */ 1 1 7 7 /* B1: back_hor_f_code back_vert_f_code search_width/height */ 1 1 7 7 /* B2: forw_hor_f_code forw_vert_f_code search_width/height */ 1 1 3 3 /* B2: back_hor_f_code back_vert_f_code search_width/height */ """ def textlist(i): return [ ] # Where will I keep all my temporary files? On Mandriva, /tmp is small # but $HOME/tmp is large. mpeg_dir = '/home/wware/tmp/mpeg' def remove_old_yuvs(): # you don't always want to do this do("rm -rf " + mpeg_dir + "/yuvs") do("mkdir -p " + mpeg_dir + "/yuvs") class MpegSequence: def __init__(self): self.frame = 0 self.width = mpeg_width self.height = mpeg_height self.size = (self.width, self.height) def __len__(self): return self.frame def yuv_format(self): # Leave off the ".yuv" so we can use it for the # mpeg2encode parameter file. return mpeg_dir + '/yuvs/foo.%06d' def yuv_name(self, i=None): if i is None: i = self.frame return (self.yuv_format() % i) + '.yuv' # By default, each title page stays up for five seconds def titleSequence(self, titlefile, frames=150): assert os.path.exists(titlefile) if framelimit is not None: frames = min(frames, framelimit) first_yuv = self.yuv_name() if border is not None: w, h = self.width - 2 * border[0], self.height - 2 * border[1] borderoption = ' -bordercolor black -border %dx%d' % border else: w, h = self.width, self.height borderoption = '' do('convert %s -geometry %dx%d! %s %s' % (titlefile, w, h, borderoption, first_yuv)) self.frame += 1 for i in range(1, frames): import shutil shutil.copy(first_yuv, self.yuv_name()) self.frame += 1 def previouslyComputed(self, fmt, frames, begin=0): assert os.path.exists(titlefile) if framelimit is not None: frames = min(frames, framelimit) for i in range(frames): import shutil src = fmt % (i + begin) shutil.copy(src, self.yuv_name()) self.frame += 1 def motionBlurSequence(self, povfmt, frames, ratio, avg, begin=0): # avg is how many subframes are averaged to produce each frame # ratio is the ratio of subframes to frames if framelimit is not None: frames = min(frames, framelimit) pq = PovrayJobQueue() yuvs = [ ] srcdir, povfmt = os.path.split(povfmt) for i in range(frames): yuv = self.yuv_name() yuvs.append(yuv) dstdir, yuv = os.path.split(yuv) ywidth, yheight = mpeg_width, mpeg_height if border is not None: ywidth -= 2 * border[0] yheight -= 2 * border[1] job = PovrayJob(srcdir, dstdir, povfmt, begin + i * ratio, begin + i * ratio + avg, yuv, povray_width, povray_height, ywidth, yheight, textlist(i)) pq.append(job) self.frame += 1 pq.start() pq.wait() def encode(self): parfil = mpeg_dir + "/foo.par" outf = open(parfil, "w") outf.write(params % {'sourcefileformat': self.yuv_format(), 'frames': len(self), 'height': self.height, 'width': self.width, 'bitrate': bitrate}) outf.close() # encoding is an inexpensive operation, do it even if not for real do('mpeg2encode %s/foo.par %s/foo.mpeg' % (mpeg_dir, mpeg_dir)) do('rm -f %s/foo.mp4' % mpeg_dir) do('ffmpeg -i %s/foo.mpeg -sameq %s/foo.mp4' % (mpeg_dir, mpeg_dir)) """ Here is an example usage of this stuff. import os, sys, animate, string animate.worker_list = [ ('localhost', '/tmp/mpeg'), ('server', '/tmp/mpeg'), ('laptop', '/tmp/mpeg'), ('mac', '/Users/wware/tmp') ] for arg in sys.argv[1:]: if arg == 'debug': animate.DEBUG = True elif arg == 'ugly': animate.povray_pretty = False elif arg.startswith('framelimit='): animate.framelimit = string.atoi(arg[11:]) h = 438 w = 584 animate.set_resolution(w, h) animate.border = (w/10, h/10) ################################# N = 1 # nominally 1, test with 4, 5, or 9 animate.remove_old_yuvs() m = animate.MpegSequence() m.titleSequence('title1.gif', 150 / N) # Each frame is 5 femtoseconds, each subframe is 0.5 fs def textlist(i): nsecs = i * 5.0e-6 return [ '%.4f nanoseconds' % nsecs, '%.4f rotations' % (nsecs / 0.2), ] animate.textlist = textlist m.titleSequence('title2.gif', 150 / N) m.motionBlurSequence(os.path.join(animate.mpeg_dir, 'fastpov/fast.%06d.pov'), 450 / N, 10 * N, 10 / N) # Each frame is 20 femtoseconds, each subframe is 2 fs def textlist(i): nsecs = i * 20.0e-6 return [ '%.3f nanoseconds' % nsecs, '%.3f rotations' % (nsecs / 0.2), ] animate.textlist = textlist m.titleSequence('title3.gif', 150 / N) m.motionBlurSequence(os.path.join(animate.mpeg_dir, 'medpov/med.%06d.pov'), 450 / N, 10 * N, 10 / N) # Each frame is 200 femtoseconds, each subframe is 20 fs def textlist(i): nsecs = i * 200.0e-6 return [ '%.2f nanoseconds' % nsecs, '%.2f rotations' % (nsecs / 0.2), ] animate.textlist = textlist m.titleSequence('title4.gif', 150 / N) m.motionBlurSequence(os.path.join(animate.mpeg_dir, 'slowpov/slow.%06d.pov'), 450 / N, 10 * N, 10 / N) m.encode() """