Patch by Keary Griffin to use qt-faststart automatically. Needs more
[pyTivo/wmcbrine.git] / plugins / video / transcode.py
blobe8516e0ee89c9c407aec301449b44847df385b5b
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
14 import qtfaststart
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 GOOD_MPEG_FPS = ['23.97', '24.00', '25.00', '29.97',
22 '30.00', '50.00', '59.94', '60.00']
24 def ffmpeg_path():
25 return config.get('Server', 'ffmpeg')
27 # XXX BIG HACK
28 # subprocess is broken for me on windows so super hack
29 def patchSubprocess():
30 o = subprocess.Popen._make_inheritable
32 def _make_inheritable(self, handle):
33 if not handle: return subprocess.GetCurrentProcess()
34 return o(self, handle)
36 subprocess.Popen._make_inheritable = _make_inheritable
37 mswindows = (sys.platform == "win32")
38 if mswindows:
39 patchSubprocess()
41 def output_video(inFile, outFile, tsn='', mime=''):
42 if tivo_compatible(inFile, tsn, mime)[0]:
43 logger.debug('%s is tivo compatible' % inFile)
44 f = file(inFile, 'rb')
45 if mime == 'video/mp4':
46 qtfaststart.fast_start(f, outFile)
47 else:
48 shutil.copyfileobj(f, outFile)
49 f.close()
50 else:
51 logger.debug('%s is not tivo compatible' % inFile)
52 transcode(False, inFile, outFile, tsn)
54 def transcode(isQuery, inFile, outFile, tsn=''):
55 settings = {'video_codec': select_videocodec(tsn),
56 'video_br': select_videobr(inFile, tsn),
57 'video_fps': select_videofps(inFile, tsn),
58 'max_video_br': select_maxvideobr(tsn),
59 'buff_size': select_buffsize(tsn),
60 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
61 'audio_br': select_audiobr(tsn),
62 'audio_fr': select_audiofr(inFile, tsn),
63 'audio_ch': select_audioch(tsn),
64 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
65 'audio_lang': select_audiolang(inFile, tsn),
66 'ffmpeg_pram': select_ffmpegprams(tsn),
67 'format': select_format(tsn)}
69 if isQuery:
70 return settings
72 cmd_string = config.getFFmpegTemplate(tsn) % settings
74 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
75 logging.debug('transcoding to tivo model ' + tsn[:3] +
76 ' using ffmpeg command:')
77 logging.debug(' '.join(cmd))
78 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
79 stdout=subprocess.PIPE)
80 try:
81 shutil.copyfileobj(ffmpeg.stdout, outFile)
82 except:
83 kill(ffmpeg)
85 def select_audiocodec(isQuery, inFile, tsn=''):
86 vInfo = video_info(inFile)
87 codectype = vInfo['vCodec']
88 codec = config.getAudioCodec(tsn)
89 if not codec:
90 # Default, compatible with all TiVo's
91 codec = 'ac3'
92 if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'):
93 if vInfo['aKbps'] == None:
94 if not isQuery:
95 cmd_string = ('-y -vcodec mpeg2video -r 29.97 ' +
96 '-b 1000k -acodec copy ' +
97 select_audiolang(inFile, tsn) +
98 ' -t 00:00:01 -f vob -')
99 if video_check(inFile, cmd_string):
100 vInfo = video_info(videotest)
101 else:
102 codec = 'TBD'
103 if (not vInfo['aKbps'] == None and
104 int(vInfo['aKbps']) <= config.getMaxAudioBR(tsn)):
105 # compatible codec and bitrate, do not reencode audio
106 codec = 'copy'
107 copy_flag = config.getCopyTS(tsn)
108 copyts = ' -copyts'
109 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
110 (copy_flag and copy_flag.lower() == 'false')):
111 copyts = ''
112 return '-acodec ' + codec + copyts
114 def select_audiofr(inFile, tsn):
115 freq = '48000' #default
116 vInfo = video_info(inFile)
117 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
118 # compatible frequency
119 freq = vInfo['aFreq']
120 if config.getAudioFR(tsn) != None:
121 freq = config.getAudioFR(tsn)
122 return '-ar ' + freq
124 def select_audioch(tsn):
125 ch = config.getAudioCH(tsn)
126 if ch:
127 return '-ac ' + ch
128 return ''
130 def select_audiolang(inFile, tsn):
131 vInfo = video_info(inFile)
132 if config.getAudioLang(tsn) != None and vInfo['mapVideo'] != None:
133 stream = vInfo['mapAudio'][0][0]
134 langmatch = []
135 for lang in config.getAudioLang(tsn).replace(' ','').lower().split(','):
136 for s, l in vInfo['mapAudio']:
137 if lang in s + l.replace(' ','').lower():
138 langmatch.append(s)
139 stream = s
140 break
141 if langmatch: break
142 if stream is not '':
143 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
144 return ''
146 def select_videofps(inFile, tsn):
147 vInfo = video_info(inFile)
148 fps = '-r 29.97' # default
149 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
150 fps = ' '
151 if config.getVideoFPS(tsn) != None:
152 fps = '-r ' + config.getVideoFPS(tsn)
153 return fps
155 def select_videocodec(tsn):
156 codec = config.getVideoCodec(tsn)
157 if not codec:
158 codec = 'mpeg2video' # default
159 return '-vcodec ' + codec
161 def select_videobr(inFile, tsn):
162 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
164 def select_videostr(inFile, tsn):
165 video_str = config.strtod(config.getVideoBR(tsn))
166 if config.isHDtivo(tsn):
167 vInfo = video_info(inFile)
168 if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0:
169 video_percent = int(vInfo['kbps']) * 10 * config.getVideoPCT(tsn)
170 video_str = max(video_str, video_percent)
171 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
172 video_str))
173 return video_str
175 def select_audiobr(tsn):
176 return '-ab ' + config.getAudioBR(tsn)
178 def select_maxvideobr(tsn):
179 return '-maxrate ' + config.getMaxVideoBR(tsn)
181 def select_buffsize(tsn):
182 return '-bufsize ' + config.getBuffSize(tsn)
184 def select_ffmpegprams(tsn):
185 params = config.getFFmpegPrams(tsn)
186 if not params:
187 params = ''
188 return params
190 def select_format(tsn):
191 fmt = config.getFormat(tsn)
192 if not fmt:
193 fmt = 'vob'
194 return '-f %s -' % fmt
196 def select_aspect(inFile, tsn = ''):
197 TIVO_WIDTH = config.getTivoWidth(tsn)
198 TIVO_HEIGHT = config.getTivoHeight(tsn)
200 vInfo = video_info(inFile)
202 logging.debug('tsn: %s' % tsn)
204 aspect169 = config.get169Setting(tsn)
206 logging.debug('aspect169: %s' % aspect169)
208 optres = config.getOptres(tsn)
210 logging.debug('optres: %s' % optres)
212 if optres:
213 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
214 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
215 if optHeight < TIVO_HEIGHT:
216 TIVO_HEIGHT = optHeight
217 if optWidth < TIVO_WIDTH:
218 TIVO_WIDTH = optWidth
220 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
221 ratio = vInfo['vWidth'] * 100 / vInfo['vHeight']
222 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
224 logger.debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s ' +
225 'millisecs=%s ratio=%s rheight=%s rwidth=%s ' +
226 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile,
227 vInfo['vCodec'], vInfo['vWidth'], vInfo['vHeight'],
228 vInfo['vFps'], vInfo['millisecs'], ratio, rheight,
229 rwidth, TIVO_HEIGHT, TIVO_WIDTH))
231 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
232 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
234 if config.isHDtivo(tsn) and not optres:
235 if config.getPixelAR(0) or vInfo['par']:
236 if vInfo['par2'] == None:
237 if vInfo['par']:
238 npar = float(vInfo['par'])
239 else:
240 npar = config.getPixelAR(1)
241 else:
242 npar = vInfo['par2']
244 # adjust for pixel aspect ratio, if set, because TiVo
245 # expects square pixels
247 if npar < 1.0:
248 return ['-s', str(vInfo['vWidth']) + 'x' +
249 str(int(math.ceil(vInfo['vHeight'] / npar)))]
250 elif npar > 1.0:
251 # FFMPEG expects width to be a multiple of two
253 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
254 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
256 if vInfo['vHeight'] <= TIVO_HEIGHT:
257 # pass all resolutions to S3, except heights greater than
258 # conf height
259 return []
260 # else, resize video.
262 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
263 logger.debug('File + PAR is within 4:3.')
264 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
266 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
267 (59, 72), (59, 36), (59, 54)] or
268 vInfo['dar1'] == '4:3'):
269 logger.debug('File is within 4:3 list.')
270 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
272 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
273 (59, 27)] or vInfo['dar1'] == '16:9')
274 and (aspect169 or config.get169Letterbox(tsn))):
275 logger.debug('File is within 16:9 list and 16:9 allowed.')
277 if config.get169Blacklist(tsn) or (aspect169 and
278 config.get169Letterbox(tsn)):
279 return ['-aspect', '4:3', '-s', '%sx%s' %
280 (TIVO_WIDTH, TIVO_HEIGHT)]
281 else:
282 return ['-aspect', '16:9', '-s', '%sx%s' %
283 (TIVO_WIDTH, TIVO_HEIGHT)]
284 else:
285 settings = []
287 # If video is wider than 4:3 add top and bottom padding
289 if ratio > 133: # Might be 16:9 file, or just need padding on
290 # top and bottom
292 if aspect169 and ratio > 135: # If file would fall in 4:3
293 # assume it is supposed to be 4:3
295 if ratio > 177: # too short needs padding top and bottom
296 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
297 vInfo['vWidth']) * multiplier16by9)
298 settings.append('-aspect')
299 if (config.get169Blacklist(tsn) or
300 config.get169Letterbox(tsn)):
301 settings.append('4:3')
302 else:
303 settings.append('16:9')
304 if endHeight % 2:
305 endHeight -= 1
306 if endHeight < TIVO_HEIGHT * 0.99:
307 topPadding = (TIVO_HEIGHT - endHeight) / 2
308 if topPadding % 2:
309 topPadding -= 1
310 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
311 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
312 '-padtop', str(topPadding),
313 '-padbottom', str(bottomPadding)]
314 else: # if only very small amount of padding
315 # needed, then just stretch it
316 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
318 logger.debug(('16:9 aspect allowed, file is wider ' +
319 'than 16:9 padding top and bottom\n%s') %
320 ' '.join(settings))
322 else: # too skinny needs padding on left and right.
323 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
324 (vInfo['vHeight'] * multiplier16by9))
325 settings.append('-aspect')
326 if (config.get169Blacklist(tsn) or
327 config.get169Letterbox(tsn)):
328 settings.append('4:3')
329 else:
330 settings.append('16:9')
331 if endWidth % 2:
332 endWidth -= 1
333 if endWidth < (TIVO_WIDTH - 10):
334 leftPadding = (TIVO_WIDTH - endWidth) / 2
335 if leftPadding % 2:
336 leftPadding -= 1
337 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
338 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
339 '-padleft', str(leftPadding),
340 '-padright', str(rightPadding)]
341 else: # if only very small amount of padding needed,
342 # then just stretch it
343 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
344 logger.debug(('16:9 aspect allowed, file is narrower ' +
345 'than 16:9 padding left and right\n%s') %
346 ' '.join(settings))
347 else: # this is a 4:3 file or 16:9 output not allowed
348 multiplier = multiplier4by3
349 settings.append('-aspect')
350 if ratio > 135 and config.get169Letterbox(tsn):
351 settings.append('16:9')
352 multiplier = multiplier16by9
353 else:
354 settings.append('4:3')
355 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
356 vInfo['vWidth']) * multiplier)
357 if endHeight % 2:
358 endHeight -= 1
359 if endHeight < TIVO_HEIGHT * 0.99:
360 topPadding = (TIVO_HEIGHT - endHeight) / 2
361 if topPadding % 2:
362 topPadding -= 1
363 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
364 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
365 '-padtop', str(topPadding),
366 '-padbottom', str(bottomPadding)]
367 else: # if only very small amount of padding needed,
368 # then just stretch it
369 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
370 logging.debug(('File is wider than 4:3 padding ' +
371 'top and bottom\n%s') % ' '.join(settings))
373 return settings
375 # If video is taller than 4:3 add left and right padding, this
376 # is rare. All of these files will always be sent in an aspect
377 # ratio of 4:3 since they are so narrow.
379 else:
380 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
381 (vInfo['vHeight'] * multiplier4by3))
382 settings += ['-aspect', '4:3']
383 if endWidth % 2:
384 endWidth -= 1
385 if endWidth < (TIVO_WIDTH * 0.99):
386 leftPadding = (TIVO_WIDTH - endWidth) / 2
387 if leftPadding % 2:
388 leftPadding -= 1
389 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
390 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
391 '-padleft', str(leftPadding),
392 '-padright', str(rightPadding)]
393 else: # if only very small amount of padding needed, then
394 # just stretch it
395 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
397 logger.debug('File is taller than 4:3 padding left and right\n%s'
398 % ' '.join(settings))
400 return settings
402 def tivo_compatible_mp4(inFile, tsn=''):
403 # This should also check for container type == mp4.
404 vInfo = video_info(inFile)
406 if vInfo['vCodec'] != 'h264':
407 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
408 vInfo['vCodec'])
409 elif vInfo['aCodec'] not in ('mpeg4aac', 'ac3', 'liba52'):
410 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
411 vInfo['aCodec'])
412 else:
413 message = (True, 'TRANSCODE=NO, passing through mp4.')
415 logger.debug('%s, %s' % (message, inFile))
416 return message
418 def tivo_compatible(inFile, tsn='', mime=''):
419 if mime == 'video/mp4':
420 return tivo_compatible_mp4(inFile, tsn)
422 supportedModes = [(720, 480), (704, 480), (544, 480),
423 (528, 480), (480, 480), (352, 480)]
424 vInfo = video_info(inFile)
426 while True:
427 if (inFile[-5:]).lower() == '.tivo':
428 message = (True, 'TRANSCODE=NO, ends with .tivo.')
429 break
431 if vInfo['vCodec'] != 'mpeg2video':
432 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
433 vInfo['vCodec'])
434 break
436 if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv',
437 '.tp', '.dvr-ms'):
438 message = (False, 'TRANSCODE=YES, ext %s not compatible.' %
439 os.path.splitext(inFile)[-1])
440 break
442 if vInfo['aCodec'] == 'dca':
443 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
444 vInfo['aCodec'])
445 break
447 if vInfo['aCodec'] != None:
448 if (not vInfo['aKbps'] or
449 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
450 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
451 'audio bitrate.') % vInfo['aKbps'])
452 break
454 if vInfo['kbps'] != None:
455 abit = max('0', vInfo['aKbps'])
456 if (int(vInfo['kbps']) - int(abit) >
457 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
458 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
459 'video bitrate.') % vInfo['kbps'])
460 break
461 else:
462 message = (False, 'TRANSCODE=YES, %s kbps not supported.' %
463 vInfo['kbps'])
464 break
466 if config.getAudioLang(tsn):
467 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
468 message = (False, ('TRANSCODE=YES, %s preferred audio ' +
469 'track exists.') % config.getAudioLang(tsn))
470 break
472 if config.isHDtivo(tsn):
473 if vInfo['par2'] != 1.0:
474 if config.getPixelAR(0):
475 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
476 message = (False, 'TRANSCODE=YES, %s not correct PAR.'
477 % vInfo['par2'])
478 break
479 message = (True, 'TRANSCODE=NO, HD Tivo detected, skipping ' +
480 'remaining tests.')
481 break
483 if not vInfo['vFps'] == '29.97':
484 message = (False, 'TRANSCODE=YES, %s vFps, should be 29.97.' %
485 vInfo['vFps'])
486 break
488 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
489 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
490 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9'):
491 message = (False, ('TRANSCODE=YES, DAR %s not supported ' +
492 'by BLACKLIST_169 tivos.') % vInfo['dar1'])
493 break
495 for mode in supportedModes:
496 if mode == (vInfo['vWidth'], vInfo['vHeight']):
497 message = (True, 'TRANSCODE=NO, %s x %s is valid.' %
498 (vInfo['vWidth'], vInfo['vHeight']))
499 break
500 message = (False, 'TRANSCODE=YES, %s x %s not in supported modes.'
501 % (vInfo['vWidth'], vInfo['vHeight']))
502 break
504 logger.debug('%s, %s' % (message, inFile))
505 return message
507 def video_info(inFile):
508 vInfo = dict()
509 mtime = os.stat(inFile).st_mtime
510 if inFile != videotest:
511 if inFile in info_cache and info_cache[inFile][0] == mtime:
512 logging.debug('CACHE HIT! %s' % inFile)
513 return info_cache[inFile][1]
515 vInfo['Supported'] = True
517 if (inFile[-5:]).lower() == '.tivo':
518 vInfo['millisecs'] = 0
519 info_cache[inFile] = (mtime, vInfo)
520 logger.debug('VALID, ends in .tivo. %s' % inFile)
521 return vInfo
523 cmd = [ffmpeg_path(), '-i', inFile]
524 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
525 err_tmp = tempfile.TemporaryFile()
526 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
527 stdin=subprocess.PIPE)
529 # wait configured # of seconds: if ffmpeg is not back give up
530 wait = config.getFFmpegWait()
531 logging.debug(
532 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
533 for i in xrange(wait * 20):
534 time.sleep(.05)
535 if not ffmpeg.poll() == None:
536 break
538 if ffmpeg.poll() == None:
539 kill(ffmpeg)
540 vInfo['Supported'] = False
541 info_cache[inFile] = (mtime, vInfo)
542 return vInfo
544 err_tmp.seek(0)
545 output = err_tmp.read()
546 err_tmp.close()
547 logging.debug('ffmpeg output=%s' % output)
549 rezre = re.compile(r'.*Video: ([^,]+),.*')
550 x = rezre.search(output)
551 if x:
552 vInfo['vCodec'] = x.group(1)
553 else:
554 vInfo['vCodec'] = ''
555 vInfo['Supported'] = False
556 logger.debug('failed at vCodec')
558 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
559 x = rezre.search(output)
560 if x:
561 vInfo['vWidth'] = int(x.group(1))
562 vInfo['vHeight'] = int(x.group(2))
563 else:
564 vInfo['vWidth'] = ''
565 vInfo['vHeight'] = ''
566 vInfo['Supported'] = False
567 logger.debug('failed at vWidth/vHeight')
569 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
570 x = rezre.search(output)
571 if x:
572 vInfo['vFps'] = x.group(1)
574 # Allow override only if it is mpeg2 and frame rate was doubled
575 # to 59.94
577 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
578 # First look for the build 7215 version
579 rezre = re.compile(r'.*film source: 29.97.*')
580 x = rezre.search(output.lower())
581 if x:
582 logger.debug('film source: 29.97 setting vFps to 29.97')
583 vInfo['vFps'] = '29.97'
584 else:
585 # for build 8047:
586 rezre = re.compile(r'.*frame rate differs from container ' +
587 r'frame rate: 29.97.*')
588 logger.debug('Bug in VideoReDo')
589 x = rezre.search(output.lower())
590 if x:
591 vInfo['vFps'] = '29.97'
592 else:
593 vInfo['vFps'] = ''
594 vInfo['Supported'] = False
595 logger.debug('failed at vFps')
597 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
598 d = durre.search(output)
600 if d:
601 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
602 int(d.group(2)) * 60 +
603 int(d.group(3))) * 1000 +
604 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
605 else:
606 vInfo['millisecs'] = 0
608 # get bitrate of source for tivo compatibility test.
609 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
610 x = rezre.search(output)
611 if x:
612 vInfo['kbps'] = x.group(1)
613 else:
614 # Fallback method of getting video bitrate
615 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
616 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
617 rezre = re.compile(r'.*Stream #0\.0: Video: mpeg2video, \S+, ' +
618 r'\S+ \[.*\], (\d+) (?:kb/s).*')
619 x = rezre.search(output)
620 if x:
621 vInfo['kbps'] = x.group(1)
622 else:
623 vInfo['kbps'] = None
624 logger.debug('failed at kbps')
626 # get audio bitrate of source for tivo compatibility test.
627 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
628 x = rezre.search(output)
629 if x:
630 vInfo['aKbps'] = x.group(1)
631 else:
632 vInfo['aKbps'] = None
633 logger.debug('failed at aKbps')
635 # get audio codec of source for tivo compatibility test.
636 rezre = re.compile(r'.*Audio: ([^,]+),.*')
637 x = rezre.search(output)
638 if x:
639 vInfo['aCodec'] = x.group(1)
640 else:
641 vInfo['aCodec'] = None
642 logger.debug('failed at aCodec')
644 # get audio frequency of source for tivo compatibility test.
645 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
646 x = rezre.search(output)
647 if x:
648 vInfo['aFreq'] = x.group(1)
649 else:
650 vInfo['aFreq'] = None
651 logger.debug('failed at aFreq')
653 # get par.
654 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
655 x = rezre.search(output)
656 if x and x.group(1) != "0" and x.group(2) != "0":
657 vInfo['par1'] = x.group(1) + ':' + x.group(2)
658 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
659 else:
660 vInfo['par1'], vInfo['par2'] = None, None
662 # get dar.
663 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
664 x = rezre.search(output)
665 if x and x.group(1) != "0" and x.group(2) != "0":
666 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
667 vInfo['dar2'] = float(x.group(1)) / float(x.group(2))
668 else:
669 vInfo['dar1'], vInfo['dar2'] = None, None
671 # get Video Stream mapping.
672 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
673 x = rezre.search(output)
674 if x:
675 vInfo['mapVideo'] = x.group(1)
676 else:
677 vInfo['mapVideo'] = None
678 logger.debug('failed at mapVideo')
680 # get Audio Stream mapping.
681 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
682 x = rezre.search(output)
683 amap = []
684 if x:
685 for x in rezre.finditer(output):
686 amap.append(x.groups())
687 else:
688 amap.append(('', ''))
689 logger.debug('failed at mapAudio')
690 vInfo['mapAudio'] = amap
692 vInfo['par'] = None
693 videoPlugin = GetPlugin('video')
694 metadata = videoPlugin.getMetadataFromTxt(inFile)
696 for key in metadata:
697 if key.startswith('Override_'):
698 vInfo['Supported'] = True
699 if key.startswith('Override_mapAudio'):
700 audiomap = dict(vInfo['mapAudio'])
701 stream = key.replace('Override_mapAudio', '').strip()
702 if stream in audiomap:
703 newaudiomap = (stream, metadata[key])
704 audiomap.update([newaudiomap])
705 vInfo['mapAudio'] = sorted(audiomap.items(),
706 key=lambda (k,v): (k,v))
707 elif key.startswith('Override_millisecs'):
708 vInfo[key.replace('Override_', '')] = int(metadata[key])
709 else:
710 vInfo[key.replace('Override_', '')] = metadata[key]
712 info_cache[inFile] = (mtime, vInfo)
713 logger.debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
714 return vInfo
716 def video_check(inFile, cmd_string):
717 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
718 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
719 try:
720 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
721 return True
722 except:
723 kill(ffmpeg)
724 return False
726 def supported_format(inFile):
727 if video_info(inFile)['Supported']:
728 return True
729 else:
730 logger.debug('FALSE, file not supported %s' % inFile)
731 return False
733 def kill(popen):
734 logger.debug('killing pid=%s' % str(popen.pid))
735 if mswindows:
736 win32kill(popen.pid)
737 else:
738 import os, signal
739 for i in xrange(3):
740 logger.debug('sending SIGTERM to pid: %s' % popen.pid)
741 os.kill(popen.pid, signal.SIGTERM)
742 time.sleep(.5)
743 if popen.poll() is not None:
744 logger.debug('process %s has exited' % popen.pid)
745 break
746 else:
747 while popen.poll() is None:
748 logger.debug('sending SIGKILL to pid: %s' % popen.pid)
749 os.kill(popen.pid, signal.SIGKILL)
750 time.sleep(.5)
752 def win32kill(pid):
753 import ctypes
754 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
755 ctypes.windll.kernel32.TerminateProcess(handle, -1)
756 ctypes.windll.kernel32.CloseHandle(handle)
758 def gcd(a, b):
759 while b:
760 a, b = b, a % b
761 return a