]> code.delx.au - transcoding/blob - encode.py
31b765fe72a1652b6529a61af3199065b340c341
[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 if self.opts.detelecine:
113 self.opts.ofps = "24000/1001"
114 cmd += ["-vf-add", "pullup,softskip"]
115 try_opt("-fps", self.opts.ifps)
116 try_opt("-ofps", self.opts.ofps)
117 try_opt("-ss", self.opts.startpos)
118 try_opt("-endpos", self.opts.endpos)
119 try_opt("-dvd-device", self.opts.dvd)
120 try_opt("-chapter", self.opts.chapter)
121 try_opt("-aid", self.opts.audioid)
122 try_opt("-sid", self.opts.subtitleid)
123 try_opt("-vf-add", self.opts.vfilters)
124 try_opt("-af", self.opts.afilters)
125
126 def subst_values(self, cmd, vpass):
127 subst = {
128 "vbitrate": self.opts.vbitrate,
129 "abitrate": self.opts.abitrate,
130 "input": self.opts.input,
131 "output": self.opts.output + ".avi",
132 "vpass": vpass,
133 }
134
135 return [x % subst for x in cmd]
136
137 def pass1(self):
138 p = self.profile
139 cmd = []
140 cmd += ["mencoder", "%(input)s", "-o", "/dev/null"]
141 self.insert_options(cmd)
142 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
143 cmd += ["-oac", "copy"]
144 cmd += self.profile.extra + self.profile.extra1
145 cmd = self.subst_values(cmd, vpass=1)
146 return cmd
147
148 def pass2(self):
149 p = self.profile
150 cmd = []
151 cmd += ["mencoder", "%(input)s", "-o", "%(output)s"]
152 self.insert_options(cmd)
153 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
154 cmd += ["-oac", p.acodec, self.codec2opts[p.acodec], p.aopts]
155 if self.opts.episode_name:
156 cmd += ["-info", "name='%s'" % self.opts.episode_name]
157 cmd += self.profile.extra + self.profile.extra2
158 cmd = self.subst_values(cmd, vpass=2)
159 return cmd
160
161 def check(self):
162 self.check_command("mencoder")
163 self.check_no_file(self.opts.output + ".avi")
164
165 def run(self):
166 self.do_exec(self.pass1())
167 self.do_exec(self.pass2())
168
169
170
171 class Profile(object):
172 def __init__(self, commands, **kwargs):
173 self.default_opts = {
174 "vbitrate": 1000,
175 "abitrate": 192,
176 }
177 self.extra = []
178 self.extra1 = []
179 self.extra2 = []
180 self.commands = commands
181 self.__dict__.update(kwargs)
182
183 def __contains__(self, keyname):
184 return hasattr(self, keyname)
185
186
187 profiles = {
188 "qt7" :
189 Profile(
190 commands=[Mencoder, MP4Box],
191 vcodec="x264",
192 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:me=umh:partitions=all:trellis=1:subq=7:bframes=1:direct_pred=auto",
193 acodec="faac",
194 aopts="br=%(abitrate)d:mpeg=4:object=2",
195 ),
196
197 "x264" :
198 Profile(
199 commands=[Mencoder, MKVMerge],
200 vcodec="x264",
201 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:subq=6:frameref=6:me=umh:partitions=all:bframes=4:b_adapt:qcomp=0.7:keyint=250",
202 acodec="mp3lame",
203 aopts="abr:br=%(abitrate)d",
204 ),
205
206 "xvid" :
207 Profile(
208 commands=[Mencoder],
209 vcodec="xvid",
210 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect",
211 acodec="mp3lame",
212 aopts="abr:br=%(abitrate)d",
213 extra2=["-ffourcc", "DX50"],
214 ),
215
216 "ipodxvid" :
217 Profile(
218 commands=[Mencoder, MP4Box],
219 vcodec="xvid",
220 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
221 acodec="faac",
222 aopts="br=%(abitrate)d:mpeg=4:object=2",
223 extra=["-vf-add", "scale=480:-10"],
224 ),
225
226 "ipodx264" :
227 Profile(
228 commands=[Mencoder, MP4Box],
229 vcodec="x264",
230 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",
231 acodec="faac",
232 aopts="br=%(abitrate)d:mpeg=4:object=2",
233 extra=["-vf-add", "scale=480:-10"],
234 extra2=["-channels", "2", "-srate", "48000"],
235 ),
236
237 "nokiax264" :
238 Profile(
239 commands=[Mencoder, MP4Box],
240 default_opts={
241 "vbitrate": 256,
242 "abitrate": 96,
243 },
244 vcodec="x264",
245 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:nocabac:me=umh:partitions=all:trellis=1:subq=7:bframes=0:direct_pred=auto",
246 acodec="faac",
247 aopts="br=%(abitrate)d:mpeg=4:object=2",
248 extra=["-vf-add", "scale=320:-10"],
249 ),
250
251 "n97xvid" :
252 Profile(
253 commands=[Mencoder, MP4Box],
254 default_opts={
255 "vbitrate": 1000,
256 "abitrate": 96,
257 },
258 vcodec="xvid",
259 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
260 acodec="faac",
261 aopts="br=%(abitrate)d:mpeg=4:object=2",
262 extra=["-vf-add", "scale=640:-10"],
263 ),
264
265 "n97x264" :
266 Profile(
267 commands=[Mencoder, MP4Box],
268 default_opts={
269 "vbitrate": 1000,
270 "abitrate": 96,
271 },
272 vcodec="x264",
273 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",
274 acodec="faac",
275 aopts="br=%(abitrate)d:mpeg=4:object=2",
276 extra=["-vf-add", "scale=640:-10"],
277 ),
278 }
279
280
281
282
283 def parse_args():
284 for profile_name in profiles.keys():
285 if sys.argv[0].find(profile_name) >= 0:
286 break
287 else:
288 profile_name = "xvid"
289
290 parser = optparse.OptionParser(usage="%prog [options] input [output]")
291 parser.add_option("--dvd", action="store", dest="dvd")
292 parser.add_option("--deinterlace", action="store_true", dest="deinterlace")
293 parser.add_option("--detelecine", action="store_true", dest="detelecine")
294 parser.add_option("--vfilters", action="store", dest="vfilters")
295 parser.add_option("--afilters", action="store", dest="afilters")
296 parser.add_option("--vbitrate", action="store", dest="vbitrate", type="int")
297 parser.add_option("--abitrate", action="store", dest="abitrate", type="int")
298 parser.add_option("--chapter", action="store", dest="chapter")
299 parser.add_option("--ifps", action="store", dest="ifps")
300 parser.add_option("--ofps", action="store", dest="ofps")
301 parser.add_option("--startpos", action="store", dest="startpos")
302 parser.add_option("--endpos", action="store", dest="endpos")
303 parser.add_option("--audioid", action="store", dest="audioid")
304 parser.add_option("--subtitleid", action="store", dest="subtitleid")
305 parser.add_option("--profile", action="store", dest="profile_name", default=profile_name)
306 parser.add_option("--episode-name", action="store", dest="episode_name")
307 parser.add_option("--dump", action="store_true", dest="dump")
308 try:
309 opts, args = parser.parse_args(sys.argv[1:])
310 if len(args) == 1:
311 input = args[0]
312 output = os.path.splitext(os.path.basename(input))[0]
313 elif len(args) == 2:
314 input, output = args
315 else:
316 raise ValueError
317 except Exception:
318 parser.print_usage()
319 sys.exit(1)
320
321 if "://" not in input:
322 opts.input = os.path.abspath(input)
323 else:
324 if opts.dvd:
325 opts.dvd = os.path.abspath(opts.dvd)
326 opts.input = input
327
328 opts.output = os.path.abspath(output)
329
330 return opts
331
332 def main():
333 opts = parse_args()
334
335 # Find our profile
336 try:
337 profile = profiles[opts.profile_name]
338 except KeyError:
339 print >>sys.stderr, "Profile '%s' not found!" % opts.profile_name
340 sys.exit(1)
341
342 # Pull in default option values from the profile
343 for key, value in profile.default_opts.iteritems():
344 if getattr(opts, key) is None:
345 setattr(opts, key, value)
346
347 # Run in a temp dir so that multiple instances can be run simultaneously
348 tempdir = tempfile.mkdtemp()
349 try:
350 os.chdir(tempdir)
351
352 try:
353 commands = []
354 for CommandClass in profile.commands:
355 command = CommandClass(profile, opts)
356 commands.append(command)
357 command.check()
358 for command in commands:
359 command.run()
360
361 except FatalException, e:
362 print >>sys.stderr, "Error:", str(e)
363 sys.exit(1)
364
365 finally:
366 os.chdir("/")
367 shutil.rmtree(tempdir)
368
369 if __name__ == "__main__":
370 main()
371