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