#!/usr/bin/env python from functools import partial import optparse import re import subprocess import sys import os import shutil import tempfile class FatalException(Exception): pass def mkarg(arg): if re.match("^[a-zA-Z0-9\-\\.,/@_:=]*$", arg): return arg if "'" not in arg: return "'%s'" % arg out = "\"" for c in arg: if c in "\\$\"`": out += "\\" out += c out += "\"" return out def midentify(source, field): process = subprocess.Popen( [ "mplayer", source, "-ao", "null", "-vo", "null", "-frames", "0", "-identify", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) for line in process.stdout: try: key, value = line.split("=") except ValueError: continue if key == field: return value.strip() def append_cmd(cmd, opt, var): if var is not None: cmd.append(opt) cmd.append(str(var)) def duplicate_opts(opts): return optparse.Values(opts.__dict__) def insert_mplayer_options(cmd, o): if o.mplayer_done: return do_opt = partial(append_cmd, cmd) do_opt("-mc", o.mc) do_opt("-ss", o.startpos) do_opt("-endpos", o.endpos) do_opt("-dvd-device", o.dvd) do_opt("-chapter", o.chapter) do_opt("-aid", o.audioid) do_opt("-sid", o.subtitleid) do_opt("-vf", o.vfilters) do_opt("-af", o.afilters) if o.deinterlace: cmd += ["-vf-pre", "yadif"] if o.detelecine: cmd += ["-vf-pre", "pullup,softskip", "-ofps", "24000/1001"] if o.noskip: cmd += ["-noskip"] if o.skipkb: cmd += ["-sb", str(o.skipkb * 1024)] class Command(object): def __init__(self, profile, opts): self.profile = profile self.opts = opts self.__process = None self.init() def init(self): pass def check_command(self, cmd): if self.opts.dump: return if subprocess.Popen(["which", cmd], stdout=open("/dev/null", "w")).wait() != 0: raise FatalException("Command '%s' is required" % cmd) def check_no_file(self, path): if os.path.exists(path): raise FatalException("Output file '%s' exists." % path) def do_exec(self, args, wait=True): if self.opts.dump: print " ".join(map(mkarg, args)) else: self.__process = subprocess.Popen(args) self.__args = args if wait: self.wait() def wait(self): if self.__process == None: return if self.__process.wait() != 0: raise FatalException("Failure executing command: %s" % self.__args) self.__process = None class MP4Box(Command): def init(self): self.check_command("MP4Box") self.check_no_file(self.opts.output + ".mp4") def run(self): o = self.opts p = self.profile if o.dump: fps = "???" else: fps = midentify(p.video_tmp, "ID_VIDEO_FPS") self.do_exec([ "MP4Box", "-fps", fps, "-add", p.video_tmp, "-add", p.audio_tmp, o.output + ".mp4" ]) class MKVMerge(Command): def init(self): self.check_command("mkvmerge") self.check_no_file(self.opts.output + ".mkv") def run(self): o = self.opts p = self.profile if o.dump: fps = "???" else: fps = midentify(p.video_tmp, "ID_VIDEO_FPS") self.do_exec([ "mkvmerge", "-o", o.output + ".mkv", "--default-duration", "0:%sfps"%fps, p.video_tmp, p.audio_tmp, ]) class MencoderLossless(Command): def init(self): self.check_command("mencoder") self.check_no_file("lossless.avi") ofut = self.opts self.opts = duplicate_opts(ofut) ofut.input = "lossless.avi" ofut.mplayer_done = True def run(self): fifo = False if fifo: os.mkfifo("lossless.avi") o = self.opts cmd = [] cmd += ["mencoder", self.opts.input, "-o", "lossless.avi"] cmd += ["-noconfig", "all"] cmd += ["-oac", "copy", "-ovc", "lavc", "-lavcopts", "vcodec=ffv1:autoaspect"] insert_mplayer_options(cmd, self.opts) cmd += ["-vf-add", "harddup"] self.do_exec(cmd, wait=not fifo) class MPlayer(Command): def init(self): self.check_command("mplayer") self.check_no_file("video.y4m") self.check_no_file("audio.wav") def run(self): os.mkfifo("video.y4m") os.mkfifo("audio.wav") cmd = [] cmd += ["mplayer", self.opts.input] cmd += ["-benchmark", "-noconsolecontrols", "-noconfig", "all"] cmd += ["-vo", "yuv4mpeg:file=video.y4m"] cmd += ["-ao", "pcm:waveheader:file=audio.wav"] insert_mplayer_options(cmd, self.opts) cmd += self.profile.mplayeropts self.do_exec(cmd, wait=False) class MencoderCopyAC3(Command): def init(self): self.check_command("mplayer") self.check_no_file("audio.ac3") self.profile.audio_tmp = "audio.ac3" def run(self): cmd = [] cmd += ["mencoder", self.opts.input] cmd += ["-noconfig", "all"] cmd += ["-ovc", "copy", "-oac", "copy"] cmd += ["-of", "rawaudio", "-o", "audio.ac3"] insert_mplayer_options(cmd, self.opts) self.do_exec(cmd) class X264(Command): def init(self): self.check_command("x264") self.profile.video_tmp = "video.h264" def run(self): p = self.profile cmd = [] cmd += ["x264", "--no-progress"] cmd += p.x264opts cmd += ["-o", p.video_tmp] cmd += ["video.y4m"] self.do_exec(cmd, wait=False) class Lame(Command): def init(self): self.check_command("lame") self.profile.audio_tmp = "audio.mp3" def run(self): p = self.profile cmd = [] cmd += ["lame", "--quiet"] cmd += p.lameopts cmd += ["audio.wav"] cmd += [p.audio_tmp] self.do_exec(cmd, wait=False) class Faac(Command): def init(self): self.check_command("faac") self.profile.audio_tmp = "audio.aac" def run(self): p = self.profile cmd = [] cmd += ["faac"] cmd += ["-o", p.audio_tmp] cmd += p.faacopts cmd += ["audio.wav"] self.do_exec(cmd, wait=False) class Mencoder(Command): codec2opts = { "xvid": "-xvidencopts", "x264": "-x264encopts", "faac": "-faacopts", "mp3lame": "-lameopts", } def init(self): o = self.opts p = self.profile self.check_command("mencoder") self.check_no_file(o.output + ".avi") p.video_tmp = o.output + ".avi" p.audio_tmp = o.output + ".avi" def run(self): o = self.opts p = self.profile cmd = [] cmd += ["mencoder", o.input] cmd += ["-noconfig", "all"] insert_mplayer_options(cmd, o) cmd += ["-vf-add", "harddup"] cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts] cmd += ["-oac", p.acodec] if p.aopts: cmd += [self.codec2opts[p.acodec], p.aopts] cmd += self.profile.mplayeropts cmd += ["-o", self.opts.output + ".avi"] self.do_exec(cmd) class MencoderDemux(Command): codec2exts = { "xvid": "m4v", "x264": "h264", "faac": "aac", "mp3lame": "mp3", "copyac3": "ac3", } def init(self): o = self.opts p = self.profile self.check_command("mencoder") p.audio_tmp = "audio." + self.codec2exts[p.acodec] p.video_tmp = "video." + self.codec2exts[p.vcodec] self.check_no_file(p.audio_tmp) self.check_no_file(p.video_tmp) def run(self): o = self.opts p = self.profile cmd = ["mencoder", "-ovc", "copy", "-oac", "copy", o.output + ".avi"] cmd += ["-noconfig", "all", "-noskip", "-mc", "0"] self.do_exec(cmd + ["-of", "rawaudio", "-o", p.audio_tmp]) self.do_exec(cmd + ["-of", "rawvideo", "-o", p.video_tmp]) self.do_exec(["rm", "-f", o.output + ".avi"]) class Profile(object): def __init__(self, commands, **kwargs): self.commands = commands self.__dict__.update(kwargs) def __contains__(self, keyname): return hasattr(self, keyname) class Wait(object): def __init__(self, commands): self.commands = commands[:] def run(self): for command in self.commands: command.wait() profiles = { "x264/lame" : Profile( commands=[MPlayer, X264, Lame, Wait, MKVMerge], mplayeropts=[], x264opts=["--preset", "veryslow", "--crf", "20"], lameopts=["--preset", "medium"], ), "x264/copyac3" : Profile( commands=[MPlayer, X264, Wait, MencoderCopyAC3, MKVMerge], mplayeropts=["-nosound"], x264opts=["--preset", "veryslow", "--crf", "20"], ), "x264/lame/fast" : Profile( commands=[MPlayer, X264, Lame, Wait, MKVMerge], mplayeropts=[], x264opts=["--preset", "fast", "--crf", "23"], lameopts=["--preset", "medium"], ), "xvid/lame" : Profile( commands=[Mencoder], mplayeropts=["-ffourcc", "DX50"], vcodec="xvid", vopts="fixed_quant=2:vhq=4:autoaspect", acodec="mp3lame", aopts="cbr:br=128", ), "apple-quicktime" : Profile( commands=[MPlayer, X264, Faac, Wait, MP4Box], mplayeropts=[], x264opts=["--crf", "20", "--bframes", "1"], faacopts=["-q", "100", "--mpeg-vers", "4"], ), "nokia-n97" : Profile( commands=[Mencoder, MencoderDemux, MP4Box], mplayeropts=["-vf-add", "scale=640:-10"], vcodec="xvid", vopts="bitrate=384:vhq=4:autoaspect:max_bframes=0", acodec="faac", aopts="br=64:mpeg=4:object=2", ), } mappings = { "x264": "x264/lame", "xvid": "xvid/lame", } for x, y in mappings.iteritems(): profiles[x] = profiles[y] def parse_args(): for profile_name in profiles.keys(): if sys.argv[0].find(profile_name) >= 0: break else: profile_name = "xvid/lame" parser = optparse.OptionParser(usage="%prog [options] input [output]") parser.add_option("--dvd", action="store", dest="dvd") parser.add_option("--fixmux", action="store_true", dest="fixmux") parser.add_option("--deinterlace", action="store_true", dest="deinterlace") parser.add_option("--detelecine", action="store_true", dest="detelecine") parser.add_option("--mc", action="store", dest="mc", type="float") parser.add_option("--noskip", action="store_true", dest="noskip") parser.add_option("--vfilters", action="store", dest="vfilters") parser.add_option("--afilters", action="store", dest="afilters") parser.add_option("--chapter", action="store", dest="chapter") parser.add_option("--skipkb", action="store", dest="skipkb", type="int") parser.add_option("--startpos", action="store", dest="startpos") parser.add_option("--endpos", action="store", dest="endpos") parser.add_option("--audioid", action="store", dest="audioid") parser.add_option("--subtitleid", action="store", dest="subtitleid") parser.add_option("--profile", action="store", dest="profile_name", default=profile_name) parser.add_option("--dump", action="store_true", dest="dump") try: opts, args = parser.parse_args(sys.argv[1:]) if len(args) == 1: input = args[0] output = os.path.splitext(os.path.basename(input))[0] elif len(args) == 2: input, output = args else: raise ValueError except Exception: parser.print_usage() sys.exit(1) if "://" not in input: opts.input = os.path.abspath(input) else: if opts.dvd: opts.dvd = os.path.abspath(opts.dvd) opts.input = input opts.output = os.path.abspath(output) opts.mplayer_done = False return opts def main(): os.nice(1) opts = parse_args() # Find our profile try: profile = profiles[opts.profile_name] except KeyError: print >>sys.stderr, "Profile '%s' not found!" % opts.profile_name sys.exit(1) # Run in a temp dir so that multiple instances can be run simultaneously tempdir = tempfile.mkdtemp() try: os.chdir(tempdir) try: commands = [] if opts.fixmux or opts.detelecine: profile.commands.insert(0, MencoderLossless) for CommandClass in profile.commands: if Command in CommandClass.__bases__: command = CommandClass(profile, opts) else: command = CommandClass(commands) commands.append(command) for command in commands: command.run() except FatalException, e: print >>sys.stderr, "Error:", str(e) sys.exit(1) finally: os.chdir("/") shutil.rmtree(tempdir) if __name__ == "__main__": main()