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