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