Added support for ffmpeg command templates
[pyTivo.git] / plugins / video / transcode.py
blobc1bc638250841b56675d2aa9eddb4b61e0b1ddc9
1 import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache
2 import Config
4 info_cache = lrucache.LRUCache(1000)
7 debug = Config.getDebug()
8 MAX_VIDEO_BR = Config.getMaxVideoBR()
9 BUFF_SIZE = Config.getBuffSize()
11 FFMPEG = Config.get('Server', 'ffmpeg')
13 def debug_write(data):
14 if debug:
15 debug_out = []
16 for x in data:
17 debug_out.append(str(x))
18 fdebug = open('debug.txt', 'a')
19 fdebug.write(' '.join(debug_out))
20 fdebug.close()
22 # XXX BIG HACK
23 # subprocess is broken for me on windows so super hack
24 def patchSubprocess():
25 o = subprocess.Popen._make_inheritable
27 def _make_inheritable(self, handle):
28 if not handle: return subprocess.GetCurrentProcess()
29 return o(self, handle)
31 subprocess.Popen._make_inheritable = _make_inheritable
32 mswindows = (sys.platform == "win32")
33 if mswindows:
34 patchSubprocess()
36 def output_video(inFile, outFile, tsn=''):
37 if tivo_compatable(inFile):
38 debug_write(['output_video: ', inFile, ' is tivo compatible\n'])
39 f = file(inFile, 'rb')
40 shutil.copyfileobj(f, outFile)
41 f.close()
42 else:
43 debug_write(['output_video: ', inFile, ' is not tivo compatible\n'])
44 transcode(inFile, outFile, tsn)
46 def transcode(inFile, outFile, tsn=''):
48 settings = {}
49 settings['audio_br'] = Config.getAudioBR(tsn)
50 settings['video_br'] = Config.getVideoBR(tsn)
51 settings['in_file'] = inFile
52 settings['max_video_br'] = MAX_VIDEO_BR
53 settings['buff_size'] = BUFF_SIZE
54 settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
56 cmd_string = Config.getFFMPEGTemplate(tsn) % settings
58 cmd = [FFMPEG] + cmd_string.split()
60 debug_write(['transcode: ffmpeg command is ', ' '.join(cmd), '\n'])
61 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
62 try:
63 shutil.copyfileobj(ffmpeg.stdout, outFile)
64 except:
65 kill(ffmpeg.pid)
67 def select_aspect(inFile, tsn = ''):
68 TIVO_WIDTH = Config.getTivoWidth(tsn)
69 TIVO_HEIGHT = Config.getTivoHeight(tsn)
71 type, width, height, fps, millisecs = video_info(inFile)
73 debug_write(['tsn:', tsn, '\n'])
75 aspect169 = Config.get169Setting(tsn)
77 debug_write(['aspect169:', aspect169, '\n'])
79 d = gcd(height,width)
80 ratio = (width*100)/height
81 rheight, rwidth = height/d, width/d
83 debug_write(['select_aspect: File=', inFile, ' Type=', type, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' ratio=', ratio, ' rheight=', rheight, ' rwidth=', rwidth, '\n'])
85 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
86 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
88 if (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]:
89 debug_write(['select_aspect: File is within 4:3 list.\n'])
90 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
91 elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169:
92 debug_write(['select_aspect: File is within 16:9 list and 16:9 allowed.\n'])
93 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
94 else:
95 settings = []
96 #If video is wider than 4:3 add top and bottom padding
97 if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom
98 if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3
99 if (ratio > 177):#too short needs padding top and bottom
100 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9)
101 settings.append('-aspect')
102 settings.append('16:9')
103 if endHeight % 2:
104 endHeight -= 1
105 if endHeight < TIVO_HEIGHT * 0.99:
106 settings.append('-s')
107 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
109 topPadding = ((TIVO_HEIGHT - endHeight)/2)
110 if topPadding % 2:
111 topPadding -= 1
113 settings.append('-padtop')
114 settings.append(str(topPadding))
115 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
116 settings.append('-padbottom')
117 settings.append(str(bottomPadding))
118 else: #if only very small amount of padding needed, then just stretch it
119 settings.append('-s')
120 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
121 debug_write(['select_aspect: 16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n', ' '.join(settings), '\n'])
122 else: #too skinny needs padding on left and right.
123 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9))
124 settings.append('-aspect')
125 settings.append('16:9')
126 if endWidth % 2:
127 endWidth -= 1
128 if endWidth < (TIVO_WIDTH-10):
129 settings.append('-s')
130 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
132 leftPadding = ((TIVO_WIDTH - endWidth)/2)
133 if leftPadding % 2:
134 leftPadding -= 1
136 settings.append('-padleft')
137 settings.append(str(leftPadding))
138 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
139 settings.append('-padright')
140 settings.append(str(rightPadding))
141 else: #if only very small amount of padding needed, then just stretch it
142 settings.append('-s')
143 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
144 debug_write(['select_aspect: 16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings), '\n'])
145 else: #this is a 4:3 file or 16:9 output not allowed
146 settings.append('-aspect')
147 settings.append('4:3')
148 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3)
149 if endHeight % 2:
150 endHeight -= 1
151 if endHeight < TIVO_HEIGHT * 0.99:
152 settings.append('-s')
153 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
155 topPadding = ((TIVO_HEIGHT - endHeight)/2)
156 if topPadding % 2:
157 topPadding -= 1
159 settings.append('-padtop')
160 settings.append(str(topPadding))
161 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
162 settings.append('-padbottom')
163 settings.append(str(bottomPadding))
164 else: #if only very small amount of padding needed, then just stretch it
165 settings.append('-s')
166 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
167 debug_write(['select_aspect: File is wider than 4:3 padding top and bottom\n', ' '.join(settings), '\n'])
169 return settings
170 #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in
171 #an aspect ratio of 4:3 since they are so narrow.
172 else:
173 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3))
174 settings.append('-aspect')
175 settings.append('4:3')
176 if endWidth % 2:
177 endWidth -= 1
178 if endWidth < (TIVO_WIDTH * 0.99):
179 settings.append('-s')
180 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
182 leftPadding = ((TIVO_WIDTH - endWidth)/2)
183 if leftPadding % 2:
184 leftPadding -= 1
186 settings.append('-padleft')
187 settings.append(str(leftPadding))
188 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
189 settings.append('-padright')
190 settings.append(str(rightPadding))
191 else: #if only very small amount of padding needed, then just stretch it
192 settings.append('-s')
193 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
195 debug_write(['select_aspect: File is taller than 4:3 padding left and right\n', ' '.join(settings), '\n'])
197 return settings
199 def tivo_compatable(inFile):
200 suportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]]
201 type, width, height, fps, millisecs = video_info(inFile)
202 #print type, width, height, fps, millisecs
204 if (inFile[-5:]).lower() == '.tivo':
205 debug_write(['tivo_compatible: ', inFile, ' ends with .tivo\n'])
206 return True
208 if not type == 'mpeg2video':
209 #print 'Not Tivo Codec'
210 debug_write(['tivo_compatible: ', inFile, ' is not mpeg2video it is ', type, '\n'])
211 return False
213 if not fps == '29.97':
214 #print 'Not Tivo fps'
215 debug_write(['tivo_compatible: ', inFile, ' is not correct fps it is ', fps, '\n'])
216 return False
218 for mode in suportedModes:
219 if (mode[0], mode[1]) == (width, height):
220 #print 'Is TiVo!'
221 debug_write(['tivo_compatible: ', inFile, ' has correct width of ', width, ' and height of ', height, '\n'])
222 return True
223 #print 'Not Tivo dimensions'
224 return False
226 def video_info(inFile):
227 mtime = os.stat(inFile).st_mtime
228 if inFile in info_cache and info_cache[inFile][0] == mtime:
229 debug_write(['video_info: ', inFile, ' cache hit!', '\n'])
230 return info_cache[inFile][1]
232 if (inFile[-5:]).lower() == '.tivo':
233 info_cache[inFile] = (mtime, (True, True, True, True, True))
234 debug_write(['video_info: ', inFile, ' ends in .tivo.\n'])
235 return True, True, True, True, True
237 cmd = [FFMPEG, '-i', inFile ]
238 ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
240 # wait 4 sec if ffmpeg is not back give up
241 for i in range(80):
242 time.sleep(.05)
243 if not ffmpeg.poll() == None:
244 break
246 if ffmpeg.poll() == None:
247 kill(ffmpeg.pid)
248 info_cache[inFile] = (mtime, (None, None, None, None, None))
249 return None, None, None, None, None
251 output = ffmpeg.stderr.read()
252 debug_write(['video_info: ffmpeg output=', output, '\n'])
254 durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),')
255 d = durre.search(output)
257 rezre = re.compile(r'.*Video: ([^,]+),.*')
258 x = rezre.search(output)
259 if x:
260 codec = x.group(1)
261 else:
262 info_cache[inFile] = (mtime, (None, None, None, None, None))
263 debug_write(['video_info: failed at codec\n'])
264 return None, None, None, None, None
266 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+),.*')
267 x = rezre.search(output)
268 if x:
269 width = int(x.group(1))
270 height = int(x.group(2))
271 else:
272 info_cache[inFile] = (mtime, (None, None, None, None, None))
273 debug_write(['video_info: failed at width/height\n'])
274 return None, None, None, None, None
276 rezre = re.compile(r'.*Video: .+, (.+) fps.*')
277 x = rezre.search(output)
278 if x:
279 fps = x.group(1)
280 else:
281 info_cache[inFile] = (mtime, (None, None, None, None, None))
282 debug_write(['video_info: failed at fps\n'])
283 return None, None, None, None, None
285 # Allow override only if it is mpeg2 and frame rate was doubled to 59.94
286 if (not fps == '29.97') and (codec == 'mpeg2video'):
287 # First look for the build 7215 version
288 rezre = re.compile(r'.*film source: 29.97.*')
289 x = rezre.search(output.lower() )
290 if x:
291 debug_write(['video_info: film source: 29.97 setting fps to 29.97\n'])
292 fps = '29.97'
293 else:
294 # for build 8047:
295 rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*')
296 debug_write(['video_info: Bug in VideoReDo\n'])
297 x = rezre.search(output.lower() )
298 if x:
299 fps = '29.97'
301 millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100)
302 info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs))
303 debug_write(['video_info: Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, '\n'])
304 return codec, width, height, fps, millisecs
306 def suported_format(inFile):
307 if video_info(inFile)[0]:
308 return True
309 else:
310 debug_write(['supported_format: ', inFile, ' is not supported\n'])
311 return False
313 def kill(pid):
314 debug_write(['kill: killing pid=', str(pid), '\n'])
315 if mswindows:
316 win32kill(pid)
317 else:
318 import os, signal
319 os.kill(pid, signal.SIGKILL)
321 def win32kill(pid):
322 import ctypes
323 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
324 ctypes.windll.kernel32.TerminateProcess(handle, -1)
325 ctypes.windll.kernel32.CloseHandle(handle)
327 def gcd(a,b):
328 while b:
329 a, b = b, a % b
330 return a