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