A more thorough kill(), by Anton Solovyev.
[pyTivo/wgw.git] / plugins / video / transcode.py
blobda21ffa425afbee363ee425a69d99940ef3f6b86
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 BAD_MPEG_FPS = ['15.00']
22 def ffmpeg_path():
23 return config.get('Server', 'ffmpeg')
25 # XXX BIG HACK
26 # subprocess is broken for me on windows so super hack
27 def patchSubprocess():
28 o = subprocess.Popen._make_inheritable
30 def _make_inheritable(self, handle):
31 if not handle: return subprocess.GetCurrentProcess()
32 return o(self, handle)
34 subprocess.Popen._make_inheritable = _make_inheritable
35 mswindows = (sys.platform == "win32")
36 if mswindows:
37 patchSubprocess()
39 def output_video(inFile, outFile, tsn=''):
40 if tivo_compatible(inFile, tsn)[0]:
41 logger.debug('%s is tivo compatible' % inFile)
42 f = file(inFile, 'rb')
43 shutil.copyfileobj(f, outFile)
44 f.close()
45 else:
46 logger.debug('%s is not tivo compatible' % inFile)
47 transcode(False, inFile, outFile, tsn)
49 def transcode(isQuery, inFile, outFile, tsn=''):
50 settings = {'video_codec': select_videocodec(tsn),
51 'video_br': select_videobr(inFile, tsn),
52 'video_fps': select_videofps(inFile, tsn),
53 'max_video_br': select_maxvideobr(),
54 'buff_size': select_buffsize(tsn),
55 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
56 'audio_br': select_audiobr(tsn),
57 'audio_fr': select_audiofr(inFile, tsn),
58 'audio_ch': select_audioch(tsn),
59 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
60 'audio_lang': select_audiolang(inFile, tsn),
61 'ffmpeg_pram': select_ffmpegprams(tsn),
62 'format': select_format(tsn)}
64 if isQuery:
65 return settings
67 cmd_string = config.getFFmpegTemplate(tsn) % settings
69 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
70 logging.debug('transcoding to tivo model ' + tsn[:3] +
71 ' using ffmpeg command:')
72 logging.debug(' '.join(cmd))
73 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
74 stdout=subprocess.PIPE)
75 try:
76 shutil.copyfileobj(ffmpeg.stdout, outFile)
77 except:
78 kill(ffmpeg)
80 def select_audiocodec(isQuery, inFile, tsn=''):
81 vInfo = video_info(inFile)
82 codectype = vInfo['vCodec']
83 codec = config.getAudioCodec(tsn)
84 if not codec:
85 # Default, compatible with all TiVo's
86 codec = 'ac3'
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 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'] not in BAD_MPEG_FPS:
145 fps = ' '
146 if config.getVideoFPS(tsn) != None:
147 fps = '-r ' + config.getVideoFPS(tsn)
148 return fps
150 def select_videocodec(tsn):
151 codec = config.getVideoCodec(tsn)
152 if not codec:
153 codec = 'mpeg2video' # default
154 return '-vcodec ' + codec
156 def select_videobr(inFile, tsn):
157 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
159 def select_videostr(inFile, tsn):
160 video_str = config.strtod(config.getVideoBR(tsn))
161 if config.isHDtivo(tsn):
162 vInfo = video_info(inFile)
163 if vInfo['kbps'] != None and config.getVideoPCT() > 0:
164 video_percent = int(vInfo['kbps']) * 10 * config.getVideoPCT()
165 video_str = max(video_str, video_percent)
166 video_str = int(min(config.strtod(config.getMaxVideoBR()) * 0.95,
167 video_str))
168 return video_str
170 def select_audiobr(tsn):
171 return '-ab ' + config.getAudioBR(tsn)
173 def select_maxvideobr():
174 return '-maxrate ' + config.getMaxVideoBR()
176 def select_buffsize(tsn):
177 return '-bufsize ' + config.getBuffSize(tsn)
179 def select_ffmpegprams(tsn):
180 params = config.getFFmpegPrams(tsn)
181 if not params:
182 params = ''
183 return params
185 def select_format(tsn):
186 fmt = config.getFormat(tsn)
187 if not fmt:
188 fmt = 'vob'
189 return '-f %s -' % 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) or vInfo['par']:
231 if vInfo['par2'] == None:
232 if vInfo['par']:
233 npar = float(vInfo['par'])
234 else:
235 npar = config.getPixelAR(1)
236 else:
237 npar = vInfo['par2']
239 # adjust for pixel aspect ratio, if set, because TiVo
240 # expects square pixels
242 if npar < 1.0:
243 return ['-s', str(vInfo['vWidth']) + 'x' +
244 str(int(math.ceil(vInfo['vHeight'] / npar)))]
245 elif npar > 1.0:
246 # FFMPEG expects width to be a multiple of two
248 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
249 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
251 if vInfo['vHeight'] <= TIVO_HEIGHT:
252 # pass all resolutions to S3, except heights greater than
253 # conf height
254 return []
255 # else, resize video.
257 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
258 logger.debug('File + PAR is within 4:3.')
259 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
261 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
262 (59, 72), (59, 36), (59, 54)] or
263 vInfo['dar1'] == '4:3'):
264 logger.debug('File is within 4:3 list.')
265 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
267 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
268 (59, 27)] or vInfo['dar1'] == '16:9')
269 and (aspect169 or config.get169Letterbox(tsn))):
270 logger.debug('File is within 16:9 list and 16:9 allowed.')
272 if config.get169Blacklist(tsn) or (aspect169 and
273 config.get169Letterbox(tsn)):
274 return ['-aspect', '4:3', '-s', '%sx%s' %
275 (TIVO_WIDTH, TIVO_HEIGHT)]
276 else:
277 return ['-aspect', '16:9', '-s', '%sx%s' %
278 (TIVO_WIDTH, TIVO_HEIGHT)]
279 else:
280 settings = []
282 # If video is wider than 4:3 add top and bottom padding
284 if ratio > 133: # Might be 16:9 file, or just need padding on
285 # top and bottom
287 if aspect169 and ratio > 135: # If file would fall in 4:3
288 # assume it is supposed to be 4:3
290 if ratio > 177: # too short needs padding top and bottom
291 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
292 vInfo['vWidth']) * multiplier16by9)
293 settings.append('-aspect')
294 if (config.get169Blacklist(tsn) or
295 config.get169Letterbox(tsn)):
296 settings.append('4:3')
297 else:
298 settings.append('16:9')
299 if endHeight % 2:
300 endHeight -= 1
301 if endHeight < TIVO_HEIGHT * 0.99:
302 topPadding = (TIVO_HEIGHT - endHeight) / 2
303 if topPadding % 2:
304 topPadding -= 1
305 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
306 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
307 '-padtop', str(topPadding),
308 '-padbottom', str(bottomPadding)]
309 else: # if only very small amount of padding
310 # needed, then just stretch it
311 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
313 logger.debug(('16:9 aspect allowed, file is wider ' +
314 'than 16:9 padding top and bottom\n%s') %
315 ' '.join(settings))
317 else: # too skinny needs padding on left and right.
318 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
319 (vInfo['vHeight'] * multiplier16by9))
320 settings.append('-aspect')
321 if (config.get169Blacklist(tsn) or
322 config.get169Letterbox(tsn)):
323 settings.append('4:3')
324 else:
325 settings.append('16:9')
326 if endWidth % 2:
327 endWidth -= 1
328 if endWidth < (TIVO_WIDTH - 10):
329 leftPadding = (TIVO_WIDTH - endWidth) / 2
330 if leftPadding % 2:
331 leftPadding -= 1
332 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
333 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
334 '-padleft', str(leftPadding),
335 '-padright', str(rightPadding)]
336 else: # if only very small amount of padding needed,
337 # then just stretch it
338 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
339 logger.debug(('16:9 aspect allowed, file is narrower ' +
340 'than 16:9 padding left and right\n%s') %
341 ' '.join(settings))
342 else: # this is a 4:3 file or 16:9 output not allowed
343 multiplier = multiplier4by3
344 settings.append('-aspect')
345 if ratio > 135 and config.get169Letterbox(tsn):
346 settings.append('16:9')
347 multiplier = multiplier16by9
348 else:
349 settings.append('4:3')
350 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
351 vInfo['vWidth']) * multiplier)
352 if endHeight % 2:
353 endHeight -= 1
354 if endHeight < TIVO_HEIGHT * 0.99:
355 topPadding = (TIVO_HEIGHT - endHeight) / 2
356 if topPadding % 2:
357 topPadding -= 1
358 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
359 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
360 '-padtop', str(topPadding),
361 '-padbottom', str(bottomPadding)]
362 else: # if only very small amount of padding needed,
363 # then just stretch it
364 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
365 logging.debug(('File is wider than 4:3 padding ' +
366 'top and bottom\n%s') % ' '.join(settings))
368 return settings
370 # If video is taller than 4:3 add left and right padding, this
371 # is rare. All of these files will always be sent in an aspect
372 # ratio of 4:3 since they are so narrow.
374 else:
375 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
376 (vInfo['vHeight'] * multiplier4by3))
377 settings += ['-aspect', '4:3']
378 if endWidth % 2:
379 endWidth -= 1
380 if endWidth < (TIVO_WIDTH * 0.99):
381 leftPadding = (TIVO_WIDTH - endWidth) / 2
382 if leftPadding % 2:
383 leftPadding -= 1
384 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
385 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
386 '-padleft', str(leftPadding),
387 '-padright', str(rightPadding)]
388 else: # if only very small amount of padding needed, then
389 # just stretch it
390 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
392 logger.debug('File is taller than 4:3 padding left and right\n%s'
393 % ' '.join(settings))
395 return settings
397 def tivo_compatible(inFile, tsn=''):
398 supportedModes = [(720, 480), (704, 480), (544, 480),
399 (528, 480), (480, 480), (352, 480)]
400 vInfo = video_info(inFile)
402 while True:
403 if (inFile[-5:]).lower() == '.tivo':
404 message = (True, 'TRANSCODE=NO, ends with .tivo.')
405 break
407 if not vInfo['vCodec'] == 'mpeg2video':
408 #print 'Not Tivo Codec'
409 message = (False, 'TRANSCODE=YES, vCodec %s not compatible.' %
410 vInfo['vCodec'])
411 break
413 if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv',
414 '.tp', '.dvr-ms'):
415 message = (False, 'TRANSCODE=YES, ext %s not compatible.' %
416 os.path.splitext(inFile)[-1])
417 break
419 if vInfo['aCodec'] == 'dca':
420 message = (False, 'TRANSCODE=YES, aCodec %s not compatible.' %
421 vInfo['aCodec'])
422 break
424 if vInfo['aCodec'] != None:
425 if (not vInfo['aKbps'] or
426 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
427 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
428 'audio bitrate.') % vInfo['aKbps'])
429 break
431 if vInfo['kbps'] != None:
432 abit = max('0', vInfo['aKbps'])
433 if (int(vInfo['kbps']) - int(abit) >
434 config.strtod(config.getMaxVideoBR()) / 1000):
435 message = (False, ('TRANSCODE=YES, %s kbps exceeds max ' +
436 'video bitrate.') % vInfo['kbps'])
437 break
438 else:
439 message = (False, 'TRANSCODE=YES, %s kbps not supported.' %
440 vInfo['kbps'])
441 break
443 if config.getAudioLang(tsn):
444 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
445 message = (False, ('TRANSCODE=YES, %s preferred audio ' +
446 'track exists.') % config.getAudioLang(tsn))
447 break
449 if config.isHDtivo(tsn):
450 if vInfo['par2'] != 1.0:
451 if config.getPixelAR(0):
452 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
453 message = (False, 'TRANSCODE=YES, %s not correct PAR.'
454 % vInfo['par2'])
455 break
456 message = (True, 'TRANSCODE=NO, HD Tivo detected, skipping ' +
457 'remaining tests.')
458 break
460 if not vInfo['vFps'] == '29.97':
461 #print 'Not Tivo fps'
462 message = (False, 'TRANSCODE=YES, %s vFps, should be 29.97.' %
463 vInfo['vFps'])
464 break
466 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
467 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
468 if vInfo['dar1'] == None or not vInfo['dar1'] in ('4:3', '8:9'):
469 message = (False, ('TRANSCODE=YES, DAR %s not supported ' +
470 'by BLACKLIST_169 tivos.') % vInfo['dar1'])
471 break
473 for mode in supportedModes:
474 if mode == (vInfo['vWidth'], vInfo['vHeight']):
475 message = (True, 'TRANSCODE=NO, %s x %s is valid.' %
476 (vInfo['vWidth'], vInfo['vHeight']))
477 break
478 #print 'Not Tivo dimensions'
479 message = (False, 'TRANSCODE=YES, %s x %s not in supported modes.'
480 % (vInfo['vWidth'], vInfo['vHeight']))
481 break
483 logger.debug('%s, %s' % (message, inFile))
484 return message
487 def video_info(inFile):
488 vInfo = dict()
489 mtime = os.stat(inFile).st_mtime
490 if inFile != videotest:
491 if inFile in info_cache and info_cache[inFile][0] == mtime:
492 logging.debug('CACHE HIT! %s' % inFile)
493 return info_cache[inFile][1]
495 vInfo['Supported'] = True
497 if (inFile[-5:]).lower() == '.tivo':
498 vInfo['millisecs'] = 0
499 info_cache[inFile] = (mtime, vInfo)
500 logger.debug('VALID, ends in .tivo. %s' % inFile)
501 return vInfo
503 cmd = [ffmpeg_path(), '-i', inFile]
504 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
505 err_tmp = tempfile.TemporaryFile()
506 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
507 stdin=subprocess.PIPE)
509 # wait configured # of seconds: if ffmpeg is not back give up
510 wait = config.getFFmpegWait()
511 logging.debug(
512 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
513 for i in xrange(wait * 20):
514 time.sleep(.05)
515 if not ffmpeg.poll() == None:
516 break
518 if ffmpeg.poll() == None:
519 kill(ffmpeg)
520 vInfo['Supported'] = False
521 info_cache[inFile] = (mtime, vInfo)
522 return vInfo
524 err_tmp.seek(0)
525 output = err_tmp.read()
526 err_tmp.close()
527 logging.debug('ffmpeg output=%s' % output)
529 rezre = re.compile(r'.*Video: ([^,]+),.*')
530 x = rezre.search(output)
531 if x:
532 vInfo['vCodec'] = x.group(1)
533 else:
534 vInfo['vCodec'] = ''
535 vInfo['Supported'] = False
536 logger.debug('failed at vCodec')
538 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
539 x = rezre.search(output)
540 if x:
541 vInfo['vWidth'] = int(x.group(1))
542 vInfo['vHeight'] = int(x.group(2))
543 else:
544 vInfo['vWidth'] = ''
545 vInfo['vHeight'] = ''
546 vInfo['Supported'] = False
547 logger.debug('failed at vWidth/vHeight')
549 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
550 x = rezre.search(output)
551 if x:
552 vInfo['vFps'] = x.group(1)
554 # Allow override only if it is mpeg2 and frame rate was doubled
555 # to 59.94
557 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
558 # First look for the build 7215 version
559 rezre = re.compile(r'.*film source: 29.97.*')
560 x = rezre.search(output.lower())
561 if x:
562 logger.debug('film source: 29.97 setting vFps to 29.97')
563 vInfo['vFps'] = '29.97'
564 else:
565 # for build 8047:
566 rezre = re.compile(r'.*frame rate differs from container ' +
567 r'frame rate: 29.97.*')
568 logger.debug('Bug in VideoReDo')
569 x = rezre.search(output.lower())
570 if x:
571 vInfo['vFps'] = '29.97'
572 else:
573 vInfo['vFps'] = ''
574 vInfo['Supported'] = False
575 logger.debug('failed at vFps')
577 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
578 d = durre.search(output)
580 if d:
581 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
582 int(d.group(2)) * 60 +
583 int(d.group(3))) * 1000 +
584 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
585 else:
586 vInfo['millisecs'] = 0
588 # get bitrate of source for tivo compatibility test.
589 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
590 x = rezre.search(output)
591 if x:
592 vInfo['kbps'] = x.group(1)
593 else:
594 vInfo['kbps'] = None
595 logger.debug('failed at kbps')
597 # get audio bitrate of source for tivo compatibility test.
598 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
599 x = rezre.search(output)
600 if x:
601 vInfo['aKbps'] = x.group(1)
602 else:
603 vInfo['aKbps'] = None
604 logger.debug('failed at aKbps')
606 # get audio codec of source for tivo compatibility test.
607 rezre = re.compile(r'.*Audio: ([^,]+),.*')
608 x = rezre.search(output)
609 if x:
610 vInfo['aCodec'] = x.group(1)
611 else:
612 vInfo['aCodec'] = None
613 logger.debug('failed at aCodec')
615 # get audio frequency of source for tivo compatibility test.
616 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
617 x = rezre.search(output)
618 if x:
619 vInfo['aFreq'] = x.group(1)
620 else:
621 vInfo['aFreq'] = None
622 logger.debug('failed at aFreq')
624 # get par.
625 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
626 x = rezre.search(output)
627 if x and x.group(1) != "0" and x.group(2) != "0":
628 vInfo['par1'] = x.group(1) + ':' + x.group(2)
629 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
630 else:
631 vInfo['par1'], vInfo['par2'] = None, None
633 # get dar.
634 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
635 x = rezre.search(output)
636 if x and x.group(1) != "0" and x.group(2) != "0":
637 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
638 vInfo['dar2'] = float(x.group(1)) / float(x.group(2))
639 else:
640 vInfo['dar1'], vInfo['dar2'] = None, None
642 # get Video Stream mapping.
643 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
644 x = rezre.search(output)
645 if x:
646 vInfo['mapVideo'] = x.group(1)
647 else:
648 vInfo['mapVideo'] = None
649 logger.debug('failed at mapVideo')
651 # get Audio Stream mapping.
652 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
653 x = rezre.search(output)
654 amap = []
655 if x:
656 for x in rezre.finditer(output):
657 amap.append(x.groups())
658 else:
659 amap.append(('', ''))
660 logger.debug('failed at mapAudio')
661 vInfo['mapAudio'] = amap
663 vInfo['par'] = None
664 videoPlugin = GetPlugin('video')
665 metadata = videoPlugin.getMetadataFromTxt(inFile)
667 for key in metadata:
668 if key.startswith('Override_'):
669 vInfo['Supported'] = True
670 if key.startswith('Override_mapAudio'):
671 audiomap = dict(vInfo['mapAudio'])
672 stream = key.replace('Override_mapAudio', '').strip()
673 if stream in audiomap:
674 newaudiomap = (stream, metadata[key])
675 audiomap.update([newaudiomap])
676 vInfo['mapAudio'] = sorted(audiomap.items(),
677 key=lambda (k,v): (k,v))
678 elif key.startswith('Override_millisecs'):
679 vInfo[key.replace('Override_', '')] = int(metadata[key])
680 else:
681 vInfo[key.replace('Override_', '')] = metadata[key]
683 info_cache[inFile] = (mtime, vInfo)
684 logger.debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
685 return vInfo
687 def video_check(inFile, cmd_string):
688 cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
689 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
690 try:
691 shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
692 return True
693 except:
694 kill(ffmpeg)
695 return False
697 def supported_format(inFile):
698 if video_info(inFile)['Supported']:
699 return True
700 else:
701 logger.debug('FALSE, file not supported %s' % inFile)
702 return False
704 def kill(popen):
705 logger.debug('killing pid=%s' % str(popen.pid))
706 if mswindows:
707 win32kill(popen.pid)
708 else:
709 import os, signal
710 for i in xrange(3):
711 logger.debug('sending SIGTERM to pid: %s' % popen.pid)
712 os.kill(popen.pid, signal.SIGTERM)
713 time.sleep(.5)
714 if popen.poll() is not None:
715 logger.debug('process %s has exited' % popen.pid)
716 break
717 else:
718 while popen.poll() is None:
719 logger.debug('sending SIGKILL to pid: %s' % popen.pid)
720 os.kill(popen.pid, signal.SIGKILL)
721 time.sleep(.5)
723 def win32kill(pid):
724 import ctypes
725 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
726 ctypes.windll.kernel32.TerminateProcess(handle, -1)
727 ctypes.windll.kernel32.CloseHandle(handle)
729 def gcd(a, b):
730 while b:
731 a, b = b, a % b
732 return a