It seems to me that there are many more invalid rates than valid ones.
[pyTivo/wmcbrine.git] / plugins / video / transcode.py
blob222ab454985fa8b00784ef357ac753ab5a3478a6
1 import logging
2 import math
3 import os
4 import re
5 import shutil
6 import subprocess
7 import sys
8 import tempfile
9 import time
11 import lrucache
12 import config
13 from plugin import GetPlugin
15 logger = logging.getLogger('pyTivo.video.transcode')
17 info_cache = lrucache.LRUCache(1000)
18 videotest = os.path.join(os.path.dirname(__file__), 'videotest.mpg')
20 GOOD_MPEG_FPS = ['23.97', '24.00', '25.00', '29.97',
21 '30.00', '50.00', '59.94', '60.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='', mime=''):
41 if tivo_compatible(inFile, tsn, mime)[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(tsn),
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)
81 def select_audiocodec(isQuery, inFile, tsn=''):
82 vInfo = video_info(inFile)
83 codectype = vInfo['vCodec']
84 codec = config.getAudioCodec(tsn)
85 if not codec:
86 # Default, compatible with all TiVo's
87 codec = 'ac3'
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 copy_flag = config.getCopyTS(tsn)
104 copyts = ' -copyts'
105 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
106 (copy_flag and copy_flag.lower() == 'false')):
107 copyts = ''
108 return '-acodec ' + codec + copyts
110 def select_audiofr(inFile, tsn):
111 freq = '48000' #default
112 vInfo = video_info(inFile)
113 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
114 # compatible frequency
115 freq = vInfo['aFreq']
116 if config.getAudioFR(tsn) != None:
117 freq = config.getAudioFR(tsn)
118 return '-ar ' + freq
120 def select_audioch(tsn):
121 ch = config.getAudioCH(tsn)
122 if ch:
123 return '-ac ' + ch
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'] in GOOD_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 = config.getVideoCodec(tsn)
153 if not codec:
154 codec = 'mpeg2video' # default
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(tsn) > 0:
165 video_percent = int(vInfo['kbps']) * 10 * config.getVideoPCT(tsn)
166 video_str = max(video_str, video_percent)
167 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
168 video_str))
169 return video_str
171 def select_audiobr(tsn):
172 return '-ab ' + config.getAudioBR(tsn)
174 def select_maxvideobr(tsn):
175 return '-maxrate ' + config.getMaxVideoBR(tsn)
177 def select_buffsize(tsn):
178 return '-bufsize ' + config.getBuffSize(tsn)
180 def select_ffmpegprams(tsn):
181 params = config.getFFmpegPrams(tsn)
182 if not params:
183 params = ''
184 return params
186 def select_format(tsn):
187 fmt = config.getFormat(tsn)
188 if not fmt:
189 fmt = 'vob'
190 return '-f %s -' % 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) or vInfo['par']:
232 if vInfo['par2'] == None:
233 if vInfo['par']:
234 npar = float(vInfo['par'])
235 else:
236 npar = config.getPixelAR(1)
237 else:
238 npar = vInfo['par2']
240 # adjust for pixel aspect ratio, if set, because TiVo
241 # expects square pixels
243 if npar < 1.0:
244 return ['-s', str(vInfo['vWidth']) + 'x' +
245 str(int(math.ceil(vInfo['vHeight'] / npar)))]
246 elif npar > 1.0:
247 # FFMPEG expects width to be a multiple of two
249 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
250 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
252 if vInfo['vHeight'] <= TIVO_HEIGHT:
253 # pass all resolutions to S3, except heights greater than
254 # conf height
255 return []
256 # else, resize video.
258 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
259 logger.debug('File + PAR is within 4:3.')
260 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
262 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
263 (59, 72), (59, 36), (59, 54)] or
264 vInfo['dar1'] == '4:3'):
265 logger.debug('File is within 4:3 list.')
266 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
268 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
269 (59, 27)] or vInfo['dar1'] == '16:9')
270 and (aspect169 or config.get169Letterbox(tsn))):
271 logger.debug('File is within 16:9 list and 16:9 allowed.')
273 if config.get169Blacklist(tsn) or (aspect169 and
274 config.get169Letterbox(tsn)):
275 return ['-aspect', '4:3', '-s', '%sx%s' %
276 (TIVO_WIDTH, TIVO_HEIGHT)]
277 else:
278 return ['-aspect', '16:9', '-s', '%sx%s' %
279 (TIVO_WIDTH, TIVO_HEIGHT)]
280 else:
281 settings = []
283 # If video is wider than 4:3 add top and bottom padding
285 if ratio > 133: # Might be 16:9 file, or just need padding on
286 # top and bottom
288 if aspect169 and ratio > 135: # If file would fall in 4:3
289 # assume it is supposed to be 4:3
291 if ratio > 177: # too short needs padding top and bottom
292 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
293 vInfo['vWidth']) * multiplier16by9)
294 settings.append('-aspect')
295 if (config.get169Blacklist(tsn) or
296 config.get169Letterbox(tsn)):
297 settings.append('4:3')
298 else:
299 settings.append('16:9')
300 if endHeight % 2:
301 endHeight -= 1
302 if endHeight < TIVO_HEIGHT * 0.99:
303 topPadding = (TIVO_HEIGHT - endHeight) / 2
304 if topPadding % 2:
305 topPadding -= 1
306 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
307 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
308 '-padtop', str(topPadding),
309 '-padbottom', str(bottomPadding)]
310 else: # if only very small amount of padding
311 # needed, then just stretch it
312 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
314 logger.debug(('16:9 aspect allowed, file is wider ' +
315 'than 16:9 padding top and bottom\n%s') %
316 ' '.join(settings))
318 else: # too skinny needs padding on left and right.
319 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
320 (vInfo['vHeight'] * multiplier16by9))
321 settings.append('-aspect')
322 if (config.get169Blacklist(tsn) or
323 config.get169Letterbox(tsn)):
324 settings.append('4:3')
325 else:
326 settings.append('16:9')
327 if endWidth % 2:
328 endWidth -= 1
329 if endWidth < (TIVO_WIDTH - 10):
330 leftPadding = (TIVO_WIDTH - endWidth) / 2
331 if leftPadding % 2:
332 leftPadding -= 1
333 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
334 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
335 '-padleft', str(leftPadding),
336 '-padright', str(rightPadding)]
337 else: # if only very small amount of padding needed,
338 # then just stretch it
339 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
340 logger.debug(('16:9 aspect allowed, file is narrower ' +
341 'than 16:9 padding left and right\n%s') %
342 ' '.join(settings))
343 else: # this is a 4:3 file or 16:9 output not allowed
344 multiplier = multiplier4by3
345 settings.append('-aspect')
346 if ratio > 135 and config.get169Letterbox(tsn):
347 settings.append('16:9')
348 multiplier = multiplier16by9
349 else:
350 settings.append('4:3')
351 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
352 vInfo['vWidth']) * multiplier)
353 if endHeight % 2:
354 endHeight -= 1
355 if endHeight < TIVO_HEIGHT * 0.99:
356 topPadding = (TIVO_HEIGHT - endHeight) / 2
357 if topPadding % 2:
358 topPadding -= 1
359 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
360 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
361 '-padtop', str(topPadding),
362 '-padbottom', str(bottomPadding)]
363 else: # if only very small amount of padding needed,
364 # then just stretch it
365 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
366 logging.debug(('File is wider than 4:3 padding ' +
367 'top and bottom\n%s') % ' '.join(settings))
369 return settings
371 # If video is taller than 4:3 add left and right padding, this
372 # is rare. All of these files will always be sent in an aspect
373 # ratio of 4:3 since they are so narrow.
375 else:
376 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
377 (vInfo['vHeight'] * multiplier4by3))
378 settings += ['-aspect', '4:3']
379 if endWidth % 2:
380 endWidth -= 1
381 if endWidth < (TIVO_WIDTH * 0.99):
382 leftPadding = (TIVO_WIDTH - endWidth) / 2
383 if leftPadding % 2:
384 leftPadding -= 1
385 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
386 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
387 '-padleft', str(leftPadding),
388 '-padright', str(rightPadding)]
389 else: # if only very small amount of padding needed, then
390 # just stretch it
391 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
393 logger.debug('File is taller than 4:3 padding left and right\n%s'
394 % ' '.join(settings))
396 return settings
398 def tivo_compatible_mp4(inFile, tsn=''):
399 # This should also check for container type == mp4.
400 vInfo = video_info(inFile)
402 if vInfo['vCodec'] != 'h264':
403 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
404 vInfo['vCodec'])
405 elif vInfo['aCodec'] not in ('mpeg4aac', 'ac3', 'liba52'):
406 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
407 vInfo['aCodec'])
408 else:
409 message = (True, 'TRANSCODE=NO, passing through mp4.')
411 logger.debug('%s, %s' % (message, inFile))
412 return message
414 def tivo_compatible(inFile, tsn='', mime=''):
415 if mime == 'video/mp4':
416 return tivo_compatible_mp4(inFile, tsn)
418 supportedModes = [(720, 480), (704, 480), (544, 480),
419 (528, 480), (480, 480), (352, 480)]
420 vInfo = video_info(inFile)
422 while True:
423 if (inFile[-5:]).lower() == '.tivo':
424 message = (True, 'TRANSCODE=NO, ends with .tivo.')
425 break
427 if vInfo['vCodec'] != 'mpeg2video':
428 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
429 vInfo['vCodec'])
430 break
432 if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv',
433 '.tp', '.dvr-ms'):
434 message = (False, 'TRANSCODE=YES, ext %s not compatible.' %
435 os.path.splitext(inFile)[-1])
436 break
438 if vInfo['aCodec'] == 'dca':
439 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
440 vInfo['aCodec'])
441 break
443 if vInfo['aCodec'] != None:
444 if (not vInfo['aKbps'] or
445 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
446 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
447 'audio bitrate.') % vInfo['aKbps'])
448 break
450 if vInfo['kbps'] != None:
451 abit = max('0', vInfo['aKbps'])
452 if (int(vInfo['kbps']) - int(abit) >
453 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
454 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
455 'video bitrate.') % vInfo['kbps'])
456 break
457 else:
458 message = (False, 'TRANSCODE=YES, %s kbps not supported.' %
459 vInfo['kbps'])
460 break
462 if config.getAudioLang(tsn):
463 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
464 message = (False, ('TRANSCODE=YES, %s preferred audio ' +
465 'track exists.') % config.getAudioLang(tsn))
466 break
468 if config.isHDtivo(tsn):
469 if vInfo['par2'] != 1.0:
470 if config.getPixelAR(0):
471 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
472 message = (False, 'TRANSCODE=YES, %s not correct PAR.'
473 % vInfo['par2'])
474 break
475 message = (True, 'TRANSCODE=NO, HD Tivo detected, skipping ' +
476 'remaining tests.')
477 break
479 if not vInfo['vFps'] == '29.97':
480 message = (False, 'TRANSCODE=YES, %s vFps, should be 29.97.' %
481 vInfo['vFps'])
482 break
484 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
485 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
486 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9'):
487 message = (False, ('TRANSCODE=YES, DAR %s not supported ' +
488 'by BLACKLIST_169 tivos.') % vInfo['dar1'])
489 break
491 for mode in supportedModes:
492 if mode == (vInfo['vWidth'], vInfo['vHeight']):
493 message = (True, 'TRANSCODE=NO, %s x %s is valid.' %
494 (vInfo['vWidth'], vInfo['vHeight']))
495 break
496 message = (False, 'TRANSCODE=YES, %s x %s not in supported modes.'
497 % (vInfo['vWidth'], vInfo['vHeight']))
498 break
500 logger.debug('%s, %s' % (message, inFile))
501 return message
503 def video_info(inFile):
504 vInfo = dict()
505 mtime = os.stat(inFile).st_mtime
506 if inFile != videotest:
507 if inFile in info_cache and info_cache[inFile][0] == mtime:
508 logging.debug('CACHE HIT! %s' % inFile)
509 return info_cache[inFile][1]
511 vInfo['Supported'] = True
513 if (inFile[-5:]).lower() == '.tivo':
514 vInfo['millisecs'] = 0
515 info_cache[inFile] = (mtime, vInfo)
516 logger.debug('VALID, ends in .tivo. %s' % inFile)
517 return vInfo
519 cmd = [ffmpeg_path(), '-i', inFile]
520 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
521 err_tmp = tempfile.TemporaryFile()
522 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
523 stdin=subprocess.PIPE)
525 # wait configured # of seconds: if ffmpeg is not back give up
526 wait = config.getFFmpegWait()
527 logging.debug(
528 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
529 for i in xrange(wait * 20):
530 time.sleep(.05)
531 if not ffmpeg.poll() == None:
532 break
534 if ffmpeg.poll() == None:
535 kill(ffmpeg)
536 vInfo['Supported'] = False
537 info_cache[inFile] = (mtime, vInfo)
538 return vInfo
540 err_tmp.seek(0)
541 output = err_tmp.read()
542 err_tmp.close()
543 logging.debug('ffmpeg output=%s' % output)
545 rezre = re.compile(r'.*Video: ([^,]+),.*')
546 x = rezre.search(output)
547 if x:
548 vInfo['vCodec'] = x.group(1)
549 else:
550 vInfo['vCodec'] = ''
551 vInfo['Supported'] = False
552 logger.debug('failed at vCodec')
554 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
555 x = rezre.search(output)
556 if x:
557 vInfo['vWidth'] = int(x.group(1))
558 vInfo['vHeight'] = int(x.group(2))
559 else:
560 vInfo['vWidth'] = ''
561 vInfo['vHeight'] = ''
562 vInfo['Supported'] = False
563 logger.debug('failed at vWidth/vHeight')
565 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
566 x = rezre.search(output)
567 if x:
568 vInfo['vFps'] = x.group(1)
570 # Allow override only if it is mpeg2 and frame rate was doubled
571 # to 59.94
573 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
574 # First look for the build 7215 version
575 rezre = re.compile(r'.*film source: 29.97.*')
576 x = rezre.search(output.lower())
577 if x:
578 logger.debug('film source: 29.97 setting vFps to 29.97')
579 vInfo['vFps'] = '29.97'
580 else:
581 # for build 8047:
582 rezre = re.compile(r'.*frame rate differs from container ' +
583 r'frame rate: 29.97.*')
584 logger.debug('Bug in VideoReDo')
585 x = rezre.search(output.lower())
586 if x:
587 vInfo['vFps'] = '29.97'
588 else:
589 vInfo['vFps'] = ''
590 vInfo['Supported'] = False
591 logger.debug('failed at vFps')
593 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
594 d = durre.search(output)
596 if d:
597 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
598 int(d.group(2)) * 60 +
599 int(d.group(3))) * 1000 +
600 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
601 else:
602 vInfo['millisecs'] = 0
604 # get bitrate of source for tivo compatibility test.
605 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
606 x = rezre.search(output)
607 if x:
608 vInfo['kbps'] = x.group(1)
609 else:
610 # Fallback method of getting video bitrate
611 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
612 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
613 rezre = re.compile(r'.*Stream #0\.0: Video: mpeg2video, \S+, ' +
614 r'\S+ \[.*\], (\d+) (?:kb/s).*')
615 x = rezre.search(output)
616 if x:
617 vInfo['kbps'] = x.group(1)
618 else:
619 vInfo['kbps'] = None
620 logger.debug('failed at kbps')
622 # get audio bitrate of source for tivo compatibility test.
623 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
624 x = rezre.search(output)
625 if x:
626 vInfo['aKbps'] = x.group(1)
627 else:
628 vInfo['aKbps'] = None
629 logger.debug('failed at aKbps')
631 # get audio codec of source for tivo compatibility test.
632 rezre = re.compile(r'.*Audio: ([^,]+),.*')
633 x = rezre.search(output)
634 if x:
635 vInfo['aCodec'] = x.group(1)
636 else:
637 vInfo['aCodec'] = None
638 logger.debug('failed at aCodec')
640 # get audio frequency of source for tivo compatibility test.
641 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
642 x = rezre.search(output)
643 if x:
644 vInfo['aFreq'] = x.group(1)
645 else:
646 vInfo['aFreq'] = None
647 logger.debug('failed at aFreq')
649 # get par.
650 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
651 x = rezre.search(output)
652 if x and x.group(1) != "0" and x.group(2) != "0":
653 vInfo['par1'] = x.group(1) + ':' + x.group(2)
654 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
655 else:
656 vInfo['par1'], vInfo['par2'] = None, None
658 # get dar.
659 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
660 x = rezre.search(output)
661 if x and x.group(1) != "0" and x.group(2) != "0":
662 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
663 vInfo['dar2'] = float(x.group(1)) / float(x.group(2))
664 else:
665 vInfo['dar1'], vInfo['dar2'] = None, None
667 # get Video Stream mapping.
668 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
669 x = rezre.search(output)
670 if x:
671 vInfo['mapVideo'] = x.group(1)
672 else:
673 vInfo['mapVideo'] = None
674 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
688 vInfo['par'] = None
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 stream in audiomap:
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)
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(popen):
730 logger.debug('killing pid=%s' % str(popen.pid))
731 if mswindows:
732 win32kill(popen.pid)
733 else:
734 import os, signal
735 for i in xrange(3):
736 logger.debug('sending SIGTERM to pid: %s' % popen.pid)
737 os.kill(popen.pid, signal.SIGTERM)
738 time.sleep(.5)
739 if popen.poll() is not None:
740 logger.debug('process %s has exited' % popen.pid)
741 break
742 else:
743 while popen.poll() is None:
744 logger.debug('sending SIGKILL to pid: %s' % popen.pid)
745 os.kill(popen.pid, signal.SIGKILL)
746 time.sleep(.5)
748 def win32kill(pid):
749 import ctypes
750 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
751 ctypes.windll.kernel32.TerminateProcess(handle, -1)
752 ctypes.windll.kernel32.CloseHandle(handle)
754 def gcd(a, b):
755 while b:
756 a, b = b, a % b
757 return a