Trying to tame this beast known as select_aspect()...
[pyTivo/TheBayer.git] / plugins / video / transcode.py
blobb3cca9866647877a88b0b4c3ef2978ee6c7d989a
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 pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
309 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
310 vInfo['vWidth']) * multiplier)
311 if endHeight % 2:
312 endHeight -= 1
313 if endHeight < TIVO_HEIGHT * 0.99:
314 topPadding = (TIVO_HEIGHT - endHeight) / 2
315 if topPadding % 2:
316 topPadding -= 1
317 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
318 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
319 '-padtop', str(topPadding),
320 '-padbottom', str(bottomPadding)]
321 else: # if only very small amount of padding needed, then
322 # just stretch it
323 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
325 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
326 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
327 (vInfo['vHeight'] * multiplier))
328 if endWidth % 2:
329 endWidth -= 1
330 if endWidth < TIVO_WIDTH * 0.99:
331 leftPadding = (TIVO_WIDTH - endWidth) / 2
332 if leftPadding % 2:
333 leftPadding -= 1
334 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
335 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
336 '-padleft', str(leftPadding),
337 '-padright', str(rightPadding)]
338 else: # if only very small amount of padding needed, then
339 # just stretch it
340 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
342 def select_aspect(inFile, tsn = ''):
343 TIVO_WIDTH = config.getTivoWidth(tsn)
344 TIVO_HEIGHT = config.getTivoHeight(tsn)
346 vInfo = video_info(inFile)
348 debug('tsn: %s' % tsn)
350 aspect169 = config.get169Setting(tsn)
352 debug('aspect169: %s' % aspect169)
354 optres = config.getOptres(tsn)
356 debug('optres: %s' % optres)
358 if optres:
359 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
360 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
361 if optHeight < TIVO_HEIGHT:
362 TIVO_HEIGHT = optHeight
363 if optWidth < TIVO_WIDTH:
364 TIVO_WIDTH = optWidth
366 if vInfo.get('par2'):
367 par2 = vInfo['par2']
368 elif vInfo.get('par'):
369 par2 = float(vInfo['par'])
370 else:
371 # Assume PAR = 1.0
372 par2 = 1.0
374 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
375 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
376 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
377 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
379 if config.isHDtivo(tsn) and not optres:
380 if config.getPixelAR(0) or vInfo['par']:
381 if vInfo['par2'] == None:
382 if vInfo['par']:
383 npar = par2
384 else:
385 npar = config.getPixelAR(1)
386 else:
387 npar = par2
389 # adjust for pixel aspect ratio, if set
391 if npar < 1.0:
392 return ['-s', '%dx%d' % (vInfo['vWidth'],
393 math.ceil(vInfo['vHeight'] / npar))]
394 elif npar > 1.0:
395 # FFMPEG expects width to be a multiple of two
396 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
397 vInfo['vHeight'])]
399 if vInfo['vHeight'] <= TIVO_HEIGHT:
400 # pass all resolutions to S3, except heights greater than
401 # conf height
402 return []
403 # else, resize video.
405 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
406 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
407 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
409 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
410 debug('File + PAR is within 4:3.')
411 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
413 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
414 (59, 72), (59, 36), (59, 54)] or
415 vInfo['dar1'] == '4:3'):
416 debug('File is within 4:3 list.')
417 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
419 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
420 (59, 27)] or vInfo['dar1'] == '16:9')
421 and (aspect169 or config.get169Letterbox(tsn))):
422 debug('File is within 16:9 list and 16:9 allowed.')
424 if config.get169Blacklist(tsn) or (aspect169 and
425 config.get169Letterbox(tsn)):
426 aspect = '4:3'
427 else:
428 aspect = '16:9'
429 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
431 else:
432 settings = ['-aspect']
434 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
435 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
436 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
437 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
438 multiplier4by3))
440 # If video is wider than 4:3 add top and bottom padding
442 if ratio > 133: # Might be 16:9 file, or just need padding on
443 # top and bottom
445 if aspect169 and ratio > 135: # If file would fall in 4:3
446 # assume it is supposed to be 4:3
448 if (config.get169Blacklist(tsn) or
449 config.get169Letterbox(tsn)):
450 settings.append('4:3')
451 else:
452 settings.append('16:9')
454 if ratio > 177: # too short needs padding top and bottom
455 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
456 multiplier16by9, vInfo)
457 debug(('16:9 aspect allowed, file is wider ' +
458 'than 16:9 padding top and bottom\n%s') %
459 ' '.join(settings))
461 else: # too skinny needs padding on left and right.
462 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
463 multiplier16by9, vInfo)
464 debug(('16:9 aspect allowed, file is narrower ' +
465 'than 16:9 padding left and right\n%s') %
466 ' '.join(settings))
468 else: # this is a 4:3 file or 16:9 output not allowed
469 if ratio > 135 and config.get169Letterbox(tsn):
470 settings.append('16:9')
471 multiplier = multiplier16by9
472 else:
473 settings.append('4:3')
474 multiplier = multiplier4by3
475 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
476 multiplier, vInfo)
477 debug(('File is wider than 4:3 padding ' +
478 'top and bottom\n%s') % ' '.join(settings))
480 # If video is taller than 4:3 add left and right padding, this
481 # is rare. All of these files will always be sent in an aspect
482 # ratio of 4:3 since they are so narrow.
484 else:
485 settings.append('4:3')
486 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
487 debug('File is taller than 4:3 padding left and right\n%s'
488 % ' '.join(settings))
490 return settings
492 def tivo_compatible_video(vInfo, tsn, mime=''):
493 message = (True, '')
494 while True:
495 codec = vInfo['vCodec']
496 if mime == 'video/mp4':
497 if codec != 'h264':
498 message = (False, 'vCodec %s not compatible' % codec)
500 break
502 if mime == 'video/bif':
503 if codec != 'vc1':
504 message = (False, 'vCodec %s not compatible' % codec)
506 break
508 if codec not in ('mpeg2video', 'mpeg1video'):
509 message = (False, 'vCodec %s not compatible' % codec)
510 break
512 if vInfo['kbps'] != None:
513 abit = max('0', vInfo['aKbps'])
514 if (int(vInfo['kbps']) - int(abit) >
515 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
516 message = (False, '%s kbps exceeds max video bitrate' %
517 vInfo['kbps'])
518 break
519 else:
520 message = (False, '%s kbps not supported' % vInfo['kbps'])
521 break
523 if config.isHDtivo(tsn):
524 if vInfo['par2'] != 1.0:
525 if config.getPixelAR(0):
526 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
527 message = (False, '%s not correct PAR' % vInfo['par2'])
528 break
529 # HD Tivo detected, skipping remaining tests.
530 break
532 if not vInfo['vFps'] in ['29.97', '59.94']:
533 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
534 break
536 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
537 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
538 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
539 message = (False, ('DAR %s not supported ' +
540 'by BLACKLIST_169 tivos') % vInfo['dar1'])
541 break
543 mode = (vInfo['vWidth'], vInfo['vHeight'])
544 if mode not in [(720, 480), (704, 480), (544, 480),
545 (528, 480), (480, 480), (352, 480), (352, 240)]:
546 message = (False, '%s x %s not in supported modes' % mode)
547 break
549 return message
551 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
552 message = (True, '')
553 while True:
554 codec = vInfo['aCodec']
555 if mime == 'video/mp4':
556 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
557 'ac3', 'liba52'):
558 message = (False, 'aCodec %s not compatible' % codec)
560 break
562 if mime == 'video/bif':
563 if codec != 'wmav2':
564 message = (False, 'aCodec %s not compatible' % codec)
566 break
568 if inFile[-5:].lower() == '.tivo':
569 break
571 if codec not in ('ac3', 'liba52', 'mp2'):
572 message = (False, 'aCodec %s not compatible' % codec)
573 break
575 if (not vInfo['aKbps'] or
576 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
577 message = (False, '%s kbps exceeds max audio bitrate' %
578 vInfo['aKbps'])
579 break
581 audio_lang = config.get_tsn('audio_lang', tsn)
582 if audio_lang:
583 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
584 message = (False, '%s preferred audio track exists' %
585 audio_lang)
586 break
588 return message
590 def tivo_compatible_container(vInfo, mime=''):
591 message = (True, '')
592 container = vInfo['container']
593 if ((mime == 'video/mp4' and container != 'mov') or
594 (mime == 'video/bif' and container != 'asf') or
595 (mime in ['video/mpeg', ''] and
596 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
597 message = (False, 'container %s not compatible' % container)
599 return message
601 def tivo_compatible(inFile, tsn='', mime=''):
602 vInfo = video_info(inFile)
604 message = (True, 'all compatible')
605 if not config.get_bin('ffmpeg'):
606 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
607 message = (False, 'no ffmpeg')
608 return message
610 while True:
611 vmessage = tivo_compatible_video(vInfo, tsn, mime)
612 if not vmessage[0]:
613 message = vmessage
614 break
616 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
617 if not amessage[0]:
618 message = amessage
619 break
621 cmessage = tivo_compatible_container(vInfo, mime)
622 if not cmessage[0]:
623 message = cmessage
625 break
627 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
628 message[1], inFile))
629 return message
631 def video_info(inFile, cache=True):
632 vInfo = dict()
633 mtime = os.stat(inFile).st_mtime
634 if cache:
635 if inFile in info_cache and info_cache[inFile][0] == mtime:
636 debug('CACHE HIT! %s' % inFile)
637 return info_cache[inFile][1]
639 vInfo['Supported'] = True
641 ffmpeg_path = config.get_bin('ffmpeg')
642 if not ffmpeg_path:
643 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
644 '.vob', '.tivo']:
645 vInfo['Supported'] = False
646 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480})
647 if cache:
648 info_cache[inFile] = (mtime, vInfo)
649 return vInfo
651 cmd = [ffmpeg_path, '-i', inFile]
652 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
653 err_tmp = tempfile.TemporaryFile()
654 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
655 stdin=subprocess.PIPE)
657 # wait configured # of seconds: if ffmpeg is not back give up
658 wait = config.getFFmpegWait()
659 debug(
660 'starting ffmpeg, will wait %s seconds for it to complete' % wait)
661 for i in xrange(wait * 20):
662 time.sleep(.05)
663 if not ffmpeg.poll() == None:
664 break
666 if ffmpeg.poll() == None:
667 kill(ffmpeg)
668 vInfo['Supported'] = False
669 if cache:
670 info_cache[inFile] = (mtime, vInfo)
671 return vInfo
673 err_tmp.seek(0)
674 output = err_tmp.read()
675 err_tmp.close()
676 debug('ffmpeg output=%s' % output)
678 rezre = re.compile(r'Input #0, ([^,]+),')
679 x = rezre.search(output)
680 if x:
681 vInfo['container'] = x.group(1)
682 else:
683 vInfo['container'] = ''
684 vInfo['Supported'] = False
685 debug('failed at container')
687 rezre = re.compile(r'.*Video: ([^,]+),.*')
688 x = rezre.search(output)
689 if x:
690 vInfo['vCodec'] = x.group(1)
691 else:
692 vInfo['vCodec'] = ''
693 vInfo['Supported'] = False
694 debug('failed at vCodec')
696 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
697 x = rezre.search(output)
698 if x:
699 vInfo['vWidth'] = int(x.group(1))
700 vInfo['vHeight'] = int(x.group(2))
701 else:
702 vInfo['vWidth'] = ''
703 vInfo['vHeight'] = ''
704 vInfo['Supported'] = False
705 debug('failed at vWidth/vHeight')
707 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
708 x = rezre.search(output)
709 if x:
710 vInfo['vFps'] = x.group(1)
711 if '.' not in vInfo['vFps']:
712 vInfo['vFps'] += '.00'
714 # Allow override only if it is mpeg2 and frame rate was doubled
715 # to 59.94
717 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
718 # First look for the build 7215 version
719 rezre = re.compile(r'.*film source: 29.97.*')
720 x = rezre.search(output.lower())
721 if x:
722 debug('film source: 29.97 setting vFps to 29.97')
723 vInfo['vFps'] = '29.97'
724 else:
725 # for build 8047:
726 rezre = re.compile(r'.*frame rate differs from container ' +
727 r'frame rate: 29.97.*')
728 debug('Bug in VideoReDo')
729 x = rezre.search(output.lower())
730 if x:
731 vInfo['vFps'] = '29.97'
732 else:
733 vInfo['vFps'] = ''
734 vInfo['Supported'] = False
735 debug('failed at vFps')
737 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
738 d = durre.search(output)
740 if d:
741 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
742 int(d.group(2)) * 60 +
743 int(d.group(3))) * 1000 +
744 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
745 else:
746 vInfo['millisecs'] = 0
748 # get bitrate of source for tivo compatibility test.
749 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
750 x = rezre.search(output)
751 if x:
752 vInfo['kbps'] = x.group(1)
753 else:
754 # Fallback method of getting video bitrate
755 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
756 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
757 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
758 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
759 x = rezre.search(output)
760 if x:
761 vInfo['kbps'] = x.group(1)
762 else:
763 vInfo['kbps'] = None
764 debug('failed at kbps')
766 # get audio bitrate of source for tivo compatibility test.
767 rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
768 x = rezre.search(output)
769 if x:
770 vInfo['aKbps'] = x.group(1)
771 else:
772 vInfo['aKbps'] = None
773 debug('failed at aKbps')
775 # get audio codec of source for tivo compatibility test.
776 rezre = re.compile(r'.*Audio: ([^,]+),.*')
777 x = rezre.search(output)
778 if x:
779 vInfo['aCodec'] = x.group(1)
780 else:
781 vInfo['aCodec'] = None
782 debug('failed at aCodec')
784 # get audio frequency of source for tivo compatibility test.
785 rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
786 x = rezre.search(output)
787 if x:
788 vInfo['aFreq'] = x.group(1)
789 else:
790 vInfo['aFreq'] = None
791 debug('failed at aFreq')
793 # get par.
794 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
795 x = rezre.search(output)
796 if x and x.group(1) != "0" and x.group(2) != "0":
797 vInfo['par1'] = x.group(1) + ':' + x.group(2)
798 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
799 else:
800 vInfo['par1'], vInfo['par2'] = None, None
802 # get dar.
803 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
804 x = rezre.search(output)
805 if x and x.group(1) != "0" and x.group(2) != "0":
806 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
807 else:
808 vInfo['dar1'] = None
810 # get Video Stream mapping.
811 rezre = re.compile(r'([0-9]+\.[0-9]+).*: Video:.*')
812 x = rezre.search(output)
813 if x:
814 vInfo['mapVideo'] = x.group(1)
815 else:
816 vInfo['mapVideo'] = None
817 debug('failed at mapVideo')
819 # get Audio Stream mapping.
820 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:.*')
821 x = rezre.search(output)
822 amap = []
823 if x:
824 for x in rezre.finditer(output):
825 amap.append(x.groups())
826 else:
827 amap.append(('', ''))
828 debug('failed at mapAudio')
829 vInfo['mapAudio'] = amap
831 vInfo['par'] = None
833 data = metadata.from_text(inFile)
834 for key in data:
835 if key.startswith('Override_'):
836 vInfo['Supported'] = True
837 if key.startswith('Override_mapAudio'):
838 audiomap = dict(vInfo['mapAudio'])
839 stream = key.replace('Override_mapAudio', '').strip()
840 if stream in audiomap:
841 newaudiomap = (stream, data[key])
842 audiomap.update([newaudiomap])
843 vInfo['mapAudio'] = sorted(audiomap.items(),
844 key=lambda (k,v): (k,v))
845 elif key.startswith('Override_millisecs'):
846 vInfo[key.replace('Override_', '')] = int(data[key])
847 else:
848 vInfo[key.replace('Override_', '')] = data[key]
850 if cache:
851 info_cache[inFile] = (mtime, vInfo)
852 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
853 return vInfo
855 def audio_check(inFile, tsn):
856 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
857 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
858 cmd = [config.get_bin('ffmpeg'), '-i', inFile] + cmd_string.split()
859 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
860 fd, testname = tempfile.mkstemp()
861 testfile = os.fdopen(fd, 'wb')
862 try:
863 shutil.copyfileobj(ffmpeg.stdout, testfile)
864 except:
865 kill(ffmpeg)
866 testfile.close()
867 aKbps = None
868 else:
869 testfile.close()
870 aKbps = video_info(testname, False)['aKbps']
871 os.remove(testname)
872 return aKbps
874 def supported_format(inFile):
875 if video_info(inFile)['Supported']:
876 return True
877 else:
878 debug('FALSE, file not supported %s' % inFile)
879 return False
881 def kill(popen):
882 debug('killing pid=%s' % str(popen.pid))
883 if mswindows:
884 win32kill(popen.pid)
885 else:
886 import os, signal
887 for i in xrange(3):
888 debug('sending SIGTERM to pid: %s' % popen.pid)
889 os.kill(popen.pid, signal.SIGTERM)
890 time.sleep(.5)
891 if popen.poll() is not None:
892 debug('process %s has exited' % popen.pid)
893 break
894 else:
895 while popen.poll() is None:
896 debug('sending SIGKILL to pid: %s' % popen.pid)
897 os.kill(popen.pid, signal.SIGKILL)
898 time.sleep(.5)
900 def win32kill(pid):
901 import ctypes
902 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
903 ctypes.windll.kernel32.TerminateProcess(handle, -1)
904 ctypes.windll.kernel32.CloseHandle(handle)
906 def gcd(a, b):
907 while b:
908 a, b = b, a % b
909 return a