Merge branch 'beta-wgw'
[pyTivo.git] / plugins / video / transcode.py
blob9e55d1f7a4eecdb1e6ca7a7dc108ad11df20c4c7
1 import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache
2 import config
4 info_cache = lrucache.LRUCache(1000)
6 debug = config.getDebug()
7 BUFF_SIZE = config.getBuffSize()
8 FFMPEG = config.get('Server', 'ffmpeg')
9 videotest = os.path.join(os.path.dirname(__file__), 'videotest.mpg')
11 def debug_write(data):
12 if debug:
13 debug_out = []
14 for x in data:
15 debug_out.append(str(x))
16 fdebug = open('debug.txt', 'a')
17 fdebug.write(' '.join(debug_out))
18 fdebug.close()
20 # XXX BIG HACK
21 # subprocess is broken for me on windows so super hack
22 def patchSubprocess():
23 o = subprocess.Popen._make_inheritable
25 def _make_inheritable(self, handle):
26 if not handle: return subprocess.GetCurrentProcess()
27 return o(self, handle)
29 subprocess.Popen._make_inheritable = _make_inheritable
30 mswindows = (sys.platform == "win32")
31 if mswindows:
32 patchSubprocess()
34 def output_video(inFile, outFile, tsn=''):
35 if tivo_compatable(inFile, tsn):
36 debug_write(['output_video: ', inFile, ' is tivo compatible\n'])
37 f = file(inFile, 'rb')
38 shutil.copyfileobj(f, outFile)
39 f.close()
40 else:
41 debug_write(['output_video: ', inFile, ' is not tivo compatible\n'])
42 transcode(inFile, outFile, tsn)
44 def transcode(inFile, outFile, tsn=''):
46 settings = {}
47 settings['audio_br'] = config.getAudioBR(tsn)
48 settings['audio_codec'] = select_audiocodec(inFile, tsn)
49 settings['video_br'] = config.getVideoBR(tsn)
50 settings['max_video_br'] = config.getMaxVideoBR()
51 settings['buff_size'] = BUFF_SIZE
52 settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
54 cmd_string = config.getFFMPEGTemplate(tsn) % settings
56 cmd = [FFMPEG, '-i', inFile] + cmd_string.split()
57 print cmd
58 debug_write(['transcode: ffmpeg command is ', ' '.join(cmd), '\n'])
59 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
60 try:
61 shutil.copyfileobj(ffmpeg.stdout, outFile)
62 except:
63 kill(ffmpeg.pid)
65 def select_audiocodec(inFile, tsn = ''):
66 # Default, compatible with all TiVo's
67 codec = '-acodec mp2 -ac 2 -ar 44100'
68 type, width, height, fps, millisecs, kbps, akbps, acodec = video_info(inFile)
69 if akbps == None and acodec in ('liba52', 'mp2'):
70 cmd_string = '-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy -t 00:00:01 -f vob -'
71 if video_check(inFile, cmd_string):
72 type, width, height, fps, millisecs, kbps, akbps, acodec = video_info(videotest)
73 if config.isHDtivo(tsn):
74 # Is HD Tivo, use ac3
75 codec = '-acodec ac3 -ar 48000'
76 if acodec == 'liba52' and not akbps == None and \
77 int(akbps) <= config.getMaxAudioBR(tsn):
78 # compatible codec and bitrate, do not reencode audio
79 codec = '-acodec copy'
80 if acodec == 'mp2' and not akbps == None and \
81 int(akbps) <= config.getMaxAudioBR(tsn):
82 # compatible codec and bitrate, do not reencode audio
83 codec = '-acodec copy'
84 return codec
86 def select_aspect(inFile, tsn = ''):
87 TIVO_WIDTH = config.getTivoWidth(tsn)
88 TIVO_HEIGHT = config.getTivoHeight(tsn)
90 type, width, height, fps, millisecs, kbps, akbps, acodec = video_info(inFile)
92 debug_write(['tsn:', tsn, '\n'])
94 aspect169 = config.get169Setting(tsn)
96 debug_write(['aspect169:', aspect169, '\n'])
98 optres = config.getOptres()
100 debug_write(['optres:', optres, '\n'])
102 if optres:
103 optHeight = config.nearestTivoHeight(height)
104 optWidth = config.nearestTivoWidth(width)
105 if optHeight < TIVO_HEIGHT:
106 TIVO_HEIGHT = optHeight
107 if optWidth < TIVO_WIDTH:
108 TIVO_WIDTH = optWidth
110 d = gcd(height,width)
111 ratio = (width*100)/height
112 rheight, rwidth = height/d, width/d
114 debug_write(['select_aspect: File=', inFile, ' Type=', type, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' ratio=', ratio, ' rheight=', rheight, ' rwidth=', rwidth, ' TIVO_HEIGHT=', TIVO_HEIGHT, 'TIVO_WIDTH=', TIVO_WIDTH, '\n'])
116 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
117 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
119 if config.isHDtivo(tsn) and height <= TIVO_HEIGHT and config.getOptres() == False:
120 return [] #pass all resolutions to S3/HD, except heights greater than conf height
121 # else, optres is enabled and resizes SD video to the "S2" standard on S3/HD.
122 elif (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]:
123 debug_write(['select_aspect: File is within 4:3 list.\n'])
124 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
125 elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169:
126 debug_write(['select_aspect: File is within 16:9 list and 16:9 allowed.\n'])
127 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
128 else:
129 settings = []
130 #If video is wider than 4:3 add top and bottom padding
131 if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom
132 if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3
133 if (ratio > 177):#too short needs padding top and bottom
134 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9)
135 settings.append('-aspect')
136 settings.append('16:9')
137 if endHeight % 2:
138 endHeight -= 1
139 if endHeight < TIVO_HEIGHT * 0.99:
140 settings.append('-s')
141 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
143 topPadding = ((TIVO_HEIGHT - endHeight)/2)
144 if topPadding % 2:
145 topPadding -= 1
147 settings.append('-padtop')
148 settings.append(str(topPadding))
149 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
150 settings.append('-padbottom')
151 settings.append(str(bottomPadding))
152 else: #if only very small amount of padding needed, then just stretch it
153 settings.append('-s')
154 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
155 debug_write(['select_aspect: 16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n', ' '.join(settings), '\n'])
156 else: #too skinny needs padding on left and right.
157 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9))
158 settings.append('-aspect')
159 settings.append('16:9')
160 if endWidth % 2:
161 endWidth -= 1
162 if endWidth < (TIVO_WIDTH-10):
163 settings.append('-s')
164 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
166 leftPadding = ((TIVO_WIDTH - endWidth)/2)
167 if leftPadding % 2:
168 leftPadding -= 1
170 settings.append('-padleft')
171 settings.append(str(leftPadding))
172 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
173 settings.append('-padright')
174 settings.append(str(rightPadding))
175 else: #if only very small amount of padding needed, then just stretch it
176 settings.append('-s')
177 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
178 debug_write(['select_aspect: 16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings), '\n'])
179 else: #this is a 4:3 file or 16:9 output not allowed
180 settings.append('-aspect')
181 settings.append('4:3')
182 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3)
183 if endHeight % 2:
184 endHeight -= 1
185 if endHeight < TIVO_HEIGHT * 0.99:
186 settings.append('-s')
187 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
189 topPadding = ((TIVO_HEIGHT - endHeight)/2)
190 if topPadding % 2:
191 topPadding -= 1
193 settings.append('-padtop')
194 settings.append(str(topPadding))
195 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
196 settings.append('-padbottom')
197 settings.append(str(bottomPadding))
198 else: #if only very small amount of padding needed, then just stretch it
199 settings.append('-s')
200 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
201 debug_write(['select_aspect: File is wider than 4:3 padding top and bottom\n', ' '.join(settings), '\n'])
203 return settings
204 #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in
205 #an aspect ratio of 4:3 since they are so narrow.
206 else:
207 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3))
208 settings.append('-aspect')
209 settings.append('4:3')
210 if endWidth % 2:
211 endWidth -= 1
212 if endWidth < (TIVO_WIDTH * 0.99):
213 settings.append('-s')
214 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
216 leftPadding = ((TIVO_WIDTH - endWidth)/2)
217 if leftPadding % 2:
218 leftPadding -= 1
220 settings.append('-padleft')
221 settings.append(str(leftPadding))
222 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
223 settings.append('-padright')
224 settings.append(str(rightPadding))
225 else: #if only very small amount of padding needed, then just stretch it
226 settings.append('-s')
227 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
229 debug_write(['select_aspect: File is taller than 4:3 padding left and right\n', ' '.join(settings), '\n'])
231 return settings
233 def tivo_compatable(inFile, tsn = ''):
234 supportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]]
235 type, width, height, fps, millisecs, kbps, akbps, acodec = video_info(inFile)
236 #print type, width, height, fps, millisecs, kbps, akbps, acodec
238 if (inFile[-5:]).lower() == '.tivo':
239 debug_write(['tivo_compatible: ', inFile, ' ends with .tivo\n'])
240 return True
242 if not type == 'mpeg2video':
243 #print 'Not Tivo Codec'
244 debug_write(['tivo_compatible: ', inFile, ' is not mpeg2video it is ', type, '\n'])
245 return False
247 if (inFile[-3:]).lower() == '.ts':
248 debug_write(['tivo_compatible: ', inFile, ' transport stream not supported ', '\n'])
249 return False
251 if not akbps or int(akbps) > config.getMaxAudioBR(tsn):
252 debug_write(['tivo_compatible: ', inFile, ' max audio bitrate exceeded it is ', akbps, '\n'])
253 return False
255 if not kbps or int(kbps)-int(akbps) > config.strtod(config.getMaxVideoBR())/1000:
256 debug_write(['tivo_compatible: ', inFile, ' max video bitrate exceeded it is ', kbps, '\n'])
257 return False
259 if config.isHDtivo(tsn):
260 debug_write(['tivo_compatible: ', inFile, ' you have a S3 skiping the rest of the tests', '\n'])
261 return True
263 if not fps == '29.97':
264 #print 'Not Tivo fps'
265 debug_write(['tivo_compatible: ', inFile, ' is not correct fps it is ', fps, '\n'])
266 return False
268 for mode in supportedModes:
269 if (mode[0], mode[1]) == (width, height):
270 #print 'Is TiVo!'
271 debug_write(['tivo_compatible: ', inFile, ' has correct width of ', width, ' and height of ', height, '\n'])
272 return True
273 #print 'Not Tivo dimensions'
274 return False
276 def video_info(inFile):
277 mtime = os.stat(inFile).st_mtime
278 if inFile != videotest:
279 if inFile in info_cache and info_cache[inFile][0] == mtime:
280 debug_write(['video_info: ', inFile, ' cache hit!', '\n'])
281 return info_cache[inFile][1]
283 if (inFile[-5:]).lower() == '.tivo':
284 info_cache[inFile] = (mtime, (True, True, True, True, True, True, True, True))
285 debug_write(['video_info: ', inFile, ' ends in .tivo.\n'])
286 return True, True, True, True, True, True, True, True
288 cmd = [FFMPEG, '-i', inFile ]
289 ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
291 # wait 10 sec if ffmpeg is not back give up
292 for i in xrange(200):
293 time.sleep(.05)
294 if not ffmpeg.poll() == None:
295 break
297 if ffmpeg.poll() == None:
298 kill(ffmpeg.pid)
299 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None))
300 return None, None, None, None, None, None, None, None
302 output = ffmpeg.stderr.read()
303 debug_write(['video_info: ffmpeg output=', output, '\n'])
305 rezre = re.compile(r'.*Video: ([^,]+),.*')
306 x = rezre.search(output)
307 if x:
308 codec = x.group(1)
309 else:
310 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None))
311 debug_write(['video_info: failed at codec\n'])
312 return None, None, None, None, None, None, None, None
314 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
315 x = rezre.search(output)
316 if x:
317 width = int(x.group(1))
318 height = int(x.group(2))
319 else:
320 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None))
321 debug_write(['video_info: failed at width/height\n'])
322 return None, None, None, None, None, None, None, None
324 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
325 x = rezre.search(output)
326 if x:
327 fps = x.group(1)
328 else:
329 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None))
330 debug_write(['video_info: failed at fps\n'])
331 return None, None, None, None, None, None, None, None
333 # Allow override only if it is mpeg2 and frame rate was doubled to 59.94
334 if (not fps == '29.97') and (codec == 'mpeg2video'):
335 # First look for the build 7215 version
336 rezre = re.compile(r'.*film source: 29.97.*')
337 x = rezre.search(output.lower() )
338 if x:
339 debug_write(['video_info: film source: 29.97 setting fps to 29.97\n'])
340 fps = '29.97'
341 else:
342 # for build 8047:
343 rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*')
344 debug_write(['video_info: Bug in VideoReDo\n'])
345 x = rezre.search(output.lower() )
346 if x:
347 fps = '29.97'
349 durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),')
350 d = durre.search(output)
351 if d:
352 millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100)
353 else:
354 millisecs = 0
356 #get bitrate of source for tivo compatibility test.
357 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
358 x = rezre.search(output)
359 if x:
360 kbps = x.group(1)
361 else:
362 kbps = None
363 debug_write(['video_info: failed at kbps\n'])
365 #get audio bitrate of source for tivo compatibility test.
366 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
367 x = rezre.search(output)
368 if x:
369 akbps = x.group(1)
370 else:
371 akbps = None
372 debug_write(['video_info: failed at akbps\n'])
374 #get audio codec of source for tivo compatibility test.
375 rezre = re.compile(r'.*Audio: ([^,]+),.*')
376 x = rezre.search(output)
377 if x:
378 acodec = x.group(1)
379 else:
380 acodec = None
381 debug_write(['video_info: failed at acodec\n'])
383 info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs, kbps, akbps, acodec))
384 debug_write(['video_info: Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' kbps=', kbps, ' akbps=', akbps, ' acodec=', acodec, '\n'])
385 return codec, width, height, fps, millisecs, kbps, akbps, acodec
387 def video_check(inFile, cmd_string):
388 cmd = [FFMPEG, '-i', inFile] + cmd_string.split()
389 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
390 try:
391 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
392 return True
393 except:
394 kill(ffmpeg.pid)
395 return False
397 def supported_format(inFile):
398 if video_info(inFile)[0]:
399 return True
400 else:
401 debug_write(['supported_format: ', inFile, ' is not supported\n'])
402 return False
404 def kill(pid):
405 debug_write(['kill: killing pid=', str(pid), '\n'])
406 if mswindows:
407 win32kill(pid)
408 else:
409 import os, signal
410 os.kill(pid, signal.SIGTERM)
412 def win32kill(pid):
413 import ctypes
414 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
415 ctypes.windll.kernel32.TerminateProcess(handle, -1)
416 ctypes.windll.kernel32.CloseHandle(handle)
418 def gcd(a,b):
419 while b:
420 a, b = b, a % b
421 return a