Spacing.
[pyTivo/wgw.git] / plugins / video / transcode.py
blob5e1479a1bd6c01e095ded10955658f0ba9fe77af
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=''):
51 settings = {'video_codec': select_videocodec(tsn),
52 'video_br': select_videobr(inFile, tsn),
53 'video_fps': select_videofps(inFile, tsn),
54 'max_video_br': select_maxvideobr(),
55 'buff_size': select_buffsize(tsn),
56 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
57 'audio_br': select_audiobr(tsn),
58 'audio_fr': select_audiofr(inFile, tsn),
59 'audio_ch': select_audioch(tsn),
60 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
61 'audio_lang': select_audiolang(inFile, tsn),
62 'ffmpeg_pram': select_ffmpegprams(tsn),
63 'format': select_format(tsn)}
65 if isQuery:
66 return settings
68 cmd_string = config.getFFmpegTemplate(tsn) % settings
70 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
71 logging.debug('transcoding to tivo model ' + tsn[:3] +
72 ' using ffmpeg command:')
73 logging.debug(' '.join(cmd))
74 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
75 stdout=subprocess.PIPE)
76 try:
77 shutil.copyfileobj(ffmpeg.stdout, outFile)
78 except:
79 kill(ffmpeg.pid)
81 def select_audiocodec(isQuery, inFile, tsn = ''):
82 # Default, compatible with all TiVo's
83 codec = 'ac3'
84 vInfo = video_info(inFile)
85 codectype = vInfo['vCodec']
86 if config.getAudioCodec(tsn) == None:
87 if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'):
88 if vInfo['aKbps'] == None:
89 if not isQuery:
90 cmd_string = ('-y -vcodec mpeg2video -r 29.97 ' +
91 '-b 1000k -acodec copy ' +
92 select_audiolang(inFile, tsn) +
93 ' -t 00:00:01 -f vob -')
94 if video_check(inFile, cmd_string):
95 vInfo = video_info(videotest)
96 else:
97 codec = 'TBD'
98 if (not vInfo['aKbps'] == None and
99 int(vInfo['aKbps']) <= config.getMaxAudioBR(tsn)):
100 # compatible codec and bitrate, do not reencode audio
101 codec = 'copy'
102 else:
103 codec = config.getAudioCodec(tsn)
104 copyts = ' -copyts'
105 if ((codec == 'copy' and config.getCopyTS(tsn).lower() == 'none'
106 and codectype == 'mpeg2video') or
107 config.getCopyTS(tsn).lower() == 'false'):
108 copyts = ''
109 return '-acodec ' + codec + copyts
111 def select_audiofr(inFile, tsn):
112 freq = '48000' #default
113 vInfo = video_info(inFile)
114 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
115 # compatible frequency
116 freq = vInfo['aFreq']
117 if config.getAudioFR(tsn) != None:
118 freq = config.getAudioFR(tsn)
119 return '-ar '+freq
121 def select_audioch(tsn):
122 if config.getAudioCH(tsn) != None:
123 return '-ac '+config.getAudioCH(tsn)
124 return ''
126 def select_audiolang(inFile, tsn):
127 vInfo = video_info(inFile)
128 if config.getAudioLang(tsn) != None and vInfo['mapVideo'] != None:
129 stream = vInfo['mapAudio'][0][0]
130 langmatch = []
131 for lang in config.getAudioLang(tsn).replace(' ','').lower().split(','):
132 for s, l in vInfo['mapAudio']:
133 if lang in s + l.replace(' ','').lower():
134 langmatch.append(s)
135 stream = s
136 break
137 if langmatch: break
138 if stream is not '':
139 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
140 return ''
142 def select_videofps(inFile, tsn):
143 vInfo = video_info(inFile)
144 fps = '-r 29.97' #default
145 if config.isHDtivo(tsn) and vInfo['vFps'] not in BAD_MPEG_FPS:
146 fps = ' '
147 if config.getVideoFPS(tsn) != None:
148 fps = '-r '+config.getVideoFPS(tsn)
149 return fps
151 def select_videocodec(tsn):
152 codec = 'mpeg2video' #default
153 if config.getVideoCodec(tsn) != None:
154 codec = config.getVideoCodec(tsn)
155 return '-vcodec '+codec
157 def select_videobr(inFile, tsn):
158 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
160 def select_videostr(inFile, tsn):
161 video_str = config.strtod(config.getVideoBR(tsn))
162 if config.isHDtivo(tsn):
163 vInfo = video_info(inFile)
164 if vInfo['kbps'] != None and config.getVideoPCT() > 0:
165 video_percent = int(vInfo['kbps']) * 10 * config.getVideoPCT()
166 video_str = max(video_str, video_percent)
167 video_str = int(min(config.strtod(config.getMaxVideoBR()) * 0.95,
168 video_str))
169 return video_str
171 def select_audiobr(tsn):
172 return '-ab ' + config.getAudioBR(tsn)
174 def select_maxvideobr():
175 return '-maxrate ' + config.getMaxVideoBR()
177 def select_buffsize(tsn):
178 return '-bufsize ' + config.getBuffSize(tsn)
180 def select_ffmpegprams(tsn):
181 if config.getFFmpegPrams(tsn) != None:
182 return config.getFFmpegPrams(tsn)
183 return ''
185 def select_format(tsn):
186 fmt = 'vob'
187 if config.getFormat(tsn) != None:
188 fmt = config.getFormat(tsn)
189 return '-f ' + fmt + ' -'
191 def select_aspect(inFile, tsn = ''):
192 TIVO_WIDTH = config.getTivoWidth(tsn)
193 TIVO_HEIGHT = config.getTivoHeight(tsn)
195 vInfo = video_info(inFile)
197 logging.debug('tsn: %s' % tsn)
199 aspect169 = config.get169Setting(tsn)
201 logging.debug('aspect169:%s' % aspect169)
203 optres = config.getOptres(tsn)
205 logging.debug('optres:%s' % optres)
207 if optres:
208 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
209 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
210 if optHeight < TIVO_HEIGHT:
211 TIVO_HEIGHT = optHeight
212 if optWidth < TIVO_WIDTH:
213 TIVO_WIDTH = optWidth
215 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
216 ratio = (vInfo['vWidth'] * 100) / vInfo['vHeight']
217 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
219 logger.debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s ' +
220 'millisecs=%s ratio=%s rheight=%s rwidth=%s ' +
221 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile,
222 vInfo['vCodec'], vInfo['vWidth'], vInfo['vHeight'],
223 vInfo['vFps'], vInfo['millisecs'], ratio, rheight,
224 rwidth, TIVO_HEIGHT, TIVO_WIDTH))
226 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
227 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
229 if config.isHDtivo(tsn) and not optres:
230 if config.getPixelAR(0):
231 if vInfo['par2'] == None:
232 npar = config.getPixelAR(1)
233 else:
234 npar = vInfo['par2']
236 # adjust for pixel aspect ratio, if set, because TiVo
237 # expects square pixels
239 if npar < 1.0:
240 return ['-s', str(vInfo['vWidth']) + 'x' +
241 str(int(math.ceil(vInfo['vHeight'] / npar)))]
242 elif npar > 1.0:
243 # FFMPEG expects width to be a multiple of two
245 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
246 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
248 if vInfo['vHeight'] <= TIVO_HEIGHT:
249 # pass all resolutions to S3, except heights greater than
250 # conf height
251 return []
252 # else, resize video.
254 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
255 logger.debug('File + PAR is within 4:3.')
256 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
257 str(TIVO_HEIGHT)]
259 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
260 (59, 72), (59, 36), (59, 54)] or
261 vInfo['dar1'] == '4:3'):
262 logger.debug('File is within 4:3 list.')
263 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
264 str(TIVO_HEIGHT)]
266 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
267 (59, 27)] or vInfo['dar1'] == '16:9')
268 and (aspect169 or config.get169Letterbox(tsn))):
269 logger.debug('File is within 16:9 list and 16:9 allowed.')
271 if config.get169Blacklist(tsn) or (aspect169 and
272 config.get169Letterbox(tsn)):
273 return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' +
274 str(TIVO_HEIGHT)]
275 else:
276 return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' +
277 str(TIVO_HEIGHT)]
278 else:
279 settings = []
281 # If video is wider than 4:3 add top and bottom padding
283 if (ratio > 133): # Might be 16:9 file, or just need padding on
284 # top and bottom
286 if aspect169 and (ratio > 135): # If file would fall in 4:3
287 # assume it is supposed to be 4:3
289 if (ratio > 177): # too short needs padding top and bottom
290 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
291 vInfo['vWidth']) * multiplier16by9)
292 settings.append('-aspect')
293 if (config.get169Blacklist(tsn) or
294 config.get169Letterbox(tsn)):
295 settings.append('4:3')
296 else:
297 settings.append('16:9')
298 if endHeight % 2:
299 endHeight -= 1
300 if endHeight < TIVO_HEIGHT * 0.99:
301 settings.append('-s')
302 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
304 topPadding = ((TIVO_HEIGHT - endHeight) / 2)
305 if topPadding % 2:
306 topPadding -= 1
308 settings.append('-padtop')
309 settings.append(str(topPadding))
310 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
311 settings.append('-padbottom')
312 settings.append(str(bottomPadding))
313 else: # if only very small amount of padding
314 # needed, then just stretch it
315 settings.append('-s')
316 settings.append(str(TIVO_WIDTH) + 'x' +
317 str(TIVO_HEIGHT))
319 logger.debug(('16:9 aspect allowed, file is wider ' +
320 'than 16:9 padding top and bottom\n%s') %
321 ' '.join(settings))
323 else: # too skinny needs padding on left and right.
324 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
325 (vInfo['vHeight'] * multiplier16by9))
326 settings.append('-aspect')
327 if (config.get169Blacklist(tsn) or
328 config.get169Letterbox(tsn)):
329 settings.append('4:3')
330 else:
331 settings.append('16:9')
332 if endWidth % 2:
333 endWidth -= 1
334 if endWidth < (TIVO_WIDTH - 10):
335 settings.append('-s')
336 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
338 leftPadding = ((TIVO_WIDTH - endWidth) / 2)
339 if leftPadding % 2:
340 leftPadding -= 1
342 settings.append('-padleft')
343 settings.append(str(leftPadding))
344 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
345 settings.append('-padright')
346 settings.append(str(rightPadding))
347 else: # if only very small amount of padding needed,
348 # then just stretch it
349 settings.append('-s')
350 settings.append(str(TIVO_WIDTH) + 'x' +
351 str(TIVO_HEIGHT))
352 logger.debug(('16:9 aspect allowed, file is narrower ' +
353 'than 16:9 padding left and right\n%s') %
354 ' '.join(settings))
355 else: # this is a 4:3 file or 16:9 output not allowed
356 multiplier = multiplier4by3
357 settings.append('-aspect')
358 if ratio > 135 and config.get169Letterbox(tsn):
359 settings.append('16:9')
360 multiplier = multiplier16by9
361 else:
362 settings.append('4:3')
363 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
364 vInfo['vWidth']) * multiplier)
365 if endHeight % 2:
366 endHeight -= 1
367 if endHeight < TIVO_HEIGHT * 0.99:
368 settings.append('-s')
369 settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
371 topPadding = ((TIVO_HEIGHT - endHeight)/2)
372 if topPadding % 2:
373 topPadding -= 1
375 settings.append('-padtop')
376 settings.append(str(topPadding))
377 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
378 settings.append('-padbottom')
379 settings.append(str(bottomPadding))
380 else: # if only very small amount of padding needed,
381 # then just stretch it
382 settings.append('-s')
383 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
384 logging.debug(('File is wider than 4:3 padding ' +
385 'top and bottom\n%s') % ' '.join(settings))
387 return settings
389 # If video is taller than 4:3 add left and right padding, this
390 # is rare. All of these files will always be sent in an aspect
391 # ratio of 4:3 since they are so narrow.
393 else:
394 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
395 (vInfo['vHeight'] * multiplier4by3))
396 settings.append('-aspect')
397 settings.append('4:3')
398 if endWidth % 2:
399 endWidth -= 1
400 if endWidth < (TIVO_WIDTH * 0.99):
401 settings.append('-s')
402 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
404 leftPadding = ((TIVO_WIDTH - endWidth) / 2)
405 if leftPadding % 2:
406 leftPadding -= 1
408 settings.append('-padleft')
409 settings.append(str(leftPadding))
410 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
411 settings.append('-padright')
412 settings.append(str(rightPadding))
413 else: # if only very small amount of padding needed, then
414 # just stretch it
415 settings.append('-s')
416 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
418 logger.debug('File is taller than 4:3 padding left and right\n%s'
419 % ' '.join(settings))
421 return settings
423 def tivo_compatible(inFile, tsn = ''):
424 supportedModes = [[720, 480], [704, 480], [544, 480], [528, 480],
425 [480, 480], [352, 480]]
426 vInfo = video_info(inFile)
428 while True:
429 if (inFile[-5:]).lower() == '.tivo':
430 message = (True, 'TRANSCODE=NO, ends with .tivo.')
431 break
433 if not vInfo['vCodec'] == 'mpeg2video':
434 #print 'Not Tivo Codec'
435 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
436 vInfo['vCodec'])
437 break
439 if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv',
440 '.tp', '.dvr-ms'):
441 message = (False, 'TRANSCODE=YES, ext %s not compatible.' %
442 os.path.splitext(inFile)[-1])
443 break
445 if vInfo['aCodec'] == 'dca':
446 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
447 vInfo['aCodec'])
448 break
450 if vInfo['aCodec'] != None:
451 if (not vInfo['aKbps'] or
452 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
453 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
454 'audio bitrate.') % vInfo['aKbps'])
455 break
457 if vInfo['kbps'] != None:
458 abit = max('0', vInfo['aKbps'])
459 if (int(vInfo['kbps']) - int(abit) >
460 config.strtod(config.getMaxVideoBR()) / 1000):
461 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
462 'video bitrate.') % vInfo['kbps'])
463 break
464 else:
465 message = (False, 'TRANSCODE=YES, %s kbps not supported.' %
466 vInfo['kbps'])
467 break
469 if config.getAudioLang(tsn) is not None:
470 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
471 message = (False, ('TRANSCODE=YES, %s preferred audio ' +
472 'track exists.') % config.getAudioLang(tsn))
473 break
475 if config.isHDtivo(tsn):
476 if vInfo['par2'] != 1.0:
477 if config.getPixelAR(0):
478 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
479 message = (False, 'TRANSCODE=YES, %s not correct PAR.'
480 % vInfo['par2'])
481 break
482 message = (True, 'TRANSCODE=NO, HD Tivo detected, skipping ' +
483 'remaining tests.')
484 break
486 if not vInfo['vFps'] == '29.97':
487 #print 'Not Tivo fps'
488 message = (False, 'TRANSCODE=YES, %s vFps, should be 29.97.' %
489 vInfo['vFps'])
490 break
492 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
493 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
494 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9'):
495 message = (False, ('TRANSCODE=YES, DAR %s not supported ' +
496 'by BLACKLIST_169 tivos.') % vInfo['dar1'])
497 break
499 for mode in supportedModes:
500 if (mode[0], mode[1]) == (vInfo['vWidth'], vInfo['vHeight']):
501 message = (True, 'TRANSCODE=NO, %s x %s is valid.' %
502 (vInfo['vWidth'], vInfo['vHeight']))
503 break
504 #print 'Not Tivo dimensions'
505 message = (False, 'TRANSCODE=YES, %s x %s not in supported modes.'
506 % (vInfo['vWidth'], vInfo['vHeight']))
507 break
509 logger.debug('%s, %s' % (message, inFile))
510 return message
513 def video_info(inFile):
514 vInfo = dict()
515 mtime = os.stat(inFile).st_mtime
516 if inFile != videotest:
517 if inFile in info_cache and info_cache[inFile][0] == mtime:
518 logging.debug('CACHE HIT! %s' % inFile)
519 return info_cache[inFile][1]
521 vInfo['Supported'] = True
523 if (inFile[-5:]).lower() == '.tivo':
524 vInfo['millisecs'] = 0
525 info_cache[inFile] = (mtime, vInfo)
526 logger.debug('VALID, ends in .tivo. %s' % inFile)
527 return vInfo
529 cmd = [ffmpeg_path(), '-i', inFile ]
530 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
531 err_tmp = tempfile.TemporaryFile()
532 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
533 stdin=subprocess.PIPE)
535 # wait 10 sec if ffmpeg is not back give up
536 for i in xrange(200):
537 time.sleep(.05)
538 if not ffmpeg.poll() == None:
539 break
541 if ffmpeg.poll() == None:
542 kill(ffmpeg.pid)
543 vInfo['Supported'] = False
544 info_cache[inFile] = (mtime, vInfo)
545 return vInfo
547 err_tmp.seek(0)
548 output = err_tmp.read()
549 err_tmp.close()
550 logging.debug('ffmpeg output=%s' % output)
552 rezre = re.compile(r'.*Video: ([^,]+),.*')
553 x = rezre.search(output)
554 if x:
555 vInfo['vCodec'] = x.group(1)
556 else:
557 vInfo['vCodec'] = ''
558 vInfo['Supported'] = False
559 logger.debug('failed at vCodec')
561 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
562 x = rezre.search(output)
563 if x:
564 vInfo['vWidth'] = int(x.group(1))
565 vInfo['vHeight'] = int(x.group(2))
566 else:
567 vInfo['vWidth'] = ''
568 vInfo['vHeight'] = ''
569 vInfo['Supported'] = False
570 logger.debug('failed at vWidth/vHeight')
572 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
573 x = rezre.search(output)
574 if x:
575 vInfo['vFps'] = x.group(1)
577 # Allow override only if it is mpeg2 and frame rate was doubled
578 # to 59.94
580 if (not vInfo['vFps'] == '29.97') and (vInfo['vCodec'] == 'mpeg2video'):
581 # First look for the build 7215 version
582 rezre = re.compile(r'.*film source: 29.97.*')
583 x = rezre.search(output.lower() )
584 if x:
585 logger.debug('film source: 29.97 setting vFps to 29.97')
586 vInfo['vFps'] = '29.97'
587 else:
588 # for build 8047:
589 rezre = re.compile(r'.*frame rate differs from container ' +
590 r'frame rate: 29.97.*')
591 logger.debug('Bug in VideoReDo')
592 x = rezre.search(output.lower() )
593 if x:
594 vInfo['vFps'] = '29.97'
595 else:
596 vInfo['vFps'] = ''
597 vInfo['Supported'] = False
598 logger.debug('failed at vFps')
600 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
601 d = durre.search(output)
603 if d:
604 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
605 int(d.group(2)) * 60 +
606 int(d.group(3))) * 1000 +
607 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
608 else:
609 vInfo['millisecs'] = 0
611 #get bitrate of source for tivo compatibility test.
612 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
613 x = rezre.search(output)
614 if x:
615 vInfo['kbps'] = x.group(1)
616 else:
617 vInfo['kbps'] = None
618 logger.debug('failed at kbps')
620 #get audio bitrate of source for tivo compatibility test.
621 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
622 x = rezre.search(output)
623 if x:
624 vInfo['aKbps'] = x.group(1)
625 else:
626 vInfo['aKbps'] = None
627 logger.debug('failed at aKbps')
629 #get audio codec of source for tivo compatibility test.
630 rezre = re.compile(r'.*Audio: ([^,]+),.*')
631 x = rezre.search(output)
632 if x:
633 vInfo['aCodec'] = x.group(1)
634 else:
635 vInfo['aCodec'] = None
636 logger.debug('failed at aCodec')
638 #get audio frequency of source for tivo compatibility test.
639 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
640 x = rezre.search(output)
641 if x:
642 vInfo['aFreq'] = x.group(1)
643 else:
644 vInfo['aFreq'] = None
645 logger.debug('failed at aFreq')
647 #get par.
648 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
649 x = rezre.search(output)
650 if x and x.group(1) != "0" and x.group(2) != "0":
651 vInfo['par1'] = x.group(1) + ':' + x.group(2)
652 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
653 else:
654 vInfo['par1'], vInfo['par2'] = None, None
656 #get dar.
657 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
658 x = rezre.search(output)
659 if x and x.group(1) != "0" and x.group(2) != "0":
660 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
661 vInfo['dar2'] = float(x.group(1)) / float(x.group(2))
662 else:
663 vInfo['dar1'], vInfo['dar2'] = None, None
665 #get Video Stream mapping.
666 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
667 x = rezre.search(output)
668 if x:
669 vInfo['mapVideo'] = x.group(1)
670 else:
671 vInfo['mapVideo'] = None
672 logger.debug('failed at mapVideo')
675 #get Audio Stream mapping.
676 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
677 x = rezre.search(output)
678 amap = []
679 if x:
680 for x in rezre.finditer(output):
681 amap.append(x.groups())
682 else:
683 amap.append(('', ''))
684 logger.debug('failed at mapAudio')
685 vInfo['mapAudio'] = amap
688 videoPlugin = GetPlugin('video')
689 metadata = videoPlugin.getMetadataFromTxt(inFile)
691 for key in metadata:
692 if key.startswith('Override_'):
693 vInfo['Supported'] = True
694 if key.startswith('Override_mapAudio'):
695 audiomap = dict(vInfo['mapAudio'])
696 stream = key.replace('Override_mapAudio', '').strip()
697 if audiomap.has_key(stream):
698 newaudiomap = (stream, metadata[key])
699 audiomap.update([newaudiomap])
700 vInfo['mapAudio'] = sorted(audiomap.items(),
701 key=lambda (k,v): (k,v))
702 elif key.startswith('Override_millisecs'):
703 vInfo[key.replace('Override_', '')] = int(metadata[key])
704 else:
705 vInfo[key.replace('Override_', '')] = metadata[key]
707 info_cache[inFile] = (mtime, vInfo)
708 logger.debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
709 return vInfo
711 def video_check(inFile, cmd_string):
712 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
713 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
714 try:
715 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
716 return True
717 except:
718 kill(ffmpeg.pid)
719 return False
721 def supported_format(inFile):
722 if video_info(inFile)['Supported']:
723 return True
724 else:
725 logger.debug('FALSE, file not supported %s' % inFile)
726 return False
728 def kill(pid):
729 logger.debug('killing pid=%s' % str(pid))
730 if mswindows:
731 win32kill(pid)
732 else:
733 import os, signal
734 os.kill(pid, signal.SIGTERM)
736 def win32kill(pid):
737 import ctypes
738 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
739 ctypes.windll.kernel32.TerminateProcess(handle, -1)
740 ctypes.windll.kernel32.CloseHandle(handle)
742 def gcd(a,b):
743 while b:
744 a, b = b, a % b
745 return a