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