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