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