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