1 #!/usr/bin/python
2
3 # Copyright 2003 Tom Rothamel <tom-potw@rothamel.us>
4 #
5 # Permission is hereby granted, free of charge, to any person
6 # obtaining a copy of this software and associated documentation files
7 # (the "Software"), to deal in the Software without restriction,
8 # including without limitation the rights to use, copy, modify, merge,
9 # publish, distribute, sublicense, and/or sell copies of the Software,
10 # and to permit persons to whom the Software is furnished to do so,
11 # subject to the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24
25 # Attempts to (smartly) turn a video file into an mpeg that is suitable
26 # for use in a DVD player.
27
28 # The input is expected to be encoded at either 29.97 fps or at 23.976
29 # fps. These are the NTSC tv and movie framerates, respectively.
30
31 # This requires that both transcode and mplayer are installed on the
32 # system. The latter is only needed because I have no idea how to use
33 # just transcode to detect pulldown in mpeg video.
34
35 import optparse
36 import os
37 import re
38 import signal
39 import sys
40
41 # A map from a possible frame size name to a tuple containing width, height,
42 # optional x-border, and optional y-border.
43
44 sizes = {
45 'full' : ( 704, 480, 48, 80 ),
46 'half' : ( 352, 480, 24, 80 ),
47 'quarter' : ( 352, 240, 24, 40 ),
48 }
49
50
51 def probe(input):
52 """This probes the input video, and returns a tuple of things about
53 it. In order, the members of this tuple are:
54
55 pulldown -- Is pulldown needed?
56 aspect -- The aspect ratio. Either '4:3' or '16:9', defaulting to the
57 former.
58 length -- The length of the movie in frames, or None if it could not
59 be easily determined.
60 """
61
62 # We first use tcprobe to try and guess the aspect ratio and
63 # framerate of the input file.
64
65 pulldown = False
66 aspect = "4:3"
67 length = None
68
69 f = os.popen("tcprobe -i '%s'" % input, "r")
70 for l in f:
71
72 # Frame size (guess aspect ratio.)
73 m = re.search(r"-g (\d+)x(\d+)", l)
74 if m:
75 x = float(m.group(1))
76 y = float(m.group(2))
77
78 if x / y > 1.7:
79 aspect = "16:9"
80 else:
81 aspect = "4:3"
82
83 # Explicit aspect ratio.
84 m = re.search(r"aspect ratio: ([\d:]+)", l)
85 if m:
86 aspect = m.group(1)
87
88 # If the framerate is less than 25 fps, enable pulldown.
89 m = re.search(r"-f ([\d\.]+)", l)
90 if m:
91 framerate = float(m.group(1))
92 print "Detected framerate is:", framerate, "fps"
93 if framerate < 25.0:
94 pulldown = True
95
96 # Try to find the length of the movie.
97 m = re.search(r"length: (\d+) frames", l)
98 if m:
99 length = int(m.group(1))
100
101 f.close()
102
103 # The problem with that previous test is that tcprobe sometimes lies.
104 # Mpeg video may have pulldown, the repetition of frames. This results
105 # in a 24fps mpeg claiming to be 30fps. To fix this, we play 60 frames
106 # of the video through mplayer, and see if it mentions detecting
107 # telecine.
108
109 f = os.popen("mplayer -vo null -ao null '%s' -frames 60 2>/dev/null", "r")
110 for l in f:
111 if re.search(r"TELECINE", l):
112 if not pulldown:
113 print "TELECINE detected, enabling pulldown."
114 pulldown = True
115
116 f.close()
117
118 return pulldown, aspect, length
119
120 # Displays a command on the screen, with all arguments escaped.
121 def quote_command(args):
122 l = ""
123
124 for w in ['"%s"' % i.replace("\\", "\\\\").replace('"', '\\"')
125 for i in args]:
126
127 if len(l + " " + w) > 78:
128 print l[1:], "\\"
129 l = " " + w
130 else:
131 l = l + " " + w
132
133 print l[1:]
134
135 # Runs the command with the given list of arguments.
136 def run_command(args):
137 print "Running %s." % args[0]
138
139 # This is ugly. We have to go and remove the sigpipe handler
140 # that python puts in for some reason.
141
142 # Grr... Took me a few hours to find this one.
143
144 pid = os.fork()
145
146 if not pid:
147 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
148 os.execvp(args[0], args)
149 os._exit(-1)
150
151 pid, rv = os.waitpid(pid, 0)
152
153 return rv
154
155
156 def main():
157
158 # Parse the options.
159 op = optparse.OptionParser(usage="%prog [options] <from> <to> [ -- <transcode options> ]",
160 version="%prog 1")
161
162 op.add_option("--full", action="store_const", dest="size", const="full",
163 default="full",
164 help="encode full size frames (704x480)")
165 op.add_option("--half", action="store_const", dest="size", const="half",
166 help="encode half size frames (352x480)")
167 op.add_option("--quarter", action="store_const", dest="size", const="quarter",
168 help="encode quarter size frames (352x240)")
169 op.add_option("--border", "--anime", action="store_true", dest="border", default=False,
170 help="add a border to the outside of the movie.")
171
172 options, args = op.parse_args()
173
174 if len(args) < 2:
175 op.error("At least two arguments are required.")
176
177 input = args[0]
178 output = args[1]
179 transcode_args = args[2:]
180
181 # The name in tmp.
182 tmp = "/tmp/encoding"
183
184 # Probe for some things.
185 pulldown, aspect, count = probe(input)
186
187 # This contains the arguments to transcode that we are
188 # building up.
189
190 tcargs = [ "transcode",
191 "-i", input, # The input file.
192 "-o", tmp, # The root name of the output files.
193 "-y", "mpeg2enc,mp2enc", # mpeg2 and mp2.
194 "-F", "8", # Dvd video.
195 "-w", "8000", # Dvd bitrate.
196 "-V", # Work in YUV, for a speedup.
197 "-E", "48000", # Dvd audio.
198 ]
199
200 # Ditto, for tcmplex.
201
202 tmargs = [ "tcmplex",
203 "-o", output, # The output.
204 "-m", "d", # DVD mode.
205 "-i", tmp + ".m2v", # The encoded mpeg video.
206 "-p", tmp + ".mpa", # The encoded mpeg audio.
207 ]
208
209 # Look up the sizes.
210 xsize, ysize, xborder, yborder = sizes[options.size]
211
212 # If we need a border, subtract it from the x and y sizes.
213 if options.border:
214 xsize -= xborder
215 ysize -= yborder
216
217 print "Encoding to size: %dx%d" % (xsize, ysize)
218 tcargs += [ "-Z", "%dx%d,fast" % (xsize, ysize) ]
219
220 # Add --pulldown, if needed.
221 if pulldown:
222 tcargs += [ "--pulldown" ]
223
224 if options.size == "quarter":
225 op.error("Can't have pulldown and a quarter-size image. Try --half.")
226
227 # Add aspect ratio, if known.
228
229 # A map from aspect ration to number in transcode.
230 asrs = { '16:9' : '3', '4:3' : '2' }
231
232 if aspect in asrs:
233 tcargs += [ "--export_asr", asrs[aspect] ]
234
235 # Add frame count, if known.
236 if count:
237 tcargs += [ "-c", "1-%d" % count ]
238
239 # Add border, if necessary.
240 if options.border:
241 vborder = -(yborder/2)
242 hborder = -(xborder/2)
243
244 tcargs += [ "-Y", "%d,%d" % (vborder, hborder) ]
245
246
247 # Add additional transcode arguments.
248 tcargs += transcode_args
249
250 # Show the various commands.
251 print
252 print "Transcode Command:"
253 quote_command(tcargs)
254 print
255 print "Tcmplex Command:"
256 quote_command(tmargs)
257 print
258
259 os.nice(10)
260
261 # Run the commands.
262 ec = run_command(tcargs)
263
264 if not ec:
265 run_command(tmargs)
266
267 if __name__ == "__main__":
268 main()
269