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