Default ffmpeg_wait to 0, which makes pyTivo skip the poll()ing and just
[pyTivo/wmcbrine/lucasnz.git] / plugins / video / transcode.py
blob8e47a10ffbcfc2569e1f035ca46d8adbdcea988c
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='', mime='', thead=''):
62 settings = {'video_codec': select_videocodec(inFile, tsn, mime),
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(inFile, 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, mime)}
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 if thead:
111 ffmpeg_procs[inFile]['blocks'].append(thead)
112 reap_process(inFile)
113 return resume_transfer(inFile, outFile, 0)
115 def is_resumable(inFile, offset):
116 if inFile in ffmpeg_procs:
117 proc = ffmpeg_procs[inFile]
118 if proc['start'] <= offset < proc['end']:
119 return True
120 else:
121 cleanup(inFile)
122 kill(proc['process'])
123 return False
125 def resume_transfer(inFile, outFile, offset):
126 proc = ffmpeg_procs[inFile]
127 offset -= proc['start']
128 count = 0
130 try:
131 for block in proc['blocks']:
132 length = len(block)
133 if offset < length:
134 if offset > 0:
135 block = block[offset:]
136 outFile.write('%x\r\n' % len(block))
137 outFile.write(block)
138 outFile.write('\r\n')
139 count += len(block)
140 offset -= length
141 outFile.flush()
142 except Exception, msg:
143 logger.info(msg)
144 return count
146 proc['start'] = proc['end']
147 proc['blocks'] = []
149 return count + transfer_blocks(inFile, outFile)
151 def transfer_blocks(inFile, outFile):
152 proc = ffmpeg_procs[inFile]
153 blocks = proc['blocks']
154 count = 0
156 while True:
157 try:
158 block = proc['process'].stdout.read(BLOCKSIZE)
159 proc['last_read'] = time.time()
160 except Exception, msg:
161 logger.info(msg)
162 cleanup(inFile)
163 kill(proc['process'])
164 break
166 if not block:
167 try:
168 outFile.flush()
169 except Exception, msg:
170 logger.info(msg)
171 else:
172 cleanup(inFile)
173 break
175 blocks.append(block)
176 proc['end'] += len(block)
177 if len(blocks) > MAXBLOCKS:
178 proc['start'] += len(blocks[0])
179 blocks.pop(0)
181 try:
182 outFile.write('%x\r\n' % len(block))
183 outFile.write(block)
184 outFile.write('\r\n')
185 count += len(block)
186 except Exception, msg:
187 logger.info(msg)
188 break
190 return count
192 def reap_process(inFile):
193 if ffmpeg_procs and inFile in ffmpeg_procs:
194 proc = ffmpeg_procs[inFile]
195 if proc['last_read'] + TIMEOUT < time.time():
196 del ffmpeg_procs[inFile]
197 del reapers[inFile]
198 kill(proc['process'])
199 else:
200 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
201 reapers[inFile] = reaper
202 reaper.start()
204 def cleanup(inFile):
205 del ffmpeg_procs[inFile]
206 reapers[inFile].cancel()
207 del reapers[inFile]
209 def select_audiocodec(isQuery, inFile, tsn='', mime=''):
210 if inFile[-5:].lower() == '.tivo':
211 return '-acodec copy'
212 vInfo = video_info(inFile)
213 codectype = vInfo['vCodec']
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'] in compatiblecodecs:
223 aKbps = vInfo['aKbps']
224 aCh = vInfo['aCh']
225 if aKbps == None:
226 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
227 # along with the channel check below this should
228 # pass any AAC audio that has undefined 'aKbps' and
229 # is <= 2 channels. Should be TiVo compatible.
230 codec = 'copy'
231 elif not isQuery:
232 vInfoQuery = audio_check(inFile, tsn)
233 if vInfoQuery == None:
234 aKbps = None
235 aCh = None
236 else:
237 aKbps = vInfoQuery['aKbps']
238 aCh = vInfoQuery['aCh']
239 else:
240 codec = 'TBA'
241 if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn):
242 # compatible codec and bitrate, do not reencode audio
243 codec = 'copy'
244 if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2):
245 codec = 'ac3'
246 copy_flag = config.get_tsn('copy_ts', tsn)
247 copyts = ' -copyts'
248 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
249 (copy_flag and copy_flag.lower() == 'false')):
250 copyts = ''
251 return '-acodec ' + codec + copyts
253 def select_audiofr(inFile, tsn):
254 freq = '48000' # default
255 vInfo = video_info(inFile)
256 if vInfo['aFreq'] == '44100':
257 # compatible frequency
258 freq = vInfo['aFreq']
259 audio_fr = config.get_tsn('audio_fr', tsn)
260 if audio_fr != None:
261 freq = audio_fr
262 return '-ar ' + freq
264 def select_audioch(inFile, tsn):
265 ch = config.get_tsn('audio_ch', tsn)
266 if ch:
267 return '-ac ' + ch
268 # AC-3 max channels is 5.1
269 if video_info(inFile)['aCh'] > 6:
270 debug('Too many audio channels for AC-3, using 5.1 instead')
271 return '-ac 6'
272 elif video_info(inFile)['aCh']:
273 return '-ac %i' % video_info(inFile)['aCh']
274 return ''
276 def select_audiolang(inFile, tsn):
277 vInfo = video_info(inFile)
278 audio_lang = config.get_tsn('audio_lang', tsn)
279 debug('audio_lang: %s' % audio_lang)
280 if vInfo['mapAudio']:
281 # default to first detected audio stream to begin with
282 stream = vInfo['mapAudio'][0][0]
283 if audio_lang != None and vInfo['mapVideo'] != None:
284 langmatch_curr = []
285 langmatch_prev = vInfo['mapAudio'][:]
286 for lang in audio_lang.replace(' ', '').lower().split(','):
287 for s, l in langmatch_prev:
288 if lang in s + l.replace(' ', '').lower():
289 langmatch_curr.append((s, l))
290 stream = s
291 # if only 1 item matched we're done
292 if len(langmatch_curr) == 1:
293 break
294 # if more than 1 item matched copy the curr area to the prev
295 # array we only need to look at the new shorter list from
296 # now on
297 elif len(langmatch_curr) > 1:
298 langmatch_prev = langmatch_curr[:]
299 langmatch_curr = []
300 # if we drop out of the loop with more than 1 item default to
301 # the first item
302 if len(langmatch_curr) > 1:
303 stream = langmatch_curr[0][0]
304 # don't let FFmpeg auto select audio stream, pyTivo defaults to
305 # first detected
306 if stream:
307 debug('selected audio stream: %s' % stream)
308 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
309 # if no audio is found
310 debug('selected audio stream: None detected')
311 return ''
313 def select_videofps(inFile, tsn):
314 vInfo = video_info(inFile)
315 fps = '-r 29.97' # default
316 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
317 fps = ' '
318 video_fps = config.get_tsn('video_fps', tsn)
319 if video_fps != None:
320 fps = '-r ' + video_fps
321 return fps
323 def select_videocodec(inFile, tsn, mime=''):
324 vInfo = video_info(inFile)
325 if tivo_compatible_video(vInfo, tsn, mime)[0]:
326 codec = 'copy'
327 if (mime == 'video/x-tivo-mpeg-ts' and
328 vInfo.get('vCodec', '') == 'h264'):
329 codec += ' -bsf h264_mp4toannexb'
330 else:
331 codec = 'mpeg2video' # default
332 return '-vcodec ' + codec
334 def select_videobr(inFile, tsn, mime=''):
335 return '-b ' + str(select_videostr(inFile, tsn, mime) / 1000) + 'k'
337 def select_videostr(inFile, tsn, mime=''):
338 vInfo = video_info(inFile)
339 if tivo_compatible_video(vInfo, tsn, mime)[0]:
340 video_str = int(vInfo['kbps'])
341 if vInfo['aKbps']:
342 video_str -= int(vInfo['aKbps'])
343 video_str *= 1000
344 else:
345 video_str = config.strtod(config.getVideoBR(tsn))
346 if config.isHDtivo(tsn) and vInfo['kbps']:
347 video_str = max(video_str, int(vInfo['kbps']) * 1000)
348 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
349 video_str))
350 return video_str
352 def select_audiobr(tsn):
353 return '-ab ' + config.getAudioBR(tsn)
355 def select_maxvideobr(tsn):
356 return '-maxrate ' + config.getMaxVideoBR(tsn)
358 def select_buffsize(tsn):
359 return '-bufsize ' + config.getBuffSize(tsn)
361 def select_ffmpegprams(tsn):
362 params = config.getFFmpegPrams(tsn)
363 if not params:
364 params = ''
365 return params
367 def select_format(tsn, mime):
368 if mime == 'video/x-tivo-mpeg-ts':
369 fmt = 'mpegts'
370 else:
371 fmt = 'vob'
372 return '-f %s -' % fmt
374 def pad_check():
375 global pad_style
376 if pad_style == UNSET:
377 pad_style = OLD_PAD
378 filters = tempfile.TemporaryFile()
379 cmd = [config.get_bin('ffmpeg'), '-filters']
380 ffmpeg = subprocess.Popen(cmd, stdout=filters, stderr=subprocess.PIPE)
381 ffmpeg.wait()
382 filters.seek(0)
383 for line in filters:
384 if line.startswith('pad'):
385 pad_style = NEW_PAD
386 break
387 filters.close()
388 return pad_style == NEW_PAD
390 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
391 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
392 vInfo['vWidth']) * multiplier)
393 if endHeight % 2:
394 endHeight -= 1
395 topPadding = (TIVO_HEIGHT - endHeight) / 2
396 if topPadding % 2:
397 topPadding -= 1
398 newpad = pad_check()
399 if newpad:
400 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH,
401 endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
402 else:
403 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
404 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
405 '-padtop', str(topPadding),
406 '-padbottom', str(bottomPadding)]
408 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
409 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
410 (vInfo['vHeight'] * multiplier))
411 if endWidth % 2:
412 endWidth -= 1
413 leftPadding = (TIVO_WIDTH - endWidth) / 2
414 if leftPadding % 2:
415 leftPadding -= 1
416 newpad = pad_check()
417 if newpad:
418 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth,
419 TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
420 else:
421 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
422 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
423 '-padleft', str(leftPadding),
424 '-padright', str(rightPadding)]
426 def select_aspect(inFile, tsn = ''):
427 TIVO_WIDTH = config.getTivoWidth(tsn)
428 TIVO_HEIGHT = config.getTivoHeight(tsn)
430 vInfo = video_info(inFile)
432 debug('tsn: %s' % tsn)
434 aspect169 = config.get169Setting(tsn)
436 debug('aspect169: %s' % aspect169)
438 optres = config.getOptres(tsn)
440 debug('optres: %s' % optres)
442 if optres:
443 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
444 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
445 if optHeight < TIVO_HEIGHT:
446 TIVO_HEIGHT = optHeight
447 if optWidth < TIVO_WIDTH:
448 TIVO_WIDTH = optWidth
450 if vInfo.get('par2'):
451 par2 = vInfo['par2']
452 elif vInfo.get('par'):
453 par2 = float(vInfo['par'])
454 else:
455 # Assume PAR = 1.0
456 par2 = 1.0
458 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
459 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
460 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
461 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
463 if config.isHDtivo(tsn) and not optres:
464 if vInfo['par']:
465 npar = par2
467 # adjust for pixel aspect ratio, if set
469 if npar < 1.0:
470 return ['-s', '%dx%d' % (vInfo['vWidth'],
471 math.ceil(vInfo['vHeight'] / npar))]
472 elif npar > 1.0:
473 # FFMPEG expects width to be a multiple of two
474 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
475 vInfo['vHeight'])]
477 if vInfo['vHeight'] <= TIVO_HEIGHT:
478 # pass all resolutions to S3, except heights greater than
479 # conf height
480 return []
481 # else, resize video.
483 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
484 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
485 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
487 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
488 debug('File + PAR is within 4:3.')
489 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
491 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
492 (59, 72), (59, 36), (59, 54)] or
493 vInfo['dar1'] == '4:3'):
494 debug('File is within 4:3 list.')
495 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
497 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
498 (59, 27)] or vInfo['dar1'] == '16:9')
499 and (aspect169 or config.get169Letterbox(tsn))):
500 debug('File is within 16:9 list and 16:9 allowed.')
502 if config.get169Blacklist(tsn) or (aspect169 and
503 config.get169Letterbox(tsn)):
504 aspect = '4:3'
505 else:
506 aspect = '16:9'
507 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
509 else:
510 settings = ['-aspect']
512 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
513 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
514 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
515 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
516 multiplier4by3))
518 # If video is wider than 4:3 add top and bottom padding
520 if ratio > 133: # Might be 16:9 file, or just need padding on
521 # top and bottom
523 if aspect169 and ratio > 135: # If file would fall in 4:3
524 # assume it is supposed to be 4:3
526 if (config.get169Blacklist(tsn) or
527 config.get169Letterbox(tsn)):
528 settings.append('4:3')
529 else:
530 settings.append('16:9')
532 if ratio > 177: # too short needs padding top and bottom
533 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
534 multiplier16by9, vInfo)
535 debug(('16:9 aspect allowed, file is wider ' +
536 'than 16:9 padding top and bottom\n%s') %
537 ' '.join(settings))
539 else: # too skinny needs padding on left and right.
540 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
541 multiplier16by9, vInfo)
542 debug(('16:9 aspect allowed, file is narrower ' +
543 'than 16:9 padding left and right\n%s') %
544 ' '.join(settings))
546 else: # this is a 4:3 file or 16:9 output not allowed
547 if ratio > 135 and config.get169Letterbox(tsn):
548 settings.append('16:9')
549 multiplier = multiplier16by9
550 else:
551 settings.append('4:3')
552 multiplier = multiplier4by3
553 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
554 multiplier, vInfo)
555 debug(('File is wider than 4:3 padding ' +
556 'top and bottom\n%s') % ' '.join(settings))
558 # If video is taller than 4:3 add left and right padding, this
559 # is rare. All of these files will always be sent in an aspect
560 # ratio of 4:3 since they are so narrow.
562 else:
563 settings.append('4:3')
564 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
565 debug('File is taller than 4:3 padding left and right\n%s'
566 % ' '.join(settings))
568 return settings
570 def tivo_compatible_video(vInfo, tsn, mime=''):
571 message = (True, '')
572 while True:
573 codec = vInfo.get('vCodec', '')
574 if mime == 'video/mp4':
575 if codec != 'h264':
576 message = (False, 'vCodec %s not compatible' % codec)
578 break
580 if mime == 'video/bif':
581 if codec != 'vc1':
582 message = (False, 'vCodec %s not compatible' % codec)
584 break
586 if mime == 'video/x-tivo-mpeg-ts':
587 if codec not in ('h264', 'mpeg2video'):
588 message = (False, 'vCodec %s not compatible' % codec)
590 break
592 if codec not in ('mpeg2video', 'mpeg1video'):
593 message = (False, 'vCodec %s not compatible' % codec)
594 break
596 if vInfo['kbps'] != None:
597 abit = max('0', vInfo['aKbps'])
598 if (int(vInfo['kbps']) - int(abit) >
599 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
600 message = (False, '%s kbps exceeds max video bitrate' %
601 vInfo['kbps'])
602 break
603 else:
604 message = (False, '%s kbps not supported' % vInfo['kbps'])
605 break
607 if config.isHDtivo(tsn):
608 # HD Tivo detected, skipping remaining tests.
609 break
611 if not vInfo['vFps'] in ['29.97', '59.94']:
612 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
613 break
615 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
616 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
617 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
618 message = (False, ('DAR %s not supported ' +
619 'by BLACKLIST_169 tivos') % vInfo['dar1'])
620 break
622 mode = (vInfo['vWidth'], vInfo['vHeight'])
623 if mode not in [(720, 480), (704, 480), (544, 480),
624 (528, 480), (480, 480), (352, 480), (352, 240)]:
625 message = (False, '%s x %s not in supported modes' % mode)
626 break
628 return message
630 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
631 message = (True, '')
632 while True:
633 codec = vInfo.get('aCodec', '')
635 if codec == None:
636 debug('No audio stream detected')
637 break
639 if mime == 'video/mp4':
640 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
641 'ac3', 'liba52'):
642 message = (False, 'aCodec %s not compatible' % codec)
643 break
644 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2):
645 message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh']))
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 if mime == 'video/bif':
656 if codec != 'wmav2':
657 message = (False, 'aCodec %s not compatible' % codec)
659 break
661 if inFile[-5:].lower() == '.tivo':
662 break
664 if mime == 'video/x-tivo-mpeg-ts':
665 if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
666 message = (False, 'aCodec %s not compatible' % codec)
668 break
670 if codec not in ('ac3', 'liba52', 'mp2'):
671 message = (False, 'aCodec %s not compatible' % codec)
672 break
674 if (not vInfo['aKbps'] or
675 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
676 message = (False, '%s kbps exceeds max audio bitrate' %
677 vInfo['aKbps'])
678 break
680 audio_lang = config.get_tsn('audio_lang', tsn)
681 if audio_lang:
682 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
683 message = (False, '%s preferred audio track exists' %
684 audio_lang)
685 break
687 return message
689 def tivo_compatible_container(vInfo, inFile, mime=''):
690 message = (True, '')
691 container = vInfo.get('container', '')
692 if ((mime == 'video/mp4' and
693 (container != 'mov' or inFile.lower().endswith('.mov'))) or
694 (mime == 'video/bif' and container != 'asf') or
695 (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
696 (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
697 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
698 message = (False, 'container %s not compatible' % container)
700 return message
702 def mp4_remuxable(inFile, tsn=''):
703 vInfo = video_info(inFile)
704 return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0]
706 def mp4_remux(inFile, basename, tsn='', temp_share_path=''):
707 outFile = inFile + '.pyTivo-temp'
708 newname = basename + '.pyTivo-temp'
710 if temp_share_path:
711 newname = os.path.splitext(os.path.split(basename)[1])[0] + '.mp4.pyTivo-temp'
712 outFile = os.path.join(temp_share_path, newname)
714 if os.path.exists(outFile):
715 return None # ugh!
717 ffmpeg_path = config.get_bin('ffmpeg')
718 fname = unicode(inFile, 'utf-8')
719 oname = unicode(outFile, 'utf-8')
720 if mswindows:
721 fname = fname.encode('iso8859-1')
722 oname = oname.encode('iso8859-1')
724 settings = {'video_codec': '-vcodec copy',
725 'video_br': select_videobr(inFile, tsn),
726 'video_fps': select_videofps(inFile, tsn),
727 'max_video_br': select_maxvideobr(tsn),
728 'buff_size': select_buffsize(tsn),
729 'aspect_ratio': ' '.join(select_aspect(inFile, tsn)),
730 'audio_br': select_audiobr(tsn),
731 'audio_fr': select_audiofr(inFile, tsn),
732 'audio_ch': select_audioch(inFile, tsn),
733 'audio_codec': select_audiocodec(False, inFile, tsn, 'video/mp4'),
734 'audio_lang': select_audiolang(inFile, tsn),
735 'ffmpeg_pram': select_ffmpegprams(tsn),
736 'format': '-f mp4'}
738 cmd_string = config.getFFmpegTemplate(tsn) % settings
739 cmd = [ffmpeg_path, '-i', fname] + cmd_string.split() + [oname]
741 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
742 debug(' '.join(cmd))
744 ffmpeg = subprocess.Popen(cmd)
745 debug('remuxing ' + inFile + ' to ' + outFile)
746 if ffmpeg.wait():
747 debug('error during remuxing')
748 os.remove(outFile)
749 return None
751 return newname
753 def tivo_compatible(inFile, tsn='', mime=''):
754 vInfo = video_info(inFile)
756 message = (True, 'all compatible')
757 if not config.get_bin('ffmpeg'):
758 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
759 message = (False, 'no ffmpeg')
760 return message
762 while True:
763 vmessage = tivo_compatible_video(vInfo, tsn, mime)
764 if not vmessage[0]:
765 message = vmessage
766 break
768 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
769 if not amessage[0]:
770 message = amessage
771 break
773 cmessage = tivo_compatible_container(vInfo, inFile, mime)
774 if not cmessage[0]:
775 message = cmessage
777 break
779 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
780 message[1], inFile))
781 return message
783 def video_info(inFile, cache=True):
784 vInfo = dict()
785 fname = unicode(inFile, 'utf-8')
786 mtime = os.stat(fname).st_mtime
787 if cache:
788 if inFile in info_cache and info_cache[inFile][0] == mtime:
789 debug('CACHE HIT! %s' % inFile)
790 return info_cache[inFile][1]
792 vInfo['Supported'] = True
794 ffmpeg_path = config.get_bin('ffmpeg')
795 if not ffmpeg_path:
796 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
797 '.vob', '.tivo']:
798 vInfo['Supported'] = False
799 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
800 'rawmeta': {}})
801 if cache:
802 info_cache[inFile] = (mtime, vInfo)
803 return vInfo
805 if mswindows:
806 fname = fname.encode('iso8859-1')
807 cmd = [ffmpeg_path, '-i', fname]
808 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
809 err_tmp = tempfile.TemporaryFile()
810 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
811 stdin=subprocess.PIPE)
813 # wait configured # of seconds: if ffmpeg is not back give up
814 limit = config.getFFmpegWait()
815 if limit:
816 for i in xrange(limit * 20):
817 time.sleep(.05)
818 if not ffmpeg.poll() == None:
819 break
821 if ffmpeg.poll() == None:
822 kill(ffmpeg)
823 vInfo['Supported'] = False
824 if cache:
825 info_cache[inFile] = (mtime, vInfo)
826 return vInfo
827 else:
828 ffmpeg.wait()
830 err_tmp.seek(0)
831 output = err_tmp.read()
832 err_tmp.close()
833 debug('ffmpeg output=%s' % output)
835 attrs = {'container': r'Input #0, ([^,]+),',
836 'vCodec': r'Video: ([^, ]+)', # video codec
837 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
838 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
839 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
840 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
842 for attr in attrs:
843 rezre = re.compile(attrs[attr])
844 x = rezre.search(output)
845 if x:
846 vInfo[attr] = x.group(1)
847 else:
848 if attr in ['container', 'vCodec']:
849 vInfo[attr] = ''
850 vInfo['Supported'] = False
851 else:
852 vInfo[attr] = None
853 debug('failed at ' + attr)
855 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
856 x = rezre.search(output)
857 if x:
858 if x.group(3):
859 if x.group(3) == 'stereo':
860 vInfo['aCh'] = 2
861 elif x.group(3) == 'mono':
862 vInfo['aCh'] = 1
863 elif x.group(2):
864 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
865 elif x.group(1):
866 vInfo['aCh'] = int(x.group(1))
867 else:
868 vInfo['aCh'] = None
869 debug('failed at aCh')
870 else:
871 vInfo['aCh'] = None
872 debug('failed at aCh')
874 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
875 x = rezre.search(output)
876 if x:
877 vInfo['vWidth'] = int(x.group(1))
878 vInfo['vHeight'] = int(x.group(2))
879 else:
880 vInfo['vWidth'] = ''
881 vInfo['vHeight'] = ''
882 vInfo['Supported'] = False
883 debug('failed at vWidth/vHeight')
885 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
886 x = rezre.search(output)
887 if x:
888 vInfo['vFps'] = x.group(1)
889 if '.' not in vInfo['vFps']:
890 vInfo['vFps'] += '.00'
892 # Allow override only if it is mpeg2 and frame rate was doubled
893 # to 59.94
895 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
896 # First look for the build 7215 version
897 rezre = re.compile(r'.*film source: 29.97.*')
898 x = rezre.search(output.lower())
899 if x:
900 debug('film source: 29.97 setting vFps to 29.97')
901 vInfo['vFps'] = '29.97'
902 else:
903 # for build 8047:
904 rezre = re.compile(r'.*frame rate differs from container ' +
905 r'frame rate: 29.97.*')
906 debug('Bug in VideoReDo')
907 x = rezre.search(output.lower())
908 if x:
909 vInfo['vFps'] = '29.97'
910 else:
911 vInfo['vFps'] = ''
912 vInfo['Supported'] = False
913 debug('failed at vFps')
915 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
916 d = durre.search(output)
918 if d:
919 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
920 int(d.group(2)) * 60 +
921 int(d.group(3))) * 1000 +
922 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
923 else:
924 vInfo['millisecs'] = 0
926 # get bitrate of source for tivo compatibility test.
927 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
928 x = rezre.search(output)
929 if x:
930 vInfo['kbps'] = x.group(1)
931 else:
932 # Fallback method of getting video bitrate
933 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
934 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
935 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
936 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
937 x = rezre.search(output)
938 if x:
939 vInfo['kbps'] = x.group(1)
940 else:
941 vInfo['kbps'] = None
942 debug('failed at kbps')
944 # get par.
945 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
946 x = rezre.search(output)
947 if x and x.group(1) != "0" and x.group(2) != "0":
948 vInfo['par1'] = x.group(1) + ':' + x.group(2)
949 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
950 else:
951 vInfo['par1'], vInfo['par2'] = None, None
953 # get dar.
954 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
955 x = rezre.search(output)
956 if x and x.group(1) != "0" and x.group(2) != "0":
957 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
958 else:
959 vInfo['dar1'] = None
961 # get Audio Stream mapping.
962 rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
963 x = rezre.search(output)
964 amap = []
965 if x:
966 for x in rezre.finditer(output):
967 amap.append((x.group(1), x.group(2) + x.group(3)))
968 else:
969 amap.append(('', ''))
970 debug('failed at mapAudio')
971 vInfo['mapAudio'] = amap
973 vInfo['par'] = None
975 # get Metadata dump (newer ffmpeg).
976 lines = output.split('\n')
977 rawmeta = {}
978 flag = False
980 for line in lines:
981 if line.startswith(' Metadata:'):
982 flag = True
983 else:
984 if flag:
985 if line.startswith(' Duration:'):
986 flag = False
987 else:
988 try:
989 key, value = [x.strip() for x in line.split(':', 1)]
990 try:
991 value = value.decode('utf-8')
992 except:
993 if sys.platform == 'darwin':
994 value = value.decode('macroman')
995 else:
996 value = value.decode('iso8859-1')
997 rawmeta[key] = [value]
998 except:
999 pass
1001 vInfo['rawmeta'] = rawmeta
1003 data = metadata.from_text(inFile)
1004 for key in data:
1005 if key.startswith('Override_'):
1006 vInfo['Supported'] = True
1007 if key.startswith('Override_mapAudio'):
1008 audiomap = dict(vInfo['mapAudio'])
1009 stream = key.replace('Override_mapAudio', '').strip()
1010 if stream in audiomap:
1011 newaudiomap = (stream, data[key])
1012 audiomap.update([newaudiomap])
1013 vInfo['mapAudio'] = sorted(audiomap.items(),
1014 key=lambda (k,v): (k,v))
1015 elif key.startswith('Override_millisecs'):
1016 vInfo[key.replace('Override_', '')] = int(data[key])
1017 else:
1018 vInfo[key.replace('Override_', '')] = data[key]
1020 if cache:
1021 info_cache[inFile] = (mtime, vInfo)
1022 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
1023 return vInfo
1025 def audio_check(inFile, tsn):
1026 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
1027 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
1028 fname = unicode(inFile, 'utf-8')
1029 if mswindows:
1030 fname = fname.encode('iso8859-1')
1031 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
1032 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
1033 fd, testname = tempfile.mkstemp()
1034 testfile = os.fdopen(fd, 'wb')
1035 try:
1036 shutil.copyfileobj(ffmpeg.stdout, testfile)
1037 except:
1038 kill(ffmpeg)
1039 testfile.close()
1040 vInfo = None
1041 else:
1042 testfile.close()
1043 vInfo = video_info(testname, False)
1044 os.remove(testname)
1045 return vInfo
1047 def supported_format(inFile):
1048 if video_info(inFile)['Supported']:
1049 return True
1050 else:
1051 debug('FALSE, file not supported %s' % inFile)
1052 return False
1054 def kill(popen):
1055 debug('killing pid=%s' % str(popen.pid))
1056 if mswindows:
1057 win32kill(popen.pid)
1058 else:
1059 import os, signal
1060 for i in xrange(3):
1061 debug('sending SIGTERM to pid: %s' % popen.pid)
1062 os.kill(popen.pid, signal.SIGTERM)
1063 time.sleep(.5)
1064 if popen.poll() is not None:
1065 debug('process %s has exited' % popen.pid)
1066 break
1067 else:
1068 while popen.poll() is None:
1069 debug('sending SIGKILL to pid: %s' % popen.pid)
1070 os.kill(popen.pid, signal.SIGKILL)
1071 time.sleep(.5)
1073 def win32kill(pid):
1074 import ctypes
1075 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1076 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1077 ctypes.windll.kernel32.CloseHandle(handle)
1079 def gcd(a, b):
1080 while b:
1081 a, b = b, a % b
1082 return a