Oops -- % has precedence over +.
[pyTivo/wgw.git] / plugins / video / transcode.py
bloba2f1266f475797829cacdb0de59a7e2a59347f95
1 import ConfigParser
2 import logging
3 import math
4 import os
5 import re
6 import shutil
7 import subprocess
8 import sys
9 import tempfile
10 import time
12 import lrucache
13 import config
14 from plugin import GetPlugin
16 logger = logging.getLogger('pyTivo.video.transcode')
18 info_cache = lrucache.LRUCache(1000)
19 videotest = os.path.join(os.path.dirname(__file__), 'videotest.mpg')
21 BAD_MPEG_FPS = ['15.00']
23 def ffmpeg_path():
24 return config.get('Server', 'ffmpeg')
26 # XXX BIG HACK
27 # subprocess is broken for me on windows so super hack
28 def patchSubprocess():
29 o = subprocess.Popen._make_inheritable
31 def _make_inheritable(self, handle):
32 if not handle: return subprocess.GetCurrentProcess()
33 return o(self, handle)
35 subprocess.Popen._make_inheritable = _make_inheritable
36 mswindows = (sys.platform == "win32")
37 if mswindows:
38 patchSubprocess()
40 def output_video(inFile, outFile, tsn=''):
41 if tivo_compatible(inFile, tsn)[0]:
42 logger.debug('%s is tivo compatible' % inFile)
43 f = file(inFile, 'rb')
44 shutil.copyfileobj(f, outFile)
45 f.close()
46 else:
47 logger.debug('%s is not tivo compatible' % inFile)
48 transcode(False, inFile, outFile, tsn)
50 def transcode(isQuery, inFile, outFile, tsn=''):
52 settings = {}
53 settings['video_codec'] = select_videocodec(tsn)
54 settings['video_br'] = select_videobr(inFile, tsn)
55 settings['video_fps'] = select_videofps(inFile, tsn)
56 settings['max_video_br'] = select_maxvideobr()
57 settings['buff_size'] = select_buffsize(tsn)
58 settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
59 settings['audio_br'] = select_audiobr(tsn)
60 settings['audio_fr'] = select_audiofr(inFile, tsn)
61 settings['audio_ch'] = select_audioch(tsn)
62 settings['audio_codec'] = select_audiocodec(isQuery, inFile, tsn)
63 settings['audio_lang'] = select_audiolang(inFile, tsn)
64 settings['ffmpeg_pram'] = select_ffmpegprams(tsn)
65 settings['format'] = select_format(tsn)
67 if isQuery:
68 return settings
70 cmd_string = config.getFFmpegTemplate(tsn) % settings
72 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
73 logging.debug('transcoding to tivo model '+tsn[:3]+' using ffmpeg command:')
74 logging.debug(' '.join(cmd))
75 ffmpeg = subprocess.Popen(cmd, bufsize=512*1024, stdout=subprocess.PIPE)
77 try:
78 shutil.copyfileobj(ffmpeg.stdout, outFile)
79 except:
80 kill(ffmpeg.pid)
82 def select_audiocodec(isQuery, inFile, tsn = ''):
83 # Default, compatible with all TiVo's
84 codec = 'ac3'
85 vInfo = video_info(inFile)
86 codectype = vInfo['vCodec']
87 if config.getAudioCodec(tsn) == None:
88 if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'):
89 if vInfo['aKbps'] == None:
90 if not isQuery:
91 cmd_string = '-y -vcodec mpeg2video -r 29.97 ' + \
92 '-b 1000k -acodec copy ' + \
93 select_audiolang(inFile, tsn) + \
94 ' -t 00:00:01 -f vob -'
95 if video_check(inFile, cmd_string):
96 vInfo = video_info(videotest)
97 else:
98 codec = 'TBD'
99 if (not vInfo['aKbps'] == None and
100 int(vInfo['aKbps']) <= config.getMaxAudioBR(tsn)):
101 # compatible codec and bitrate, do not reencode audio
102 codec = 'copy'
103 else:
104 codec = config.getAudioCodec(tsn)
105 copyts = ' -copyts'
106 if ((codec == 'copy' and config.getCopyTS(tsn).lower() == 'none'
107 and codectype == 'mpeg2video') or
108 config.getCopyTS(tsn).lower() == 'false'):
109 copyts = ''
110 return '-acodec ' + codec + copyts
112 def select_audiofr(inFile, tsn):
113 freq = '48000' #default
114 vInfo = video_info(inFile)
115 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
116 # compatible frequency
117 freq = vInfo['aFreq']
118 if config.getAudioFR(tsn) != None:
119 freq = config.getAudioFR(tsn)
120 return '-ar '+freq
122 def select_audioch(tsn):
123 if config.getAudioCH(tsn) != None:
124 return '-ac '+config.getAudioCH(tsn)
125 return ''
127 def select_audiolang(inFile, tsn):
128 vInfo = video_info(inFile)
129 if config.getAudioLang(tsn) != None and vInfo['mapVideo'] != None:
130 stream = vInfo['mapAudio'][0][0]
131 langmatch = []
132 for lang in config.getAudioLang(tsn).replace(' ','').lower().split(','):
133 for s, l in vInfo['mapAudio']:
134 if lang in s + l.replace(' ','').lower():
135 langmatch.append(s)
136 stream = s
137 break
138 if langmatch: break
139 if stream is not '':
140 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
141 return ''
143 def select_videofps(inFile, tsn):
144 vInfo = video_info(inFile)
145 fps = '-r 29.97' #default
146 if config.isHDtivo(tsn) and vInfo['vFps'] not in BAD_MPEG_FPS:
147 fps = ' '
148 if config.getVideoFPS(tsn) != None:
149 fps = '-r '+config.getVideoFPS(tsn)
150 return fps
152 def select_videocodec(tsn):
153 codec = 'mpeg2video' #default
154 if config.getVideoCodec(tsn) != None:
155 codec = config.getVideoCodec(tsn)
156 return '-vcodec '+codec
158 def select_videobr(inFile, tsn):
159 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
161 def select_videostr(inFile, tsn):
162 video_str = config.strtod(config.getVideoBR(tsn))
163 if config.isHDtivo(tsn):
164 vInfo = video_info(inFile)
165 if vInfo['kbps'] != None and config.getVideoPCT() > 0:
166 video_percent = int(vInfo['kbps']) * 10 * config.getVideoPCT()
167 video_str = max(video_str, video_percent)
168 video_str = int(min(config.strtod(config.getMaxVideoBR()) * 0.95,
169 video_str))
170 return video_str
172 def select_audiobr(tsn):
173 return '-ab ' + config.getAudioBR(tsn)
175 def select_maxvideobr():
176 return '-maxrate ' + config.getMaxVideoBR()
178 def select_buffsize(tsn):
179 return '-bufsize ' + config.getBuffSize(tsn)
181 def select_ffmpegprams(tsn):
182 if config.getFFmpegPrams(tsn) != None:
183 return config.getFFmpegPrams(tsn)
184 return ''
186 def select_format(tsn):
187 fmt = 'vob'
188 if config.getFormat(tsn) != None:
189 fmt = config.getFormat(tsn)
190 return '-f ' + fmt + ' -'
192 def select_aspect(inFile, tsn = ''):
193 TIVO_WIDTH = config.getTivoWidth(tsn)
194 TIVO_HEIGHT = config.getTivoHeight(tsn)
196 vInfo = video_info(inFile)
198 logging.debug('tsn: %s' % tsn)
200 aspect169 = config.get169Setting(tsn)
202 logging.debug('aspect169:%s' % aspect169)
204 optres = config.getOptres(tsn)
206 logging.debug('optres:%s' % optres)
208 if optres:
209 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
210 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
211 if optHeight < TIVO_HEIGHT:
212 TIVO_HEIGHT = optHeight
213 if optWidth < TIVO_WIDTH:
214 TIVO_WIDTH = optWidth
216 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
217 ratio = (vInfo['vWidth'] * 100) / vInfo['vHeight']
218 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
220 logger.debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s ' +
221 'millisecs=%s ratio=%s rheight=%s rwidth=%s ' +
222 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile,
223 vInfo['vCodec'], vInfo['vWidth'], vInfo['vHeight'],
224 vInfo['vFps'], vInfo['millisecs'], ratio, rheight,
225 rwidth, TIVO_HEIGHT, TIVO_WIDTH))
227 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
228 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
230 if config.isHDtivo(tsn) and not optres:
231 if config.getPixelAR(0):
232 if vInfo['par2'] == None:
233 npar = config.getPixelAR(1)
234 else:
235 npar = vInfo['par2']
237 # adjust for pixel aspect ratio, if set, because TiVo
238 # expects square pixels
240 if npar < 1.0:
241 return ['-s', str(vInfo['vWidth']) + 'x' +
242 str(int(math.ceil(vInfo['vHeight'] / npar)))]
243 elif npar > 1.0:
244 # FFMPEG expects width to be a multiple of two
246 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
247 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
249 if vInfo['vHeight'] <= TIVO_HEIGHT:
250 # pass all resolutions to S3, except heights greater than
251 # conf height
252 return []
253 # else, resize video.
255 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
256 logger.debug('File + PAR is within 4:3.')
257 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
258 str(TIVO_HEIGHT)]
260 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
261 (59, 72), (59, 36), (59, 54)] or
262 vInfo['dar1'] == '4:3'):
263 logger.debug('File is within 4:3 list.')
264 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
265 str(TIVO_HEIGHT)]
267 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
268 (59, 27)] or vInfo['dar1'] == '16:9')
269 and (aspect169 or config.get169Letterbox(tsn))):
270 logger.debug('File is within 16:9 list and 16:9 allowed.')
272 if config.get169Blacklist(tsn) or (aspect169 and
273 config.get169Letterbox(tsn)):
274 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
275 str(TIVO_HEIGHT)]
276 else:
277 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' +
278 str(TIVO_HEIGHT)]
279 else:
280 settings = []
282 # If video is wider than 4:3 add top and bottom padding
284 if (ratio > 133): # Might be 16:9 file, or just need padding on
285 # top and bottom
287 if aspect169 and (ratio > 135): # If file would fall in 4:3
288 # assume it is supposed to be 4:3
290 if (ratio > 177): # too short needs padding top and bottom
291 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
292 vInfo['vWidth']) * multiplier16by9)
293 settings.append('-aspect')
294 if (config.get169Blacklist(tsn) or
295 config.get169Letterbox(tsn)):
296 settings.append('4:3')
297 else:
298 settings.append('16:9')
299 if endHeight % 2:
300 endHeight -= 1
301 if endHeight < TIVO_HEIGHT * 0.99:
302 settings.append('-s')
303 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
305 topPadding = ((TIVO_HEIGHT - endHeight) / 2)
306 if topPadding % 2:
307 topPadding -= 1
309 settings.append('-padtop')
310 settings.append(str(topPadding))
311 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
312 settings.append('-padbottom')
313 settings.append(str(bottomPadding))
314 else: # if only very small amount of padding
315 # needed, then just stretch it
316 settings.append('-s')
317 settings.append(str(TIVO_WIDTH) + 'x' +
318 str(TIVO_HEIGHT))
320 logger.debug(('16:9 aspect allowed, file is wider ' +
321 'than 16:9 padding top and bottom\n%s') %
322 ' '.join(settings))
324 else: # too skinny needs padding on left and right.
325 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
326 (vInfo['vHeight'] * multiplier16by9))
327 settings.append('-aspect')
328 if (config.get169Blacklist(tsn) or
329 config.get169Letterbox(tsn)):
330 settings.append('4:3')
331 else:
332 settings.append('16:9')
333 if endWidth % 2:
334 endWidth -= 1
335 if endWidth < (TIVO_WIDTH - 10):
336 settings.append('-s')
337 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
339 leftPadding = ((TIVO_WIDTH - endWidth) / 2)
340 if leftPadding % 2:
341 leftPadding -= 1
343 settings.append('-padleft')
344 settings.append(str(leftPadding))
345 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
346 settings.append('-padright')
347 settings.append(str(rightPadding))
348 else: # if only very small amount of padding needed,
349 # then just stretch it
350 settings.append('-s')
351 settings.append(str(TIVO_WIDTH) + 'x' +
352 str(TIVO_HEIGHT))
353 logger.debug(('16:9 aspect allowed, file is narrower ' +
354 'than 16:9 padding left and right\n%s') %
355 ' '.join(settings))
356 else: # this is a 4:3 file or 16:9 output not allowed
357 multiplier = multiplier4by3
358 settings.append('-aspect')
359 if ratio > 135 and config.get169Letterbox(tsn):
360 settings.append('16:9')
361 multiplier = multiplier16by9
362 else:
363 settings.append('4:3')
364 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
365 vInfo['vWidth']) * multiplier)
366 if endHeight % 2:
367 endHeight -= 1
368 if endHeight < TIVO_HEIGHT * 0.99:
369 settings.append('-s')
370 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
372 topPadding = ((TIVO_HEIGHT - endHeight)/2)
373 if topPadding % 2:
374 topPadding -= 1
376 settings.append('-padtop')
377 settings.append(str(topPadding))
378 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
379 settings.append('-padbottom')
380 settings.append(str(bottomPadding))
381 else: # if only very small amount of padding needed,
382 # then just stretch it
383 settings.append('-s')
384 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
385 logging.debug(('File is wider than 4:3 padding ' +
386 'top and bottom\n%s') % ' '.join(settings))
388 return settings
390 # If video is taller than 4:3 add left and right padding, this
391 # is rare. All of these files will always be sent in an aspect
392 # ratio of 4:3 since they are so narrow.
394 else:
395 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
396 (vInfo['vHeight'] * multiplier4by3))
397 settings.append('-aspect')
398 settings.append('4:3')
399 if endWidth % 2:
400 endWidth -= 1
401 if endWidth < (TIVO_WIDTH * 0.99):
402 settings.append('-s')
403 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
405 leftPadding = ((TIVO_WIDTH - endWidth) / 2)
406 if leftPadding % 2:
407 leftPadding -= 1
409 settings.append('-padleft')
410 settings.append(str(leftPadding))
411 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
412 settings.append('-padright')
413 settings.append(str(rightPadding))
414 else: # if only very small amount of padding needed, then
415 # just stretch it
416 settings.append('-s')
417 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
419 logger.debug('File is taller than 4:3 padding left and right\n%s'
420 % ' '.join(settings))
422 return settings
424 def tivo_compatible(inFile, tsn = ''):
425 supportedModes = [[720, 480], [704, 480], [544, 480], [528, 480],
426 [480, 480], [352, 480]]
427 vInfo = video_info(inFile)
429 while True:
430 if (inFile[-5:]).lower() == '.tivo':
431 message = (True, 'TRANSCODE=NO, ends with .tivo.')
432 break
434 if not vInfo['vCodec'] == 'mpeg2video':
435 #print 'Not Tivo Codec'
436 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
437 vInfo['vCodec'])
438 break
440 if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv',
441 '.tp', '.dvr-ms'):
442 message = (False, 'TRANSCODE=YES, ext %s not compatible.' %
443 os.path.splitext(inFile)[-1])
444 break
446 if vInfo['aCodec'] == 'dca':
447 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
448 vInfo['aCodec'])
449 break
451 if vInfo['aCodec'] != None:
452 if (not vInfo['aKbps'] or
453 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
454 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
455 'audio bitrate.') % vInfo['aKbps'])
456 break
458 if vInfo['kbps'] != None:
459 abit = max('0', vInfo['aKbps'])
460 if (int(vInfo['kbps']) - int(abit) >
461 config.strtod(config.getMaxVideoBR()) / 1000):
462 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
463 'video bitrate.') % vInfo['kbps'])
464 break
465 else:
466 message = (False, 'TRANSCODE=YES, %s kbps not supported.' %
467 vInfo['kbps'])
468 break
470 if config.getAudioLang(tsn) is not None:
471 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
472 message = (False, ('TRANSCODE=YES, %s preferred audio ' +
473 'track exists.') % config.getAudioLang(tsn))
474 break
476 if config.isHDtivo(tsn):
477 if vInfo['par2'] != 1.0:
478 if config.getPixelAR(0):
479 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
480 message = (False, 'TRANSCODE=YES, %s not correct PAR.'
481 % vInfo['par2'])
482 break
483 message = (True, 'TRANSCODE=NO, HD Tivo detected, skipping ' +
484 'remaining tests.')
485 break
487 if not vInfo['vFps'] == '29.97':
488 #print 'Not Tivo fps'
489 message = (False, 'TRANSCODE=YES, %s vFps, should be 29.97.' %
490 vInfo['vFps'])
491 break
493 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
494 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
495 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9'):
496 message = (False, ('TRANSCODE=YES, DAR %s not supported ' +
497 'by BLACKLIST_169 tivos.') % vInfo['dar1'])
498 break
500 for mode in supportedModes:
501 if (mode[0], mode[1]) == (vInfo['vWidth'], vInfo['vHeight']):
502 message = (True, 'TRANSCODE=NO, %s x %s is valid.' %
503 (vInfo['vWidth'], vInfo['vHeight']))
504 break
505 #print 'Not Tivo dimensions'
506 message = (False, 'TRANSCODE=YES, %s x %s not in supported modes.'
507 % (vInfo['vWidth'], vInfo['vHeight']))
508 break
510 logger.debug('%s, %s' % (message, inFile))
511 return message
514 def video_info(inFile):
515 vInfo = dict()
516 mtime = os.stat(inFile).st_mtime
517 if inFile != videotest:
518 if inFile in info_cache and info_cache[inFile][0] == mtime:
519 logging.debug('CACHE HIT! %s' % inFile)
520 return info_cache[inFile][1]
522 vInfo['Supported'] = True
524 if (inFile[-5:]).lower() == '.tivo':
525 vInfo['millisecs'] = 0
526 info_cache[inFile] = (mtime, vInfo)
527 logger.debug('VALID, ends in .tivo. %s' % inFile)
528 return vInfo
530 cmd = [ffmpeg_path(), '-i', inFile ]
531 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
532 err_tmp = tempfile.TemporaryFile()
533 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
534 stdin=subprocess.PIPE)
536 # wait 10 sec if ffmpeg is not back give up
537 for i in xrange(200):
538 time.sleep(.05)
539 if not ffmpeg.poll() == None:
540 break
542 if ffmpeg.poll() == None:
543 kill(ffmpeg.pid)
544 vInfo['Supported'] = False
545 info_cache[inFile] = (mtime, vInfo)
546 return vInfo
548 err_tmp.seek(0)
549 output = err_tmp.read()
550 err_tmp.close()
551 logging.debug('ffmpeg output=%s' % output)
553 rezre = re.compile(r'.*Video: ([^,]+),.*')
554 x = rezre.search(output)
555 if x:
556 vInfo['vCodec'] = x.group(1)
557 else:
558 vInfo['vCodec'] = ''
559 vInfo['Supported'] = False
560 logger.debug('failed at vCodec')
562 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
563 x = rezre.search(output)
564 if x:
565 vInfo['vWidth'] = int(x.group(1))
566 vInfo['vHeight'] = int(x.group(2))
567 else:
568 vInfo['vWidth'] = ''
569 vInfo['vHeight'] = ''
570 vInfo['Supported'] = False
571 logger.debug('failed at vWidth/vHeight')
573 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
574 x = rezre.search(output)
575 if x:
576 vInfo['vFps'] = x.group(1)
578 # Allow override only if it is mpeg2 and frame rate was doubled
579 # to 59.94
581 if (not vInfo['vFps'] == '29.97') and (vInfo['vCodec'] == 'mpeg2video'):
582 # First look for the build 7215 version
583 rezre = re.compile(r'.*film source: 29.97.*')
584 x = rezre.search(output.lower() )
585 if x:
586 logger.debug('film source: 29.97 setting vFps to 29.97')
587 vInfo['vFps'] = '29.97'
588 else:
589 # for build 8047:
590 rezre = re.compile(r'.*frame rate differs from container ' +
591 r'frame rate: 29.97.*')
592 logger.debug('Bug in VideoReDo')
593 x = rezre.search(output.lower() )
594 if x:
595 vInfo['vFps'] = '29.97'
596 else:
597 vInfo['vFps'] = ''
598 vInfo['Supported'] = False
599 logger.debug('failed at vFps')
601 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
602 d = durre.search(output)
604 if d:
605 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
606 int(d.group(2)) * 60 +
607 int(d.group(3))) * 1000 +
608 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
609 else:
610 vInfo['millisecs'] = 0
612 #get bitrate of source for tivo compatibility test.
613 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
614 x = rezre.search(output)
615 if x:
616 vInfo['kbps'] = x.group(1)
617 else:
618 vInfo['kbps'] = None
619 logger.debug('failed at kbps')
621 #get audio bitrate of source for tivo compatibility test.
622 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
623 x = rezre.search(output)
624 if x:
625 vInfo['aKbps'] = x.group(1)
626 else:
627 vInfo['aKbps'] = None
628 logger.debug('failed at aKbps')
630 #get audio codec of source for tivo compatibility test.
631 rezre = re.compile(r'.*Audio: ([^,]+),.*')
632 x = rezre.search(output)
633 if x:
634 vInfo['aCodec'] = x.group(1)
635 else:
636 vInfo['aCodec'] = None
637 logger.debug('failed at aCodec')
639 #get audio frequency of source for tivo compatibility test.
640 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
641 x = rezre.search(output)
642 if x:
643 vInfo['aFreq'] = x.group(1)
644 else:
645 vInfo['aFreq'] = None
646 logger.debug('failed at aFreq')
648 #get par.
649 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
650 x = rezre.search(output)
651 if x and x.group(1) != "0" and x.group(2) != "0":
652 vInfo['par1'] = x.group(1) + ':' + x.group(2)
653 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
654 else:
655 vInfo['par1'], vInfo['par2'] = None, None
657 #get dar.
658 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
659 x = rezre.search(output)
660 if x and x.group(1) != "0" and x.group(2) != "0":
661 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
662 vInfo['dar2'] = float(x.group(1)) / float(x.group(2))
663 else:
664 vInfo['dar1'], vInfo['dar2'] = None, None
666 #get Video Stream mapping.
667 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
668 x = rezre.search(output)
669 if x:
670 vInfo['mapVideo'] = x.group(1)
671 else:
672 vInfo['mapVideo'] = None
673 logger.debug('failed at mapVideo')
676 #get Audio Stream mapping.
677 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
678 x = rezre.search(output)
679 amap = []
680 if x:
681 for x in rezre.finditer(output):
682 amap.append(x.groups())
683 else:
684 amap.append(('', ''))
685 logger.debug('failed at mapAudio')
686 vInfo['mapAudio'] = amap
689 videoPlugin = GetPlugin('video')
690 metadata = videoPlugin.getMetadataFromTxt(inFile)
692 for key in metadata:
693 if key.startswith('Override_'):
694 vInfo['Supported'] = True
695 if key.startswith('Override_mapAudio'):
696 audiomap = dict(vInfo['mapAudio'])
697 stream = key.replace('Override_mapAudio', '').strip()
698 if audiomap.has_key(stream):
699 newaudiomap = (stream, metadata[key])
700 audiomap.update([newaudiomap])
701 vInfo['mapAudio'] = sorted(audiomap.items(),
702 key=lambda (k,v): (k,v))
703 elif key.startswith('Override_millisecs'):
704 vInfo[key.replace('Override_', '')] = int(metadata[key])
705 else:
706 vInfo[key.replace('Override_', '')] = metadata[key]
708 info_cache[inFile] = (mtime, vInfo)
709 logger.debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
710 return vInfo
712 def video_check(inFile, cmd_string):
713 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
714 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
715 try:
716 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
717 return True
718 except:
719 kill(ffmpeg.pid)
720 return False
722 def supported_format(inFile):
723 if video_info(inFile)['Supported']:
724 return True
725 else:
726 logger.debug('FALSE, file not supported %s' % inFile)
727 return False
729 def kill(pid):
730 logger.debug('killing pid=%s' % str(pid))
731 if mswindows:
732 win32kill(pid)
733 else:
734 import os, signal
735 os.kill(pid, signal.SIGTERM)
737 def win32kill(pid):
738 import ctypes
739 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
740 ctypes.windll.kernel32.TerminateProcess(handle, -1)
741 ctypes.windll.kernel32.CloseHandle(handle)
743 def gcd(a,b):
744 while b:
745 a, b = b, a % b
746 return a