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