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