added subtitle support for ass and srt files
[pyTivo/wmcbrine/lucasnz.git] / plugins / video / transcode.py
blob2457f97107f37add5a9e430022ae9921e54c9465
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('iso8859-1')
54 logger.debug(msg)
56 def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''):
57 settings = {'video_codec': select_videocodec(inFile, tsn, mime),
58 'video_br': select_videobr(inFile, tsn),
59 'video_fps': select_videofps(inFile, tsn),
60 'max_video_br': select_maxvideobr(tsn),
61 'buff_size': select_buffsize(tsn),
62 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
63 'audio_br': select_audiobr(tsn),
64 'audio_fr': select_audiofr(inFile, tsn),
65 'audio_ch': select_audioch(inFile, tsn),
66 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
67 'audio_lang': select_audiolang(inFile, tsn),
68 'ffmpeg_pram': select_ffmpegprams(tsn),
69 'format': select_format(tsn, mime)}
71 if isQuery:
72 return settings
74 ffmpeg_path = config.get_bin('ffmpeg')
75 cmd_string = config.getFFmpegTemplate(tsn) % settings
76 fname = unicode(inFile, 'utf-8')
77 if mswindows:
78 fname = fname.encode('iso8859-1')
80 if inFile[-5:].lower() == '.tivo':
81 tivodecode_path = config.get_bin('tivodecode')
82 tivo_mak = config.get_server('tivo_mak')
83 tcmd = [tivodecode_path, '-m', tivo_mak, fname]
84 tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE,
85 bufsize=(512 * 1024))
86 if tivo_compatible(inFile, tsn)[0]:
87 cmd = ''
88 ffmpeg = tivodecode
89 else:
90 cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split()
91 ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout,
92 stdout=subprocess.PIPE,
93 bufsize=(512 * 1024))
94 else:
95 cmd = [ffmpeg_path, '-i', fname] + cmd_string.split()
96 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
97 stdout=subprocess.PIPE, cwd=os.path.split(inFile)[0])
99 if cmd:
100 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
101 debug(' '.join(cmd))
103 ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0,
104 'last_read': time.time(), 'blocks': []}
105 if thead:
106 ffmpeg_procs[inFile]['blocks'].append(thead)
107 reap_process(inFile)
108 return resume_transfer(inFile, outFile, 0)
110 def is_resumable(inFile, offset):
111 if inFile in ffmpeg_procs:
112 proc = ffmpeg_procs[inFile]
113 if proc['start'] <= offset < proc['end']:
114 return True
115 else:
116 cleanup(inFile)
117 kill(proc['process'])
118 return False
120 def resume_transfer(inFile, outFile, offset):
121 proc = ffmpeg_procs[inFile]
122 offset -= proc['start']
123 count = 0
125 try:
126 for block in proc['blocks']:
127 length = len(block)
128 if offset < length:
129 if offset > 0:
130 block = block[offset:]
131 outFile.write('%x\r\n' % len(block))
132 outFile.write(block)
133 outFile.write('\r\n')
134 count += len(block)
135 offset -= length
136 outFile.flush()
137 except Exception, msg:
138 logger.info(msg)
139 return count
141 proc['start'] = proc['end']
142 proc['blocks'] = []
144 return count + transfer_blocks(inFile, outFile)
146 def transfer_blocks(inFile, outFile):
147 proc = ffmpeg_procs[inFile]
148 blocks = proc['blocks']
149 count = 0
151 while True:
152 try:
153 block = proc['process'].stdout.read(BLOCKSIZE)
154 proc['last_read'] = time.time()
155 except Exception, msg:
156 logger.info(msg)
157 cleanup(inFile)
158 kill(proc['process'])
159 break
161 if not block:
162 try:
163 outFile.flush()
164 except Exception, msg:
165 logger.info(msg)
166 else:
167 cleanup(inFile)
168 break
170 blocks.append(block)
171 proc['end'] += len(block)
172 if len(blocks) > MAXBLOCKS:
173 proc['start'] += len(blocks[0])
174 blocks.pop(0)
176 try:
177 outFile.write('%x\r\n' % len(block))
178 outFile.write(block)
179 outFile.write('\r\n')
180 count += len(block)
181 except Exception, msg:
182 logger.info(msg)
183 break
185 return count
187 def reap_process(inFile):
188 if ffmpeg_procs and inFile in ffmpeg_procs:
189 proc = ffmpeg_procs[inFile]
190 if proc['last_read'] + TIMEOUT < time.time():
191 del ffmpeg_procs[inFile]
192 del reapers[inFile]
193 kill(proc['process'])
194 else:
195 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
196 reapers[inFile] = reaper
197 reaper.start()
199 def cleanup(inFile):
200 del ffmpeg_procs[inFile]
201 reapers[inFile].cancel()
202 del reapers[inFile]
204 def select_audiocodec(isQuery, inFile, tsn='', mime=''):
205 if inFile[-5:].lower() == '.tivo':
206 return '-acodec copy'
207 vInfo = video_info(inFile)
208 codectype = vInfo['vCodec']
209 # Default, compatible with all TiVo's
210 codec = 'ac3'
211 if mime == 'video/mp4':
212 compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
213 'ac3', 'liba52')
214 else:
215 compatiblecodecs = ('ac3', 'liba52', 'mp2')
217 if vInfo['aCodec'] in compatiblecodecs:
218 aKbps = vInfo['aKbps']
219 aCh = vInfo['aCh']
220 if aKbps == None:
221 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
222 # along with the channel check below this should
223 # pass any AAC audio that has undefined 'aKbps' and
224 # is <= 2 channels. Should be TiVo compatible.
225 codec = 'copy'
226 elif not isQuery:
227 vInfoQuery = audio_check(inFile, tsn)
228 if vInfoQuery == None:
229 aKbps = None
230 aCh = None
231 else:
232 aKbps = vInfoQuery['aKbps']
233 aCh = vInfoQuery['aCh']
234 else:
235 codec = 'TBA'
236 if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn):
237 # compatible codec and bitrate, do not reencode audio
238 codec = 'copy'
239 if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2):
240 codec = 'ac3'
241 copy_flag = config.get_tsn('copy_ts', tsn)
242 copyts = ' -copyts'
243 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
244 (copy_flag and copy_flag.lower() == 'false')):
245 copyts = ''
246 return '-acodec ' + codec + copyts
248 def select_audiofr(inFile, tsn):
249 freq = '48000' # default
250 vInfo = video_info(inFile)
251 if vInfo['aFreq'] == '44100':
252 # compatible frequency
253 freq = vInfo['aFreq']
254 audio_fr = config.get_tsn('audio_fr', tsn)
255 if audio_fr != None:
256 freq = audio_fr
257 return '-ar ' + freq
259 def select_audioch(inFile, tsn):
260 ch = config.get_tsn('audio_ch', tsn)
261 if ch:
262 return '-ac ' + ch
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 elif video_info(inFile)['aCh']:
268 return '-ac %i' % video_info(inFile)['aCh']
269 return ''
271 def select_audiolang(inFile, tsn):
272 vInfo = video_info(inFile)
273 audio_lang = config.get_tsn('audio_lang', tsn)
274 debug('audio_lang: %s' % audio_lang)
275 if vInfo['mapAudio']:
276 # default to first detected audio stream to begin with
277 stream = vInfo['mapAudio'][0][0]
278 debug('set first detected audio stream by default: %s' % stream)
279 if audio_lang != None and vInfo['mapVideo'] != None:
280 langmatch_curr = []
281 langmatch_prev = vInfo['mapAudio'][:]
282 for lang in audio_lang.replace(' ', '').lower().split(','):
283 debug('matching lang: %s' % lang)
284 for s, l in langmatch_prev:
285 if lang in s + l.replace(' ', '').lower():
286 debug('matched: %s' % s + l.replace(' ', '').lower())
287 langmatch_curr.append((s, l))
288 # if only 1 item matched we're done
289 if len(langmatch_curr) == 1:
290 stream = langmatch_curr[0][0]
291 debug('found exactly one match: %s' % stream)
292 break
293 # if more than 1 item matched copy the curr area to the prev
294 # array we only need to look at the new shorter list from
295 # now on
296 elif len(langmatch_curr) > 1:
297 langmatch_prev = langmatch_curr[:]
298 # default to the first item matched thus far
299 stream = langmatch_curr[0][0]
300 debug('remember first match: %s' % stream)
301 langmatch_curr = []
302 # don't let FFmpeg auto select audio stream, pyTivo defaults to
303 # first detected
304 if stream:
305 debug('selected audio stream: %s' % stream)
306 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
307 # if no audio is found
308 debug('selected audio stream: None detected')
309 return ''
311 def select_videofps(inFile, tsn):
312 vInfo = video_info(inFile)
313 fps = '-r 29.97' # default
314 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
315 fps = ' '
316 video_fps = config.get_tsn('video_fps', tsn)
317 if video_fps != None:
318 fps = '-r ' + video_fps
319 return fps
321 def select_videocodec(inFile, tsn, mime=''):
322 vInfo = video_info(inFile)
323 subtitles = vInfo.get('subtitles')
324 vf = ''
325 if subtitles:
326 vf = '-vf %s ' % subtitles
327 if tivo_compatible_video(vInfo, tsn, mime)[0]:
328 codec = 'copy'
329 if (mime == 'video/x-tivo-mpeg-ts' and
330 vInfo.get('vCodec', '') == 'h264'):
331 codec += ' -bsf h264_mp4toannexb'
332 else:
333 codec = 'mpeg2video' # default
334 return vf + '-vcodec ' + codec
336 def select_videobr(inFile, tsn, mime=''):
337 return '-b ' + str(select_videostr(inFile, tsn, mime) / 1000) + 'k'
339 def select_videostr(inFile, tsn, mime=''):
340 vInfo = video_info(inFile)
341 if tivo_compatible_video(vInfo, tsn, mime)[0]:
342 video_str = int(vInfo['kbps'])
343 if vInfo['aKbps']:
344 video_str -= int(vInfo['aKbps'])
345 video_str *= 1000
346 else:
347 video_str = config.strtod(config.getVideoBR(tsn))
348 if config.isHDtivo(tsn) and vInfo['kbps']:
349 video_str = max(video_str, int(vInfo['kbps']) * 1000)
350 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
351 video_str))
352 return video_str
354 def select_audiobr(tsn):
355 return '-ab ' + config.getAudioBR(tsn)
357 def select_maxvideobr(tsn):
358 return '-maxrate ' + config.getMaxVideoBR(tsn)
360 def select_buffsize(tsn):
361 return '-bufsize ' + config.getBuffSize(tsn)
363 def select_ffmpegprams(tsn):
364 params = config.getFFmpegPrams(tsn)
365 if not params:
366 params = ''
367 return params
369 def select_format(tsn, mime):
370 if mime == 'video/x-tivo-mpeg-ts':
371 fmt = 'mpegts'
372 else:
373 fmt = 'vob'
374 return '-f %s -' % fmt
376 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
377 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
378 vInfo['vWidth']) * multiplier)
379 if endHeight % 2:
380 endHeight -= 1
381 topPadding = (TIVO_HEIGHT - endHeight) / 2
382 if topPadding % 2:
383 topPadding -= 1
384 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH,
385 endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
387 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
388 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
389 (vInfo['vHeight'] * multiplier))
390 if endWidth % 2:
391 endWidth -= 1
392 leftPadding = (TIVO_WIDTH - endWidth) / 2
393 if leftPadding % 2:
394 leftPadding -= 1
395 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth,
396 TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
398 def select_aspect(inFile, tsn = ''):
399 TIVO_WIDTH = config.getTivoWidth(tsn)
400 TIVO_HEIGHT = config.getTivoHeight(tsn)
402 vInfo = video_info(inFile)
404 debug('tsn: %s' % tsn)
406 aspect169 = config.get169Setting(tsn)
408 debug('aspect169: %s' % aspect169)
410 optres = config.getOptres(tsn)
412 debug('optres: %s' % optres)
414 if optres:
415 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
416 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
417 if optHeight < TIVO_HEIGHT:
418 TIVO_HEIGHT = optHeight
419 if optWidth < TIVO_WIDTH:
420 TIVO_WIDTH = optWidth
422 if vInfo.get('par2'):
423 par2 = vInfo['par2']
424 elif vInfo.get('par'):
425 par2 = float(vInfo['par'])
426 else:
427 # Assume PAR = 1.0
428 par2 = 1.0
430 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
431 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
432 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
433 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
435 if config.isHDtivo(tsn) and not optres:
436 if vInfo['par']:
437 npar = par2
439 # adjust for pixel aspect ratio, if set
441 if npar < 1.0:
442 return ['-s', '%dx%d' % (vInfo['vWidth'],
443 math.ceil(vInfo['vHeight'] / npar))]
444 elif npar > 1.0:
445 # FFMPEG expects width to be a multiple of two
446 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
447 vInfo['vHeight'])]
449 if vInfo['vHeight'] <= TIVO_HEIGHT:
450 # pass all resolutions to S3, except heights greater than
451 # conf height
452 return []
453 # else, resize video.
455 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
456 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
457 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
459 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
460 debug('File + PAR is within 4:3.')
461 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
463 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
464 (59, 72), (59, 36), (59, 54)] or
465 vInfo['dar1'] == '4:3'):
466 debug('File is within 4:3 list.')
467 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
469 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
470 (59, 27)] or vInfo['dar1'] == '16:9')
471 and (aspect169 or config.get169Letterbox(tsn))):
472 debug('File is within 16:9 list and 16:9 allowed.')
474 if config.get169Blacklist(tsn) or (aspect169 and
475 config.get169Letterbox(tsn)):
476 aspect = '4:3'
477 else:
478 aspect = '16:9'
479 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
481 else:
482 settings = ['-aspect']
484 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
485 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
486 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
487 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
488 multiplier4by3))
490 # If video is wider than 4:3 add top and bottom padding
492 if ratio > 133: # Might be 16:9 file, or just need padding on
493 # top and bottom
495 if aspect169 and ratio > 135: # If file would fall in 4:3
496 # assume it is supposed to be 4:3
498 if (config.get169Blacklist(tsn) or
499 config.get169Letterbox(tsn)):
500 settings.append('4:3')
501 else:
502 settings.append('16:9')
504 if ratio > 177: # too short needs padding top and bottom
505 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
506 multiplier16by9, vInfo)
507 debug(('16:9 aspect allowed, file is wider ' +
508 'than 16:9 padding top and bottom\n%s') %
509 ' '.join(settings))
511 else: # too skinny needs padding on left and right.
512 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
513 multiplier16by9, vInfo)
514 debug(('16:9 aspect allowed, file is narrower ' +
515 'than 16:9 padding left and right\n%s') %
516 ' '.join(settings))
518 else: # this is a 4:3 file or 16:9 output not allowed
519 if ratio > 135 and config.get169Letterbox(tsn):
520 settings.append('16:9')
521 multiplier = multiplier16by9
522 else:
523 settings.append('4:3')
524 multiplier = multiplier4by3
525 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
526 multiplier, vInfo)
527 debug(('File is wider than 4:3 padding ' +
528 'top and bottom\n%s') % ' '.join(settings))
530 # If video is taller than 4:3 add left and right padding, this
531 # is rare. All of these files will always be sent in an aspect
532 # ratio of 4:3 since they are so narrow.
534 else:
535 settings.append('4:3')
536 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
537 debug('File is taller than 4:3 padding left and right\n%s'
538 % ' '.join(settings))
540 return settings
542 def tivo_compatible_video(vInfo, tsn, mime=''):
543 message = (True, '')
544 while True:
545 if vInfo.get('subtitles'):
546 message = (False, 'Subtitle hard-coding requires reencoding')
548 break
550 codec = vInfo.get('vCodec', '')
551 if mime == 'video/mp4':
552 if codec != 'h264':
553 message = (False, 'vCodec %s not compatible' % codec)
555 break
557 if mime == 'video/bif':
558 if codec != 'vc1':
559 message = (False, 'vCodec %s not compatible' % codec)
561 break
563 if mime == 'video/x-tivo-mpeg-ts':
564 if codec not in ('h264', 'mpeg2video'):
565 message = (False, 'vCodec %s not compatible' % codec)
567 break
569 if codec not in ('mpeg2video', 'mpeg1video'):
570 message = (False, 'vCodec %s not compatible' % codec)
571 break
573 if vInfo['kbps'] != None:
574 abit = max('0', vInfo['aKbps'])
575 if (int(vInfo['kbps']) - int(abit) >
576 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
577 message = (False, '%s kbps exceeds max video bitrate' %
578 vInfo['kbps'])
579 break
580 else:
581 message = (False, '%s kbps not supported' % vInfo['kbps'])
582 break
584 if config.isHDtivo(tsn):
585 # HD Tivo detected, skipping remaining tests.
586 break
588 if not vInfo['vFps'] in ['29.97', '59.94']:
589 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
590 break
592 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
593 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
594 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
595 message = (False, ('DAR %s not supported ' +
596 'by BLACKLIST_169 tivos') % vInfo['dar1'])
597 break
599 mode = (vInfo['vWidth'], vInfo['vHeight'])
600 if mode not in [(720, 480), (704, 480), (544, 480),
601 (528, 480), (480, 480), (352, 480), (352, 240)]:
602 message = (False, '%s x %s not in supported modes' % mode)
603 break
605 return message
607 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
608 message = (True, '')
609 while True:
610 codec = vInfo.get('aCodec', '')
612 if codec == None:
613 debug('No audio stream detected')
614 break
616 if mime == 'video/mp4':
617 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
618 'ac3', 'liba52'):
619 message = (False, 'aCodec %s not compatible' % codec)
620 break
621 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2):
622 message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh']))
623 break
625 audio_lang = config.get_tsn('audio_lang', tsn)
626 if audio_lang:
627 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
628 message = (False, '%s preferred audio track exists' %
629 audio_lang)
630 break
632 if mime == 'video/bif':
633 if codec != 'wmav2':
634 message = (False, 'aCodec %s not compatible' % codec)
636 break
638 if inFile[-5:].lower() == '.tivo':
639 break
641 if mime == 'video/x-tivo-mpeg-ts':
642 if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
643 message = (False, 'aCodec %s not compatible' % codec)
645 break
647 if codec not in ('ac3', 'liba52', 'mp2'):
648 message = (False, 'aCodec %s not compatible' % codec)
649 break
651 if (not vInfo['aKbps'] or
652 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
653 message = (False, '%s kbps exceeds max audio bitrate' %
654 vInfo['aKbps'])
655 break
657 audio_lang = config.get_tsn('audio_lang', tsn)
658 if audio_lang:
659 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
660 message = (False, '%s preferred audio track exists' %
661 audio_lang)
662 break
664 return message
666 def tivo_compatible_container(vInfo, inFile, mime=''):
667 message = (True, '')
668 container = vInfo.get('container', '')
669 if ((mime == 'video/mp4' and
670 (container != 'mov' or inFile.lower().endswith('.mov'))) or
671 (mime == 'video/bif' and container != 'asf') or
672 (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
673 (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
674 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
675 message = (False, 'container %s not compatible' % container)
677 return message
679 def mp4_remuxable(inFile, tsn=''):
680 vInfo = video_info(inFile)
681 return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0]
683 def mp4_remux(inFile, basename, tsn='', temp_share_path=''):
684 outFile = inFile + '.pyTivo-temp'
685 newname = basename + '.pyTivo-temp'
687 if temp_share_path:
688 newname = os.path.splitext(os.path.split(basename)[1])[0] + '.mp4.pyTivo-temp'
689 outFile = os.path.join(temp_share_path, newname)
691 if os.path.exists(outFile):
692 return None # ugh!
694 ffmpeg_path = config.get_bin('ffmpeg')
695 fname = unicode(inFile, 'utf-8')
696 oname = unicode(outFile, 'utf-8')
697 if mswindows:
698 fname = fname.encode('iso8859-1')
699 oname = oname.encode('iso8859-1')
701 settings = {'video_codec': '-vcodec copy',
702 'video_br': select_videobr(inFile, tsn),
703 'video_fps': select_videofps(inFile, tsn),
704 'max_video_br': select_maxvideobr(tsn),
705 'buff_size': select_buffsize(tsn),
706 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
707 'audio_br': select_audiobr(tsn),
708 'audio_fr': select_audiofr(inFile, tsn),
709 'audio_ch': select_audioch(inFile, tsn),
710 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'),
711 'audio_lang': select_audiolang(inFile, tsn),
712 'ffmpeg_pram': select_ffmpegprams(tsn),
713 'format': '-f mp4'}
715 cmd_string = config.getFFmpegTemplate(tsn) % settings
716 cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname]
718 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
719 debug(' '.join(cmd))
721 ffmpeg = subprocess.Popen(cmd)
722 debug('remuxing ' + inFile + ' to ' + outFile)
723 if ffmpeg.wait():
724 debug('error during remuxing')
725 os.remove(outFile)
726 return None
728 return newname
730 def tivo_compatible(inFile, tsn='', mime=''):
731 vInfo = video_info(inFile)
733 message = (True, 'all compatible')
734 if not config.get_bin('ffmpeg'):
735 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
736 message = (False, 'no ffmpeg')
737 return message
739 while True:
740 vmessage = tivo_compatible_video(vInfo, tsn, mime)
741 if not vmessage[0]:
742 message = vmessage
743 break
745 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
746 if not amessage[0]:
747 message = amessage
748 break
750 cmessage = tivo_compatible_container(vInfo, inFile, mime)
751 if not cmessage[0]:
752 message = cmessage
754 break
756 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
757 message[1], inFile))
758 return message
760 def video_info(inFile, cache=True):
761 vInfo = dict()
762 fname = unicode(inFile, 'utf-8')
763 mtime = os.stat(fname).st_mtime
764 if cache:
765 if inFile in info_cache and info_cache[inFile][0] == mtime:
766 debug('CACHE HIT! %s' % inFile)
767 return info_cache[inFile][1]
769 vInfo['Supported'] = True
771 ffmpeg_path = config.get_bin('ffmpeg')
772 if not ffmpeg_path:
773 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
774 '.vob', '.tivo']:
775 vInfo['Supported'] = False
776 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
777 'rawmeta': {}})
778 if cache:
779 info_cache[inFile] = (mtime, vInfo)
780 return vInfo
782 if mswindows:
783 fname = fname.encode('iso8859-1')
784 cmd = [ffmpeg_path, '-i', fname]
785 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
786 err_tmp = tempfile.TemporaryFile()
787 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
788 stdin=subprocess.PIPE)
790 # wait configured # of seconds: if ffmpeg is not back give up
791 limit = config.getFFmpegWait()
792 if limit:
793 for i in xrange(limit * 20):
794 time.sleep(.05)
795 if not ffmpeg.poll() == None:
796 break
798 if ffmpeg.poll() == None:
799 kill(ffmpeg)
800 vInfo['Supported'] = False
801 if cache:
802 info_cache[inFile] = (mtime, vInfo)
803 return vInfo
804 else:
805 ffmpeg.wait()
807 err_tmp.seek(0)
808 output = err_tmp.read()
809 err_tmp.close()
810 debug('ffmpeg output=%s' % output)
812 attrs = {'container': r'Input #0, ([^,]+),',
813 'vCodec': r'Video: ([^, ]+)', # video codec
814 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
815 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
816 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
817 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
819 for attr in attrs:
820 rezre = re.compile(attrs[attr])
821 x = rezre.search(output)
822 if x:
823 vInfo[attr] = x.group(1)
824 else:
825 if attr in ['container', 'vCodec']:
826 vInfo[attr] = ''
827 vInfo['Supported'] = False
828 else:
829 vInfo[attr] = None
830 debug('failed at ' + attr)
832 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
833 x = rezre.search(output)
834 if x:
835 if x.group(3):
836 if x.group(3) == 'stereo':
837 vInfo['aCh'] = 2
838 elif x.group(3) == 'mono':
839 vInfo['aCh'] = 1
840 elif x.group(2):
841 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
842 elif x.group(1):
843 vInfo['aCh'] = int(x.group(1))
844 else:
845 vInfo['aCh'] = None
846 debug('failed at aCh')
847 else:
848 vInfo['aCh'] = None
849 debug('failed at aCh')
851 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
852 x = rezre.search(output)
853 if x:
854 vInfo['vWidth'] = int(x.group(1))
855 vInfo['vHeight'] = int(x.group(2))
856 else:
857 vInfo['vWidth'] = ''
858 vInfo['vHeight'] = ''
859 vInfo['Supported'] = False
860 debug('failed at vWidth/vHeight')
862 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
863 x = rezre.search(output)
864 if x:
865 vInfo['vFps'] = x.group(1)
866 if '.' not in vInfo['vFps']:
867 vInfo['vFps'] += '.00'
869 # Allow override only if it is mpeg2 and frame rate was doubled
870 # to 59.94
872 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
873 # First look for the build 7215 version
874 rezre = re.compile(r'.*film source: 29.97.*')
875 x = rezre.search(output.lower())
876 if x:
877 debug('film source: 29.97 setting vFps to 29.97')
878 vInfo['vFps'] = '29.97'
879 else:
880 # for build 8047:
881 rezre = re.compile(r'.*frame rate differs from container ' +
882 r'frame rate: 29.97.*')
883 debug('Bug in VideoReDo')
884 x = rezre.search(output.lower())
885 if x:
886 vInfo['vFps'] = '29.97'
887 else:
888 vInfo['vFps'] = ''
889 vInfo['Supported'] = False
890 debug('failed at vFps')
892 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
893 d = durre.search(output)
895 if d:
896 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
897 int(d.group(2)) * 60 +
898 int(d.group(3))) * 1000 +
899 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
900 else:
901 vInfo['millisecs'] = 0
903 # get bitrate of source for tivo compatibility test.
904 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
905 x = rezre.search(output)
906 if x:
907 vInfo['kbps'] = x.group(1)
908 else:
909 # Fallback method of getting video bitrate
910 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
911 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
912 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
913 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
914 x = rezre.search(output)
915 if x:
916 vInfo['kbps'] = x.group(1)
917 else:
918 vInfo['kbps'] = None
919 debug('failed at kbps')
921 # get par.
922 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
923 x = rezre.search(output)
924 if x and x.group(1) != "0" and x.group(2) != "0":
925 vInfo['par1'] = x.group(1) + ':' + x.group(2)
926 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
927 else:
928 vInfo['par1'], vInfo['par2'] = None, None
930 # get dar.
931 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
932 x = rezre.search(output)
933 if x and x.group(1) != "0" and x.group(2) != "0":
934 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
935 else:
936 vInfo['dar1'] = None
938 # get Audio Stream mapping.
939 rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
940 x = rezre.search(output)
941 amap = []
942 if x:
943 for x in rezre.finditer(output):
944 amap.append((x.group(1), x.group(2) + x.group(3)))
945 else:
946 amap.append(('', ''))
947 debug('failed at mapAudio')
948 vInfo['mapAudio'] = amap
950 vInfo['par'] = None
952 # get Metadata dump (newer ffmpeg).
953 lines = output.split('\n')
954 rawmeta = {}
955 flag = False
957 for line in lines:
958 if line.startswith(' Metadata:'):
959 flag = True
960 else:
961 if flag:
962 if line.startswith(' Duration:'):
963 flag = False
964 else:
965 try:
966 key, value = [x.strip() for x in line.split(':', 1)]
967 try:
968 value = value.decode('utf-8')
969 except:
970 if sys.platform == 'darwin':
971 value = value.decode('macroman')
972 else:
973 value = value.decode('iso8859-1')
974 rawmeta[key] = [value]
975 except:
976 pass
978 vInfo['rawmeta'] = rawmeta
980 data = metadata.from_text(inFile)
981 for key in data:
982 if key.startswith('Override_'):
983 vInfo['Supported'] = True
984 if key.startswith('Override_mapAudio'):
985 audiomap = dict(vInfo['mapAudio'])
986 newmap = shlex.split(data[key])
987 audiomap.update(zip(newmap[::2], newmap[1::2]))
988 vInfo['mapAudio'] = sorted(audiomap.items(),
989 key=lambda (k,v): (k,v))
990 elif key.startswith('Override_millisecs'):
991 vInfo[key.replace('Override_', '')] = int(data[key])
992 else:
993 vInfo[key.replace('Override_', '')] = data[key]
994 elif key.lower() == 'subtitles':
995 vInfo['subtitles'] = data[key]
997 if cache:
998 info_cache[inFile] = (mtime, vInfo)
999 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
1000 return vInfo
1002 def audio_check(inFile, tsn):
1003 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
1004 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
1005 fname = unicode(inFile, 'utf-8')
1006 if mswindows:
1007 fname = fname.encode('iso8859-1')
1008 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
1009 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
1010 fd, testname = tempfile.mkstemp()
1011 testfile = os.fdopen(fd, 'wb')
1012 try:
1013 shutil.copyfileobj(ffmpeg.stdout, testfile)
1014 except:
1015 kill(ffmpeg)
1016 testfile.close()
1017 vInfo = None
1018 else:
1019 testfile.close()
1020 vInfo = video_info(testname, False)
1021 os.remove(testname)
1022 return vInfo
1024 def supported_format(inFile):
1025 if video_info(inFile)['Supported']:
1026 return True
1027 else:
1028 debug('FALSE, file not supported %s' % inFile)
1029 return False
1031 def kill(popen):
1032 debug('killing pid=%s' % str(popen.pid))
1033 if mswindows:
1034 win32kill(popen.pid)
1035 else:
1036 import os, signal
1037 for i in xrange(3):
1038 debug('sending SIGTERM to pid: %s' % popen.pid)
1039 os.kill(popen.pid, signal.SIGTERM)
1040 time.sleep(.5)
1041 if popen.poll() is not None:
1042 debug('process %s has exited' % popen.pid)
1043 break
1044 else:
1045 while popen.poll() is None:
1046 debug('sending SIGKILL to pid: %s' % popen.pid)
1047 os.kill(popen.pid, signal.SIGKILL)
1048 time.sleep(.5)
1050 def win32kill(pid):
1051 import ctypes
1052 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1053 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1054 ctypes.windll.kernel32.CloseHandle(handle)
1056 def gcd(a, b):
1057 while b:
1058 a, b = b, a % b
1059 return a