Common ffmpeg_path().
[pyTivo/TheBayer.git] / plugins / video / transcode.py
blob16cd65229a43338bc71ee2a3d323e678e41610c5
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)
20 GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97',
21 '30.00', '50.00', '59.94', '60.00']
23 # XXX BIG HACK
24 # subprocess is broken for me on windows so super hack
25 def patchSubprocess():
26 o = subprocess.Popen._make_inheritable
28 def _make_inheritable(self, handle):
29 if not handle: return subprocess.GetCurrentProcess()
30 return o(self, handle)
32 subprocess.Popen._make_inheritable = _make_inheritable
33 mswindows = (sys.platform == "win32")
34 if mswindows:
35 patchSubprocess()
37 def output_video(inFile, outFile, tsn='', mime=''):
38 if tivo_compatible(inFile, tsn, mime)[0]:
39 logger.debug('%s is tivo compatible' % inFile)
40 f = open(inFile, 'rb')
41 try:
42 if mime == 'video/mp4':
43 qtfaststart.fast_start(f, outFile)
44 else:
45 shutil.copyfileobj(f, outFile)
46 except Exception, msg:
47 logger.info(msg)
48 f.close()
49 else:
50 logger.debug('%s is not tivo compatible' % inFile)
51 transcode(False, inFile, outFile, tsn)
52 logger.debug("Finished outputing video")
54 def transcode(isQuery, inFile, outFile, tsn=''):
55 settings = {'video_codec': select_videocodec(inFile, 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 = [config.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 aKbps = vInfo['aKbps']
94 if aKbps == None:
95 if not isQuery:
96 aKbps = audio_check(inFile, tsn)
97 else:
98 codec = 'TBD'
99 if aKbps != None and int(aKbps) <= config.getMaxAudioBR(tsn):
100 # compatible codec and bitrate, do not reencode audio
101 codec = 'copy'
102 copy_flag = config.getCopyTS(tsn)
103 copyts = ' -copyts'
104 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
105 (copy_flag and copy_flag.lower() == 'false')):
106 copyts = ''
107 return '-acodec ' + codec + copyts
109 def select_audiofr(inFile, tsn):
110 freq = '48000' #default
111 vInfo = video_info(inFile)
112 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
113 # compatible frequency
114 freq = vInfo['aFreq']
115 if config.getAudioFR(tsn) != None:
116 freq = config.getAudioFR(tsn)
117 return '-ar ' + freq
119 def select_audioch(tsn):
120 ch = config.getAudioCH(tsn)
121 if ch:
122 return '-ac ' + ch
123 return ''
125 def select_audiolang(inFile, tsn):
126 vInfo = video_info(inFile)
127 if config.getAudioLang(tsn) != None and vInfo['mapVideo'] != None:
128 stream = vInfo['mapAudio'][0][0]
129 langmatch = []
130 for lang in config.getAudioLang(tsn).replace(' ','').lower().split(','):
131 for s, l in vInfo['mapAudio']:
132 if lang in s + l.replace(' ','').lower():
133 langmatch.append(s)
134 stream = s
135 break
136 if langmatch: break
137 if stream is not '':
138 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
139 return ''
141 def select_videofps(inFile, tsn):
142 vInfo = video_info(inFile)
143 fps = '-r 29.97' # default
144 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
145 fps = ' '
146 if config.getVideoFPS(tsn) != None:
147 fps = '-r ' + config.getVideoFPS(tsn)
148 return fps
150 def select_videocodec(inFile, tsn):
151 vInfo = video_info(inFile)
152 if tivo_compatible_video(vInfo, tsn)[0]:
153 codec = 'copy'
154 else:
155 codec = 'mpeg2video' # default
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 vInfo = video_info(inFile)
163 if tivo_compatible_video(vInfo, tsn)[0]:
164 video_str = int(vInfo['kbps'])
165 if vInfo['aKbps']:
166 video_str -= int(vInfo['aKbps'])
167 video_str *= 1000
168 else:
169 video_str = config.strtod(config.getVideoBR(tsn))
170 if config.isHDtivo(tsn):
171 if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0:
172 video_percent = (int(vInfo['kbps']) * 10 *
173 config.getVideoPCT(tsn))
174 video_str = max(video_str, video_percent)
175 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
176 video_str))
177 return video_str
179 def select_audiobr(tsn):
180 return '-ab ' + config.getAudioBR(tsn)
182 def select_maxvideobr(tsn):
183 return '-maxrate ' + config.getMaxVideoBR(tsn)
185 def select_buffsize(tsn):
186 return '-bufsize ' + config.getBuffSize(tsn)
188 def select_ffmpegprams(tsn):
189 params = config.getFFmpegPrams(tsn)
190 if not params:
191 params = ''
192 return params
194 def select_format(tsn):
195 fmt = 'vob'
196 return '-f %s -' % fmt
198 def select_aspect(inFile, tsn = ''):
199 TIVO_WIDTH = config.getTivoWidth(tsn)
200 TIVO_HEIGHT = config.getTivoHeight(tsn)
202 vInfo = video_info(inFile)
204 logging.debug('tsn: %s' % tsn)
206 aspect169 = config.get169Setting(tsn)
208 logging.debug('aspect169: %s' % aspect169)
210 optres = config.getOptres(tsn)
212 logging.debug('optres: %s' % optres)
214 if optres:
215 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
216 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
217 if optHeight < TIVO_HEIGHT:
218 TIVO_HEIGHT = optHeight
219 if optWidth < TIVO_WIDTH:
220 TIVO_WIDTH = optWidth
222 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
223 ratio = vInfo['vWidth'] * 100 / vInfo['vHeight']
224 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
226 logger.debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s ' +
227 'millisecs=%s ratio=%s rheight=%s rwidth=%s ' +
228 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile,
229 vInfo['vCodec'], vInfo['vWidth'], vInfo['vHeight'],
230 vInfo['vFps'], vInfo['millisecs'], ratio, rheight,
231 rwidth, TIVO_HEIGHT, TIVO_WIDTH))
233 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
234 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
236 if config.isHDtivo(tsn) and not optres:
237 if config.getPixelAR(0) or vInfo['par']:
238 if vInfo['par2'] == None:
239 if vInfo['par']:
240 npar = float(vInfo['par'])
241 else:
242 npar = config.getPixelAR(1)
243 else:
244 npar = vInfo['par2']
246 # adjust for pixel aspect ratio, if set, because TiVo
247 # expects square pixels
249 if npar < 1.0:
250 return ['-s', str(vInfo['vWidth']) + 'x' +
251 str(int(math.ceil(vInfo['vHeight'] / npar)))]
252 elif npar > 1.0:
253 # FFMPEG expects width to be a multiple of two
255 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
256 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
258 if vInfo['vHeight'] <= TIVO_HEIGHT:
259 # pass all resolutions to S3, except heights greater than
260 # conf height
261 return []
262 # else, resize video.
264 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
265 logger.debug('File + PAR is within 4:3.')
266 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
268 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
269 (59, 72), (59, 36), (59, 54)] or
270 vInfo['dar1'] == '4:3'):
271 logger.debug('File is within 4:3 list.')
272 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
274 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
275 (59, 27)] or vInfo['dar1'] == '16:9')
276 and (aspect169 or config.get169Letterbox(tsn))):
277 logger.debug('File is within 16:9 list and 16:9 allowed.')
279 if config.get169Blacklist(tsn) or (aspect169 and
280 config.get169Letterbox(tsn)):
281 return ['-aspect', '4:3', '-s', '%sx%s' %
282 (TIVO_WIDTH, TIVO_HEIGHT)]
283 else:
284 return ['-aspect', '16:9', '-s', '%sx%s' %
285 (TIVO_WIDTH, TIVO_HEIGHT)]
286 else:
287 settings = []
289 # If video is wider than 4:3 add top and bottom padding
291 if ratio > 133: # Might be 16:9 file, or just need padding on
292 # top and bottom
294 if aspect169 and ratio > 135: # If file would fall in 4:3
295 # assume it is supposed to be 4:3
297 if ratio > 177: # too short needs padding top and bottom
298 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
299 vInfo['vWidth']) * multiplier16by9)
300 settings.append('-aspect')
301 if (config.get169Blacklist(tsn) or
302 config.get169Letterbox(tsn)):
303 settings.append('4:3')
304 else:
305 settings.append('16:9')
306 if endHeight % 2:
307 endHeight -= 1
308 if endHeight < TIVO_HEIGHT * 0.99:
309 topPadding = (TIVO_HEIGHT - endHeight) / 2
310 if topPadding % 2:
311 topPadding -= 1
312 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
313 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
314 '-padtop', str(topPadding),
315 '-padbottom', str(bottomPadding)]
316 else: # if only very small amount of padding
317 # needed, then just stretch it
318 settings += ['-s', '%sx%s' % (TIVO_WIDTH, 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 leftPadding = (TIVO_WIDTH - endWidth) / 2
337 if leftPadding % 2:
338 leftPadding -= 1
339 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
340 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
341 '-padleft', str(leftPadding),
342 '-padright', str(rightPadding)]
343 else: # if only very small amount of padding needed,
344 # then just stretch it
345 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
346 logger.debug(('16:9 aspect allowed, file is narrower ' +
347 'than 16:9 padding left and right\n%s') %
348 ' '.join(settings))
349 else: # this is a 4:3 file or 16:9 output not allowed
350 multiplier = multiplier4by3
351 settings.append('-aspect')
352 if ratio > 135 and config.get169Letterbox(tsn):
353 settings.append('16:9')
354 multiplier = multiplier16by9
355 else:
356 settings.append('4:3')
357 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
358 vInfo['vWidth']) * multiplier)
359 if endHeight % 2:
360 endHeight -= 1
361 if endHeight < TIVO_HEIGHT * 0.99:
362 topPadding = (TIVO_HEIGHT - endHeight) / 2
363 if topPadding % 2:
364 topPadding -= 1
365 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
366 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
367 '-padtop', str(topPadding),
368 '-padbottom', str(bottomPadding)]
369 else: # if only very small amount of padding needed,
370 # then just stretch it
371 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
372 logging.debug(('File is wider than 4:3 padding ' +
373 'top and bottom\n%s') % ' '.join(settings))
375 return settings
377 # If video is taller than 4:3 add left and right padding, this
378 # is rare. All of these files will always be sent in an aspect
379 # ratio of 4:3 since they are so narrow.
381 else:
382 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
383 (vInfo['vHeight'] * multiplier4by3))
384 settings += ['-aspect', '4:3']
385 if endWidth % 2:
386 endWidth -= 1
387 if endWidth < (TIVO_WIDTH * 0.99):
388 leftPadding = (TIVO_WIDTH - endWidth) / 2
389 if leftPadding % 2:
390 leftPadding -= 1
391 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
392 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
393 '-padleft', str(leftPadding),
394 '-padright', str(rightPadding)]
395 else: # if only very small amount of padding needed, then
396 # just stretch it
397 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
399 logger.debug('File is taller than 4:3 padding left and right\n%s'
400 % ' '.join(settings))
402 return settings
404 def tivo_compatible_video(vInfo, tsn, mime=''):
405 message = (True, '')
406 while True:
407 codec = vInfo['vCodec']
408 if mime == 'video/mp4':
409 if codec != 'h264':
410 message = (False, 'vCodec %s not compatible' % codec)
412 break
414 if mime == 'video/bif':
415 if codec != 'vc1':
416 message = (False, 'vCodec %s not compatible' % codec)
418 break
420 if codec not in ('mpeg2video', 'mpeg1video'):
421 message = (False, 'vCodec %s not compatible' % codec)
422 break
424 if vInfo['kbps'] != None:
425 abit = max('0', vInfo['aKbps'])
426 if (int(vInfo['kbps']) - int(abit) >
427 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
428 message = (False, '%s kbps exceeds max video bitrate' %
429 vInfo['kbps'])
430 break
431 else:
432 message = (False, '%s kbps not supported' % vInfo['kbps'])
433 break
435 if config.isHDtivo(tsn):
436 if vInfo['par2'] != 1.0:
437 if config.getPixelAR(0):
438 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
439 message = (False, '%s not correct PAR' % vInfo['par2'])
440 break
441 # HD Tivo detected, skipping remaining tests.
442 break
444 if not vInfo['vFps'] == '29.97':
445 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
446 break
448 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
449 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
450 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9',
451 '880:657'):
452 message = (False, ('DAR %s not supported ' +
453 'by BLACKLIST_169 tivos') % vInfo['dar1'])
454 break
456 mode = (vInfo['vWidth'], vInfo['vHeight'])
457 if mode not in [(720, 480), (704, 480), (544, 480),
458 (528, 480), (480, 480), (352, 480), (352, 240)]:
459 message = (False, '%s x %s not in supported modes' % mode)
460 break
462 return message
464 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
465 message = (True, '')
466 while True:
467 codec = vInfo['aCodec']
468 if mime == 'video/mp4':
469 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
470 'ac3', 'liba52'):
471 message = (False, 'aCodec %s not compatible' % codec)
473 break
475 if mime == 'video/bif':
476 if codec != 'wmav2':
477 message = (False, 'aCodec %s not compatible' % codec)
479 break
481 if codec not in ('ac3', 'liba52', 'mp2'):
482 message = (False, 'aCodec %s not compatible' % codec)
483 break
485 if (not vInfo['aKbps'] or
486 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
487 message = (False, '%s kbps exceeds max audio bitrate' %
488 vInfo['aKbps'])
489 break
491 if config.getAudioLang(tsn):
492 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
493 message = (False, '%s preferred audio track exists' %
494 config.getAudioLang(tsn))
495 break
497 return message
499 def tivo_compatible_container(vInfo, mime=''):
500 message = (True, '')
501 container = vInfo['container']
502 if ((mime == 'video/mp4' and container != 'mov') or
503 (mime == 'video/bif' and container != 'asf') or
504 (mime in ['video/mpeg', ''] and
505 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
506 message = (False, 'container %s not compatible' % container)
508 return message
510 def tivo_compatible(inFile, tsn='', mime=''):
511 vInfo = video_info(inFile)
513 message = (True, 'all compatible')
514 while True:
515 if inFile[-5:].lower() == '.tivo':
516 e = 'ends with .tivo'
517 if mime in ['video/mp4', 'video/bif']:
518 message = (False, e)
519 else:
520 message = (True, e)
521 break
523 vmessage = tivo_compatible_video(vInfo, tsn, mime)
524 if not vmessage[0]:
525 message = vmessage
526 break
528 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
529 if not amessage[0]:
530 message = amessage
531 break
533 cmessage = tivo_compatible_container(vInfo, mime)
534 if not cmessage[0]:
535 message = cmessage
537 break
539 logger.debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
540 message[1], inFile))
541 return message
543 def video_info(inFile, cache=True):
544 vInfo = dict()
545 mtime = os.stat(inFile).st_mtime
546 if cache:
547 if inFile in info_cache and info_cache[inFile][0] == mtime:
548 logging.debug('CACHE HIT! %s' % inFile)
549 return info_cache[inFile][1]
551 vInfo['Supported'] = True
553 if inFile[-5:].lower() == '.tivo':
554 vInfo['millisecs'] = 0
555 if cache:
556 info_cache[inFile] = (mtime, vInfo)
557 logger.debug('VALID, ends in .tivo. %s' % inFile)
558 return vInfo
560 cmd = [config.ffmpeg_path(), '-i', inFile]
561 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
562 err_tmp = tempfile.TemporaryFile()
563 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
564 stdin=subprocess.PIPE)
566 # wait configured # of seconds: if ffmpeg is not back give up
567 wait = config.getFFmpegWait()
568 logging.debug(
569 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
570 for i in xrange(wait * 20):
571 time.sleep(.05)
572 if not ffmpeg.poll() == None:
573 break
575 if ffmpeg.poll() == None:
576 kill(ffmpeg)
577 vInfo['Supported'] = False
578 if cache:
579 info_cache[inFile] = (mtime, vInfo)
580 return vInfo
582 err_tmp.seek(0)
583 output = err_tmp.read()
584 err_tmp.close()
585 logging.debug('ffmpeg output=%s' % output)
587 rezre = re.compile(r'Input #0, ([^,]+),')
588 x = rezre.search(output)
589 if x:
590 vInfo['container'] = x.group(1)
591 else:
592 vInfo['container'] = ''
593 vInfo['Supported'] = False
594 logger.debug('failed at container')
596 rezre = re.compile(r'.*Video: ([^,]+),.*')
597 x = rezre.search(output)
598 if x:
599 vInfo['vCodec'] = x.group(1)
600 else:
601 vInfo['vCodec'] = ''
602 vInfo['Supported'] = False
603 logger.debug('failed at vCodec')
605 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
606 x = rezre.search(output)
607 if x:
608 vInfo['vWidth'] = int(x.group(1))
609 vInfo['vHeight'] = int(x.group(2))
610 else:
611 vInfo['vWidth'] = ''
612 vInfo['vHeight'] = ''
613 vInfo['Supported'] = False
614 logger.debug('failed at vWidth/vHeight')
616 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
617 x = rezre.search(output)
618 if x:
619 vInfo['vFps'] = x.group(1)
621 # Allow override only if it is mpeg2 and frame rate was doubled
622 # to 59.94
624 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
625 # First look for the build 7215 version
626 rezre = re.compile(r'.*film source: 29.97.*')
627 x = rezre.search(output.lower())
628 if x:
629 logger.debug('film source: 29.97 setting vFps to 29.97')
630 vInfo['vFps'] = '29.97'
631 else:
632 # for build 8047:
633 rezre = re.compile(r'.*frame rate differs from container ' +
634 r'frame rate: 29.97.*')
635 logger.debug('Bug in VideoReDo')
636 x = rezre.search(output.lower())
637 if x:
638 vInfo['vFps'] = '29.97'
639 else:
640 vInfo['vFps'] = ''
641 vInfo['Supported'] = False
642 logger.debug('failed at vFps')
644 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
645 d = durre.search(output)
647 if d:
648 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
649 int(d.group(2)) * 60 +
650 int(d.group(3))) * 1000 +
651 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
652 else:
653 vInfo['millisecs'] = 0
655 # get bitrate of source for tivo compatibility test.
656 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
657 x = rezre.search(output)
658 if x:
659 vInfo['kbps'] = x.group(1)
660 else:
661 # Fallback method of getting video bitrate
662 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
663 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
664 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
665 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
666 x = rezre.search(output)
667 if x:
668 vInfo['kbps'] = x.group(1)
669 else:
670 vInfo['kbps'] = None
671 logger.debug('failed at kbps')
673 # get audio bitrate of source for tivo compatibility test.
674 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
675 x = rezre.search(output)
676 if x:
677 vInfo['aKbps'] = x.group(1)
678 else:
679 vInfo['aKbps'] = None
680 logger.debug('failed at aKbps')
682 # get audio codec of source for tivo compatibility test.
683 rezre = re.compile(r'.*Audio: ([^,]+),.*')
684 x = rezre.search(output)
685 if x:
686 vInfo['aCodec'] = x.group(1)
687 else:
688 vInfo['aCodec'] = None
689 logger.debug('failed at aCodec')
691 # get audio frequency of source for tivo compatibility test.
692 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
693 x = rezre.search(output)
694 if x:
695 vInfo['aFreq'] = x.group(1)
696 else:
697 vInfo['aFreq'] = None
698 logger.debug('failed at aFreq')
700 # get par.
701 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
702 x = rezre.search(output)
703 if x and x.group(1) != "0" and x.group(2) != "0":
704 vInfo['par1'] = x.group(1) + ':' + x.group(2)
705 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
706 else:
707 vInfo['par1'], vInfo['par2'] = None, None
709 # get dar.
710 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
711 x = rezre.search(output)
712 if x and x.group(1) != "0" and x.group(2) != "0":
713 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
714 else:
715 vInfo['dar1'] = None
717 # get Video Stream mapping.
718 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
719 x = rezre.search(output)
720 if x:
721 vInfo['mapVideo'] = x.group(1)
722 else:
723 vInfo['mapVideo'] = None
724 logger.debug('failed at mapVideo')
726 # get Audio Stream mapping.
727 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
728 x = rezre.search(output)
729 amap = []
730 if x:
731 for x in rezre.finditer(output):
732 amap.append(x.groups())
733 else:
734 amap.append(('', ''))
735 logger.debug('failed at mapAudio')
736 vInfo['mapAudio'] = amap
738 vInfo['par'] = None
739 videoPlugin = GetPlugin('video')
740 metadata = videoPlugin.getMetadataFromTxt(inFile)
742 for key in metadata:
743 if key.startswith('Override_'):
744 vInfo['Supported'] = True
745 if key.startswith('Override_mapAudio'):
746 audiomap = dict(vInfo['mapAudio'])
747 stream = key.replace('Override_mapAudio', '').strip()
748 if stream in audiomap:
749 newaudiomap = (stream, metadata[key])
750 audiomap.update([newaudiomap])
751 vInfo['mapAudio'] = sorted(audiomap.items(),
752 key=lambda (k,v): (k,v))
753 elif key.startswith('Override_millisecs'):
754 vInfo[key.replace('Override_', '')] = int(metadata[key])
755 else:
756 vInfo[key.replace('Override_', '')] = metadata[key]
758 if cache:
759 info_cache[inFile] = (mtime, vInfo)
760 logger.debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
761 return vInfo
763 def audio_check(inFile, tsn):
764 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
765 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
766 cmd = [config.ffmpeg_path(), '-i', inFile] + cmd_string.split()
767 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
768 fd, testname = tempfile.mkstemp()
769 testfile = os.fdopen(fd, 'wb')
770 try:
771 shutil.copyfileobj(ffmpeg.stdout, testfile)
772 except:
773 kill(ffmpeg)
774 testfile.close()
775 aKbps = None
776 else:
777 testfile.close()
778 aKbps = video_info(testname, False)['aKbps']
779 os.remove(testname)
780 return aKbps
782 def supported_format(inFile):
783 if video_info(inFile)['Supported']:
784 return True
785 else:
786 logger.debug('FALSE, file not supported %s' % inFile)
787 return False
789 def kill(popen):
790 logger.debug('killing pid=%s' % str(popen.pid))
791 if mswindows:
792 win32kill(popen.pid)
793 else:
794 import os, signal
795 for i in xrange(3):
796 logger.debug('sending SIGTERM to pid: %s' % popen.pid)
797 os.kill(popen.pid, signal.SIGTERM)
798 time.sleep(.5)
799 if popen.poll() is not None:
800 logger.debug('process %s has exited' % popen.pid)
801 break
802 else:
803 while popen.poll() is None:
804 logger.debug('sending SIGKILL to pid: %s' % popen.pid)
805 os.kill(popen.pid, signal.SIGKILL)
806 time.sleep(.5)
808 def win32kill(pid):
809 import ctypes
810 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
811 ctypes.windll.kernel32.TerminateProcess(handle, -1)
812 ctypes.windll.kernel32.CloseHandle(handle)
814 def gcd(a, b):
815 while b:
816 a, b = b, a % b
817 return a