per tivo optres needed
[pyTivo/wgw.git] / plugins / video / transcode.py
blob20a9e812aed1de7c7f7cdedbdbc2fc7f952c2760
1 import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache
2 import config
3 from debug import debug_write, fn_attr
5 info_cache = lrucache.LRUCache(1000)
6 videotest = os.path.join(os.path.dirname(__file__), 'videotest.mpg')
8 def ffmpeg_path():
9 return config.get('Server', 'ffmpeg')
11 # XXX BIG HACK
12 # subprocess is broken for me on windows so super hack
13 def patchSubprocess():
14 o = subprocess.Popen._make_inheritable
16 def _make_inheritable(self, handle):
17 if not handle: return subprocess.GetCurrentProcess()
18 return o(self, handle)
20 subprocess.Popen._make_inheritable = _make_inheritable
21 mswindows = (sys.platform == "win32")
22 if mswindows:
23 patchSubprocess()
25 def output_video(inFile, outFile, tsn=''):
26 if tivo_compatable(inFile, tsn):
27 debug_write(__name__, fn_attr(), [inFile, ' is tivo compatible'])
28 f = file(inFile, 'rb')
29 shutil.copyfileobj(f, outFile)
30 f.close()
31 else:
32 debug_write(__name__, fn_attr(), [inFile, ' is not tivo compatible'])
33 transcode(inFile, outFile, tsn)
35 def transcode(inFile, outFile, tsn=''):
37 settings = {}
38 settings['video_codec'] = select_videocodec(tsn)
39 settings['video_br'] = select_videobr(tsn)
40 settings['video_fps'] = select_videofps(tsn)
41 settings['max_video_br'] = select_maxvideobr()
42 settings['buff_size'] = select_buffsize()
43 settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
44 settings['audio_br'] = select_audiobr(tsn)
45 settings['audio_fr'] = select_audiofr(inFile, tsn)
46 settings['audio_ch'] = select_audioch(tsn)
47 settings['audio_codec'] = select_audiocodec(inFile, tsn)
48 settings['ffmpeg_pram'] = select_ffmpegprams(tsn)
49 settings['format'] = select_format(tsn)
51 cmd_string = config.getFFmpegTemplate(tsn) % settings
53 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
54 print 'transcoding to tivo model '+tsn[:3]+' using ffmpeg command:'
55 print ' '.join(cmd)
56 debug_write(__name__, fn_attr(), ['ffmpeg command is ', ' '.join(cmd)])
57 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
58 try:
59 shutil.copyfileobj(ffmpeg.stdout, outFile)
60 except:
61 kill(ffmpeg.pid)
63 def select_audiocodec(inFile, tsn = ''):
64 # Default, compatible with all TiVo's
65 codec = 'mp2'
66 if config.getAudioCodec(tsn) == None:
67 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
68 if akbps == None and acodec in ('ac3', 'liba52', 'mp2'):
69 cmd_string = '-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy -t 00:00:01 -f vob -'
70 if video_check(inFile, cmd_string):
71 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(videotest)
72 if config.isHDtivo(tsn):
73 # Is HD Tivo, use ac3
74 codec = 'ac3'
75 if acodec in ('ac3', 'liba52') and not akbps == None and \
76 int(akbps) <= config.getMaxAudioBR(tsn):
77 # compatible codec and bitrate, do not reencode audio
78 codec = 'copy'
79 if acodec == 'mp2' and not akbps == None and \
80 int(akbps) <= config.getMaxAudioBR(tsn):
81 # compatible codec and bitrate, do not reencode audio
82 codec = 'copy'
83 else:
84 codec = config.getAudioCodec(tsn)
85 return '-acodec '+codec
87 def select_audiofr(inFile, tsn):
88 freq = '48000' #default
89 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
90 if not afreq == None and afreq in ('44100', '48000'):
91 # compatible frequency
92 freq = afreq
93 if config.getAudioFR(tsn) != None:
94 freq = config.getAudioFR(tsn)
95 return '-ar '+freq
97 def select_audioch(tsn):
98 if config.getAudioCH(tsn) != None:
99 return '-ac '+config.getAudioCH(tsn)
100 if not config.isHDtivo(tsn):
101 return '-ac 2'
102 return ''
104 def select_videofps(tsn):
105 vfps = '-r 29.97' #default
106 if config.isHDtivo(tsn):
107 vfps = ' '
108 if config.getVideoFPS(tsn) != None:
109 vfps = '-r '+config.getVideoFPS(tsn)
110 return vfps
112 def select_videocodec(tsn):
113 vcodec = 'mpeg2video' #default
114 if config.getVideoCodec(tsn) != None:
115 vcodec = config.getVideoCodec(tsn)
116 return '-vcodec '+vcodec
118 def select_videobr(tsn):
119 return '-b '+config.getVideoBR(tsn)
121 def select_audiobr(tsn):
122 return '-ab '+config.getAudioBR(tsn)
124 def select_maxvideobr():
125 return '-maxrate '+config.getMaxVideoBR()
127 def select_buffsize():
128 return '-bufsize '+config.getBuffSize()
130 def select_ffmpegprams(tsn):
131 if config.getFFmpegPrams(tsn) != None:
132 return config.getFFmpegPrams(tsn)
133 return ''
135 def select_format(tsn):
136 fmt = 'vob'
137 if config.getFormat(tsn) != None:
138 fmt = config.getFormat(tsn)
139 return '-f '+fmt+' -'
141 def select_aspect(inFile, tsn = ''):
142 TIVO_WIDTH = config.getTivoWidth(tsn)
143 TIVO_HEIGHT = config.getTivoHeight(tsn)
145 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
147 debug_write(__name__, fn_attr(), ['tsn:', tsn])
149 aspect169 = config.get169Setting(tsn)
151 debug_write(__name__, fn_attr(), ['aspect169:', aspect169])
153 optres = config.getOptres(tsn)
155 debug_write(__name__, fn_attr(), ['optres:', optres])
157 if optres:
158 optHeight = config.nearestTivoHeight(height)
159 optWidth = config.nearestTivoWidth(width)
160 if optHeight < TIVO_HEIGHT:
161 TIVO_HEIGHT = optHeight
162 if optWidth < TIVO_WIDTH:
163 TIVO_WIDTH = optWidth
165 d = gcd(height,width)
166 ratio = (width*100)/height
167 rheight, rwidth = height/d, width/d
169 debug_write(__name__, fn_attr(), ['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])
171 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
172 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
174 if config.isHDtivo(tsn) and height <= TIVO_HEIGHT and not optres:
175 return [] #pass all resolutions to S3/HD, except heights greater than conf height
176 # else, optres is enabled and resizes SD video to the "S2" standard on S3/HD.
177 elif (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]:
178 debug_write(__name__, fn_attr(), ['File is within 4:3 list.'])
179 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
180 elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169:
181 debug_write(__name__, fn_attr(), ['File is within 16:9 list and 16:9 allowed.'])
182 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
183 else:
184 settings = []
185 #If video is wider than 4:3 add top and bottom padding
186 if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom
187 if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3
188 if (ratio > 177):#too short needs padding top and bottom
189 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9)
190 settings.append('-aspect')
191 settings.append('16:9')
192 if endHeight % 2:
193 endHeight -= 1
194 if endHeight < TIVO_HEIGHT * 0.99:
195 settings.append('-s')
196 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
198 topPadding = ((TIVO_HEIGHT - endHeight)/2)
199 if topPadding % 2:
200 topPadding -= 1
202 settings.append('-padtop')
203 settings.append(str(topPadding))
204 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
205 settings.append('-padbottom')
206 settings.append(str(bottomPadding))
207 else: #if only very small amount of padding needed, then just stretch it
208 settings.append('-s')
209 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
210 debug_write(__name__, fn_attr(), ['16:9 aspect allowed, file is wider than 16:9 padding top and bottom', ' '.join(settings)])
211 else: #too skinny needs padding on left and right.
212 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9))
213 settings.append('-aspect')
214 settings.append('16:9')
215 if endWidth % 2:
216 endWidth -= 1
217 if endWidth < (TIVO_WIDTH-10):
218 settings.append('-s')
219 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
221 leftPadding = ((TIVO_WIDTH - endWidth)/2)
222 if leftPadding % 2:
223 leftPadding -= 1
225 settings.append('-padleft')
226 settings.append(str(leftPadding))
227 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
228 settings.append('-padright')
229 settings.append(str(rightPadding))
230 else: #if only very small amount of padding needed, then just stretch it
231 settings.append('-s')
232 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
233 debug_write(__name__, fn_attr(), ['16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings)])
234 else: #this is a 4:3 file or 16:9 output not allowed
235 settings.append('-aspect')
236 settings.append('4:3')
237 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3)
238 if endHeight % 2:
239 endHeight -= 1
240 if endHeight < TIVO_HEIGHT * 0.99:
241 settings.append('-s')
242 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
244 topPadding = ((TIVO_HEIGHT - endHeight)/2)
245 if topPadding % 2:
246 topPadding -= 1
248 settings.append('-padtop')
249 settings.append(str(topPadding))
250 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
251 settings.append('-padbottom')
252 settings.append(str(bottomPadding))
253 else: #if only very small amount of padding needed, then just stretch it
254 settings.append('-s')
255 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
256 debug_write(__name__, fn_attr(), ['File is wider than 4:3 padding top and bottom\n', ' '.join(settings)])
258 return settings
259 #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in
260 #an aspect ratio of 4:3 since they are so narrow.
261 else:
262 endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3))
263 settings.append('-aspect')
264 settings.append('4:3')
265 if endWidth % 2:
266 endWidth -= 1
267 if endWidth < (TIVO_WIDTH * 0.99):
268 settings.append('-s')
269 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
271 leftPadding = ((TIVO_WIDTH - endWidth)/2)
272 if leftPadding % 2:
273 leftPadding -= 1
275 settings.append('-padleft')
276 settings.append(str(leftPadding))
277 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
278 settings.append('-padright')
279 settings.append(str(rightPadding))
280 else: #if only very small amount of padding needed, then just stretch it
281 settings.append('-s')
282 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
284 debug_write(__name__, fn_attr(), ['File is taller than 4:3 padding left and right\n', ' '.join(settings)])
286 return settings
288 def tivo_compatable(inFile, tsn = ''):
289 supportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]]
290 type, width, height, fps, millisecs, kbps, akbps, acodec, afreq = video_info(inFile)
291 #print type, width, height, fps, millisecs, kbps, akbps, acodec
293 if (inFile[-5:]).lower() == '.tivo':
294 debug_write(__name__, fn_attr(), ['TRUE, ends with .tivo.', inFile])
295 return True
297 if not type == 'mpeg2video':
298 #print 'Not Tivo Codec'
299 debug_write(__name__, fn_attr(), ['FALSE, type', type, 'not mpeg2video.', inFile])
300 return False
302 if (inFile[-3:]).lower() == '.ts':
303 debug_write(__name__, fn_attr(), ['FALSE, transport stream not supported.', inFile])
304 return False
306 if not akbps or int(akbps) > config.getMaxAudioBR(tsn):
307 debug_write(__name__, fn_attr(), ['FALSE,', akbps, 'kbps exceeds max audio bitrate.', inFile])
308 return False
310 if not kbps or int(kbps)-int(akbps) > config.strtod(config.getMaxVideoBR())/1000:
311 debug_write(__name__, fn_attr(), ['FALSE,', kbps, 'kbps exceeds max video bitrate.', inFile])
312 return False
314 if config.isHDtivo(tsn):
315 debug_write(__name__, fn_attr(), ['TRUE, HD Tivo detected, skipping remaining tests', inFile])
316 return True
318 if not fps == '29.97':
319 #print 'Not Tivo fps'
320 debug_write(__name__, fn_attr(), ['FALSE,', fps, 'fps, should be 29.97.', inFile])
321 return False
323 for mode in supportedModes:
324 if (mode[0], mode[1]) == (width, height):
325 #print 'Is TiVo!'
326 debug_write(__name__, fn_attr(), ['TRUE,', width, 'x', height, 'is valid.', inFile])
327 return True
328 #print 'Not Tivo dimensions'
329 debug_write(__name__, fn_attr(), ['FALSE,', width, 'x', height, 'not in supported modes.', inFile])
330 return False
332 def video_info(inFile):
333 mtime = os.stat(inFile).st_mtime
334 if inFile != videotest:
335 if inFile in info_cache and info_cache[inFile][0] == mtime:
336 debug_write(__name__, fn_attr(), ['CACHE HIT!', inFile])
337 return info_cache[inFile][1]
339 if (inFile[-5:]).lower() == '.tivo':
340 info_cache[inFile] = (mtime, (True, True, True, True, True, True, True, True, True))
341 debug_write(__name__, fn_attr(), ['VALID, ends in .tivo.', inFile])
342 return True, True, True, True, True, True, True, True, True
344 cmd = [ffmpeg_path(), '-i', inFile ]
345 ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
347 # wait 10 sec if ffmpeg is not back give up
348 for i in xrange(200):
349 time.sleep(.05)
350 if not ffmpeg.poll() == None:
351 break
353 if ffmpeg.poll() == None:
354 kill(ffmpeg.pid)
355 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
356 return None, None, None, None, None, None, None, None, None
358 output = ffmpeg.stderr.read()
359 debug_write(__name__, fn_attr(), ['ffmpeg output=', output])
361 rezre = re.compile(r'.*Video: ([^,]+),.*')
362 x = rezre.search(output)
363 if x:
364 codec = x.group(1)
365 else:
366 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
367 debug_write(__name__, fn_attr(), ['failed at video codec'])
368 return None, None, None, None, None, None, None, None, None
370 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
371 x = rezre.search(output)
372 if x:
373 width = int(x.group(1))
374 height = int(x.group(2))
375 else:
376 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
377 debug_write(__name__, fn_attr(), ['failed at width/height'])
378 return None, None, None, None, None, None, None, None, None
380 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
381 x = rezre.search(output)
382 if x:
383 fps = x.group(1)
384 else:
385 info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None))
386 debug_write(__name__, fn_attr(), ['failed at fps'])
387 return None, None, None, None, None, None, None, None, None
389 # Allow override only if it is mpeg2 and frame rate was doubled to 59.94
390 if (not fps == '29.97') and (codec == 'mpeg2video'):
391 # First look for the build 7215 version
392 rezre = re.compile(r'.*film source: 29.97.*')
393 x = rezre.search(output.lower() )
394 if x:
395 debug_write(__name__, fn_attr(), ['film source: 29.97 setting fps to 29.97'])
396 fps = '29.97'
397 else:
398 # for build 8047:
399 rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*')
400 debug_write(__name__, fn_attr(), ['Bug in VideoReDo'])
401 x = rezre.search(output.lower() )
402 if x:
403 fps = '29.97'
405 durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),')
406 d = durre.search(output)
407 if d:
408 millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100)
409 else:
410 millisecs = 0
412 #get bitrate of source for tivo compatibility test.
413 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
414 x = rezre.search(output)
415 if x:
416 kbps = x.group(1)
417 else:
418 kbps = None
419 debug_write(__name__, fn_attr(), ['failed at kbps'])
421 #get audio bitrate of source for tivo compatibility test.
422 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
423 x = rezre.search(output)
424 if x:
425 akbps = x.group(1)
426 else:
427 akbps = None
428 debug_write(__name__, fn_attr(), ['failed at akbps'])
430 #get audio codec of source for tivo compatibility test.
431 rezre = re.compile(r'.*Audio: ([^,]+),.*')
432 x = rezre.search(output)
433 if x:
434 acodec = x.group(1)
435 else:
436 acodec = None
437 debug_write(__name__, fn_attr(), ['failed at acodec'])
439 #get audio frequency of source for tivo compatibility test.
440 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
441 x = rezre.search(output)
442 if x:
443 afreq = x.group(1)
444 else:
445 afreq = None
446 debug_write(__name__, fn_attr(), ['failed at afreq'])
448 info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq))
449 debug_write(__name__, fn_attr(), ['Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' kbps=', kbps, ' akbps=', akbps, ' acodec=', acodec, ' afreq=', afreq])
450 return codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq
452 def video_check(inFile, cmd_string):
453 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
454 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
455 try:
456 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
457 return True
458 except:
459 kill(ffmpeg.pid)
460 return False
462 def supported_format(inFile):
463 if video_info(inFile)[0]:
464 return True
465 else:
466 debug_write(__name__, fn_attr(), ['FALSE, file not supported', inFile])
467 return False
469 def kill(pid):
470 debug_write(__name__, fn_attr(), ['killing pid=', str(pid)])
471 if mswindows:
472 win32kill(pid)
473 else:
474 import os, signal
475 os.kill(pid, signal.SIGTERM)
477 def win32kill(pid):
478 import ctypes
479 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
480 ctypes.windll.kernel32.TerminateProcess(handle, -1)
481 ctypes.windll.kernel32.CloseHandle(handle)
483 def gcd(a,b):
484 while b:
485 a, b = b, a % b
486 return a