Eliminate audio_ch, audio_fr, copy_ts and video_fps. Preparatory to
[pyTivo/wmcbrine.git] / plugins / video / transcode.py
blob2c4252cc949ef2dd2c517a462dd77454ba61ac2d
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 if mime == 'video/mp4':
220 compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
221 'ac3', 'liba52')
222 else:
223 compatiblecodecs = ('ac3', 'liba52', 'mp2')
225 if vInfo['aCodec'] in compatiblecodecs:
226 aKbps = vInfo['aKbps']
227 aCh = vInfo['aCh']
228 if aKbps == None:
229 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
230 # along with the channel check below this should
231 # pass any AAC audio that has undefined 'aKbps' and
232 # is <= 2 channels. Should be TiVo compatible.
233 codec = 'copy'
234 elif not isQuery:
235 vInfoQuery = audio_check(inFile, tsn)
236 if vInfoQuery == None:
237 aKbps = None
238 aCh = None
239 else:
240 aKbps = vInfoQuery['aKbps']
241 aCh = vInfoQuery['aCh']
242 else:
243 codec = 'TBA'
244 if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn):
245 # compatible codec and bitrate, do not reencode audio
246 codec = 'copy'
247 if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2):
248 codec = 'ac3'
249 val = ['-c:a', codec]
250 if not (codec == 'copy' and codectype == 'mpeg2video'):
251 val.append('-copyts')
252 return val
254 def select_audiofr(inFile, tsn):
255 freq = '48000' # default
256 vInfo = video_info(inFile)
257 if vInfo['aFreq'] == '44100':
258 # compatible frequency
259 freq = vInfo['aFreq']
260 return ['-ar', freq]
262 def select_audioch(inFile, tsn):
263 # AC-3 max channels is 5.1
264 if video_info(inFile)['aCh'] > 6:
265 debug('Too many audio channels for AC-3, using 5.1 instead')
266 return ['-ac', '6']
267 return []
269 def select_audiolang(inFile, tsn):
270 vInfo = video_info(inFile)
271 audio_lang = config.get_tsn('audio_lang', tsn)
272 debug('audio_lang: %s' % audio_lang)
273 if vInfo['mapAudio']:
274 # default to first detected audio stream to begin with
275 stream = vInfo['mapAudio'][0][0]
276 debug('set first detected audio stream by default: %s' % stream)
277 if audio_lang != None and vInfo['mapVideo'] != None:
278 langmatch_curr = []
279 langmatch_prev = vInfo['mapAudio'][:]
280 for lang in audio_lang.replace(' ', '').lower().split(','):
281 debug('matching lang: %s' % lang)
282 for s, l in langmatch_prev:
283 if lang in s + l.replace(' ', '').lower():
284 debug('matched: %s' % s + l.replace(' ', '').lower())
285 langmatch_curr.append((s, l))
286 # if only 1 item matched we're done
287 if len(langmatch_curr) == 1:
288 stream = langmatch_curr[0][0]
289 debug('found exactly one match: %s' % stream)
290 break
291 # if more than 1 item matched copy the curr area to the prev
292 # array we only need to look at the new shorter list from
293 # now on
294 elif len(langmatch_curr) > 1:
295 langmatch_prev = langmatch_curr[:]
296 # default to the first item matched thus far
297 stream = langmatch_curr[0][0]
298 debug('remember first match: %s' % stream)
299 langmatch_curr = []
300 # don't let FFmpeg auto select audio stream, pyTivo defaults to
301 # first detected
302 if stream:
303 debug('selected audio stream: %s' % stream)
304 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
305 # if no audio is found
306 debug('selected audio stream: None detected')
307 return ''
309 def select_videofps(inFile, tsn):
310 vInfo = video_info(inFile)
311 fps = ['-r', '29.97'] # default
312 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
313 fps = []
314 return fps
316 def select_videocodec(inFile, tsn, mime=''):
317 codec = ['-c:v']
318 vInfo = video_info(inFile)
319 if tivo_compatible_video(vInfo, tsn, mime)[0]:
320 codec.append('copy')
321 if (mime == 'video/x-tivo-mpeg-ts'):
322 org_codec = vInfo.get('vCodec', '')
323 if org_codec == 'h264':
324 codec += ['-bsf', 'h264_mp4toannexb']
325 elif org_codec == 'hevc':
326 codec += ['-bsf', 'hevc_mp4toannexb']
327 else:
328 codec += ['mpeg2video', '-pix_fmt', 'yuv420p'] # default
329 return codec
331 def select_videobr(inFile, tsn, mime=''):
332 return ['-b:v', str(select_videostr(inFile, tsn, mime) / 1000) + 'k']
334 def select_videostr(inFile, tsn, mime=''):
335 vInfo = video_info(inFile)
336 if tivo_compatible_video(vInfo, tsn, mime)[0]:
337 video_str = int(vInfo['kbps'])
338 if vInfo['aKbps']:
339 video_str -= int(vInfo['aKbps'])
340 video_str *= 1000
341 else:
342 video_str = config.strtod(config.getVideoBR(tsn))
343 if config.isHDtivo(tsn) and vInfo['kbps']:
344 video_str = max(video_str, int(vInfo['kbps']) * 1000)
345 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
346 video_str))
347 return video_str
349 def select_audiobr(tsn):
350 return ['-b:a', config.getAudioBR(tsn)]
352 def select_maxvideobr(tsn):
353 return ['-maxrate', config.getMaxVideoBR(tsn)]
355 def select_buffsize(tsn):
356 return ['-bufsize', config.getBuffSize(tsn)]
358 def select_ffmpegprams(tsn):
359 params = config.getFFmpegPrams(tsn)
360 if not params:
361 params = ''
362 return params
364 def select_format(tsn, mime):
365 if mime == 'video/x-tivo-mpeg-ts':
366 fmt = 'mpegts'
367 else:
368 fmt = 'vob'
369 return ['-f', fmt, '-']
371 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
372 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
373 vInfo['vWidth']) * multiplier)
374 if endHeight % 2:
375 endHeight -= 1
376 topPadding = (TIVO_HEIGHT - endHeight) / 2
377 if topPadding % 2:
378 topPadding -= 1
379 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH,
380 endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
382 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
383 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
384 (vInfo['vHeight'] * multiplier))
385 if endWidth % 2:
386 endWidth -= 1
387 leftPadding = (TIVO_WIDTH - endWidth) / 2
388 if leftPadding % 2:
389 leftPadding -= 1
390 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth,
391 TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
393 def select_aspect(inFile, tsn = ''):
394 TIVO_WIDTH = config.getTivoWidth(tsn)
395 TIVO_HEIGHT = config.getTivoHeight(tsn)
397 vInfo = video_info(inFile)
399 debug('tsn: %s' % tsn)
401 aspect169 = config.get169Setting(tsn)
403 debug('aspect169: %s' % aspect169)
405 optres = config.getOptres(tsn)
407 debug('optres: %s' % optres)
409 if optres:
410 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
411 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
412 if optHeight < TIVO_HEIGHT:
413 TIVO_HEIGHT = optHeight
414 if optWidth < TIVO_WIDTH:
415 TIVO_WIDTH = optWidth
417 if vInfo.get('par2'):
418 par2 = vInfo['par2']
419 elif vInfo.get('par'):
420 par2 = float(vInfo['par'])
421 else:
422 # Assume PAR = 1.0
423 par2 = 1.0
425 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
426 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
427 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
428 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
430 if config.isHDtivo(tsn) and not optres:
431 if vInfo['par']:
432 npar = par2
434 # adjust for pixel aspect ratio, if set
436 if npar < 1.0:
437 return ['-s', '%dx%d' % (vInfo['vWidth'],
438 math.ceil(vInfo['vHeight'] / npar))]
439 elif npar > 1.0:
440 # FFMPEG expects width to be a multiple of two
441 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
442 vInfo['vHeight'])]
444 if vInfo['vHeight'] <= TIVO_HEIGHT:
445 # pass all resolutions to S3, except heights greater than
446 # conf height
447 return []
448 # else, resize video.
450 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
451 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
452 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
454 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
455 debug('File + PAR is within 4:3.')
456 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
458 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
459 (59, 72), (59, 36), (59, 54)] or
460 vInfo['dar1'] == '4:3'):
461 debug('File is within 4:3 list.')
462 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
464 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
465 (59, 27)] or vInfo['dar1'] == '16:9')
466 and (aspect169 or config.get169Letterbox(tsn))):
467 debug('File is within 16:9 list and 16:9 allowed.')
469 if config.get169Blacklist(tsn) or (aspect169 and
470 config.get169Letterbox(tsn)):
471 aspect = '4:3'
472 else:
473 aspect = '16:9'
474 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
476 else:
477 settings = ['-aspect']
479 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
480 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
481 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
482 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
483 multiplier4by3))
485 # If video is wider than 4:3 add top and bottom padding
487 if ratio > 133: # Might be 16:9 file, or just need padding on
488 # top and bottom
490 if aspect169 and ratio > 135: # If file would fall in 4:3
491 # assume it is supposed to be 4:3
493 if (config.get169Blacklist(tsn) or
494 config.get169Letterbox(tsn)):
495 settings.append('4:3')
496 else:
497 settings.append('16:9')
499 if ratio > 177: # too short needs padding top and bottom
500 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
501 multiplier16by9, vInfo)
502 debug(('16:9 aspect allowed, file is wider ' +
503 'than 16:9 padding top and bottom\n%s') %
504 ' '.join(settings))
506 else: # too skinny needs padding on left and right.
507 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
508 multiplier16by9, vInfo)
509 debug(('16:9 aspect allowed, file is narrower ' +
510 'than 16:9 padding left and right\n%s') %
511 ' '.join(settings))
513 else: # this is a 4:3 file or 16:9 output not allowed
514 if ratio > 135 and config.get169Letterbox(tsn):
515 settings.append('16:9')
516 multiplier = multiplier16by9
517 else:
518 settings.append('4:3')
519 multiplier = multiplier4by3
520 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
521 multiplier, vInfo)
522 debug(('File is wider than 4:3 padding ' +
523 'top and bottom\n%s') % ' '.join(settings))
525 # If video is taller than 4:3 add left and right padding, this
526 # is rare. All of these files will always be sent in an aspect
527 # ratio of 4:3 since they are so narrow.
529 else:
530 settings.append('4:3')
531 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
532 debug('File is taller than 4:3 padding left and right\n%s'
533 % ' '.join(settings))
535 return settings
537 def tivo_compatible_video(vInfo, tsn, mime=''):
538 message = (True, '')
539 while True:
540 codec = vInfo.get('vCodec', '')
541 is4k = config.is4Ktivo(tsn) and codec == 'hevc'
542 if mime == 'video/mp4':
543 if not (is4k or codec == 'h264'):
544 message = (False, 'vCodec %s not compatible' % codec)
546 break
548 if mime == 'video/bif':
549 if codec != 'vc1':
550 message = (False, 'vCodec %s not compatible' % codec)
552 break
554 if mime == 'video/x-tivo-mpeg-ts':
555 if not (is4k or codec in ('h264', 'mpeg2video')):
556 message = (False, 'vCodec %s not compatible' % codec)
558 break
560 if codec not in ('mpeg2video', 'mpeg1video'):
561 message = (False, 'vCodec %s not compatible' % codec)
562 break
564 if vInfo['kbps'] != None:
565 abit = max('0', vInfo['aKbps'])
566 if (int(vInfo['kbps']) - int(abit) >
567 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
568 message = (False, '%s kbps exceeds max video bitrate' %
569 vInfo['kbps'])
570 break
571 else:
572 message = (False, '%s kbps not supported' % vInfo['kbps'])
573 break
575 if config.isHDtivo(tsn):
576 # HD Tivo detected, skipping remaining tests.
577 break
579 if not vInfo['vFps'] in ['29.97', '59.94']:
580 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
581 break
583 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
584 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
585 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
586 message = (False, ('DAR %s not supported ' +
587 'by BLACKLIST_169 tivos') % vInfo['dar1'])
588 break
590 mode = (vInfo['vWidth'], vInfo['vHeight'])
591 if mode not in [(720, 480), (704, 480), (544, 480),
592 (528, 480), (480, 480), (352, 480), (352, 240)]:
593 message = (False, '%s x %s not in supported modes' % mode)
594 break
596 return message
598 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
599 message = (True, '')
600 while True:
601 codec = vInfo.get('aCodec', '')
603 if codec == None:
604 debug('No audio stream detected')
605 break
607 if mime == 'video/mp4':
608 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
609 'ac3', 'liba52'):
610 message = (False, 'aCodec %s not compatible' % codec)
611 break
612 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2):
613 message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh']))
614 break
616 audio_lang = config.get_tsn('audio_lang', tsn)
617 if audio_lang:
618 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
619 message = (False, '%s preferred audio track exists' %
620 audio_lang)
621 break
623 if mime == 'video/bif':
624 if codec != 'wmav2':
625 message = (False, 'aCodec %s not compatible' % codec)
627 break
629 if inFile[-5:].lower() == '.tivo':
630 break
632 if mime == 'video/x-tivo-mpeg-ts':
633 if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
634 message = (False, 'aCodec %s not compatible' % codec)
636 break
638 if codec not in ('ac3', 'liba52', 'mp2'):
639 message = (False, 'aCodec %s not compatible' % codec)
640 break
642 if (not vInfo['aKbps'] or
643 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
644 message = (False, '%s kbps exceeds max audio bitrate' %
645 vInfo['aKbps'])
646 break
648 audio_lang = config.get_tsn('audio_lang', tsn)
649 if audio_lang:
650 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
651 message = (False, '%s preferred audio track exists' %
652 audio_lang)
653 break
655 return message
657 def tivo_compatible_container(vInfo, inFile, mime=''):
658 message = (True, '')
659 container = vInfo.get('container', '')
660 if ((mime == 'video/mp4' and
661 (container != 'mov' or inFile.lower().endswith('.mov'))) or
662 (mime == 'video/bif' and container != 'asf') or
663 (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
664 (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
665 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
666 message = (False, 'container %s not compatible' % container)
668 return message
670 def mp4_remuxable(inFile, tsn=''):
671 vInfo = video_info(inFile)
672 return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0]
674 def mp4_remux(inFile, basename, tsn=''):
675 outFile = inFile + '.pyTivo-temp'
676 newname = basename + '.pyTivo-temp'
677 if os.path.exists(outFile):
678 return None # ugh!
680 ffmpeg_path = config.get_bin('ffmpeg')
681 fname = unicode(inFile, 'utf-8')
682 oname = unicode(outFile, 'utf-8')
683 if mswindows:
684 fname = fname.encode('cp1252')
685 oname = oname.encode('cp1252')
687 acodec = select_audiocodec(False, inFile, tsn, 'video/mp4')
688 settings = select_buffsize(tsn) + ['-c:v', 'copy'] + acodec
689 if not acodec[1] == 'copy':
690 settings += (select_audiobr(tsn) +
691 select_audiofr(inFile, tsn) +
692 select_audioch(inFile, tsn))
693 settings += [select_audiolang(inFile, tsn),
694 select_ffmpegprams(tsn),
695 '-f', 'mp4']
697 cmd = [ffmpeg_path, '-i', fname] + ' '.join(settings).split() + [oname]
699 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
700 debug(' '.join(cmd))
702 ffmpeg = subprocess.Popen(cmd)
703 debug('remuxing ' + inFile + ' to ' + outFile)
704 if ffmpeg.wait():
705 debug('error during remuxing')
706 os.remove(outFile)
707 return None
709 return newname
711 def tivo_compatible(inFile, tsn='', mime=''):
712 vInfo = video_info(inFile)
714 message = (True, 'all compatible')
715 if not config.get_bin('ffmpeg'):
716 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
717 message = (False, 'no ffmpeg')
718 return message
720 while True:
721 vmessage = tivo_compatible_video(vInfo, tsn, mime)
722 if not vmessage[0]:
723 message = vmessage
724 break
726 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
727 if not amessage[0]:
728 message = amessage
729 break
731 cmessage = tivo_compatible_container(vInfo, inFile, mime)
732 if not cmessage[0]:
733 message = cmessage
735 break
737 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
738 message[1], inFile))
739 return message
741 def video_info(inFile, cache=True):
742 vInfo = dict()
743 fname = unicode(inFile, 'utf-8')
744 mtime = os.path.getmtime(fname)
745 if cache:
746 if inFile in info_cache and info_cache[inFile][0] == mtime:
747 debug('CACHE HIT! %s' % inFile)
748 return info_cache[inFile][1]
750 vInfo['Supported'] = True
752 ffmpeg_path = config.get_bin('ffmpeg')
753 if not ffmpeg_path:
754 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
755 '.vob', '.tivo']:
756 vInfo['Supported'] = False
757 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
758 'rawmeta': {}})
759 if cache:
760 info_cache[inFile] = (mtime, vInfo)
761 return vInfo
763 if mswindows:
764 fname = fname.encode('cp1252')
765 cmd = [ffmpeg_path, '-i', fname]
766 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
767 err_tmp = tempfile.TemporaryFile()
768 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
769 stdin=subprocess.PIPE)
771 # wait configured # of seconds: if ffmpeg is not back give up
772 limit = config.getFFmpegWait()
773 if limit:
774 for i in xrange(limit * 20):
775 time.sleep(.05)
776 if not ffmpeg.poll() == None:
777 break
779 if ffmpeg.poll() == None:
780 kill(ffmpeg)
781 vInfo['Supported'] = False
782 if cache:
783 info_cache[inFile] = (mtime, vInfo)
784 return vInfo
785 else:
786 ffmpeg.wait()
788 err_tmp.seek(0)
789 output = err_tmp.read()
790 err_tmp.close()
791 debug('ffmpeg output=%s' % output)
793 attrs = {'container': r'Input #0, ([^,]+),',
794 'vCodec': r'Video: ([^, ]+)', # video codec
795 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
796 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
797 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
798 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
800 for attr in attrs:
801 rezre = re.compile(attrs[attr])
802 x = rezre.search(output)
803 if x:
804 vInfo[attr] = x.group(1)
805 else:
806 if attr in ['container', 'vCodec']:
807 vInfo[attr] = ''
808 vInfo['Supported'] = False
809 else:
810 vInfo[attr] = None
811 debug('failed at ' + attr)
813 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
814 x = rezre.search(output)
815 if x:
816 if x.group(3):
817 if x.group(3) == 'stereo':
818 vInfo['aCh'] = 2
819 elif x.group(3) == 'mono':
820 vInfo['aCh'] = 1
821 elif x.group(2):
822 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
823 elif x.group(1):
824 vInfo['aCh'] = int(x.group(1))
825 else:
826 vInfo['aCh'] = None
827 debug('failed at aCh')
828 else:
829 vInfo['aCh'] = None
830 debug('failed at aCh')
832 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
833 x = rezre.search(output)
834 if x:
835 vInfo['vWidth'] = int(x.group(1))
836 vInfo['vHeight'] = int(x.group(2))
837 else:
838 vInfo['vWidth'] = ''
839 vInfo['vHeight'] = ''
840 vInfo['Supported'] = False
841 debug('failed at vWidth/vHeight')
843 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
844 x = rezre.search(output)
845 if x:
846 vInfo['vFps'] = x.group(1)
847 if '.' not in vInfo['vFps']:
848 vInfo['vFps'] += '.00'
850 # Allow override only if it is mpeg2 and frame rate was doubled
851 # to 59.94
853 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
854 # First look for the build 7215 version
855 rezre = re.compile(r'.*film source: 29.97.*')
856 x = rezre.search(output.lower())
857 if x:
858 debug('film source: 29.97 setting vFps to 29.97')
859 vInfo['vFps'] = '29.97'
860 else:
861 # for build 8047:
862 rezre = re.compile(r'.*frame rate differs from container ' +
863 r'frame rate: 29.97.*')
864 debug('Bug in VideoReDo')
865 x = rezre.search(output.lower())
866 if x:
867 vInfo['vFps'] = '29.97'
868 else:
869 vInfo['vFps'] = ''
870 vInfo['Supported'] = False
871 debug('failed at vFps')
873 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
874 d = durre.search(output)
876 if d:
877 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
878 int(d.group(2)) * 60 +
879 int(d.group(3))) * 1000 +
880 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
881 else:
882 vInfo['millisecs'] = 0
884 # get bitrate of source for tivo compatibility test.
885 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
886 x = rezre.search(output)
887 if x:
888 vInfo['kbps'] = x.group(1)
889 else:
890 # Fallback method of getting video bitrate
891 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
892 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
893 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
894 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
895 x = rezre.search(output)
896 if x:
897 vInfo['kbps'] = x.group(1)
898 else:
899 vInfo['kbps'] = None
900 debug('failed at kbps')
902 # get par.
903 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
904 x = rezre.search(output)
905 if x and x.group(1) != "0" and x.group(2) != "0":
906 vInfo['par1'] = x.group(1) + ':' + x.group(2)
907 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
908 else:
909 vInfo['par1'], vInfo['par2'] = None, None
911 # get dar.
912 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
913 x = rezre.search(output)
914 if x and x.group(1) != "0" and x.group(2) != "0":
915 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
916 else:
917 vInfo['dar1'] = None
919 # get Audio Stream mapping.
920 rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
921 x = rezre.search(output)
922 amap = []
923 if x:
924 for x in rezre.finditer(output):
925 amap.append((x.group(1), x.group(2) + x.group(3)))
926 else:
927 amap.append(('', ''))
928 debug('failed at mapAudio')
929 vInfo['mapAudio'] = amap
931 vInfo['par'] = None
933 # get Metadata dump (newer ffmpeg).
934 lines = output.split('\n')
935 rawmeta = {}
936 flag = False
938 for line in lines:
939 if line.startswith(' Metadata:'):
940 flag = True
941 else:
942 if flag:
943 if line.startswith(' Duration:'):
944 flag = False
945 else:
946 try:
947 key, value = [x.strip() for x in line.split(':', 1)]
948 try:
949 value = value.decode('utf-8')
950 except:
951 if sys.platform == 'darwin':
952 value = value.decode('macroman')
953 else:
954 value = value.decode('cp1252')
955 rawmeta[key] = [value]
956 except:
957 pass
959 vInfo['rawmeta'] = rawmeta
961 data = metadata.from_text(inFile)
962 for key in data:
963 if key.startswith('Override_'):
964 vInfo['Supported'] = True
965 if key.startswith('Override_mapAudio'):
966 audiomap = dict(vInfo['mapAudio'])
967 newmap = shlex.split(data[key])
968 audiomap.update(zip(newmap[::2], newmap[1::2]))
969 vInfo['mapAudio'] = sorted(audiomap.items(),
970 key=lambda (k,v): (k,v))
971 elif key.startswith('Override_millisecs'):
972 vInfo[key.replace('Override_', '')] = int(data[key])
973 else:
974 vInfo[key.replace('Override_', '')] = data[key]
976 if cache:
977 info_cache[inFile] = (mtime, vInfo)
978 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
979 return vInfo
981 def audio_check(inFile, tsn):
982 cmd_string = ('-y -c:v mpeg2video -r 29.97 -b:v 1000k -c:a copy ' +
983 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
984 fname = unicode(inFile, 'utf-8')
985 if mswindows:
986 fname = fname.encode('cp1252')
987 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
988 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
989 fd, testname = tempfile.mkstemp()
990 testfile = os.fdopen(fd, 'wb')
991 try:
992 shutil.copyfileobj(ffmpeg.stdout, testfile)
993 except:
994 kill(ffmpeg)
995 testfile.close()
996 vInfo = None
997 else:
998 testfile.close()
999 vInfo = video_info(testname, False)
1000 os.remove(testname)
1001 return vInfo
1003 def supported_format(inFile):
1004 if video_info(inFile)['Supported']:
1005 return True
1006 else:
1007 debug('FALSE, file not supported %s' % inFile)
1008 return False
1010 def kill(popen):
1011 debug('killing pid=%s' % str(popen.pid))
1012 if mswindows:
1013 win32kill(popen.pid)
1014 else:
1015 import os, signal
1016 for i in xrange(3):
1017 debug('sending SIGTERM to pid: %s' % popen.pid)
1018 os.kill(popen.pid, signal.SIGTERM)
1019 time.sleep(.5)
1020 if popen.poll() is not None:
1021 debug('process %s has exited' % popen.pid)
1022 break
1023 else:
1024 while popen.poll() is None:
1025 debug('sending SIGKILL to pid: %s' % popen.pid)
1026 os.kill(popen.pid, signal.SIGKILL)
1027 time.sleep(.5)
1029 def win32kill(pid):
1030 import ctypes
1031 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1032 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1033 ctypes.windll.kernel32.CloseHandle(handle)
1035 def gcd(a, b):
1036 while b:
1037 a, b = b, a % b
1038 return a