#!/usr/local/bin/python #============================================================================== # # $Id$ # """ Script to make a video CD from a set of arbitrary video files. Usage: mkvcd ... Change the parameters listed below to identify the mencoder, vcdimager and cdrdao programs on your system and other global parameters. You will need to have permission to run cdrdao on your CDR drive to be able to use this. """ # # Copyright (C) 2006 Michael A. Muller # # Permission is granted to use, modify and redistribute this code, # providing that the following conditions are met: # # 1) This copyright/licensing notice must remain intact. # 2) If the code is modified and redistributed, the modifications must # include documentation indicating that the code has been modified. # 3) The author(s) of this code must be indemnified and held harmless # against any damage resulting from the use of this code. # # This code comes with ABSOLUTELY NO WARRANTEE, not even the implied # warrantee of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # #============================================================================== # -- global processing parameters - adjust these for your system configuration # set this to the location of your mencoder program mencoder = '/usr/local/bin/mencoder' # set this to the location of cdrdao cdrdao = '/usr/local/bin/cdrdao' # set this to the location of vcdimager vcdimager = '/usr/bin/vcdimager' # set this to the bus,channel,lun of your CD burner scsiDev = '0,0,0' # set this to the desired burn speed - higher is faster, if it causes problems # use lower speed = '4' # -- end of parameters import sys, re, os import select class subprocess: PIPE = -1 class _Stream: def __init__(self, handle): self.__fd = handle def read(self, size): return os.read(self.__fd, size) def write(self, data): os.write(self.__fd, data) def fileno(self): return self.__fd def close(self): os.close(self.__fd) class ForkExecer: """ Standard implementation of fork\/exec. Creates a new process on construction with wrapped pipes for communication to stdin, stdout, and stderr. """ def __init__(self, cmd, stdin = None, stdout = None, stderr = None): # create all of the pipes if stdout == subprocess.PIPE: fromChild, toParent = os.pipe() if stderr == subprocess.PIPE: fromChildErr, toParentErr = os.pipe() if stdin == subprocess.PIPE: fromParent, toChild = os.pipe() self.__pid = os.fork() if not self.__pid: # -- child process -- # close parent-side streams and dup handles to child's standard in, # out and error if stdout == subprocess.PIPE: os.close(fromChild) os.dup2(toParent, 1) if stderr == subprocess.PIPE: os.close(fromChildErr) os.dup2(toParentErr, 2) if stdin == subprocess.PIPE: os.close(toChild) os.dup2(fromParent, 0) # ... and finally, exec the process os.execv(cmd[0], cmd) else: # close out the child-side pipes and instantiate wrappers around the # streams if stdin == subprocess.PIPE: os.close(fromParent) self.stdin = _Stream(toChild) if stderr == subprocess.PIPE: os.close(toParentErr) self.stderr = _Stream(fromChildErr) if stdout == subprocess.PIPE: os.close(toParent) self.stdout = _Stream(fromChild) def wait(self): """ Wait for child process to terminate and returns its exit code. """ return os.waitpid(self.__pid, 0)[1] >> 8 def _createProcessStarter(): return ForkExecer class ChildProcess: """ Creates a child process and handles output to it and input from it. """ def __init__(self, cmd, start = False, processStarter = ForkExecer, noPipes = False, noStdIn = False, noStdOut = False, noStdErr = False ): """ parms: cmd:: [list] a list of the command arguments. The first item in the list is the fully qualified path to the program to execute. start:: [boolean] if true, start the process immediately. If false, the process must be explicitly started using the @start() method. processStarter:: [callable>] A function which accepts a list of arguments (including an executable as the zeroth argument) and returns a process object. The process object must have a wait() method (which waits for process termination) and stdin, stdout and stderr attributes, which are file objects implementing fileno() and read(). noPipes:: [boolean] disables all pipes (stdin, stdout & stderr) to and from the child. Equivalent to setting all of /noStdIn/, /noStdOut/, and /noStdErr/ to True. noStdIn:: [boolean] disables standard input to the child noStdOut:: [boolean] disables collection of standard output from the child. child standard output will go to the parent process standard output. @gotOutput() will never be called. noStdErr:: [boolean] disables collection of standard error from the child. child standard error will go to the parent process standard error. @gotError() will never be called. """ self.__cmd = cmd self.__proc = None self.__procFact = processStarter if noPipes: noStdIn = noStdOut = noStdErr = True self.__noStdIn = noStdIn self.__noStdOut = noStdOut self.__noStdErr = noStdErr if start: self.start() def start(self): """ Starts the child process. Will raise an AssertionError if the process is already started. """ assert not self.__proc self.__proc = \ self.__procFact(self.__cmd, stdin = not self.__noStdIn and subprocess.PIPE or None, stdout = not self.__noStdOut and subprocess.PIPE or None, stderr = not self.__noStdErr and subprocess.PIPE or None, ) def run(self): """ Runs the process and handles all input and output until termination. The process will be started if it has not already been. Returns the process' exit code. """ if not self.__proc: self.start() # readable streams for our select loop readStreams = [] # collect the child pipes if not self.__noStdIn: stdin = self.__proc.stdin if not self.__noStdOut: stdout = self.__proc.stdout readStreams.append(stdout) else: stdout = None if not self.__noStdErr: stderr = self.__proc.stderr readStreams.append(stderr) else: stderr = None # close standard input XXX should be able to feed data to the child # process, fix if not self.__noStdIn: stdin.close() # process iteraction loop - handle all input & output while readStreams: rdx, wrx, erx = \ select.select(readStreams, [], readStreams) # XXX I think solaris routinely generates an error on termination, # might want to just terminate clean... if erx: raise IOError('error on child process stream') # process all output streams for src, func in ((stdout, self.gotOutput), (stderr, self.gotError)): if src in rdx: if not self.__handleRead(src, func): # handleRead returns false when the stream is terminated. # Remove it from the readStreams readStreams.remove(src) # nothing left to read from ... wait for child process completion return self.__proc.wait() def __handleRead(self, src, func): # get data from the stream, if we got nothing, the stream is terminated data = src.read(4096) if not data: return False # pass it to the handler function func(data) return True def gotOutput(self, data): """ Derived classes should override this method to capture data received from the child's standard output stream. Base class version just writes to our standard output. """ sys.stdout.write(data) def gotError(self, data): """ Derived classes should override this method to capture data received from the child's standard error stream. Base class version just writes to our standard error. """ sys.stderr.write(data) class LineChildProcess(ChildProcess): """ Processor for child processes to allow you to handle complete lines of output from stdout and stderr. gotOutput() and gotError() are overriden to collect input into buffers and pass individual lines to gotOutLine() and gotErrLine(). Note that this is only suitable for processes whose output and error streams produce output as finite lines of text terminated by the newline character. """ def __init__(self, cmd, **kwargs): ChildProcess.__init__(self, cmd, **kwargs) self.__lastOutLine = '' self.__lastErrLine = '' def gotOutput(self, data): """ Overrides @ChildProcess.gotOutput() to split output into single line chunks and feed them to @gotOutLine(). """ # split the data up into lines, process all but the last (the last can # not be assumed to be a complete line) lines = (self.__lastOutLine + data).split('\n') for line in lines[:-1]: self.gotOutLine(line) self.__lastOutLine = lines[-1] def gotOutLine(self, line): """ Derived classes should implement this to capture single lines of output. Base class version does nothing. """ pass def gotError(self, data): """ Overrides @ChildProcess.gotError() to split output into single line chunks and feed them to @gotOutLine(). """ # split the data up into lines, process all but the last (the last can # not be assumed to be a complete line) lines = (self.__lastErrLine + data).split('\n') for line in lines[:-1]: self.gotErrLine(line) self.__lastErrLine = lines[-1] def getErrLine(self, data): """ Derived classes should implement this to capture single lines of output. Base class version does nothing. """ pass class _ParmExtractor(LineChildProcess): """ Used to extract video stream parameters from mencoder. """ # Rest of the example is ' 24bpp 24.000 fps 567.7 kbps (69.3 kbyte/s)' __vidRx = re.compile(r'VIDEO: (\S*) (\d+)x(\d+)') def __init__(self, filename): LineChildProcess.__init__(self, [mencoder, filename], noStdErr = True) self.width = self.height = None def gotOutLine(self, line): m = self.__vidRx.match(line) if m: self.width = int(m.group(2)) self.height = int(m.group(3)) def getSize(file): """ Returns a tuple of the width and height of the resulting video in pixels. """ extractor = _ParmExtractor(file) extractor.run() if extractor.width is None: raise Exception('can not determine size') return extractor.width, extractor.height def makeVCDImage(args, temporaryFiles): # the list of VCD encoded mpegs that have been generated vcdMpegs = [] # reencode all of the files as VCD mpegs for arg in args: width, height = size = getSize(arg) print arg, size # if the aspect ratio is not 1.45-1.47, we'll want to add some borders aspect = float(width) / height if not 1.45 <= aspect < 1.47: # pad horizontally or vertically depending on the aspect ratio if aspect > 1.47: # image is too wide - pad top and bottom scale = 352.0 / width actualHeight = int(scale * height) borderHeight = (240 - actualHeight) / 2 sizingParms = \ 'scale=352:%d,expand=0:240:0:%d' % (actualHeight, borderHeight) else: # image is too tall - pad left and right scale = 240.0 / height actualWidth = int(scale * width) borderWidth = (352 - actualWidth) / 2 sizingParms = \ 'scale=%d:240,expand=352:0:%d:0' % (actualWidth, borderWidth) else: sizingParms = 'scale=352:240' # re-encode the image as a VCD base, ext = os.path.splitext(arg) vcdFile = base + '.vcd.mpg' rc = ChildProcess([ mencoder, '-ovc', 'lavc', '-oac', 'lavc', '-of', 'mpeg', '-mpegopts', 'format=xvcd', '-vf', sizingParms + ',harddup', '-lavcopts', 'aspect=4/3:acodec=mp2:abitrate=224:vcodec=mpeg1video:keyint=15:' 'vrc_buf_size=327:vrc_minrate=1152:vrc_maxrate=1152:vbitrate=1152', '-ofps', '30000/1001', '-o', vcdFile, arg ], noPipes = True).run() if rc: raise Exception('failed to encode %s' % arg) # add the VCD file to the the list vcdMpegs.append(vcdFile) temporaryFiles.append(vcdFile) # create the VCD image rc = ChildProcess([vcdimager] + vcdMpegs, noPipes = True).run() if rc: raise Exception('failed to create videocd image') temporaryFiles.append('videocd.bin') temporaryFiles.append('videocd.cue') # run cdrdao ChildProcess([ cdrdao, 'write', '--device', scsiDev, '--speed', speed, 'videocd.cue' ], noPipes = True).run() def main(args): tempFiles = [] try: makeVCDImage(args, tempFiles) finally: # clean up all temporary files for file in tempFiles: os.remove(file) main(sys.argv[1:])