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