]> code.delx.au - transcoding/blob - encode.py
ca27aaede3e8313d8157638c2dfc1d6e4c80bb48
[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 def midentify(source, field):
29 process = subprocess.Popen(
30 [
31 "mplayer", source,
32 "-ao", "null", "-vo", "null",
33 "-frames", "0", "-identify",
34 ],
35 stdout=subprocess.PIPE,
36 stderr=subprocess.PIPE,
37 )
38 for line in process.stdout:
39 try:
40 key, value = line.split("=")
41 except ValueError:
42 continue
43 if key == field:
44 return value.strip()
45
46
47
48 class Command(object):
49 codec2exts = {
50 "xvid": "m4v",
51 "x264": "h264",
52 "faac": "aac",
53 "mp3lame": "mp3",
54 "copyac3": "ac3",
55 }
56
57 def __init__(self, profile, opts):
58 self.profile = profile
59 self.opts = opts
60 self.init()
61 self.audio_tmp = "audio." + self.codec2exts[profile.acodec]
62 self.video_tmp = "video." + self.codec2exts[profile.vcodec]
63
64 def init(self):
65 pass
66
67 def print_install_message(self):
68 print >>sys.stderr, "Problem with command: %s", self.name
69 if self.package:
70 print >>sys.stderr, "Try running:\n# aptitude install %s", self.package
71
72 def check_command(self, cmd):
73 if self.opts.dump:
74 return
75 if subprocess.Popen(["which", cmd], stdout=open("/dev/null", "w")).wait() != 0:
76 raise FatalException("Command '%s' is required" % cmd)
77
78 def check_no_file(self, path):
79 if os.path.exists(path):
80 raise FatalException("Output file '%s' exists." % path)
81
82 def do_exec(self, args):
83 if self.opts.dump:
84 print " ".join(map(mkarg, args))
85 else:
86 if subprocess.Popen(args).wait() != 0:
87 raise FatalException("Failure executing command: %s" % args)
88
89
90 class MP4Box(Command):
91 def check(self):
92 self.check_command("MP4Box")
93 self.check_no_file(self.opts.output + ".mp4")
94
95 def run(self):
96 if self.opts.dump:
97 fps = "???"
98 else:
99 fps = midentify(self.video_tmp, "ID_VIDEO_FPS")
100
101 output = self.opts.output + ".mp4"
102 self.do_exec([
103 "MP4Box",
104 "-fps", fps,
105 "-add", self.video_tmp,
106 "-add", self.audio_tmp,
107 output
108 ])
109
110
111
112 class MKVMerge(Command):
113 def check(self):
114 self.check_command("mkvmerge")
115 self.check_no_file(self.opts.output + ".mkv")
116
117 def run(self):
118 if self.opts.dump:
119 fps = "???"
120 else:
121 fps = midentify(self.video_tmp, "ID_VIDEO_FPS")
122
123 self.do_exec([
124 "mkvmerge",
125 "-o", self.opts.output + ".mkv",
126 "--default-duration", "0:%sfps"%fps,
127 self.video_tmp,
128 self.audio_tmp,
129 ])
130
131
132
133 class MencoderMux(Command):
134 def check(self):
135 self.check_command("mencoder")
136 self.check_no_file(self.opts.output + ".avi")
137
138 def run(self):
139 self.do_exec([
140 "mencoder",
141 "-o", self.opts.output + ".avi",
142 "-oac", "copy", "-ovc", "copy",
143 "-noskip", "-mc", "0",
144 "-audiofile", self.audio_tmp,
145 self.video_tmp,
146 ])
147
148
149
150 class Mencoder(Command):
151 codec2opts = {
152 "xvid": "-xvidencopts",
153 "x264": "-x264encopts",
154 "faac": "-faacopts",
155 "mp3lame": "-lameopts",
156 }
157
158 def init(self):
159 if self.opts.copyac3:
160 self.profile.acodec = "copyac3"
161 self.profile.aopts = None
162
163 def insert_options(self, cmd):
164 def try_opt(opt, var):
165 if var is not None:
166 cmd.append(opt)
167 cmd.append(var)
168 if self.opts.deinterlace:
169 cmd += ["-vf-add", "pp=lb"]
170 if self.opts.detelecine:
171 self.opts.ofps = "24000/1001"
172 cmd += ["-vf-add", "pullup,softskip"]
173 if self.opts.copyac3:
174 cmd += ["-noskip", "-mc", "0"]
175 try_opt("-fps", self.opts.ifps)
176 try_opt("-ofps", self.opts.ofps)
177 try_opt("-ss", self.opts.startpos)
178 try_opt("-endpos", self.opts.endpos)
179 try_opt("-dvd-device", self.opts.dvd)
180 try_opt("-chapter", self.opts.chapter)
181 try_opt("-aid", self.opts.audioid)
182 try_opt("-sid", self.opts.subtitleid)
183 try_opt("-vf-add", self.opts.vfilters)
184 try_opt("-af-add", self.opts.afilters)
185 cmd += ["-vf-add", "harddup"]
186
187 def subst_values(self, cmd, vpass):
188 subst = {
189 "vbitrate": self.opts.vbitrate,
190 "abitrate": self.opts.abitrate,
191 "vpass": vpass,
192 }
193
194 return [x % subst for x in cmd]
195
196 def passn(self, n):
197 p = self.profile
198
199 acodec = p.acodec
200 if acodec == "copyac3":
201 acodec = "copy"
202
203 cmd = []
204 cmd += ["mencoder", self.opts.input]
205 self.insert_options(cmd)
206 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
207 cmd += ["-oac", acodec]
208 if p.aopts:
209 cmd += [self.codec2opts[p.acodec], p.aopts]
210 cmd += self.profile.extra1 + self.profile.extra
211 cmd = self.subst_values(cmd, vpass=n)
212
213 return cmd
214
215
216 def pass1(self):
217 cmd = self.passn(1)
218 cmd += ["-o", self.audio_tmp, "-of", "rawaudio"]
219 return cmd
220
221 def pass2(self):
222 cmd = self.passn(2)
223 cmd += ["-o", self.video_tmp, "-of", "rawvideo"]
224 return cmd
225
226 def check(self):
227 self.check_command("mencoder")
228 self.check_no_file(self.audio_tmp)
229 self.check_no_file(self.video_tmp)
230
231 def run(self):
232 self.do_exec(self.pass1())
233 self.do_exec(self.pass2())
234
235
236
237 class Profile(object):
238 def __init__(self, commands, **kwargs):
239 self.default_opts = {
240 "vbitrate": 1000,
241 "abitrate": 192,
242 }
243 self.extra = []
244 self.extra1 = []
245 self.extra2 = []
246 self.commands = commands
247 self.__dict__.update(kwargs)
248
249 def __contains__(self, keyname):
250 return hasattr(self, keyname)
251
252
253 profiles = {
254 "x264" :
255 Profile(
256 commands=[Mencoder, MKVMerge],
257 vcodec="x264",
258 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:subq=6:frameref=6:me=umh:partitions=all:bframes=4:b_adapt:qcomp=0.7:keyint=250",
259 acodec="mp3lame",
260 aopts="abr:br=%(abitrate)d",
261 ),
262
263 "xvid" :
264 Profile(
265 commands=[Mencoder, MencoderMux],
266 vcodec="xvid",
267 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect",
268 acodec="mp3lame",
269 aopts="abr:br=%(abitrate)d",
270 ),
271
272 "apple-quicktime" :
273 Profile(
274 commands=[Mencoder, MP4Box],
275 vcodec="x264",
276 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:me=umh:partitions=all:trellis=1:subq=7:bframes=1:direct_pred=auto",
277 acodec="faac",
278 aopts="br=%(abitrate)d:mpeg=4:object=2",
279 ),
280
281 "ipod-xvid" :
282 Profile(
283 commands=[Mencoder, MP4Box],
284 vcodec="xvid",
285 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
286 acodec="faac",
287 aopts="br=%(abitrate)d:mpeg=4:object=2",
288 extra=["-vf-add", "scale=480:-10"],
289 ),
290
291 "ipod-x264" :
292 Profile(
293 commands=[Mencoder, MP4Box],
294 vcodec="x264",
295 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",
296 acodec="faac",
297 aopts="br=%(abitrate)d:mpeg=4:object=2",
298 extra=["-vf-add", "scale=480:-10"],
299 extra2=["-channels", "2", "-srate", "48000"],
300 ),
301
302 "nokia-n97" :
303 Profile(
304 commands=[Mencoder, MP4Box],
305 default_opts={
306 "vbitrate": 256,
307 "abitrate": 64,
308 },
309 vcodec="xvid",
310 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
311 acodec="faac",
312 aopts="br=%(abitrate)d:mpeg=4:object=2",
313 extra=["-vf-add", "scale=640:-10"],
314 ),
315 }
316
317
318
319
320 def parse_args():
321 for profile_name in profiles.keys():
322 if sys.argv[0].find(profile_name) >= 0:
323 break
324 else:
325 profile_name = "xvid"
326
327 parser = optparse.OptionParser(usage="%prog [options] input [output]")
328 parser.add_option("--dvd", action="store", dest="dvd")
329 parser.add_option("--deinterlace", action="store_true", dest="deinterlace")
330 parser.add_option("--detelecine", action="store_true", dest="detelecine")
331 parser.add_option("--copyac3", action="store_true", dest="copyac3")
332 parser.add_option("--vfilters", action="store", dest="vfilters")
333 parser.add_option("--afilters", action="store", dest="afilters")
334 parser.add_option("--vbitrate", action="store", dest="vbitrate", type="int")
335 parser.add_option("--abitrate", action="store", dest="abitrate", type="int")
336 parser.add_option("--chapter", action="store", dest="chapter")
337 parser.add_option("--ifps", action="store", dest="ifps")
338 parser.add_option("--ofps", action="store", dest="ofps")
339 parser.add_option("--startpos", action="store", dest="startpos")
340 parser.add_option("--endpos", action="store", dest="endpos")
341 parser.add_option("--audioid", action="store", dest="audioid")
342 parser.add_option("--subtitleid", action="store", dest="subtitleid")
343 parser.add_option("--profile", action="store", dest="profile_name", default=profile_name)
344 parser.add_option("--dump", action="store_true", dest="dump")
345 try:
346 opts, args = parser.parse_args(sys.argv[1:])
347 if len(args) == 1:
348 input = args[0]
349 output = os.path.splitext(os.path.basename(input))[0]
350 elif len(args) == 2:
351 input, output = args
352 else:
353 raise ValueError
354 except Exception:
355 parser.print_usage()
356 sys.exit(1)
357
358 if "://" not in input:
359 opts.input = os.path.abspath(input)
360 else:
361 if opts.dvd:
362 opts.dvd = os.path.abspath(opts.dvd)
363 opts.input = input
364
365 opts.output = os.path.abspath(output)
366
367 return opts
368
369 def main():
370 os.nice(1)
371
372 opts = parse_args()
373
374 # Find our profile
375 try:
376 profile = profiles[opts.profile_name]
377 except KeyError:
378 print >>sys.stderr, "Profile '%s' not found!" % opts.profile_name
379 sys.exit(1)
380
381 # Pull in default option values from the profile
382 for key, value in profile.default_opts.iteritems():
383 if getattr(opts, key) is None:
384 setattr(opts, key, value)
385
386 # Run in a temp dir so that multiple instances can be run simultaneously
387 tempdir = tempfile.mkdtemp()
388 try:
389 os.chdir(tempdir)
390
391 try:
392 commands = []
393 for CommandClass in profile.commands:
394 command = CommandClass(profile, opts)
395 commands.append(command)
396 command.check()
397 for command in commands:
398 command.run()
399
400 except FatalException, e:
401 print >>sys.stderr, "Error:", str(e)
402 sys.exit(1)
403
404 finally:
405 os.chdir("/")
406 shutil.rmtree(tempdir)
407
408 if __name__ == "__main__":
409 main()
410