]> code.delx.au - transcoding/blob - encode.py
6e526fd1d5e0b7102b74cb486b51842a2dad6b6a
[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 self.check_options()
163
164 def check_options(self):
165 o = self.opts
166 if o.detelecine and o.ofps:
167 raise FatalException("Cannot use --detelecine with --ofps")
168 if o.deinterlace and o.detelecine:
169 raise FatalException("Cannot use --detelecine with --deinterlace")
170
171 def insert_options(self, cmd):
172 o = self.opts
173 def do_opt(opt, var):
174 if var is not None:
175 cmd.append(opt)
176 cmd.append(str(var))
177
178 if o.deinterlace:
179 cmd += ["-vf-add", "yadif"]
180 if o.detelecine:
181 o.ofps = "24000/1001"
182 cmd += ["-vf-add", "pullup,softskip"]
183 if o.noskip:
184 cmd += ["-noskip"]
185 if o.skipkb:
186 cmd += ["-sb", str(o.skipkb * 1024)]
187
188 do_opt("-mc", o.mc)
189 do_opt("-fps", o.ifps)
190 do_opt("-ofps", o.ofps)
191 do_opt("-ss", o.startpos)
192 do_opt("-endpos", o.endpos)
193 do_opt("-dvd-device", o.dvd)
194 do_opt("-chapter", o.chapter)
195 do_opt("-aid", o.audioid)
196 do_opt("-sid", o.subtitleid)
197 do_opt("-vf-add", o.vfilters)
198 do_opt("-af-add", o.afilters)
199 cmd += ["-vf-add", "harddup"]
200
201 def subst_values(self, cmd, vpass):
202 subst = {
203 "vbitrate": self.opts.vbitrate,
204 "abitrate": self.opts.abitrate,
205 "vpass": vpass,
206 }
207
208 return [x % subst for x in cmd]
209
210 def passn(self, n):
211 p = self.profile
212
213 acodec = p.acodec
214 if acodec == "copyac3":
215 acodec = "copy"
216
217 cmd = []
218 cmd += ["mencoder", self.opts.input]
219 self.insert_options(cmd)
220 cmd += ["-ovc", p.vcodec, self.codec2opts[p.vcodec], p.vopts]
221 cmd += ["-oac", acodec]
222 if p.aopts:
223 cmd += [self.codec2opts[p.acodec], p.aopts]
224 cmd += self.profile.extra1 + self.profile.extra
225 cmd = self.subst_values(cmd, vpass=n)
226
227 return cmd
228
229
230 def pass1(self):
231 cmd = self.passn(1)
232 cmd += ["-o", self.audio_tmp, "-of", "rawaudio"]
233 return cmd
234
235 def pass2(self):
236 cmd = self.passn(2)
237 cmd += ["-o", self.video_tmp, "-of", "rawvideo"]
238 return cmd
239
240 def check(self):
241 self.check_command("mencoder")
242 self.check_no_file(self.audio_tmp)
243 self.check_no_file(self.video_tmp)
244
245 def run(self):
246 self.do_exec(self.pass1())
247 self.do_exec(self.pass2())
248
249
250
251 class Profile(object):
252 def __init__(self, commands, **kwargs):
253 self.default_opts = {
254 "vbitrate": 1000,
255 "abitrate": 192,
256 }
257 self.extra = []
258 self.extra1 = []
259 self.extra2 = []
260 self.commands = commands
261 self.__dict__.update(kwargs)
262
263 def __contains__(self, keyname):
264 return hasattr(self, keyname)
265
266
267 profiles = {
268 "x264" :
269 Profile(
270 commands=[Mencoder, MKVMerge],
271 vcodec="x264",
272 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:subq=6:frameref=6:me=umh:partitions=all:bframes=4:b_adapt:qcomp=0.7:keyint=250",
273 acodec="mp3lame",
274 aopts="abr:br=%(abitrate)d",
275 ),
276
277 "xvid" :
278 Profile(
279 commands=[Mencoder, MencoderMux],
280 vcodec="xvid",
281 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect",
282 acodec="mp3lame",
283 aopts="abr:br=%(abitrate)d",
284 ),
285
286 "apple-quicktime" :
287 Profile(
288 commands=[Mencoder, MP4Box],
289 vcodec="x264",
290 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:me=umh:partitions=all:trellis=1:subq=7:bframes=1:direct_pred=auto",
291 acodec="faac",
292 aopts="br=%(abitrate)d:mpeg=4:object=2",
293 ),
294
295 "ipod-xvid" :
296 Profile(
297 commands=[Mencoder, MP4Box],
298 vcodec="xvid",
299 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
300 acodec="faac",
301 aopts="br=%(abitrate)d:mpeg=4:object=2",
302 extra=["-vf-add", "scale=480:-10"],
303 ),
304
305 "ipod-x264" :
306 Profile(
307 commands=[Mencoder, MP4Box],
308 vcodec="x264",
309 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",
310 acodec="faac",
311 aopts="br=%(abitrate)d:mpeg=4:object=2",
312 extra=["-vf-add", "scale=480:-10"],
313 extra2=["-channels", "2", "-srate", "48000"],
314 ),
315
316 "nokia-n97" :
317 Profile(
318 commands=[Mencoder, MP4Box],
319 default_opts={
320 "vbitrate": 256,
321 "abitrate": 64,
322 },
323 vcodec="xvid",
324 vopts="pass=%(vpass)d:bitrate=%(vbitrate)d:vhq=4:autoaspect:max_bframes=0",
325 acodec="faac",
326 aopts="br=%(abitrate)d:mpeg=4:object=2",
327 extra=["-vf-add", "scale=640:-10"],
328 ),
329 }
330
331
332
333
334 def parse_args():
335 for profile_name in profiles.keys():
336 if sys.argv[0].find(profile_name) >= 0:
337 break
338 else:
339 profile_name = "xvid"
340
341 parser = optparse.OptionParser(usage="%prog [options] input [output]")
342 parser.add_option("--dvd", action="store", dest="dvd")
343 parser.add_option("--deinterlace", action="store_true", dest="deinterlace")
344 parser.add_option("--detelecine", action="store_true", dest="detelecine")
345 parser.add_option("--copyac3", action="store_true", dest="copyac3")
346 parser.add_option("--mc", action="store", dest="mc", type="int")
347 parser.add_option("--noskip", action="store_true", dest="noskip")
348 parser.add_option("--vfilters", action="store", dest="vfilters")
349 parser.add_option("--afilters", action="store", dest="afilters")
350 parser.add_option("--vbitrate", action="store", dest="vbitrate", type="int")
351 parser.add_option("--abitrate", action="store", dest="abitrate", type="int")
352 parser.add_option("--chapter", action="store", dest="chapter")
353 parser.add_option("--ifps", action="store", dest="ifps")
354 parser.add_option("--ofps", action="store", dest="ofps")
355 parser.add_option("--skipkb", action="store", dest="skipkb", type="int")
356 parser.add_option("--startpos", action="store", dest="startpos")
357 parser.add_option("--endpos", action="store", dest="endpos")
358 parser.add_option("--audioid", action="store", dest="audioid")
359 parser.add_option("--subtitleid", action="store", dest="subtitleid")
360 parser.add_option("--profile", action="store", dest="profile_name", default=profile_name)
361 parser.add_option("--dump", action="store_true", dest="dump")
362 try:
363 opts, args = parser.parse_args(sys.argv[1:])
364 if len(args) == 1:
365 input = args[0]
366 output = os.path.splitext(os.path.basename(input))[0]
367 elif len(args) == 2:
368 input, output = args
369 else:
370 raise ValueError
371 except Exception:
372 parser.print_usage()
373 sys.exit(1)
374
375 if "://" not in input:
376 opts.input = os.path.abspath(input)
377 else:
378 if opts.dvd:
379 opts.dvd = os.path.abspath(opts.dvd)
380 opts.input = input
381
382 opts.output = os.path.abspath(output)
383
384 return opts
385
386 def main():
387 os.nice(1)
388
389 opts = parse_args()
390
391 # Find our profile
392 try:
393 profile = profiles[opts.profile_name]
394 except KeyError:
395 print >>sys.stderr, "Profile '%s' not found!" % opts.profile_name
396 sys.exit(1)
397
398 # Pull in default option values from the profile
399 for key, value in profile.default_opts.iteritems():
400 if getattr(opts, key) is None:
401 setattr(opts, key, value)
402
403 # Run in a temp dir so that multiple instances can be run simultaneously
404 tempdir = tempfile.mkdtemp()
405 try:
406 os.chdir(tempdir)
407
408 try:
409 commands = []
410 for CommandClass in profile.commands:
411 command = CommandClass(profile, opts)
412 commands.append(command)
413 command.check()
414 for command in commands:
415 command.run()
416
417 except FatalException, e:
418 print >>sys.stderr, "Error:", str(e)
419 sys.exit(1)
420
421 finally:
422 os.chdir("/")
423 shutil.rmtree(tempdir)
424
425 if __name__ == "__main__":
426 main()
427