Provisional fix for timelines on remuxed h.264 videos. Should it also
[pyTivo/wmcbrine.git] / plugins / video / transcode.py
blob09599e76524e148c7a7f926c256237649cd2c185
1 import logging
2 import math
3 import os
4 import re
5 import shlex
6 import shutil
7 import subprocess
8 import sys
9 import tempfile
10 import threading
11 import time
13 import lrucache
15 import config
16 import metadata
18 logger = logging.getLogger('pyTivo.video.transcode')
20 info_cache = lrucache.LRUCache(1000)
21 ffmpeg_procs = {}
22 reapers = {}
24 GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97',
25 '30.00', '50.00', '59.94', '60.00']
27 BLOCKSIZE = 512 * 1024
28 MAXBLOCKS = 2
29 TIMEOUT = 600
31 # XXX BIG HACK
32 # subprocess is broken for me on windows so super hack
33 def patchSubprocess():
34 o = subprocess.Popen._make_inheritable
36 def _make_inheritable(self, handle):
37 if not handle: return subprocess.GetCurrentProcess()
38 return o(self, handle)
40 subprocess.Popen._make_inheritable = _make_inheritable
41 mswindows = (sys.platform == "win32")
42 if mswindows:
43 patchSubprocess()
45 def debug(msg):
46 if type(msg) == str:
47 try:
48 msg = msg.decode('utf8')
49 except:
50 if sys.platform == 'darwin':
51 msg = msg.decode('macroman')
52 else:
53 msg = msg.decode('cp1252')
54 logger.debug(msg)
56 def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''):
57 vcodec = select_videocodec(inFile, tsn, mime)
59 settings = select_buffsize(tsn) + vcodec
60 if not vcodec[1] == 'copy':
61 settings += (select_videobr(inFile, tsn) +
62 select_maxvideobr(tsn) +
63 select_videofps(inFile, tsn) +
64 select_aspect(inFile, tsn))
66 acodec = select_audiocodec(isQuery, inFile, tsn)
67 settings += acodec
68 if not acodec[1] == 'copy':
69 settings += (select_audiobr(tsn) +
70 select_audiofr(inFile, tsn) +
71 select_audioch(inFile, tsn))
73 settings += [select_audiolang(inFile, tsn),
74 select_ffmpegprams(tsn)]
76 settings += select_format(tsn, mime)
78 settings = ' '.join(settings).split()
79 if isQuery:
80 return settings
82 ffmpeg_path = config.get_bin('ffmpeg')
84 fname = unicode(inFile, 'utf-8')
85 if mswindows:
86 fname = fname.encode('cp1252')
88 if inFile[-5:].lower() == '.tivo':
89 tivodecode_path = config.get_bin('tivodecode')
90 tivo_mak = config.get_server('tivo_mak')
91 tcmd = [tivodecode_path, '-m', tivo_mak, fname]
92 tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE,
93 bufsize=(512 * 1024))
94 if tivo_compatible(inFile, tsn)[0]:
95 cmd = ''
96 ffmpeg = tivodecode
97 else:
98 cmd = [ffmpeg_path, '-i', '-'] + settings
99 ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout,
100 stdout=subprocess.PIPE,
101 bufsize=(512 * 1024))
102 else:
103 cmd = [ffmpeg_path, '-i', fname] + settings
104 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
105 stdout=subprocess.PIPE)
107 if cmd:
108 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
109 debug(' '.join(cmd))
111 ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0,
112 'last_read': time.time(), 'blocks': []}
113 if thead:
114 ffmpeg_procs[inFile]['blocks'].append(thead)
115 reap_process(inFile)
116 return resume_transfer(inFile, outFile, 0)
118 def is_resumable(inFile, offset):
119 if inFile in ffmpeg_procs:
120 proc = ffmpeg_procs[inFile]
121 if proc['start'] <= offset < proc['end']:
122 return True
123 else:
124 cleanup(inFile)
125 kill(proc['process'])
126 return False
128 def resume_transfer(inFile, outFile, offset):
129 proc = ffmpeg_procs[inFile]
130 offset -= proc['start']
131 count = 0
133 try:
134 for block in proc['blocks']:
135 length = len(block)
136 if offset < length:
137 if offset > 0:
138 block = block[offset:]
139 outFile.write('%x\r\n' % len(block))
140 outFile.write(block)
141 outFile.write('\r\n')
142 count += len(block)
143 offset -= length
144 outFile.flush()
145 except Exception, msg:
146 logger.info(msg)
147 return count
149 proc['start'] = proc['end']
150 proc['blocks'] = []
152 return count + transfer_blocks(inFile, outFile)
154 def transfer_blocks(inFile, outFile):
155 proc = ffmpeg_procs[inFile]
156 blocks = proc['blocks']
157 count = 0
159 while True:
160 try:
161 block = proc['process'].stdout.read(BLOCKSIZE)
162 proc['last_read'] = time.time()
163 except Exception, msg:
164 logger.info(msg)
165 cleanup(inFile)
166 kill(proc['process'])
167 break
169 if not block:
170 try:
171 outFile.flush()
172 except Exception, msg:
173 logger.info(msg)
174 else:
175 cleanup(inFile)
176 break
178 blocks.append(block)
179 proc['end'] += len(block)
180 if len(blocks) > MAXBLOCKS:
181 proc['start'] += len(blocks[0])
182 blocks.pop(0)
184 try:
185 outFile.write('%x\r\n' % len(block))
186 outFile.write(block)
187 outFile.write('\r\n')
188 count += len(block)
189 except Exception, msg:
190 logger.info(msg)
191 break
193 return count
195 def reap_process(inFile):
196 if ffmpeg_procs and inFile in ffmpeg_procs:
197 proc = ffmpeg_procs[inFile]
198 if proc['last_read'] + TIMEOUT < time.time():
199 del ffmpeg_procs[inFile]
200 del reapers[inFile]
201 kill(proc['process'])
202 else:
203 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
204 reapers[inFile] = reaper
205 reaper.start()
207 def cleanup(inFile):
208 del ffmpeg_procs[inFile]
209 reapers[inFile].cancel()
210 del reapers[inFile]
212 def select_audiocodec(isQuery, inFile, tsn='', mime=''):
213 if inFile[-5:].lower() == '.tivo':
214 return ['-c:a', 'copy']
215 vInfo = video_info(inFile)
216 codectype = vInfo['vCodec']
217 # Default, compatible with all TiVo's
218 codec = 'ac3'
219 compatiblecodecs = ('ac3', 'liba52', 'mp2')
221 if vInfo['aCodec'] in compatiblecodecs:
222 aKbps = vInfo['aKbps']
223 aCh = vInfo['aCh']
224 if aKbps == None:
225 if not isQuery:
226 vInfoQuery = audio_check(inFile, tsn)
227 if vInfoQuery == None:
228 aKbps = None
229 aCh = None
230 else:
231 aKbps = vInfoQuery['aKbps']
232 aCh = vInfoQuery['aCh']
233 else:
234 codec = 'TBA'
235 if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn):
236 # compatible codec and bitrate, do not reencode audio
237 codec = 'copy'
238 if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2):
239 codec = 'ac3'
240 val = ['-c:a', codec]
241 if not (codec == 'copy' and codectype == 'mpeg2video'):
242 val.append('-copyts')
243 return val
245 def select_audiofr(inFile, tsn):
246 freq = '48000' # default
247 vInfo = video_info(inFile)
248 if vInfo['aFreq'] == '44100':
249 # compatible frequency
250 freq = vInfo['aFreq']
251 return ['-ar', freq]
253 def select_audioch(inFile, tsn):
254 # AC-3 max channels is 5.1
255 if video_info(inFile)['aCh'] > 6:
256 debug('Too many audio channels for AC-3, using 5.1 instead')
257 return ['-ac', '6']
258 return []
260 def select_audiolang(inFile, tsn):
261 vInfo = video_info(inFile)
262 audio_lang = config.get_tsn('audio_lang', tsn)
263 debug('audio_lang: %s' % audio_lang)
264 if vInfo['mapAudio']:
265 # default to first detected audio stream to begin with
266 stream = vInfo['mapAudio'][0][0]
267 debug('set first detected audio stream by default: %s' % stream)
268 if audio_lang != None and vInfo['mapVideo'] != None:
269 langmatch_curr = []
270 langmatch_prev = vInfo['mapAudio'][:]
271 for lang in audio_lang.replace(' ', '').lower().split(','):
272 debug('matching lang: %s' % lang)
273 for s, l in langmatch_prev:
274 if lang in s + l.replace(' ', '').lower():
275 debug('matched: %s' % s + l.replace(' ', '').lower())
276 langmatch_curr.append((s, l))
277 # if only 1 item matched we're done
278 if len(langmatch_curr) == 1:
279 stream = langmatch_curr[0][0]
280 debug('found exactly one match: %s' % stream)
281 break
282 # if more than 1 item matched copy the curr area to the prev
283 # array we only need to look at the new shorter list from
284 # now on
285 elif len(langmatch_curr) > 1:
286 langmatch_prev = langmatch_curr[:]
287 # default to the first item matched thus far
288 stream = langmatch_curr[0][0]
289 debug('remember first match: %s' % stream)
290 langmatch_curr = []
291 # don't let FFmpeg auto select audio stream, pyTivo defaults to
292 # first detected
293 if stream:
294 debug('selected audio stream: %s' % stream)
295 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
296 # if no audio is found
297 debug('selected audio stream: None detected')
298 return ''
300 def select_videofps(inFile, tsn):
301 vInfo = video_info(inFile)
302 fps = ['-r', '29.97'] # default
303 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
304 fps = []
305 return fps
307 def select_videocodec(inFile, tsn, mime=''):
308 codec = ['-c:v']
309 vInfo = video_info(inFile)
310 if tivo_compatible_video(vInfo, tsn, mime)[0]:
311 codec.append('copy')
312 if (mime == 'video/x-tivo-mpeg-ts'):
313 org_codec = vInfo.get('vCodec', '')
314 if org_codec == 'h264':
315 codec += ['-bsf:v', 'h264_mp4toannexb', '-muxdelay', '0']
316 elif org_codec == 'hevc':
317 codec += ['-bsf:v', 'hevc_mp4toannexb']
318 else:
319 codec += ['mpeg2video', '-pix_fmt', 'yuv420p'] # default
320 return codec
322 def select_videobr(inFile, tsn, mime=''):
323 return ['-b:v', str(select_videostr(inFile, tsn, mime) / 1000) + 'k']
325 def select_videostr(inFile, tsn, mime=''):
326 vInfo = video_info(inFile)
327 if tivo_compatible_video(vInfo, tsn, mime)[0]:
328 video_str = int(vInfo['kbps'])
329 if vInfo['aKbps']:
330 video_str -= int(vInfo['aKbps'])
331 video_str *= 1000
332 else:
333 video_str = config.strtod(config.getVideoBR(tsn))
334 if config.isHDtivo(tsn) and vInfo['kbps']:
335 video_str = max(video_str, int(vInfo['kbps']) * 1000)
336 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
337 video_str))
338 return video_str
340 def select_audiobr(tsn):
341 return ['-b:a', config.getAudioBR(tsn)]
343 def select_maxvideobr(tsn):
344 return ['-maxrate', config.getMaxVideoBR(tsn)]
346 def select_buffsize(tsn):
347 return ['-bufsize', config.getBuffSize(tsn)]
349 def select_ffmpegprams(tsn):
350 params = config.getFFmpegPrams(tsn)
351 if not params:
352 params = ''
353 return params
355 def select_format(tsn, mime):
356 if mime == 'video/x-tivo-mpeg-ts':
357 fmt = 'mpegts'
358 else:
359 fmt = 'vob'
360 return ['-f', fmt, '-']
362 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
363 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
364 vInfo['vWidth']) * multiplier)
365 if endHeight % 2:
366 endHeight -= 1
367 topPadding = (TIVO_HEIGHT - endHeight) / 2
368 if topPadding % 2:
369 topPadding -= 1
370 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH,
371 endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
373 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
374 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
375 (vInfo['vHeight'] * multiplier))
376 if endWidth % 2:
377 endWidth -= 1
378 leftPadding = (TIVO_WIDTH - endWidth) / 2
379 if leftPadding % 2:
380 leftPadding -= 1
381 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth,
382 TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
384 def select_aspect(inFile, tsn = ''):
385 TIVO_WIDTH = config.getTivoWidth(tsn)
386 TIVO_HEIGHT = config.getTivoHeight(tsn)
388 vInfo = video_info(inFile)
390 debug('tsn: %s' % tsn)
392 aspect169 = config.get169Setting(tsn)
394 debug('aspect169: %s' % aspect169)
396 optres = config.getOptres(tsn)
398 debug('optres: %s' % optres)
400 if optres:
401 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
402 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
403 if optHeight < TIVO_HEIGHT:
404 TIVO_HEIGHT = optHeight
405 if optWidth < TIVO_WIDTH:
406 TIVO_WIDTH = optWidth
408 if vInfo.get('par2'):
409 par2 = vInfo['par2']
410 elif vInfo.get('par'):
411 par2 = float(vInfo['par'])
412 else:
413 # Assume PAR = 1.0
414 par2 = 1.0
416 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
417 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
418 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
419 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
421 if config.isHDtivo(tsn) and not optres:
422 if vInfo['par']:
423 npar = par2
425 # adjust for pixel aspect ratio, if set
427 if npar < 1.0:
428 return ['-s', '%dx%d' % (vInfo['vWidth'],
429 math.ceil(vInfo['vHeight'] / npar))]
430 elif npar > 1.0:
431 # FFMPEG expects width to be a multiple of two
432 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
433 vInfo['vHeight'])]
435 if vInfo['vHeight'] <= TIVO_HEIGHT:
436 # pass all resolutions to S3, except heights greater than
437 # conf height
438 return []
439 # else, resize video.
441 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
442 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
443 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
445 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
446 debug('File + PAR is within 4:3.')
447 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
449 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
450 (59, 72), (59, 36), (59, 54)] or
451 vInfo['dar1'] == '4:3'):
452 debug('File is within 4:3 list.')
453 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
455 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
456 (59, 27)] or vInfo['dar1'] == '16:9')
457 and (aspect169 or config.get169Letterbox(tsn))):
458 debug('File is within 16:9 list and 16:9 allowed.')
460 if config.get169Blacklist(tsn) or (aspect169 and
461 config.get169Letterbox(tsn)):
462 aspect = '4:3'
463 else:
464 aspect = '16:9'
465 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
467 else:
468 settings = ['-aspect']
470 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
471 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
472 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
473 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
474 multiplier4by3))
476 # If video is wider than 4:3 add top and bottom padding
478 if ratio > 133: # Might be 16:9 file, or just need padding on
479 # top and bottom
481 if aspect169 and ratio > 135: # If file would fall in 4:3
482 # assume it is supposed to be 4:3
484 if (config.get169Blacklist(tsn) or
485 config.get169Letterbox(tsn)):
486 settings.append('4:3')
487 else:
488 settings.append('16:9')
490 if ratio > 177: # too short needs padding top and bottom
491 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
492 multiplier16by9, vInfo)
493 debug(('16:9 aspect allowed, file is wider ' +
494 'than 16:9 padding top and bottom\n%s') %
495 ' '.join(settings))
497 else: # too skinny needs padding on left and right.
498 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
499 multiplier16by9, vInfo)
500 debug(('16:9 aspect allowed, file is narrower ' +
501 'than 16:9 padding left and right\n%s') %
502 ' '.join(settings))
504 else: # this is a 4:3 file or 16:9 output not allowed
505 if ratio > 135 and config.get169Letterbox(tsn):
506 settings.append('16:9')
507 multiplier = multiplier16by9
508 else:
509 settings.append('4:3')
510 multiplier = multiplier4by3
511 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
512 multiplier, vInfo)
513 debug(('File is wider than 4:3 padding ' +
514 'top and bottom\n%s') % ' '.join(settings))
516 # If video is taller than 4:3 add left and right padding, this
517 # is rare. All of these files will always be sent in an aspect
518 # ratio of 4:3 since they are so narrow.
520 else:
521 settings.append('4:3')
522 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
523 debug('File is taller than 4:3 padding left and right\n%s'
524 % ' '.join(settings))
526 return settings
528 def tivo_compatible_video(vInfo, tsn, mime=''):
529 message = (True, '')
530 while True:
531 codec = vInfo.get('vCodec', '')
532 if mime == 'video/x-tivo-mpeg-ts':
533 if not (codec in ('h264', 'mpeg2video')):
534 message = (False, 'vCodec %s not compatible' % codec)
536 break
538 if codec not in ('mpeg2video', 'mpeg1video'):
539 message = (False, 'vCodec %s not compatible' % codec)
540 break
542 if vInfo['kbps'] != None:
543 abit = max('0', vInfo['aKbps'])
544 if (int(vInfo['kbps']) - int(abit) >
545 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
546 message = (False, '%s kbps exceeds max video bitrate' %
547 vInfo['kbps'])
548 break
549 else:
550 message = (False, '%s kbps not supported' % vInfo['kbps'])
551 break
553 if config.isHDtivo(tsn):
554 # HD Tivo detected, skipping remaining tests.
555 break
557 if not vInfo['vFps'] in ['29.97', '59.94']:
558 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
559 break
561 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
562 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
563 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
564 message = (False, ('DAR %s not supported ' +
565 'by BLACKLIST_169 tivos') % vInfo['dar1'])
566 break
568 mode = (vInfo['vWidth'], vInfo['vHeight'])
569 if mode not in [(720, 480), (704, 480), (544, 480),
570 (528, 480), (480, 480), (352, 480), (352, 240)]:
571 message = (False, '%s x %s not in supported modes' % mode)
572 break
574 return message
576 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
577 message = (True, '')
578 while True:
579 codec = vInfo.get('aCodec', '')
581 if codec == None:
582 debug('No audio stream detected')
583 break
585 if inFile[-5:].lower() == '.tivo':
586 break
588 if mime == 'video/x-tivo-mpeg-ts':
589 if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
590 message = (False, 'aCodec %s not compatible' % codec)
592 break
594 if codec not in ('ac3', 'liba52', 'mp2'):
595 message = (False, 'aCodec %s not compatible' % codec)
596 break
598 if (not vInfo['aKbps'] or
599 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
600 message = (False, '%s kbps exceeds max audio bitrate' %
601 vInfo['aKbps'])
602 break
604 audio_lang = config.get_tsn('audio_lang', tsn)
605 if audio_lang:
606 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
607 message = (False, '%s preferred audio track exists' %
608 audio_lang)
609 break
611 return message
613 def tivo_compatible_container(vInfo, inFile, mime=''):
614 message = (True, '')
615 container = vInfo.get('container', '')
616 if ((mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
617 (mime in ['video/x-tivo-mpeg', '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, inFile, 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 fname = unicode(inFile, 'utf-8')
656 mtime = os.path.getmtime(fname)
657 if cache:
658 if inFile in info_cache and info_cache[inFile][0] == mtime:
659 debug('CACHE HIT! %s' % inFile)
660 return info_cache[inFile][1]
662 vInfo['Supported'] = True
664 ffmpeg_path = config.get_bin('ffmpeg')
665 if not ffmpeg_path:
666 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
667 '.vob', '.tivo', '.ts']:
668 vInfo['Supported'] = False
669 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
670 'rawmeta': {}})
671 if cache:
672 info_cache[inFile] = (mtime, vInfo)
673 return vInfo
675 if mswindows:
676 fname = fname.encode('cp1252')
677 cmd = [ffmpeg_path, '-i', fname]
678 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
679 err_tmp = tempfile.TemporaryFile()
680 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
681 stdin=subprocess.PIPE)
683 # wait configured # of seconds: if ffmpeg is not back give up
684 limit = config.getFFmpegWait()
685 if limit:
686 for i in xrange(limit * 20):
687 time.sleep(.05)
688 if not ffmpeg.poll() == None:
689 break
691 if ffmpeg.poll() == None:
692 kill(ffmpeg)
693 vInfo['Supported'] = False
694 if cache:
695 info_cache[inFile] = (mtime, vInfo)
696 return vInfo
697 else:
698 ffmpeg.wait()
700 err_tmp.seek(0)
701 output = err_tmp.read()
702 err_tmp.close()
703 debug('ffmpeg output=%s' % output)
705 attrs = {'container': r'Input #0, ([^,]+),',
706 'vCodec': r'Video: ([^, ]+)', # video codec
707 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
708 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
709 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
710 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
712 for attr in attrs:
713 rezre = re.compile(attrs[attr])
714 x = rezre.search(output)
715 if x:
716 vInfo[attr] = x.group(1)
717 else:
718 if attr in ['container', 'vCodec']:
719 vInfo[attr] = ''
720 vInfo['Supported'] = False
721 else:
722 vInfo[attr] = None
723 debug('failed at ' + attr)
725 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
726 x = rezre.search(output)
727 if x:
728 if x.group(3):
729 if x.group(3) == 'stereo':
730 vInfo['aCh'] = 2
731 elif x.group(3) == 'mono':
732 vInfo['aCh'] = 1
733 elif x.group(2):
734 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
735 elif x.group(1):
736 vInfo['aCh'] = int(x.group(1))
737 else:
738 vInfo['aCh'] = None
739 debug('failed at aCh')
740 else:
741 vInfo['aCh'] = None
742 debug('failed at aCh')
744 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
745 x = rezre.search(output)
746 if x:
747 vInfo['vWidth'] = int(x.group(1))
748 vInfo['vHeight'] = int(x.group(2))
749 else:
750 vInfo['vWidth'] = ''
751 vInfo['vHeight'] = ''
752 vInfo['Supported'] = False
753 debug('failed at vWidth/vHeight')
755 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
756 x = rezre.search(output)
757 if x:
758 vInfo['vFps'] = x.group(1)
759 if '.' not in vInfo['vFps']:
760 vInfo['vFps'] += '.00'
762 # Allow override only if it is mpeg2 and frame rate was doubled
763 # to 59.94
765 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
766 # First look for the build 7215 version
767 rezre = re.compile(r'.*film source: 29.97.*')
768 x = rezre.search(output.lower())
769 if x:
770 debug('film source: 29.97 setting vFps to 29.97')
771 vInfo['vFps'] = '29.97'
772 else:
773 # for build 8047:
774 rezre = re.compile(r'.*frame rate differs from container ' +
775 r'frame rate: 29.97.*')
776 debug('Bug in VideoReDo')
777 x = rezre.search(output.lower())
778 if x:
779 vInfo['vFps'] = '29.97'
780 else:
781 vInfo['vFps'] = ''
782 vInfo['Supported'] = False
783 debug('failed at vFps')
785 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
786 d = durre.search(output)
788 if d:
789 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
790 int(d.group(2)) * 60 +
791 int(d.group(3))) * 1000 +
792 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
793 else:
794 vInfo['millisecs'] = 0
796 # get bitrate of source for tivo compatibility test.
797 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
798 x = rezre.search(output)
799 if x:
800 vInfo['kbps'] = x.group(1)
801 else:
802 # Fallback method of getting video bitrate
803 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
804 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
805 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
806 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
807 x = rezre.search(output)
808 if x:
809 vInfo['kbps'] = x.group(1)
810 else:
811 vInfo['kbps'] = None
812 debug('failed at kbps')
814 # get par.
815 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
816 x = rezre.search(output)
817 if x and x.group(1) != "0" and x.group(2) != "0":
818 vInfo['par1'] = x.group(1) + ':' + x.group(2)
819 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
820 else:
821 vInfo['par1'], vInfo['par2'] = None, None
823 # get dar.
824 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
825 x = rezre.search(output)
826 if x and x.group(1) != "0" and x.group(2) != "0":
827 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
828 else:
829 vInfo['dar1'] = None
831 # get Audio Stream mapping.
832 rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
833 x = rezre.search(output)
834 amap = []
835 if x:
836 for x in rezre.finditer(output):
837 amap.append((x.group(1), x.group(2) + x.group(3)))
838 else:
839 amap.append(('', ''))
840 debug('failed at mapAudio')
841 vInfo['mapAudio'] = amap
843 vInfo['par'] = None
845 # get Metadata dump (newer ffmpeg).
846 lines = output.split('\n')
847 rawmeta = {}
848 flag = False
850 for line in lines:
851 if line.startswith(' Metadata:'):
852 flag = True
853 else:
854 if flag:
855 if line.startswith(' Duration:'):
856 flag = False
857 else:
858 try:
859 key, value = [x.strip() for x in line.split(':', 1)]
860 try:
861 value = value.decode('utf-8')
862 except:
863 if sys.platform == 'darwin':
864 value = value.decode('macroman')
865 else:
866 value = value.decode('cp1252')
867 rawmeta[key] = [value]
868 except:
869 pass
871 vInfo['rawmeta'] = rawmeta
873 data = metadata.from_text(inFile)
874 for key in data:
875 if key.startswith('Override_'):
876 vInfo['Supported'] = True
877 if key.startswith('Override_mapAudio'):
878 audiomap = dict(vInfo['mapAudio'])
879 newmap = shlex.split(data[key])
880 audiomap.update(zip(newmap[::2], newmap[1::2]))
881 vInfo['mapAudio'] = sorted(audiomap.items(),
882 key=lambda (k,v): (k,v))
883 elif key.startswith('Override_millisecs'):
884 vInfo[key.replace('Override_', '')] = int(data[key])
885 else:
886 vInfo[key.replace('Override_', '')] = data[key]
888 if cache:
889 info_cache[inFile] = (mtime, vInfo)
890 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
891 return vInfo
893 def audio_check(inFile, tsn):
894 cmd_string = ('-y -c:v mpeg2video -r 29.97 -b:v 1000k -c:a copy ' +
895 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
896 fname = unicode(inFile, 'utf-8')
897 if mswindows:
898 fname = fname.encode('cp1252')
899 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
900 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
901 fd, testname = tempfile.mkstemp()
902 testfile = os.fdopen(fd, 'wb')
903 try:
904 shutil.copyfileobj(ffmpeg.stdout, testfile)
905 except:
906 kill(ffmpeg)
907 testfile.close()
908 vInfo = None
909 else:
910 testfile.close()
911 vInfo = video_info(testname, False)
912 os.remove(testname)
913 return vInfo
915 def supported_format(inFile):
916 if video_info(inFile)['Supported']:
917 return True
918 else:
919 debug('FALSE, file not supported %s' % inFile)
920 return False
922 def kill(popen):
923 debug('killing pid=%s' % str(popen.pid))
924 if mswindows:
925 win32kill(popen.pid)
926 else:
927 import os, signal
928 for i in xrange(3):
929 debug('sending SIGTERM to pid: %s' % popen.pid)
930 os.kill(popen.pid, signal.SIGTERM)
931 time.sleep(.5)
932 if popen.poll() is not None:
933 debug('process %s has exited' % popen.pid)
934 break
935 else:
936 while popen.poll() is None:
937 debug('sending SIGKILL to pid: %s' % popen.pid)
938 os.kill(popen.pid, signal.SIGKILL)
939 time.sleep(.5)
941 def win32kill(pid):
942 import ctypes
943 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
944 ctypes.windll.kernel32.TerminateProcess(handle, -1)
945 ctypes.windll.kernel32.CloseHandle(handle)
947 def gcd(a, b):
948 while b:
949 a, b = b, a % b
950 return a