Moved metadata functions to their own module.
[pyTivo/TheBayer.git] / plugins / video / transcode.py
blobb1dbea8b0a6fa280429e1eecb6d1b8b33fd54536
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 threading
10 import time
12 import lrucache
14 import config
15 import metadata
17 logger = logging.getLogger('pyTivo.video.transcode')
19 info_cache = lrucache.LRUCache(1000)
20 ffmpeg_procs = {}
21 reapers = {}
23 GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97',
24 '30.00', '50.00', '59.94', '60.00']
26 BLOCKSIZE = 512 * 1024
27 MAXBLOCKS = 2
28 TIMEOUT = 600
30 # XXX BIG HACK
31 # subprocess is broken for me on windows so super hack
32 def patchSubprocess():
33 o = subprocess.Popen._make_inheritable
35 def _make_inheritable(self, handle):
36 if not handle: return subprocess.GetCurrentProcess()
37 return o(self, handle)
39 subprocess.Popen._make_inheritable = _make_inheritable
40 mswindows = (sys.platform == "win32")
41 if mswindows:
42 patchSubprocess()
44 def debug(msg):
45 if type(msg) == str:
46 try:
47 msg = msg.decode('utf8')
48 except:
49 if sys.platform == 'darwin':
50 msg = msg.decode('macroman')
51 else:
52 msg = msg.decode('iso8859-1')
53 logger.debug(msg)
55 def transcode(isQuery, inFile, outFile, tsn=''):
56 settings = {'video_codec': select_videocodec(inFile, tsn),
57 'video_br': select_videobr(inFile, tsn),
58 'video_fps': select_videofps(inFile, tsn),
59 'max_video_br': select_maxvideobr(tsn),
60 'buff_size': select_buffsize(tsn),
61 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
62 'audio_br': select_audiobr(tsn),
63 'audio_fr': select_audiofr(inFile, tsn),
64 'audio_ch': select_audioch(tsn),
65 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
66 'audio_lang': select_audiolang(inFile, tsn),
67 'ffmpeg_pram': select_ffmpegprams(tsn),
68 'format': select_format(tsn)}
70 if isQuery:
71 return settings
73 ffmpeg_path = config.get_bin('ffmpeg')
74 cmd_string = config.getFFmpegTemplate(tsn) % settings
76 if inFile[-5:].lower() == '.tivo':
77 tivodecode_path = config.get_bin('tivodecode')
78 tivo_mak = config.get_server('tivo_mak')
79 tcmd = [tivodecode_path, '-m', tivo_mak, inFile]
80 tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE,
81 bufsize=(512 * 1024))
82 if tivo_compatible(inFile, tsn)[0]:
83 cmd = ''
84 ffmpeg = tivodecode
85 else:
86 cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split()
87 ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout,
88 stdout=subprocess.PIPE,
89 bufsize=(512 * 1024))
90 else:
91 cmd = [ffmpeg_path, '-i', inFile] + cmd_string.split()
92 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
93 stdout=subprocess.PIPE)
95 if cmd:
96 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
97 debug(' '.join(cmd))
99 ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0,
100 'last_read': time.time(), 'blocks': []}
101 reap_process(inFile)
102 transfer_blocks(inFile, outFile)
104 def is_resumable(inFile, offset):
105 if inFile in ffmpeg_procs:
106 proc = ffmpeg_procs[inFile]
107 if proc['start'] <= offset < proc['end']:
108 return True
109 else:
110 cleanup(inFile)
111 kill(proc['process'])
112 return False
114 def resume_transfer(inFile, outFile, offset):
115 proc = ffmpeg_procs[inFile]
116 offset -= proc['start']
117 try:
118 for block in proc['blocks']:
119 length = len(block)
120 if offset < length:
121 if offset > 0:
122 block = block[offset:]
123 outFile.write('%x\r\n' % len(block))
124 outFile.write(block)
125 outFile.write('\r\n')
126 offset -= length
127 outFile.flush()
128 except Exception, msg:
129 logger.info(msg)
130 return
131 proc['start'] = proc['end']
132 proc['blocks'] = []
134 transfer_blocks(inFile, outFile)
136 def transfer_blocks(inFile, outFile):
137 proc = ffmpeg_procs[inFile]
138 blocks = proc['blocks']
140 while True:
141 try:
142 block = proc['process'].stdout.read(BLOCKSIZE)
143 proc['last_read'] = time.time()
144 except Exception, msg:
145 logger.info(msg)
146 cleanup(inFile)
147 kill(proc['process'])
148 break
150 if not block:
151 try:
152 outFile.flush()
153 except Exception, msg:
154 logger.info(msg)
155 else:
156 cleanup(inFile)
157 break
159 blocks.append(block)
160 proc['end'] += len(block)
161 if len(blocks) > MAXBLOCKS:
162 proc['start'] += len(blocks[0])
163 blocks.pop(0)
165 try:
166 outFile.write('%x\r\n' % len(block))
167 outFile.write(block)
168 outFile.write('\r\n')
169 except Exception, msg:
170 logger.info(msg)
171 break
173 def reap_process(inFile):
174 if inFile in ffmpeg_procs:
175 proc = ffmpeg_procs[inFile]
176 if proc['last_read'] + TIMEOUT < time.time():
177 del ffmpeg_procs[inFile]
178 del reapers[inFile]
179 kill(proc['process'])
180 else:
181 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
182 reapers[inFile] = reaper
183 reaper.start()
185 def cleanup(inFile):
186 del ffmpeg_procs[inFile]
187 reapers[inFile].cancel()
188 del reapers[inFile]
190 def select_audiocodec(isQuery, inFile, tsn=''):
191 if inFile[-5:].lower() == '.tivo':
192 return '-acodec copy'
193 vInfo = video_info(inFile)
194 codectype = vInfo['vCodec']
195 codec = config.get_tsn('audio_codec', tsn)
196 if not codec:
197 # Default, compatible with all TiVo's
198 codec = 'ac3'
199 if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'):
200 aKbps = vInfo['aKbps']
201 if aKbps == None:
202 if not isQuery:
203 aKbps = audio_check(inFile, tsn)
204 else:
205 codec = 'TBD'
206 if aKbps != None and int(aKbps) <= config.getMaxAudioBR(tsn):
207 # compatible codec and bitrate, do not reencode audio
208 codec = 'copy'
209 copy_flag = config.get_tsn('copy_ts', tsn)
210 copyts = ' -copyts'
211 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
212 (copy_flag and copy_flag.lower() == 'false')):
213 copyts = ''
214 return '-acodec ' + codec + copyts
216 def select_audiofr(inFile, tsn):
217 freq = '48000' #default
218 vInfo = video_info(inFile)
219 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
220 # compatible frequency
221 freq = vInfo['aFreq']
222 audio_fr = config.get_tsn('audio_fr', tsn)
223 if audio_fr != None:
224 freq = audio_fr
225 return '-ar ' + freq
227 def select_audioch(tsn):
228 ch = config.get_tsn('audio_ch', tsn)
229 if ch:
230 return '-ac ' + ch
231 return ''
233 def select_audiolang(inFile, tsn):
234 vInfo = video_info(inFile)
235 audio_lang = config.get_tsn('audio_lang', tsn)
236 if audio_lang != None and vInfo['mapVideo'] != None:
237 stream = vInfo['mapAudio'][0][0]
238 langmatch = []
239 for lang in audio_lang.replace(' ','').lower().split(','):
240 for s, l in vInfo['mapAudio']:
241 if lang in s + l.replace(' ','').lower():
242 langmatch.append(s)
243 stream = s
244 break
245 if langmatch: break
246 if stream is not '':
247 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
248 return ''
250 def select_videofps(inFile, tsn):
251 vInfo = video_info(inFile)
252 fps = '-r 29.97' # default
253 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
254 fps = ' '
255 video_fps = config.get_tsn('video_fps', tsn)
256 if video_fps != None:
257 fps = '-r ' + video_fps
258 return fps
260 def select_videocodec(inFile, tsn):
261 vInfo = video_info(inFile)
262 if tivo_compatible_video(vInfo, tsn)[0]:
263 codec = 'copy'
264 else:
265 codec = 'mpeg2video' # default
266 return '-vcodec ' + codec
268 def select_videobr(inFile, tsn):
269 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
271 def select_videostr(inFile, tsn):
272 vInfo = video_info(inFile)
273 if tivo_compatible_video(vInfo, tsn)[0]:
274 video_str = int(vInfo['kbps'])
275 if vInfo['aKbps']:
276 video_str -= int(vInfo['aKbps'])
277 video_str *= 1000
278 else:
279 video_str = config.strtod(config.getVideoBR(tsn))
280 if config.isHDtivo(tsn):
281 if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0:
282 video_percent = (int(vInfo['kbps']) * 10 *
283 config.getVideoPCT(tsn))
284 video_str = max(video_str, video_percent)
285 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
286 video_str))
287 return video_str
289 def select_audiobr(tsn):
290 return '-ab ' + config.getAudioBR(tsn)
292 def select_maxvideobr(tsn):
293 return '-maxrate ' + config.getMaxVideoBR(tsn)
295 def select_buffsize(tsn):
296 return '-bufsize ' + config.getBuffSize(tsn)
298 def select_ffmpegprams(tsn):
299 params = config.getFFmpegPrams(tsn)
300 if not params:
301 params = ''
302 return params
304 def select_format(tsn):
305 fmt = 'vob'
306 return '-f %s -' % fmt
308 def select_aspect(inFile, tsn = ''):
309 TIVO_WIDTH = config.getTivoWidth(tsn)
310 TIVO_HEIGHT = config.getTivoHeight(tsn)
312 vInfo = video_info(inFile)
314 debug('tsn: %s' % tsn)
316 aspect169 = config.get169Setting(tsn)
318 debug('aspect169: %s' % aspect169)
320 optres = config.getOptres(tsn)
322 debug('optres: %s' % optres)
324 if optres:
325 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
326 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
327 if optHeight < TIVO_HEIGHT:
328 TIVO_HEIGHT = optHeight
329 if optWidth < TIVO_WIDTH:
330 TIVO_WIDTH = optWidth
332 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
333 ratio = vInfo['vWidth'] * 100 / vInfo['vHeight']
334 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
336 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s ' +
337 'millisecs=%s ratio=%s rheight=%s rwidth=%s ' +
338 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile,
339 vInfo['vCodec'], vInfo['vWidth'], vInfo['vHeight'],
340 vInfo['vFps'], vInfo['millisecs'], ratio, rheight,
341 rwidth, TIVO_HEIGHT, TIVO_WIDTH))
343 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
344 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
346 if config.isHDtivo(tsn) and not optres:
347 if config.getPixelAR(0) or vInfo['par']:
348 if vInfo['par2'] == None:
349 if vInfo['par']:
350 npar = float(vInfo['par'])
351 else:
352 npar = config.getPixelAR(1)
353 else:
354 npar = vInfo['par2']
356 # adjust for pixel aspect ratio, if set, because TiVo
357 # expects square pixels
359 if npar < 1.0:
360 return ['-s', str(vInfo['vWidth']) + 'x' +
361 str(int(math.ceil(vInfo['vHeight'] / npar)))]
362 elif npar > 1.0:
363 # FFMPEG expects width to be a multiple of two
365 return ['-s', str(int(math.ceil(vInfo['vWidth'] * npar /
366 2.0) * 2)) + 'x' + str(vInfo['vHeight'])]
368 if vInfo['vHeight'] <= TIVO_HEIGHT:
369 # pass all resolutions to S3, except heights greater than
370 # conf height
371 return []
372 # else, resize video.
374 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
375 debug('File + PAR is within 4:3.')
376 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
378 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
379 (59, 72), (59, 36), (59, 54)] or
380 vInfo['dar1'] == '4:3'):
381 debug('File is within 4:3 list.')
382 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
384 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
385 (59, 27)] or vInfo['dar1'] == '16:9')
386 and (aspect169 or config.get169Letterbox(tsn))):
387 debug('File is within 16:9 list and 16:9 allowed.')
389 if config.get169Blacklist(tsn) or (aspect169 and
390 config.get169Letterbox(tsn)):
391 return ['-aspect', '4:3', '-s', '%sx%s' %
392 (TIVO_WIDTH, TIVO_HEIGHT)]
393 else:
394 return ['-aspect', '16:9', '-s', '%sx%s' %
395 (TIVO_WIDTH, TIVO_HEIGHT)]
396 else:
397 settings = []
399 # If video is wider than 4:3 add top and bottom padding
401 if ratio > 133: # Might be 16:9 file, or just need padding on
402 # top and bottom
404 if aspect169 and ratio > 135: # If file would fall in 4:3
405 # assume it is supposed to be 4:3
407 if ratio > 177: # too short needs padding top and bottom
408 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
409 vInfo['vWidth']) * multiplier16by9)
410 settings.append('-aspect')
411 if (config.get169Blacklist(tsn) or
412 config.get169Letterbox(tsn)):
413 settings.append('4:3')
414 else:
415 settings.append('16:9')
416 if endHeight % 2:
417 endHeight -= 1
418 if endHeight < TIVO_HEIGHT * 0.99:
419 topPadding = (TIVO_HEIGHT - endHeight) / 2
420 if topPadding % 2:
421 topPadding -= 1
422 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
423 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
424 '-padtop', str(topPadding),
425 '-padbottom', str(bottomPadding)]
426 else: # if only very small amount of padding
427 # needed, then just stretch it
428 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
430 debug(('16:9 aspect allowed, file is wider ' +
431 'than 16:9 padding top and bottom\n%s') %
432 ' '.join(settings))
434 else: # too skinny needs padding on left and right.
435 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
436 (vInfo['vHeight'] * multiplier16by9))
437 settings.append('-aspect')
438 if (config.get169Blacklist(tsn) or
439 config.get169Letterbox(tsn)):
440 settings.append('4:3')
441 else:
442 settings.append('16:9')
443 if endWidth % 2:
444 endWidth -= 1
445 if endWidth < (TIVO_WIDTH - 10):
446 leftPadding = (TIVO_WIDTH - endWidth) / 2
447 if leftPadding % 2:
448 leftPadding -= 1
449 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
450 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
451 '-padleft', str(leftPadding),
452 '-padright', str(rightPadding)]
453 else: # if only very small amount of padding needed,
454 # then just stretch it
455 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
456 debug(('16:9 aspect allowed, file is narrower ' +
457 'than 16:9 padding left and right\n%s') %
458 ' '.join(settings))
459 else: # this is a 4:3 file or 16:9 output not allowed
460 multiplier = multiplier4by3
461 settings.append('-aspect')
462 if ratio > 135 and config.get169Letterbox(tsn):
463 settings.append('16:9')
464 multiplier = multiplier16by9
465 else:
466 settings.append('4:3')
467 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
468 vInfo['vWidth']) * multiplier)
469 if endHeight % 2:
470 endHeight -= 1
471 if endHeight < TIVO_HEIGHT * 0.99:
472 topPadding = (TIVO_HEIGHT - endHeight) / 2
473 if topPadding % 2:
474 topPadding -= 1
475 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
476 settings += ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
477 '-padtop', str(topPadding),
478 '-padbottom', str(bottomPadding)]
479 else: # if only very small amount of padding needed,
480 # then just stretch it
481 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
482 debug(('File is wider than 4:3 padding ' +
483 'top and bottom\n%s') % ' '.join(settings))
485 return settings
487 # If video is taller than 4:3 add left and right padding, this
488 # is rare. All of these files will always be sent in an aspect
489 # ratio of 4:3 since they are so narrow.
491 else:
492 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
493 (vInfo['vHeight'] * multiplier4by3))
494 settings += ['-aspect', '4:3']
495 if endWidth % 2:
496 endWidth -= 1
497 if endWidth < (TIVO_WIDTH * 0.99):
498 leftPadding = (TIVO_WIDTH - endWidth) / 2
499 if leftPadding % 2:
500 leftPadding -= 1
501 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
502 settings += ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
503 '-padleft', str(leftPadding),
504 '-padright', str(rightPadding)]
505 else: # if only very small amount of padding needed, then
506 # just stretch it
507 settings += ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
509 debug('File is taller than 4:3 padding left and right\n%s'
510 % ' '.join(settings))
512 return settings
514 def tivo_compatible_video(vInfo, tsn, mime=''):
515 message = (True, '')
516 while True:
517 codec = vInfo['vCodec']
518 if mime == 'video/mp4':
519 if codec != 'h264':
520 message = (False, 'vCodec %s not compatible' % codec)
522 break
524 if mime == 'video/bif':
525 if codec != 'vc1':
526 message = (False, 'vCodec %s not compatible' % codec)
528 break
530 if codec not in ('mpeg2video', 'mpeg1video'):
531 message = (False, 'vCodec %s not compatible' % codec)
532 break
534 if vInfo['kbps'] != None:
535 abit = max('0', vInfo['aKbps'])
536 if (int(vInfo['kbps']) - int(abit) >
537 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
538 message = (False, '%s kbps exceeds max video bitrate' %
539 vInfo['kbps'])
540 break
541 else:
542 message = (False, '%s kbps not supported' % vInfo['kbps'])
543 break
545 if config.isHDtivo(tsn):
546 if vInfo['par2'] != 1.0:
547 if config.getPixelAR(0):
548 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
549 message = (False, '%s not correct PAR' % vInfo['par2'])
550 break
551 # HD Tivo detected, skipping remaining tests.
552 break
554 if not vInfo['vFps'] in ['29.97', '59.94']:
555 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
556 break
558 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
559 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
560 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
561 message = (False, ('DAR %s not supported ' +
562 'by BLACKLIST_169 tivos') % vInfo['dar1'])
563 break
565 mode = (vInfo['vWidth'], vInfo['vHeight'])
566 if mode not in [(720, 480), (704, 480), (544, 480),
567 (528, 480), (480, 480), (352, 480), (352, 240)]:
568 message = (False, '%s x %s not in supported modes' % mode)
569 break
571 return message
573 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
574 message = (True, '')
575 while True:
576 codec = vInfo['aCodec']
577 if mime == 'video/mp4':
578 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
579 'ac3', 'liba52'):
580 message = (False, 'aCodec %s not compatible' % codec)
582 break
584 if mime == 'video/bif':
585 if codec != 'wmav2':
586 message = (False, 'aCodec %s not compatible' % codec)
588 break
590 if inFile[-5:].lower() == '.tivo':
591 break
593 if codec not in ('ac3', 'liba52', 'mp2'):
594 message = (False, 'aCodec %s not compatible' % codec)
595 break
597 if (not vInfo['aKbps'] or
598 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
599 message = (False, '%s kbps exceeds max audio bitrate' %
600 vInfo['aKbps'])
601 break
603 audio_lang = config.get_tsn('audio_lang', tsn)
604 if audio_lang:
605 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
606 message = (False, '%s preferred audio track exists' %
607 audio_lang)
608 break
610 return message
612 def tivo_compatible_container(vInfo, mime=''):
613 message = (True, '')
614 container = vInfo['container']
615 if ((mime == 'video/mp4' and container != 'mov') or
616 (mime == 'video/bif' and container != 'asf') or
617 (mime in ['video/mpeg', ''] and
618 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
619 message = (False, 'container %s not compatible' % container)
621 return message
623 def tivo_compatible(inFile, tsn='', mime=''):
624 vInfo = video_info(inFile)
626 message = (True, 'all compatible')
627 if not config.get_bin('ffmpeg'):
628 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
629 message = (False, 'no ffmpeg')
630 return message
632 while True:
633 vmessage = tivo_compatible_video(vInfo, tsn, mime)
634 if not vmessage[0]:
635 message = vmessage
636 break
638 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
639 if not amessage[0]:
640 message = amessage
641 break
643 cmessage = tivo_compatible_container(vInfo, mime)
644 if not cmessage[0]:
645 message = cmessage
647 break
649 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
650 message[1], inFile))
651 return message
653 def video_info(inFile, cache=True):
654 vInfo = dict()
655 mtime = os.stat(inFile).st_mtime
656 if cache:
657 if inFile in info_cache and info_cache[inFile][0] == mtime:
658 debug('CACHE HIT! %s' % inFile)
659 return info_cache[inFile][1]
661 vInfo['Supported'] = True
663 ffmpeg_path = config.get_bin('ffmpeg')
664 if not ffmpeg_path:
665 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
666 '.vob', '.tivo']:
667 vInfo['Supported'] = False
668 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480})
669 if cache:
670 info_cache[inFile] = (mtime, vInfo)
671 return vInfo
673 cmd = [ffmpeg_path, '-i', inFile]
674 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
675 err_tmp = tempfile.TemporaryFile()
676 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
677 stdin=subprocess.PIPE)
679 # wait configured # of seconds: if ffmpeg is not back give up
680 wait = config.getFFmpegWait()
681 debug(
682 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
683 for i in xrange(wait * 20):
684 time.sleep(.05)
685 if not ffmpeg.poll() == None:
686 break
688 if ffmpeg.poll() == None:
689 kill(ffmpeg)
690 vInfo['Supported'] = False
691 if cache:
692 info_cache[inFile] = (mtime, vInfo)
693 return vInfo
695 err_tmp.seek(0)
696 output = err_tmp.read()
697 err_tmp.close()
698 debug('ffmpeg output=%s' % output)
700 rezre = re.compile(r'Input #0, ([^,]+),')
701 x = rezre.search(output)
702 if x:
703 vInfo['container'] = x.group(1)
704 else:
705 vInfo['container'] = ''
706 vInfo['Supported'] = False
707 debug('failed at container')
709 rezre = re.compile(r'.*Video: ([^,]+),.*')
710 x = rezre.search(output)
711 if x:
712 vInfo['vCodec'] = x.group(1)
713 else:
714 vInfo['vCodec'] = ''
715 vInfo['Supported'] = False
716 debug('failed at vCodec')
718 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
719 x = rezre.search(output)
720 if x:
721 vInfo['vWidth'] = int(x.group(1))
722 vInfo['vHeight'] = int(x.group(2))
723 else:
724 vInfo['vWidth'] = ''
725 vInfo['vHeight'] = ''
726 vInfo['Supported'] = False
727 debug('failed at vWidth/vHeight')
729 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
730 x = rezre.search(output)
731 if x:
732 vInfo['vFps'] = x.group(1)
734 # Allow override only if it is mpeg2 and frame rate was doubled
735 # to 59.94
737 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
738 # First look for the build 7215 version
739 rezre = re.compile(r'.*film source: 29.97.*')
740 x = rezre.search(output.lower())
741 if x:
742 debug('film source: 29.97 setting vFps to 29.97')
743 vInfo['vFps'] = '29.97'
744 else:
745 # for build 8047:
746 rezre = re.compile(r'.*frame rate differs from container ' +
747 r'frame rate: 29.97.*')
748 debug('Bug in VideoReDo')
749 x = rezre.search(output.lower())
750 if x:
751 vInfo['vFps'] = '29.97'
752 else:
753 vInfo['vFps'] = ''
754 vInfo['Supported'] = False
755 debug('failed at vFps')
757 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
758 d = durre.search(output)
760 if d:
761 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
762 int(d.group(2)) * 60 +
763 int(d.group(3))) * 1000 +
764 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
765 else:
766 vInfo['millisecs'] = 0
768 # get bitrate of source for tivo compatibility test.
769 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
770 x = rezre.search(output)
771 if x:
772 vInfo['kbps'] = x.group(1)
773 else:
774 # Fallback method of getting video bitrate
775 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
776 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
777 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
778 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
779 x = rezre.search(output)
780 if x:
781 vInfo['kbps'] = x.group(1)
782 else:
783 vInfo['kbps'] = None
784 debug('failed at kbps')
786 # get audio bitrate of source for tivo compatibility test.
787 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
788 x = rezre.search(output)
789 if x:
790 vInfo['aKbps'] = x.group(1)
791 else:
792 vInfo['aKbps'] = None
793 debug('failed at aKbps')
795 # get audio codec of source for tivo compatibility test.
796 rezre = re.compile(r'.*Audio: ([^,]+),.*')
797 x = rezre.search(output)
798 if x:
799 vInfo['aCodec'] = x.group(1)
800 else:
801 vInfo['aCodec'] = None
802 debug('failed at aCodec')
804 # get audio frequency of source for tivo compatibility test.
805 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
806 x = rezre.search(output)
807 if x:
808 vInfo['aFreq'] = x.group(1)
809 else:
810 vInfo['aFreq'] = None
811 debug('failed at aFreq')
813 # get par.
814 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
815 x = rezre.search(output)
816 if x and x.group(1) != "0" and x.group(2) != "0":
817 vInfo['par1'] = x.group(1) + ':' + x.group(2)
818 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
819 else:
820 vInfo['par1'], vInfo['par2'] = None, None
822 # get dar.
823 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
824 x = rezre.search(output)
825 if x and x.group(1) != "0" and x.group(2) != "0":
826 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
827 else:
828 vInfo['dar1'] = None
830 # get Video Stream mapping.
831 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
832 x = rezre.search(output)
833 if x:
834 vInfo['mapVideo'] = x.group(1)
835 else:
836 vInfo['mapVideo'] = None
837 debug('failed at mapVideo')
839 # get Audio Stream mapping.
840 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
841 x = rezre.search(output)
842 amap = []
843 if x:
844 for x in rezre.finditer(output):
845 amap.append(x.groups())
846 else:
847 amap.append(('', ''))
848 debug('failed at mapAudio')
849 vInfo['mapAudio'] = amap
851 vInfo['par'] = None
853 data = metadata.from_text(inFile)
854 for key in data:
855 if key.startswith('Override_'):
856 vInfo['Supported'] = True
857 if key.startswith('Override_mapAudio'):
858 audiomap = dict(vInfo['mapAudio'])
859 stream = key.replace('Override_mapAudio', '').strip()
860 if stream in audiomap:
861 newaudiomap = (stream, data[key])
862 audiomap.update([newaudiomap])
863 vInfo['mapAudio'] = sorted(audiomap.items(),
864 key=lambda (k,v): (k,v))
865 elif key.startswith('Override_millisecs'):
866 vInfo[key.replace('Override_', '')] = int(data[key])
867 else:
868 vInfo[key.replace('Override_', '')] = data[key]
870 if cache:
871 info_cache[inFile] = (mtime, vInfo)
872 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
873 return vInfo
875 def audio_check(inFile, tsn):
876 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
877 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
878 cmd = [config.get_bin('ffmpeg'), '-i', inFile] + cmd_string.split()
879 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
880 fd, testname = tempfile.mkstemp()
881 testfile = os.fdopen(fd, 'wb')
882 try:
883 shutil.copyfileobj(ffmpeg.stdout, testfile)
884 except:
885 kill(ffmpeg)
886 testfile.close()
887 aKbps = None
888 else:
889 testfile.close()
890 aKbps = video_info(testname, False)['aKbps']
891 os.remove(testname)
892 return aKbps
894 def supported_format(inFile):
895 if video_info(inFile)['Supported']:
896 return True
897 else:
898 debug('FALSE, file not supported %s' % inFile)
899 return False
901 def kill(popen):
902 debug('killing pid=%s' % str(popen.pid))
903 if mswindows:
904 win32kill(popen.pid)
905 else:
906 import os, signal
907 for i in xrange(3):
908 debug('sending SIGTERM to pid: %s' % popen.pid)
909 os.kill(popen.pid, signal.SIGTERM)
910 time.sleep(.5)
911 if popen.poll() is not None:
912 debug('process %s has exited' % popen.pid)
913 break
914 else:
915 while popen.poll() is None:
916 debug('sending SIGKILL to pid: %s' % popen.pid)
917 os.kill(popen.pid, signal.SIGKILL)
918 time.sleep(.5)
920 def win32kill(pid):
921 import ctypes
922 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
923 ctypes.windll.kernel32.TerminateProcess(handle, -1)
924 ctypes.windll.kernel32.CloseHandle(handle)
926 def gcd(a, b):
927 while b:
928 a, b = b, a % b
929 return a