]> code.delx.au - transcoding/blob - encode.py
Better quoting for --dump
[transcoding] / encode.py
1 #!/usr/bin/env python
2
3 import optparse
4 import re
5 import subprocess
6 import sys
7 import os
8 import shutil
9 import tempfile
10
11 class FatalException(Exception):
12 pass
13
14 def mkarg(arg):
15 if re.match("^[a-zA-Z0-9\-\\.,/@_:=]*$", arg):
16 return arg
17
18 if "'" not in arg:
19 return "'%s'" % arg
20 out = "\""
21 for c in arg:
22 if c in "\\$\"`":
23 out += "\\"
24 out += c
25 out += "\""
26 return out
27
28 class Command(object):
29 def __init__(self, profile, opts):
30 self.profile = profile
31 self.opts = opts
32
33 def print_install_message(self):
34 print >>sys.stderr, "Problem with command: %s", self.name
35 if self.package:
36 print >>sys.stderr, "Try running:\n# aptitude install %s", self.package
37
38 def check_command(self, cmd):
39 if self.opts.dump:
40 return
41 if subprocess.Popen(["which", cmd], stdout=open("/dev/null", "w")).wait() != 0:
42 raise FatalException("Command '%s' is required" % cmd)
43
44 def check_no_file(self, path):
45 if os.path.exists(path):
46 raise FatalException("Output file '%s' exists." % path)
47
48 def do_exec(self, args):
49 if self.opts.dump:
50 print " ".join(map(mkarg, args))
51 else:
52 if subprocess.Popen(args).wait() != 0:
53 raise FatalException("Failure executing command: %s" % args)
54
55
56 class MP4Box(Command):
57 codec2exts = {
58 "xvid": "m4v",
59 "x264": "h264",
60 "faac": "aac",
61 }
62
63 def check(self):
64 self.check_command("mencoder")
65 self.check_command("MP4Box")
66 self.check_no_file(self.opts.output + ".mp4")
67
68 def run(self):
69 p = self.profile
70 video = "video.%s" % self.codec2exts[p.vcodec]
71 audio = "audio.%s" % self.codec2exts[p.acodec]
72 input = self.opts.output + ".avi" # From Mencoder command
73 output = self.opts.output + ".mp4"
74 mencoder = ["mencoder", input, "-ovc", "copy", "-oac", "copy", "-of"]
75 self.do_exec(["rm", "-f", output])
76 self.do_exec(mencoder + ["rawvideo", "-o", video])
77 self.do_exec(mencoder + ["rawaudio", "-o", audio])
78 self.do_exec(["MP4Box", "-add", video, "-add", audio, output])
79 self.do_exec(["rm", "-f", video, audio, input])
80
81
82
83 class MKVMerge(Command):
84 def check(self):
85 self.check_command("mkvmerge")
86 self.check_no_file(self.opts.output + ".mkv")
87
88 def run(self):
89 input = self.opts.output + ".avi" # From Mencoder command
90 output = self.opts.output + ".mkv"
91 self.do_exec(["mkvmerge", "-o", output, input])
92 self.do_exec(["rm", "-f", input])
93
94
95
96 class Mencoder(Command):
97 codec2opts = {
98 "lavc": "-lavcopts",
99 "xvid": "-xvidencopts",
100 "x264": "-x264encopts",
101 "faac": "-faacopts",
102 "mp3lame": "-lameopts",
103 }
104
105 def insert_options(self, cmd):
106 def try_opt(opt, var):
107 if var is not None:
108 cmd.append(opt)
109 cmd.append(var)
110 if self.opts.deinterlace:
111 cmd += ["-vf-add", "pp=lb"]
112 try_opt("-ss", self.opts.startpos)
113 try_opt("-endpos", self.opts.endpos)
114 try_opt("-dvd-device", self.opts.dvd)
115 try_opt("-chapter", self.opts.chapter)
116 try_opt("-aid", self.opts.audioid)
117 try_opt("-sid", self.opts.subtitleid)
118 try_opt("-vf-add", self.opts.vfilters)
119 try_opt("-af", self.opts.afilters)
120
121 def subst_values(self, cmd, vpass):
122 subst = {
123 "vbitrate": self.opts.vbitrate,
124 "abitrate": self.opts.abitrate,
125 "input": self.opts.input,
126 "output": self.opts.output + ".avi",
127 "vpass": vpass,
128 }
129
130 return [x % subst for x in cmd]
131
132 def pass1(self):
133 p = self.profile
134 cmd = []
135 cmd += ["mencoder", "%(input)s", "-o", "/dev/null"]
136 self.insert_options(cmd)
137 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
138 cmd += ["-oac", "copy"]
139 cmd += self.profile.extra + self.profile.extra1
140 cmd = self.subst_values(cmd, vpass=1)
141 return cmd
142
143 def pass2(self):
144 p = self.profile
145 cmd = []
146 cmd += ["mencoder", "%(input)s", "-o", "%(output)s"]
147 self.insert_options(cmd)
148 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
149 cmd += ["-oac", p.acodec, self.codec2opts[p.acodec], p.aopts]
150 if self.opts.episode_name:
151 cmd += ["-info", "name='%s'" % self.opts.episode_name]
152 cmd += self.profile.extra + self.profile.extra2
153 cmd = self.subst_values(cmd, vpass=2)
154 return cmd
155
156 def check(self):
157 self.check_command("mencoder")
158 self.check_no_file(self.opts.output + ".avi")
159
160 def run(self):
161 self.do_exec(self.pass1())
162 self.do_exec(self.pass2())
163
164
165
166 class Profile(object):
167 def __init__(self, commands, **kwargs):
168 self.default_opts = {
169 "vbitrate": 1000,
170 "abitrate": 192,
171 }
172 self.extra = []
173 self.extra1 = []
174 self.extra2 = []
175 self.commands = commands
176 self.__dict__.update(kwargs)
177
178 def __contains__(self, keyname):
179 return hasattr(self, keyname)
180
181
182 profiles = {
183 "qt7" :
184 Profile(
185 commands=[Mencoder, MP4Box],
186 vcodec="x264",
187 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:me=umh:partitions=all:trellis=1:subq=7:bframes=1:direct_pred=auto",
188 acodec="faac",
189 aopts="br=%(abitrate)d:mpeg=4:object=2",
190 ),
191
192 "x264" :
193 Profile(
194 commands=[Mencoder, MKVMerge],
195 vcodec="x264",
196 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:subq=6:frameref=6:me=umh:partitions=all:bframes=4:b_adapt:qcomp=0.7:keyint=250",
197 acodec="mp3lame",
198 aopts="abr:br=%(abitrate)d",
199 ),
200
201 "xvid" :
202 Profile(
203 commands=[Mencoder],
204 vcodec="xvid",
205 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect",
206 acodec="mp3lame",
207 aopts="abr:br=%(abitrate)d",
208 extra2=["-ffourcc", "DX50"],
209 ),
210
211 "ipodxvid" :
212 Profile(
213 commands=[Mencoder, MP4Box],
214 vcodec="xvid",
215 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
216 acodec="faac",
217 aopts="br=%(abitrate)d:mpeg=4:object=2",
218 extra=["-vf-add", "scale=480:-10"],
219 ),
220
221 "ipodx264" :
222 Profile(
223 commands=[Mencoder, MP4Box],
224 vcodec="x264",
225 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vbv_maxrate=1500:vbv_bufsize=2000:nocabac:me=umh:partitions=all:trellis=1:subq=7:bframes=0:direct_pred=auto:level_idc=30:turbo",
226 acodec="faac",
227 aopts="br=%(abitrate)d:mpeg=4:object=2",
228 extra=["-vf-add", "scale=480:-10"],
229 extra2=["-channels", "2", "-srate", "48000"],
230 ),
231
232 "nokiax264" :
233 Profile(
234 commands=[Mencoder, MP4Box],
235 default_opts={
236 "vbitrate": 256,
237 "abitrate": 96,
238 },
239 vcodec="x264",
240 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:nocabac:me=umh:partitions=all:trellis=1:subq=7:bframes=0:direct_pred=auto",
241 acodec="faac",
242 aopts="br=%(abitrate)d:mpeg=4:object=2",
243 extra=["-vf-add", "scale=320:-10"],
244 ),
245
246 "n97xvid" :
247 Profile(
248 commands=[Mencoder, MP4Box],
249 default_opts={
250 "vbitrate": 1000,
251 "abitrate": 96,
252 },
253 vcodec="xvid",
254 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
255 acodec="faac",
256 aopts="br=%(abitrate)d:mpeg=4:object=2",
257 extra=["-vf-add", "scale=640:-10"],
258 ),
259
260 "n97x264" :
261 Profile(
262 commands=[Mencoder, MP4Box],
263 default_opts={
264 "vbitrate": 1000,
265 "abitrate": 96,
266 },
267 vcodec="x264",
268 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vbv_maxrate=2000:vbv_bufsize=2000:nocabac:me=umh:partitions=all:trellis=1:subq=7:bframes=0:direct_pred=auto:level_idc=20",
269 acodec="faac",
270 aopts="br=%(abitrate)d:mpeg=4:object=2",
271 extra=["-vf-add", "scale=640:-10"],
272 ),
273 }
274
275
276
277
278 def parse_args():
279 for profile_name in profiles.keys():
280 if sys.argv[0].find(profile_name) >= 0:
281 break
282 else:
283 profile_name = "xvid"
284
285 parser = optparse.OptionParser(usage="%prog [options] input [output]")
286 parser.add_option("--dvd", action="store", dest="dvd")
287 parser.add_option("--deinterlace", action="store_true", dest="deinterlace")
288 parser.add_option("--vfilters", action="store", dest="vfilters")
289 parser.add_option("--afilters", action="store", dest="afilters")
290 parser.add_option("--vbitrate", action="store", dest="vbitrate", type="int")
291 parser.add_option("--abitrate", action="store", dest="abitrate", type="int")
292 parser.add_option("--chapter", action="store", dest="chapter")
293 parser.add_option("--startpos", action="store", dest="startpos")
294 parser.add_option("--endpos", action="store", dest="endpos")
295 parser.add_option("--audioid", action="store", dest="audioid")
296 parser.add_option("--subtitleid", action="store", dest="subtitleid")
297 parser.add_option("--profile", action="store", dest="profile_name", default=profile_name)
298 parser.add_option("--episode-name", action="store", dest="episode_name")
299 parser.add_option("--dump", action="store_true", dest="dump")
300 try:
301 opts, args = parser.parse_args(sys.argv[1:])
302 if len(args) == 1:
303 input = args[0]
304 output = os.path.splitext(os.path.basename(input))[0]
305 elif len(args) == 2:
306 input, output = args
307 else:
308 raise ValueError
309 except Exception:
310 parser.print_usage()
311 sys.exit(1)
312
313 if "://" not in input:
314 opts.input = os.path.abspath(input)
315 else:
316 if opts.dvd:
317 opts.dvd = os.path.abspath(opts.dvd)
318 opts.input = input
319
320 opts.output = os.path.abspath(output)
321
322 return opts
323
324 def main():
325 opts = parse_args()
326
327 # Find our profile
328 try:
329 profile = profiles[opts.profile_name]
330 except KeyError:
331 print >>sys.stderr, "Profile '%s' not found!" % opts.profile_name
332 sys.exit(1)
333
334 # Pull in default option values from the profile
335 for key, value in profile.default_opts.iteritems():
336 if getattr(opts, key) is None:
337 setattr(opts, key, value)
338
339 # Run in a temp dir so that multiple instances can be run simultaneously
340 tempdir = tempfile.mkdtemp()
341 try:
342 os.chdir(tempdir)
343
344 try:
345 commands = []
346 for CommandClass in profile.commands:
347 command = CommandClass(profile, opts)
348 commands.append(command)
349 command.check()
350 for command in commands:
351 command.run()
352
353 except FatalException, e:
354 print >>sys.stderr, "Error:", e.message
355 sys.exit(1)
356
357 finally:
358 os.chdir("/")
359 shutil.rmtree(tempdir)
360
361 if __name__ == "__main__":
362 main()
363