print tsn, aid for user support
[pyTivo.git] / plugins / video / transcode.py
blobc273aa00924dde3f5c47879acf817bccf2de838c
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['audio_fr'] = select_audiofr(inFile)
50 settings['video_br'] = config.getVideoBR(tsn)
51 settings['max_video_br'] = config.getMaxVideoBR()
52 settings['buff_size'] = BUFF_SIZE
53 settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
55 cmd_string = config.getFFMPEGTemplate(tsn) % settings
57 cmd = [FFMPEG, '-i', inFile] + cmd_string.split()
58 print 'transcoding to tivo model '+tsn[:3]+' using ffmpeg command:'
59 print cmd
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_audiocodec(inFile, tsn = ''):
68 # Default, compatible with all TiVo's
69 codec = '-acodec mp2 -ac 2'
70 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
71 if akbps == None and acodec in ('ac3', 'liba52', 'mp2'):
72 cmd_string = '-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy -t 00:00:01 -f vob -'
73 if video_check(inFile, cmd_string):
74 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(videotest)
75 if config.isHDtivo(tsn):
76 # Is HD Tivo, use ac3
77 codec = '-acodec ac3'
78 if acodec in ('ac3', 'liba52') and not akbps == None and \
79 int(akbps) <= config.getMaxAudioBR(tsn):
80 # compatible codec and bitrate, do not reencode audio
81 codec = '-acodec copy'
82 if acodec == 'mp2' and not akbps == None and \
83 int(akbps) <= config.getMaxAudioBR(tsn):
84 # compatible codec and bitrate, do not reencode audio
85 codec = '-acodec copy'
86 return codec
88 def select_audiofr(inFile):
89 freq = '-ar 48000' #default
90 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
91 if not afreq == None and afreq in ('44100', '48000'):
92 # compatible frequency
93 freq = '-ar ' + afreq
94 return freq
96 def select_aspect(inFile, tsn = ''):
97 TIVO_WIDTH = config.getTivoWidth(tsn)
98 TIVO_HEIGHT = config.getTivoHeight(tsn)
100 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
102 debug_write(['tsn:', tsn, '\n'])
104 aspect169 = config.get169Setting(tsn)
106 debug_write(['aspect169:', aspect169, '\n'])
108 optres = config.getOptres()
110 debug_write(['optres:', optres, '\n'])
112 if optres:
113 optHeight = config.nearestTivoHeight(height)
114 optWidth = config.nearestTivoWidth(width)
115 if optHeight < TIVO_HEIGHT:
116 TIVO_HEIGHT = optHeight
117 if optWidth < TIVO_WIDTH:
118 TIVO_WIDTH = optWidth
120 d = gcd(height,width)
121 ratio = (width*100)/height
122 rheight, rwidth = height/d, width/d
124 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'])
126 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
127 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
129 if config.isHDtivo(tsn) and height <= TIVO_HEIGHT and config.getOptres() == False:
130 return [] #pass all resolutions to S3/HD, except heights greater than conf height
131 # else, optres is enabled and resizes SD video to the "S2" standard on S3/HD.
132 elif (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]:
133 debug_write(['select_aspect: File is within 4:3 list.\n'])
134 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
135 elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169:
136 debug_write(['select_aspect: File is within 16:9 list and 16:9 allowed.\n'])
137 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
138 else:
139 settings = []
140 #If video is wider than 4:3 add top and bottom padding
141 if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom
142 if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3
143 if (ratio > 177):#too short needs padding top and bottom
144 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9)
145 settings.append('-aspect')
146 settings.append('16:9')
147 if endHeight % 2:
148 endHeight -= 1
149 if endHeight < TIVO_HEIGHT * 0.99:
150 settings.append('-s')
151 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
153 topPadding = ((TIVO_HEIGHT - endHeight)/2)
154 if topPadding % 2:
155 topPadding -= 1
157 settings.append('-padtop')
158 settings.append(str(topPadding))
159 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
160 settings.append('-padbottom')
161 settings.append(str(bottomPadding))
162 else: #if only very small amount of padding needed, then just stretch it
163 settings.append('-s')
164 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
165 debug_write(['select_aspect: 16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n', ' '.join(settings), '\n'])
166 else: #too skinny needs padding on left and right.
167 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9))
168 settings.append('-aspect')
169 settings.append('16:9')
170 if endWidth % 2:
171 endWidth -= 1
172 if endWidth < (TIVO_WIDTH-10):
173 settings.append('-s')
174 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
176 leftPadding = ((TIVO_WIDTH - endWidth)/2)
177 if leftPadding % 2:
178 leftPadding -= 1
180 settings.append('-padleft')
181 settings.append(str(leftPadding))
182 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
183 settings.append('-padright')
184 settings.append(str(rightPadding))
185 else: #if only very small amount of padding needed, then just stretch it
186 settings.append('-s')
187 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
188 debug_write(['select_aspect: 16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings), '\n'])
189 else: #this is a 4:3 file or 16:9 output not allowed
190 settings.append('-aspect')
191 settings.append('4:3')
192 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3)
193 if endHeight % 2:
194 endHeight -= 1
195 if endHeight < TIVO_HEIGHT * 0.99:
196 settings.append('-s')
197 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
199 topPadding = ((TIVO_HEIGHT - endHeight)/2)
200 if topPadding % 2:
201 topPadding -= 1
203 settings.append('-padtop')
204 settings.append(str(topPadding))
205 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
206 settings.append('-padbottom')
207 settings.append(str(bottomPadding))
208 else: #if only very small amount of padding needed, then just stretch it
209 settings.append('-s')
210 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
211 debug_write(['select_aspect: File is wider than 4:3 padding top and bottom\n', ' '.join(settings), '\n'])
213 return settings
214 #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in
215 #an aspect ratio of 4:3 since they are so narrow.
216 else:
217 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3))
218 settings.append('-aspect')
219 settings.append('4:3')
220 if endWidth % 2:
221 endWidth -= 1
222 if endWidth < (TIVO_WIDTH * 0.99):
223 settings.append('-s')
224 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
226 leftPadding = ((TIVO_WIDTH - endWidth)/2)
227 if leftPadding % 2:
228 leftPadding -= 1
230 settings.append('-padleft')
231 settings.append(str(leftPadding))
232 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
233 settings.append('-padright')
234 settings.append(str(rightPadding))
235 else: #if only very small amount of padding needed, then just stretch it
236 settings.append('-s')
237 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
239 debug_write(['select_aspect: File is taller than 4:3 padding left and right\n', ' '.join(settings), '\n'])
241 return settings
243 def tivo_compatable(inFile, tsn = ''):
244 supportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]]
245 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
246 #print type, width, height, fps, millisecs, kbps, akbps, acodec
248 if (inFile[-5:]).lower() == '.tivo':
249 debug_write(['tivo_compatible: ', inFile, ' ends with .tivo\n'])
250 return True
252 if not type == 'mpeg2video':
253 #print 'Not Tivo Codec'
254 debug_write(['tivo_compatible: ', inFile, ' is not mpeg2video it is ', type, '\n'])
255 return False
257 if (inFile[-3:]).lower() == '.ts':
258 debug_write(['tivo_compatible: ', inFile, ' transport stream not supported ', '\n'])
259 return False
261 if not akbps or int(akbps) > config.getMaxAudioBR(tsn):
262 debug_write(['tivo_compatible: ', inFile, ' max audio bitrate exceeded it is ', akbps, '\n'])
263 return False
265 if not kbps or int(kbps)-int(akbps) > config.strtod(config.getMaxVideoBR())/1000:
266 debug_write(['tivo_compatible: ', inFile, ' max video bitrate exceeded it is ', kbps, '\n'])
267 return False
269 if config.isHDtivo(tsn):
270 debug_write(['tivo_compatible: ', inFile, ' you have a S3 skiping the rest of the tests', '\n'])
271 return True
273 if not fps == '29.97':
274 #print 'Not Tivo fps'
275 debug_write(['tivo_compatible: ', inFile, ' is not correct fps it is ', fps, '\n'])
276 return False
278 for mode in supportedModes:
279 if (mode[0], mode[1]) == (width, height):
280 #print 'Is TiVo!'
281 debug_write(['tivo_compatible: ', inFile, ' has correct width of ', width, ' and height of ', height, '\n'])
282 return True
283 #print 'Not Tivo dimensions'
284 return False
286 def video_info(inFile):
287 mtime = os.stat(inFile).st_mtime
288 if inFile != videotest:
289 if inFile in info_cache and info_cache[inFile][0] == mtime:
290 debug_write(['video_info: ', inFile, ' cache hit!', '\n'])
291 return info_cache[inFile][1]
293 if (inFile[-5:]).lower() == '.tivo':
294 info_cache[inFile] = (mtime, (True, True, True, True, True, True, True, True, True))
295 debug_write(['video_info: ', inFile, ' ends in .tivo.\n'])
296 return True, True, True, True, True, True, True, True, True
298 cmd = [FFMPEG, '-i', inFile ]
299 ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
301 # wait 10 sec if ffmpeg is not back give up
302 for i in xrange(200):
303 time.sleep(.05)
304 if not ffmpeg.poll() == None:
305 break
307 if ffmpeg.poll() == None:
308 kill(ffmpeg.pid)
309 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
310 return None, None, None, None, None, None, None, None, None
312 output = ffmpeg.stderr.read()
313 debug_write(['video_info: ffmpeg output=', output, '\n'])
315 rezre = re.compile(r'.*Video: ([^,]+),.*')
316 x = rezre.search(output)
317 if x:
318 codec = x.group(1)
319 else:
320 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
321 debug_write(['video_info: failed at codec\n'])
322 return None, None, None, None, None, None, None, None, None
324 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
325 x = rezre.search(output)
326 if x:
327 width = int(x.group(1))
328 height = int(x.group(2))
329 else:
330 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
331 debug_write(['video_info: failed at width/height\n'])
332 return None, None, None, None, None, None, None, None, None
334 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
335 x = rezre.search(output)
336 if x:
337 fps = x.group(1)
338 else:
339 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
340 debug_write(['video_info: failed at fps\n'])
341 return None, None, None, None, None, None, None, None, None
343 # Allow override only if it is mpeg2 and frame rate was doubled to 59.94
344 if (not fps == '29.97') and (codec == 'mpeg2video'):
345 # First look for the build 7215 version
346 rezre = re.compile(r'.*film source: 29.97.*')
347 x = rezre.search(output.lower() )
348 if x:
349 debug_write(['video_info: film source: 29.97 setting fps to 29.97\n'])
350 fps = '29.97'
351 else:
352 # for build 8047:
353 rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*')
354 debug_write(['video_info: Bug in VideoReDo\n'])
355 x = rezre.search(output.lower() )
356 if x:
357 fps = '29.97'
359 durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),')
360 d = durre.search(output)
361 if d:
362 millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100)
363 else:
364 millisecs = 0
366 #get bitrate of source for tivo compatibility test.
367 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
368 x = rezre.search(output)
369 if x:
370 kbps = x.group(1)
371 else:
372 kbps = None
373 debug_write(['video_info: failed at kbps\n'])
375 #get audio bitrate of source for tivo compatibility test.
376 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
377 x = rezre.search(output)
378 if x:
379 akbps = x.group(1)
380 else:
381 akbps = None
382 debug_write(['video_info: failed at akbps\n'])
384 #get audio codec of source for tivo compatibility test.
385 rezre = re.compile(r'.*Audio: ([^,]+),.*')
386 x = rezre.search(output)
387 if x:
388 acodec = x.group(1)
389 else:
390 acodec = None
391 debug_write(['video_info: failed at acodec\n'])
393 #get audio frequency of source for tivo compatibility test.
394 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
395 x = rezre.search(output)
396 if x:
397 afreq = x.group(1)
398 else:
399 afreq = None
400 debug_write(['video_info: failed at afreq\n'])
402 info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq))
403 debug_write(['video_info: Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' kbps=', kbps, ' akbps=', akbps, ' acodec=', acodec, ' afreq=', afreq, '\n'])
404 return codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq
406 def video_check(inFile, cmd_string):
407 cmd = [FFMPEG, '-i', inFile] + cmd_string.split()
408 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
409 try:
410 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
411 return True
412 except:
413 kill(ffmpeg.pid)
414 return False
416 def supported_format(inFile):
417 if video_info(inFile)[0]:
418 return True
419 else:
420 debug_write(['supported_format: ', inFile, ' is not supported\n'])
421 return False
423 def kill(pid):
424 debug_write(['kill: killing pid=', str(pid), '\n'])
425 if mswindows:
426 win32kill(pid)
427 else:
428 import os, signal
429 os.kill(pid, signal.SIGTERM)
431 def win32kill(pid):
432 import ctypes
433 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
434 ctypes.windll.kernel32.TerminateProcess(handle, -1)
435 ctypes.windll.kernel32.CloseHandle(handle)
437 def gcd(a,b):
438 while b:
439 a, b = b, a % b
440 return a