added support for other aac codecs and set to transcode aac if there are more than...
[pyTivo/wmcbrine/lucasnz.git] / plugins / video / transcode.py
blobbcd22643d35ce416b6fbe73dbcc4effa4d6d5299
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 UNSET = 0
31 OLD_PAD = 1
32 NEW_PAD = 2
34 pad_style = UNSET
36 # XXX BIG HACK
37 # subprocess is broken for me on windows so super hack
38 def patchSubprocess():
39 o = subprocess.Popen._make_inheritable
41 def _make_inheritable(self, handle):
42 if not handle: return subprocess.GetCurrentProcess()
43 return o(self, handle)
45 subprocess.Popen._make_inheritable = _make_inheritable
46 mswindows = (sys.platform == "win32")
47 if mswindows:
48 patchSubprocess()
50 def debug(msg):
51 if type(msg) == str:
52 try:
53 msg = msg.decode('utf8')
54 except:
55 if sys.platform == 'darwin':
56 msg = msg.decode('macroman')
57 else:
58 msg = msg.decode('iso8859-1')
59 logger.debug(msg)
61 def transcode(isQuery, inFile, outFile, tsn=''):
62 settings = {'video_codec': select_videocodec(inFile, tsn),
63 'video_br': select_videobr(inFile, tsn),
64 'video_fps': select_videofps(inFile, tsn),
65 'max_video_br': select_maxvideobr(tsn),
66 'buff_size': select_buffsize(tsn),
67 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
68 'audio_br': select_audiobr(tsn),
69 'audio_fr': select_audiofr(inFile, tsn),
70 'audio_ch': select_audioch(tsn),
71 'audio_codec': select_audiocodec(isQuery, inFile, tsn),
72 'audio_lang': select_audiolang(inFile, tsn),
73 'ffmpeg_pram': select_ffmpegprams(tsn),
74 'format': select_format(tsn)}
76 if isQuery:
77 return settings
79 ffmpeg_path = config.get_bin('ffmpeg')
80 cmd_string = config.getFFmpegTemplate(tsn) % settings
81 fname = unicode(inFile, 'utf-8')
82 if mswindows:
83 fname = fname.encode('iso8859-1')
85 if inFile[-5:].lower() == '.tivo':
86 tivodecode_path = config.get_bin('tivodecode')
87 tivo_mak = config.get_server('tivo_mak')
88 tcmd = [tivodecode_path, '-m', tivo_mak, fname]
89 tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE,
90 bufsize=(512 * 1024))
91 if tivo_compatible(inFile, tsn)[0]:
92 cmd = ''
93 ffmpeg = tivodecode
94 else:
95 cmd = [ffmpeg_path, '-i', '-'] + cmd_string.split()
96 ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout,
97 stdout=subprocess.PIPE,
98 bufsize=(512 * 1024))
99 else:
100 cmd = [ffmpeg_path, '-i', fname] + cmd_string.split()
101 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
102 stdout=subprocess.PIPE)
104 if cmd:
105 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
106 debug(' '.join(cmd))
108 ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0,
109 'last_read': time.time(), 'blocks': []}
110 reap_process(inFile)
111 return transfer_blocks(inFile, outFile)
113 def is_resumable(inFile, offset):
114 if inFile in ffmpeg_procs:
115 proc = ffmpeg_procs[inFile]
116 if proc['start'] <= offset < proc['end']:
117 return True
118 else:
119 cleanup(inFile)
120 kill(proc['process'])
121 return False
123 def resume_transfer(inFile, outFile, offset):
124 proc = ffmpeg_procs[inFile]
125 offset -= proc['start']
126 count = 0
128 try:
129 for block in proc['blocks']:
130 length = len(block)
131 if offset < length:
132 if offset > 0:
133 block = block[offset:]
134 outFile.write('%x\r\n' % len(block))
135 outFile.write(block)
136 outFile.write('\r\n')
137 count += len(block)
138 offset -= length
139 outFile.flush()
140 except Exception, msg:
141 logger.info(msg)
142 return count
144 proc['start'] = proc['end']
145 proc['blocks'] = []
147 return count + transfer_blocks(inFile, outFile)
149 def transfer_blocks(inFile, outFile):
150 proc = ffmpeg_procs[inFile]
151 blocks = proc['blocks']
152 count = 0
154 while True:
155 try:
156 block = proc['process'].stdout.read(BLOCKSIZE)
157 proc['last_read'] = time.time()
158 except Exception, msg:
159 logger.info(msg)
160 cleanup(inFile)
161 kill(proc['process'])
162 break
164 if not block:
165 try:
166 outFile.flush()
167 except Exception, msg:
168 logger.info(msg)
169 else:
170 cleanup(inFile)
171 break
173 blocks.append(block)
174 proc['end'] += len(block)
175 if len(blocks) > MAXBLOCKS:
176 proc['start'] += len(blocks[0])
177 blocks.pop(0)
179 try:
180 outFile.write('%x\r\n' % len(block))
181 outFile.write(block)
182 outFile.write('\r\n')
183 count += len(block)
184 except Exception, msg:
185 logger.info(msg)
186 break
188 return count
190 def reap_process(inFile):
191 if ffmpeg_procs and inFile in ffmpeg_procs:
192 proc = ffmpeg_procs[inFile]
193 if proc['last_read'] + TIMEOUT < time.time():
194 del ffmpeg_procs[inFile]
195 del reapers[inFile]
196 kill(proc['process'])
197 else:
198 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
199 reapers[inFile] = reaper
200 reaper.start()
202 def cleanup(inFile):
203 del ffmpeg_procs[inFile]
204 reapers[inFile].cancel()
205 del reapers[inFile]
207 def select_audiocodec(isQuery, inFile, tsn='', mime=''):
208 if inFile[-5:].lower() == '.tivo':
209 return '-acodec copy'
210 vInfo = video_info(inFile)
211 codectype = vInfo['vCodec']
212 codec = config.get_tsn('audio_codec', tsn)
213 if not codec:
214 # Default, compatible with all TiVo's
215 codec = 'ac3'
216 if mime == 'video/mp4':
217 compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
218 'ac3', 'liba52')
219 else:
220 compatiblecodecs = ('ac3', 'liba52', 'mp2')
222 if vInfo['aCodec'] not in compatiblecodecs:
223 aKbps = vInfo['aKbps']
224 aCh = vInfo['aCh']
225 if aKbps == None:
226 if 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 = 'TBD'
236 if aKbps != None and int(aKbps) <= config.getMaxAudioBR(tsn):
237 # compatible codec and bitrate, do not reencode audio
238 codec = 'copy'
239 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and 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 not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
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(tsn):
260 ch = config.get_tsn('audio_ch', tsn)
261 if ch:
262 return '-ac ' + ch
263 return ''
265 def select_audiolang(inFile, tsn):
266 vInfo = video_info(inFile)
267 audio_lang = config.get_tsn('audio_lang', tsn)
268 debug('audio_lang: %s' % audio_lang)
269 if audio_lang != None and vInfo['mapVideo'] != None:
270 stream = vInfo['mapAudio'][0][0]
271 langmatch_curr = []
272 langmatch_prev = vInfo['mapAudio'][:]
273 for lang in audio_lang.replace(' ','').lower().split(','):
274 for s, l in langmatch_prev:
275 if lang in s + (l).replace(' ','').lower():
276 langmatch_curr.append((s, l))
277 stream = s
278 #if only 1 item matched we're done
279 if len(langmatch_curr) == 1:
280 del langmatch_prev[:]
281 break
282 #if more than 1 item matched copy the curr area to the prev array
283 #we only need to look at the new shorter list from now on
284 elif len(langmatch_curr) > 1:
285 del langmatch_prev[:]
286 langmatch_prev = langmatch_curr[:]
287 del langmatch_curr[:]
288 #if nothing matched we'll keep the prev array and clear the curr array
289 else:
290 del langmatch_curr[:]
291 #if we drop out of the loop with more than 1 item default to the first item
292 if len(langmatch_prev) > 1:
293 stream = langmatch_prev[0][0]
294 if stream is not '':
295 debug('selected audio stream: %s' % stream)
296 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
297 debug('selected audio stream: %s' % '')
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 video_fps = config.get_tsn('video_fps', tsn)
306 if video_fps != None:
307 fps = '-r ' + video_fps
308 return fps
310 def select_videocodec(inFile, tsn):
311 vInfo = video_info(inFile)
312 if tivo_compatible_video(vInfo, tsn)[0]:
313 codec = 'copy'
314 else:
315 codec = 'mpeg2video' # default
316 return '-vcodec ' + codec
318 def select_videobr(inFile, tsn):
319 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
321 def select_videostr(inFile, tsn):
322 vInfo = video_info(inFile)
323 if tivo_compatible_video(vInfo, tsn)[0]:
324 video_str = int(vInfo['kbps'])
325 if vInfo['aKbps']:
326 video_str -= int(vInfo['aKbps'])
327 video_str *= 1000
328 else:
329 video_str = config.strtod(config.getVideoBR(tsn))
330 if config.isHDtivo(tsn):
331 if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0:
332 video_percent = (int(vInfo['kbps']) * 10 *
333 config.getVideoPCT(tsn))
334 video_str = max(video_str, video_percent)
335 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
336 video_str))
337 return video_str
339 def select_audiobr(tsn):
340 return '-ab ' + config.getAudioBR(tsn)
342 def select_maxvideobr(tsn):
343 return '-maxrate ' + config.getMaxVideoBR(tsn)
345 def select_buffsize(tsn):
346 return '-bufsize ' + config.getBuffSize(tsn)
348 def select_ffmpegprams(tsn):
349 params = config.getFFmpegPrams(tsn)
350 if not params:
351 params = ''
352 return params
354 def select_format(tsn):
355 fmt = 'vob'
356 return '-f %s -' % fmt
358 def pad_check():
359 global pad_style
360 if pad_style == UNSET:
361 pad_style = OLD_PAD
362 filters = tempfile.TemporaryFile()
363 cmd = [config.get_bin('ffmpeg'), '-filters']
364 ffmpeg = subprocess.Popen(cmd, stdout=filters, stderr=subprocess.PIPE)
365 ffmpeg.wait()
366 filters.seek(0)
367 for line in filters:
368 if line.startswith('pad'):
369 pad_style = NEW_PAD
370 break
371 filters.close()
372 return pad_style == NEW_PAD
374 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
375 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
376 vInfo['vWidth']) * multiplier)
377 if endHeight % 2:
378 endHeight -= 1
379 if endHeight < TIVO_HEIGHT * 0.99:
380 topPadding = (TIVO_HEIGHT - endHeight) / 2
381 if topPadding % 2:
382 topPadding -= 1
383 newpad = pad_check()
384 if newpad:
385 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), '-vf',
386 'pad=%d:%d:0:%d' % (TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
387 else:
388 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
389 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
390 '-padtop', str(topPadding),
391 '-padbottom', str(bottomPadding)]
392 else: # if only very small amount of padding needed, then
393 # just stretch it
394 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
396 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
397 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
398 (vInfo['vHeight'] * multiplier))
399 if endWidth % 2:
400 endWidth -= 1
401 if endWidth < TIVO_WIDTH * 0.99:
402 leftPadding = (TIVO_WIDTH - endWidth) / 2
403 if leftPadding % 2:
404 leftPadding -= 1
405 newpad = pad_check()
406 if newpad:
407 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), '-vf',
408 'pad=%d:%d:%d:0' % (TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
409 else:
410 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
411 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
412 '-padleft', str(leftPadding),
413 '-padright', str(rightPadding)]
414 else: # if only very small amount of padding needed, then
415 # just stretch it
416 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
418 def select_aspect(inFile, tsn = ''):
419 TIVO_WIDTH = config.getTivoWidth(tsn)
420 TIVO_HEIGHT = config.getTivoHeight(tsn)
422 vInfo = video_info(inFile)
424 debug('tsn: %s' % tsn)
426 aspect169 = config.get169Setting(tsn)
428 debug('aspect169: %s' % aspect169)
430 optres = config.getOptres(tsn)
432 debug('optres: %s' % optres)
434 if optres:
435 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
436 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
437 if optHeight < TIVO_HEIGHT:
438 TIVO_HEIGHT = optHeight
439 if optWidth < TIVO_WIDTH:
440 TIVO_WIDTH = optWidth
442 if vInfo.get('par2'):
443 par2 = vInfo['par2']
444 elif vInfo.get('par'):
445 par2 = float(vInfo['par'])
446 else:
447 # Assume PAR = 1.0
448 par2 = 1.0
450 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
451 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
452 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
453 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
455 if config.isHDtivo(tsn) and not optres:
456 if config.getPixelAR(0) or vInfo['par']:
457 if vInfo['par2'] == None:
458 if vInfo['par']:
459 npar = par2
460 else:
461 npar = config.getPixelAR(1)
462 else:
463 npar = par2
465 # adjust for pixel aspect ratio, if set
467 if npar < 1.0:
468 return ['-s', '%dx%d' % (vInfo['vWidth'],
469 math.ceil(vInfo['vHeight'] / npar))]
470 elif npar > 1.0:
471 # FFMPEG expects width to be a multiple of two
472 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
473 vInfo['vHeight'])]
475 if vInfo['vHeight'] <= TIVO_HEIGHT:
476 # pass all resolutions to S3, except heights greater than
477 # conf height
478 return []
479 # else, resize video.
481 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
482 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
483 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
485 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
486 debug('File + PAR is within 4:3.')
487 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
489 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
490 (59, 72), (59, 36), (59, 54)] or
491 vInfo['dar1'] == '4:3'):
492 debug('File is within 4:3 list.')
493 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
495 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
496 (59, 27)] or vInfo['dar1'] == '16:9')
497 and (aspect169 or config.get169Letterbox(tsn))):
498 debug('File is within 16:9 list and 16:9 allowed.')
500 if config.get169Blacklist(tsn) or (aspect169 and
501 config.get169Letterbox(tsn)):
502 aspect = '4:3'
503 else:
504 aspect = '16:9'
505 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
507 else:
508 settings = ['-aspect']
510 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
511 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
512 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
513 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
514 multiplier4by3))
516 # If video is wider than 4:3 add top and bottom padding
518 if ratio > 133: # Might be 16:9 file, or just need padding on
519 # top and bottom
521 if aspect169 and ratio > 135: # If file would fall in 4:3
522 # assume it is supposed to be 4:3
524 if (config.get169Blacklist(tsn) or
525 config.get169Letterbox(tsn)):
526 settings.append('4:3')
527 else:
528 settings.append('16:9')
530 if ratio > 177: # too short needs padding top and bottom
531 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
532 multiplier16by9, vInfo)
533 debug(('16:9 aspect allowed, file is wider ' +
534 'than 16:9 padding top and bottom\n%s') %
535 ' '.join(settings))
537 else: # too skinny needs padding on left and right.
538 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
539 multiplier16by9, vInfo)
540 debug(('16:9 aspect allowed, file is narrower ' +
541 'than 16:9 padding left and right\n%s') %
542 ' '.join(settings))
544 else: # this is a 4:3 file or 16:9 output not allowed
545 if ratio > 135 and config.get169Letterbox(tsn):
546 settings.append('16:9')
547 multiplier = multiplier16by9
548 else:
549 settings.append('4:3')
550 multiplier = multiplier4by3
551 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
552 multiplier, vInfo)
553 debug(('File is wider than 4:3 padding ' +
554 'top and bottom\n%s') % ' '.join(settings))
556 # If video is taller than 4:3 add left and right padding, this
557 # is rare. All of these files will always be sent in an aspect
558 # ratio of 4:3 since they are so narrow.
560 else:
561 settings.append('4:3')
562 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
563 debug('File is taller than 4:3 padding left and right\n%s'
564 % ' '.join(settings))
566 return settings
568 def tivo_compatible_video(vInfo, tsn, mime=''):
569 message = (True, '')
570 while True:
571 codec = vInfo['vCodec']
572 if mime == 'video/mp4':
573 if codec != 'h264':
574 message = (False, 'vCodec %s not compatible' % codec)
576 break
578 if mime == 'video/bif':
579 if codec != 'vc1':
580 message = (False, 'vCodec %s not compatible' % codec)
582 break
584 if codec not in ('mpeg2video', 'mpeg1video'):
585 message = (False, 'vCodec %s not compatible' % codec)
586 break
588 if vInfo['kbps'] != None:
589 abit = max('0', vInfo['aKbps'])
590 if (int(vInfo['kbps']) - int(abit) >
591 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
592 message = (False, '%s kbps exceeds max video bitrate' %
593 vInfo['kbps'])
594 break
595 else:
596 message = (False, '%s kbps not supported' % vInfo['kbps'])
597 break
599 if config.isHDtivo(tsn):
600 if vInfo['par2'] != 1.0:
601 if config.getPixelAR(0):
602 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
603 message = (False, '%s not correct PAR' % vInfo['par2'])
604 break
605 # HD Tivo detected, skipping remaining tests.
606 break
608 if not vInfo['vFps'] in ['29.97', '59.94']:
609 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
610 break
612 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
613 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
614 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
615 message = (False, ('DAR %s not supported ' +
616 'by BLACKLIST_169 tivos') % vInfo['dar1'])
617 break
619 mode = (vInfo['vWidth'], vInfo['vHeight'])
620 if mode not in [(720, 480), (704, 480), (544, 480),
621 (528, 480), (480, 480), (352, 480), (352, 240)]:
622 message = (False, '%s x %s not in supported modes' % mode)
623 break
625 return message
627 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
628 message = (True, '')
629 while True:
630 codec = vInfo['aCodec']
631 if mime == 'video/mp4':
632 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
633 'ac3', 'liba52'):
634 message = (False, 'aCodec %s not compatible' % codec)
635 break
636 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and vInfo['aCh'] > 2:
637 message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %d channels' % (codec, vInfo['aCh']))
638 break
639 audio_lang = config.get_tsn('audio_lang', tsn)
640 if audio_lang:
641 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
642 message = (False, '%s preferred audio track exists' %
643 audio_lang)
644 break
646 if mime == 'video/bif':
647 if codec != 'wmav2':
648 message = (False, 'aCodec %s not compatible' % codec)
650 break
652 if inFile[-5:].lower() == '.tivo':
653 break
655 if codec not in ('ac3', 'liba52', 'mp2'):
656 message = (False, 'aCodec %s not compatible' % codec)
657 break
659 if (not vInfo['aKbps'] or
660 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
661 message = (False, '%s kbps exceeds max audio bitrate' %
662 vInfo['aKbps'])
663 break
665 audio_lang = config.get_tsn('audio_lang', tsn)
666 if audio_lang:
667 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
668 message = (False, '%s preferred audio track exists' %
669 audio_lang)
670 break
672 return message
674 def tivo_compatible_container(vInfo, inFile, mime=''):
675 message = (True, '')
676 container = vInfo['container']
677 if ((mime == 'video/mp4' and
678 (container != 'mov' or inFile.endswith('.mov'))) or
679 (mime == 'video/bif' and container != 'asf') or
680 (mime in ['video/mpeg', ''] and
681 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
682 message = (False, 'container %s not compatible' % container)
684 return message
686 def mp4_remuxable(inFile, tsn=''):
687 vInfo = video_info(inFile)
688 return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0]
690 def mp4_remux(inFile, basename, tsn=''):
691 outFile = inFile + '.pyTivo-temp'
692 newname = basename + '.pyTivo-temp'
693 if os.path.exists(outFile):
694 return None # ugh!
696 ffmpeg_path = config.get_bin('ffmpeg')
697 fname = unicode(inFile, 'utf-8')
698 oname = unicode(outFile, 'utf-8')
699 if mswindows:
700 fname = fname.encode('iso8859-1')
701 oname = oname.encode('iso8859-1')
703 settings = {'video_codec': '-vcodec copy',
704 'video_br': select_videobr(inFile, tsn),
705 'video_fps': select_videofps(inFile, tsn),
706 'max_video_br': select_maxvideobr(tsn),
707 'buff_size': select_buffsize(tsn),
708 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
709 'audio_br': select_audiobr(tsn),
710 'audio_fr': select_audiofr(inFile, tsn),
711 'audio_ch': select_audioch(tsn),
712 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'),
713 'audio_lang': select_audiolang(inFile, tsn),
714 'ffmpeg_pram': select_ffmpegprams(tsn),
715 'format': '-f mp4'}
717 cmd_string = config.getFFmpegTemplate(tsn) % settings
718 cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname]
720 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
721 debug(' '.join(cmd))
723 ffmpeg = subprocess.Popen(cmd)
724 debug('remuxing ' + inFile + ' to ' + outFile)
725 if ffmpeg.wait():
726 debug('error during remuxing')
727 os.remove(outFile)
728 return None
730 return newname
732 def tivo_compatible(inFile, tsn='', mime=''):
733 vInfo = video_info(inFile)
735 message = (True, 'all compatible')
736 if not config.get_bin('ffmpeg'):
737 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
738 message = (False, 'no ffmpeg')
739 return message
741 while True:
742 vmessage = tivo_compatible_video(vInfo, tsn, mime)
743 if not vmessage[0]:
744 message = vmessage
745 break
747 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
748 if not amessage[0]:
749 message = amessage
750 break
752 cmessage = tivo_compatible_container(vInfo, inFile, mime)
753 if not cmessage[0]:
754 message = cmessage
756 break
758 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
759 message[1], inFile))
760 return message
762 def video_info(inFile, cache=True):
763 vInfo = dict()
764 fname = unicode(inFile, 'utf-8')
765 mtime = os.stat(fname).st_mtime
766 if cache:
767 if inFile in info_cache and info_cache[inFile][0] == mtime:
768 debug('CACHE HIT! %s' % inFile)
769 return info_cache[inFile][1]
771 vInfo['Supported'] = True
773 ffmpeg_path = config.get_bin('ffmpeg')
774 if not ffmpeg_path:
775 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
776 '.vob', '.tivo']:
777 vInfo['Supported'] = False
778 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480})
779 if cache:
780 info_cache[inFile] = (mtime, vInfo)
781 return vInfo
783 if mswindows:
784 fname = fname.encode('iso8859-1')
785 cmd = [ffmpeg_path, '-i', fname]
786 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
787 err_tmp = tempfile.TemporaryFile()
788 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
789 stdin=subprocess.PIPE)
791 # wait configured # of seconds: if ffmpeg is not back give up
792 wait = config.getFFmpegWait()
793 debug('starting ffmpeg, will wait %s seconds for it to complete' % wait)
794 for i in xrange(wait * 20):
795 time.sleep(.05)
796 if not ffmpeg.poll() == None:
797 break
799 if ffmpeg.poll() == None:
800 kill(ffmpeg)
801 vInfo['Supported'] = False
802 if cache:
803 info_cache[inFile] = (mtime, vInfo)
804 return vInfo
806 err_tmp.seek(0)
807 output = err_tmp.read()
808 err_tmp.close()
809 debug('ffmpeg output=%s' % output)
811 attrs = {'container': r'Input #0, ([^,]+),',
812 'vCodec': r'Video: ([^, ]+)', # video codec
813 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
814 'aCodec': r'.*Audio: ([^,]+),.*', # audio codec
815 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
816 'mapVideo': r'([0-9]+\.[0-9]+).*: Video:.*'} # video mapping
818 for attr in attrs:
819 rezre = re.compile(attrs[attr])
820 x = rezre.search(output)
821 if x:
822 vInfo[attr] = x.group(1)
823 else:
824 if attr in ['container', 'vCodec']:
825 vInfo[attr] = ''
826 vInfo['Supported'] = False
827 else:
828 vInfo[attr] = None
829 debug('failed at ' + attr)
831 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d))?(?: channels)?)|stereo),.*')
832 x = rezre.search(output)
833 if x:
834 if x.group(1) == 'stereo':
835 vInfo['aCh'] = 2
836 elif x.group(2):
837 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
838 else:
839 vInfo['aCh'] = int(x.group(1))
840 else:
841 vInfo['aCh'] = ''
842 debug('failed at aCh')
844 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
845 x = rezre.search(output)
846 if x:
847 vInfo['vWidth'] = int(x.group(1))
848 vInfo['vHeight'] = int(x.group(2))
849 else:
850 vInfo['vWidth'] = ''
851 vInfo['vHeight'] = ''
852 vInfo['Supported'] = False
853 debug('failed at vWidth/vHeight')
855 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
856 x = rezre.search(output)
857 if x:
858 vInfo['vFps'] = x.group(1)
859 if '.' not in vInfo['vFps']:
860 vInfo['vFps'] += '.00'
862 # Allow override only if it is mpeg2 and frame rate was doubled
863 # to 59.94
865 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
866 # First look for the build 7215 version
867 rezre = re.compile(r'.*film source: 29.97.*')
868 x = rezre.search(output.lower())
869 if x:
870 debug('film source: 29.97 setting vFps to 29.97')
871 vInfo['vFps'] = '29.97'
872 else:
873 # for build 8047:
874 rezre = re.compile(r'.*frame rate differs from container ' +
875 r'frame rate: 29.97.*')
876 debug('Bug in VideoReDo')
877 x = rezre.search(output.lower())
878 if x:
879 vInfo['vFps'] = '29.97'
880 else:
881 vInfo['vFps'] = ''
882 vInfo['Supported'] = False
883 debug('failed at vFps')
885 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
886 d = durre.search(output)
888 if d:
889 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
890 int(d.group(2)) * 60 +
891 int(d.group(3))) * 1000 +
892 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
893 else:
894 vInfo['millisecs'] = 0
896 # get bitrate of source for tivo compatibility test.
897 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
898 x = rezre.search(output)
899 if x:
900 vInfo['kbps'] = x.group(1)
901 else:
902 # Fallback method of getting video bitrate
903 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
904 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
905 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
906 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
907 x = rezre.search(output)
908 if x:
909 vInfo['kbps'] = x.group(1)
910 else:
911 vInfo['kbps'] = None
912 debug('failed at kbps')
914 # get par.
915 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
916 x = rezre.search(output)
917 if x and x.group(1) != "0" and x.group(2) != "0":
918 vInfo['par1'] = x.group(1) + ':' + x.group(2)
919 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
920 else:
921 vInfo['par1'], vInfo['par2'] = None, None
923 # get dar.
924 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
925 x = rezre.search(output)
926 if x and x.group(1) != "0" and x.group(2) != "0":
927 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
928 else:
929 vInfo['dar1'] = None
931 # get Audio Stream mapping.
932 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:(.*)')
933 x = rezre.search(output)
934 amap = []
935 if x:
936 for x in rezre.finditer(output):
937 amap.append((x.group(1), x.group(2)+x.group(3)))
938 else:
939 amap.append(('', ''))
940 debug('failed at mapAudio')
941 vInfo['mapAudio'] = amap
943 vInfo['par'] = None
945 data = metadata.from_text(inFile)
946 for key in data:
947 if key.startswith('Override_'):
948 vInfo['Supported'] = True
949 if key.startswith('Override_mapAudio'):
950 audiomap = dict(vInfo['mapAudio'])
951 stream = key.replace('Override_mapAudio', '').strip()
952 if stream in audiomap:
953 newaudiomap = (stream, data[key])
954 audiomap.update([newaudiomap])
955 vInfo['mapAudio'] = sorted(audiomap.items(),
956 key=lambda (k,v): (k,v))
957 elif key.startswith('Override_millisecs'):
958 vInfo[key.replace('Override_', '')] = int(data[key])
959 else:
960 vInfo[key.replace('Override_', '')] = data[key]
962 if cache:
963 info_cache[inFile] = (mtime, vInfo)
964 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
965 return vInfo
967 def audio_check(inFile, tsn):
968 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
969 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
970 fname = unicode(inFile, 'utf-8')
971 if mswindows:
972 fname = fname.encode('iso8859-1')
973 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
974 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
975 fd, testname = tempfile.mkstemp()
976 testfile = os.fdopen(fd, 'wb')
977 try:
978 shutil.copyfileobj(ffmpeg.stdout, testfile)
979 except:
980 kill(ffmpeg)
981 testfile.close()
982 vInfo = None
983 else:
984 testfile.close()
985 vInfo = video_info(testname, False)
986 os.remove(testname)
987 return vInfo
989 def supported_format(inFile):
990 if video_info(inFile)['Supported']:
991 return True
992 else:
993 debug('FALSE, file not supported %s' % inFile)
994 return False
996 def kill(popen):
997 debug('killing pid=%s' % str(popen.pid))
998 if mswindows:
999 win32kill(popen.pid)
1000 else:
1001 import os, signal
1002 for i in xrange(3):
1003 debug('sending SIGTERM to pid: %s' % popen.pid)
1004 os.kill(popen.pid, signal.SIGTERM)
1005 time.sleep(.5)
1006 if popen.poll() is not None:
1007 debug('process %s has exited' % popen.pid)
1008 break
1009 else:
1010 while popen.poll() is None:
1011 debug('sending SIGKILL to pid: %s' % popen.pid)
1012 os.kill(popen.pid, signal.SIGKILL)
1013 time.sleep(.5)
1015 def win32kill(pid):
1016 import ctypes
1017 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1018 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1019 ctypes.windll.kernel32.CloseHandle(handle)
1021 def gcd(a, b):
1022 while b:
1023 a, b = b, a % b
1024 return a