Tidy up for audio_lang. If we match more than one item we keep checking the matching...
[pyTivo/wmcbrine/lucasnz.git] / plugins / video / transcode.py
blobad87b64c7b5e12783c7127f2e76f0c40bd6453d2
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),
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, 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=''):
210 if inFile[-5:].lower() == '.tivo':
211 return '-acodec copy'
212 vInfo = video_info(inFile)
213 codectype = vInfo['vCodec']
214 codec = config.get_tsn('audio_codec', tsn)
215 if not codec:
216 # Default, compatible with all TiVo's
217 codec = 'ac3'
218 if vInfo['aCodec'] in ('ac3', 'liba52', 'mp2'):
219 aKbps = vInfo['aKbps']
220 if aKbps == None:
221 if not isQuery:
222 aKbps = audio_check(inFile, tsn)
223 else:
224 codec = 'TBD'
225 if aKbps != None and int(aKbps) <= config.getMaxAudioBR(tsn):
226 # compatible codec and bitrate, do not reencode audio
227 codec = 'copy'
228 copy_flag = config.get_tsn('copy_ts', tsn)
229 copyts = ' -copyts'
230 if ((codec == 'copy' and codectype == 'mpeg2video' and not copy_flag) or
231 (copy_flag and copy_flag.lower() == 'false')):
232 copyts = ''
233 return '-acodec ' + codec + copyts
235 def select_audiofr(inFile, tsn):
236 freq = '48000' #default
237 vInfo = video_info(inFile)
238 if not vInfo['aFreq'] == None and vInfo['aFreq'] in ('44100', '48000'):
239 # compatible frequency
240 freq = vInfo['aFreq']
241 audio_fr = config.get_tsn('audio_fr', tsn)
242 if audio_fr != None:
243 freq = audio_fr
244 return '-ar ' + freq
246 def select_audioch(tsn):
247 ch = config.get_tsn('audio_ch', tsn)
248 if ch:
249 return '-ac ' + ch
250 return ''
252 def select_audiolang(inFile, tsn):
253 vInfo = video_info(inFile)
254 audio_lang = config.get_tsn('audio_lang', tsn)
255 if audio_lang != None and vInfo['mapVideo'] != None:
256 stream = vInfo['mapAudio'][0][0]
257 langmatch_curr = []
258 langmatch_prev = vInfo['mapAudio'][:]
259 for lang in audio_lang.replace(' ','').lower().split(','):
260 for s, l, data in langmatch_prev:
261 if lang in s + (l+data).replace(' ','').lower():
262 langmatch_curr.append((s, l, data))
263 stream = s
264 #if only 1 item matched we're done
265 if len(langmatch_curr) == 1:
266 del langmatch_prev[:]
267 break
268 #if more than 1 item matched copy the curr area to the prev array
269 #we only need to look at the new shorter list from now on
270 elif len(langmatch_curr) > 1:
271 del langmatch_prev[:]
272 langmatch_prev = langmatch_curr[:]
273 del langmatch_curr[:]
274 #if nothing matched we'll keep the prev array and clear the curr array
275 else:
276 del langmatch_curr[:]
277 #if we drop out of the loop with more than 1 item default to the first item
278 if len(langmatch_prev) > 1:
279 stream = langmatch_prev[0][0]
280 if stream is not '':
281 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
282 return ''
284 def select_videofps(inFile, tsn):
285 vInfo = video_info(inFile)
286 fps = '-r 29.97' # default
287 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
288 fps = ' '
289 video_fps = config.get_tsn('video_fps', tsn)
290 if video_fps != None:
291 fps = '-r ' + video_fps
292 return fps
294 def select_videocodec(inFile, tsn):
295 vInfo = video_info(inFile)
296 if tivo_compatible_video(vInfo, tsn)[0]:
297 codec = 'copy'
298 else:
299 codec = 'mpeg2video' # default
300 return '-vcodec ' + codec
302 def select_videobr(inFile, tsn):
303 return '-b ' + str(select_videostr(inFile, tsn) / 1000) + 'k'
305 def select_videostr(inFile, tsn):
306 vInfo = video_info(inFile)
307 if tivo_compatible_video(vInfo, tsn)[0]:
308 video_str = int(vInfo['kbps'])
309 if vInfo['aKbps']:
310 video_str -= int(vInfo['aKbps'])
311 video_str *= 1000
312 else:
313 video_str = config.strtod(config.getVideoBR(tsn))
314 if config.isHDtivo(tsn):
315 if vInfo['kbps'] != None and config.getVideoPCT(tsn) > 0:
316 video_percent = (int(vInfo['kbps']) * 10 *
317 config.getVideoPCT(tsn))
318 video_str = max(video_str, video_percent)
319 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
320 video_str))
321 return video_str
323 def select_audiobr(tsn):
324 return '-ab ' + config.getAudioBR(tsn)
326 def select_maxvideobr(tsn):
327 return '-maxrate ' + config.getMaxVideoBR(tsn)
329 def select_buffsize(tsn):
330 return '-bufsize ' + config.getBuffSize(tsn)
332 def select_ffmpegprams(tsn):
333 params = config.getFFmpegPrams(tsn)
334 if not params:
335 params = ''
336 return params
338 def select_format(tsn, mime):
339 if mime == 'video/x-tivo-mpeg-ts':
340 fmt = 'mpegts'
341 else:
342 fmt = 'vob'
343 return '-f %s -' % fmt
345 def pad_check():
346 global pad_style
347 if pad_style == UNSET:
348 pad_style = OLD_PAD
349 filters = tempfile.TemporaryFile()
350 cmd = [config.get_bin('ffmpeg'), '-filters']
351 ffmpeg = subprocess.Popen(cmd, stdout=filters, stderr=subprocess.PIPE)
352 ffmpeg.wait()
353 filters.seek(0)
354 for line in filters:
355 if line.startswith('pad'):
356 pad_style = NEW_PAD
357 break
358 filters.close()
359 return pad_style == NEW_PAD
361 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
362 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
363 vInfo['vWidth']) * multiplier)
364 if endHeight % 2:
365 endHeight -= 1
366 if endHeight < TIVO_HEIGHT * 0.99:
367 topPadding = (TIVO_HEIGHT - endHeight) / 2
368 if topPadding % 2:
369 topPadding -= 1
370 newpad = pad_check()
371 if newpad:
372 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight), '-vf',
373 'pad=%d:%d:0:%d' % (TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
374 else:
375 bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
376 return ['-s', '%sx%s' % (TIVO_WIDTH, endHeight),
377 '-padtop', str(topPadding),
378 '-padbottom', str(bottomPadding)]
379 else: # if only very small amount of padding needed, then
380 # just stretch it
381 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
383 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
384 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
385 (vInfo['vHeight'] * multiplier))
386 if endWidth % 2:
387 endWidth -= 1
388 if endWidth < TIVO_WIDTH * 0.99:
389 leftPadding = (TIVO_WIDTH - endWidth) / 2
390 if leftPadding % 2:
391 leftPadding -= 1
392 newpad = pad_check()
393 if newpad:
394 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT), '-vf',
395 'pad=%d:%d:%d:0' % (TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
396 else:
397 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
398 return ['-s', '%sx%s' % (endWidth, TIVO_HEIGHT),
399 '-padleft', str(leftPadding),
400 '-padright', str(rightPadding)]
401 else: # if only very small amount of padding needed, then
402 # just stretch it
403 return ['-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
405 def select_aspect(inFile, tsn = ''):
406 TIVO_WIDTH = config.getTivoWidth(tsn)
407 TIVO_HEIGHT = config.getTivoHeight(tsn)
409 vInfo = video_info(inFile)
411 debug('tsn: %s' % tsn)
413 aspect169 = config.get169Setting(tsn)
415 debug('aspect169: %s' % aspect169)
417 optres = config.getOptres(tsn)
419 debug('optres: %s' % optres)
421 if optres:
422 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
423 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
424 if optHeight < TIVO_HEIGHT:
425 TIVO_HEIGHT = optHeight
426 if optWidth < TIVO_WIDTH:
427 TIVO_WIDTH = optWidth
429 if vInfo.get('par2'):
430 par2 = vInfo['par2']
431 elif vInfo.get('par'):
432 par2 = float(vInfo['par'])
433 else:
434 # Assume PAR = 1.0
435 par2 = 1.0
437 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
438 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
439 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
440 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
442 if config.isHDtivo(tsn) and not optres:
443 if config.getPixelAR(0) or vInfo['par']:
444 if vInfo['par2'] == None:
445 if vInfo['par']:
446 npar = par2
447 else:
448 npar = config.getPixelAR(1)
449 else:
450 npar = par2
452 # adjust for pixel aspect ratio, if set
454 if npar < 1.0:
455 return ['-s', '%dx%d' % (vInfo['vWidth'],
456 math.ceil(vInfo['vHeight'] / npar))]
457 elif npar > 1.0:
458 # FFMPEG expects width to be a multiple of two
459 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
460 vInfo['vHeight'])]
462 if vInfo['vHeight'] <= TIVO_HEIGHT:
463 # pass all resolutions to S3, except heights greater than
464 # conf height
465 return []
466 # else, resize video.
468 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
469 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
470 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
472 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
473 debug('File + PAR is within 4:3.')
474 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
476 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
477 (59, 72), (59, 36), (59, 54)] or
478 vInfo['dar1'] == '4:3'):
479 debug('File is within 4:3 list.')
480 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
482 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
483 (59, 27)] or vInfo['dar1'] == '16:9')
484 and (aspect169 or config.get169Letterbox(tsn))):
485 debug('File is within 16:9 list and 16:9 allowed.')
487 if config.get169Blacklist(tsn) or (aspect169 and
488 config.get169Letterbox(tsn)):
489 aspect = '4:3'
490 else:
491 aspect = '16:9'
492 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
494 else:
495 settings = ['-aspect']
497 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
498 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
499 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
500 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
501 multiplier4by3))
503 # If video is wider than 4:3 add top and bottom padding
505 if ratio > 133: # Might be 16:9 file, or just need padding on
506 # top and bottom
508 if aspect169 and ratio > 135: # If file would fall in 4:3
509 # assume it is supposed to be 4:3
511 if (config.get169Blacklist(tsn) or
512 config.get169Letterbox(tsn)):
513 settings.append('4:3')
514 else:
515 settings.append('16:9')
517 if ratio > 177: # too short needs padding top and bottom
518 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
519 multiplier16by9, vInfo)
520 debug(('16:9 aspect allowed, file is wider ' +
521 'than 16:9 padding top and bottom\n%s') %
522 ' '.join(settings))
524 else: # too skinny needs padding on left and right.
525 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
526 multiplier16by9, vInfo)
527 debug(('16:9 aspect allowed, file is narrower ' +
528 'than 16:9 padding left and right\n%s') %
529 ' '.join(settings))
531 else: # this is a 4:3 file or 16:9 output not allowed
532 if ratio > 135 and config.get169Letterbox(tsn):
533 settings.append('16:9')
534 multiplier = multiplier16by9
535 else:
536 settings.append('4:3')
537 multiplier = multiplier4by3
538 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
539 multiplier, vInfo)
540 debug(('File is wider than 4:3 padding ' +
541 'top and bottom\n%s') % ' '.join(settings))
543 # If video is taller than 4:3 add left and right padding, this
544 # is rare. All of these files will always be sent in an aspect
545 # ratio of 4:3 since they are so narrow.
547 else:
548 settings.append('4:3')
549 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
550 debug('File is taller than 4:3 padding left and right\n%s'
551 % ' '.join(settings))
553 return settings
555 def tivo_compatible_video(vInfo, tsn, mime=''):
556 message = (True, '')
557 while True:
558 codec = vInfo.get('vCodec', '')
559 if mime == 'video/mp4':
560 if codec != 'h264':
561 message = (False, 'vCodec %s not compatible' % codec)
563 break
565 if mime == 'video/bif':
566 if codec != 'vc1':
567 message = (False, 'vCodec %s not compatible' % codec)
569 break
571 if codec not in ('mpeg2video', 'mpeg1video'):
572 message = (False, 'vCodec %s not compatible' % codec)
573 break
575 if vInfo['kbps'] != None:
576 abit = max('0', vInfo['aKbps'])
577 if (int(vInfo['kbps']) - int(abit) >
578 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
579 message = (False, '%s kbps exceeds max video bitrate' %
580 vInfo['kbps'])
581 break
582 else:
583 message = (False, '%s kbps not supported' % vInfo['kbps'])
584 break
586 if config.isHDtivo(tsn):
587 if vInfo['par2'] != 1.0:
588 if config.getPixelAR(0):
589 if vInfo['par2'] != None or config.getPixelAR(1) != 1.0:
590 message = (False, '%s not correct PAR' % vInfo['par2'])
591 break
592 # HD Tivo detected, skipping remaining tests.
593 break
595 if not vInfo['vFps'] in ['29.97', '59.94']:
596 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
597 break
599 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
600 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
601 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
602 message = (False, ('DAR %s not supported ' +
603 'by BLACKLIST_169 tivos') % vInfo['dar1'])
604 break
606 mode = (vInfo['vWidth'], vInfo['vHeight'])
607 if mode not in [(720, 480), (704, 480), (544, 480),
608 (528, 480), (480, 480), (352, 480), (352, 240)]:
609 message = (False, '%s x %s not in supported modes' % mode)
610 break
612 return message
614 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
615 message = (True, '')
616 while True:
617 codec = vInfo.get('aCodec', '')
619 if codec == None:
620 debug('No audio stream detected')
621 break
623 if mime == 'video/mp4':
624 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
625 'ac3', 'liba52'):
626 message = (False, 'aCodec %s not compatible' % codec)
628 break
630 if mime == 'video/bif':
631 if codec != 'wmav2':
632 message = (False, 'aCodec %s not compatible' % codec)
634 break
636 if inFile[-5:].lower() == '.tivo':
637 break
639 if mime == 'video/x-tivo-mpeg-ts' and codec not in ('ac3', 'liba52'):
640 message = (False, 'aCodec %s not compatible' % codec)
641 break
643 if codec not in ('ac3', 'liba52', 'mp2'):
644 message = (False, 'aCodec %s not compatible' % codec)
645 break
647 if (not vInfo['aKbps'] or
648 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
649 message = (False, '%s kbps exceeds max audio bitrate' %
650 vInfo['aKbps'])
651 break
653 audio_lang = config.get_tsn('audio_lang', tsn)
654 if audio_lang:
655 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
656 message = (False, '%s preferred audio track exists' %
657 audio_lang)
658 break
660 return message
662 def tivo_compatible_container(vInfo, inFile, mime=''):
663 message = (True, '')
664 container = vInfo.get('container', '')
665 if ((mime == 'video/mp4' and
666 (container != 'mov' or inFile.lower().endswith('.mov'))) or
667 (mime == 'video/bif' and container != 'asf') or
668 (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
669 (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
670 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
671 message = (False, 'container %s not compatible' % container)
673 return message
675 def mp4_remuxable(inFile, tsn=''):
676 vInfo = video_info(inFile)
677 return (tivo_compatible_video(vInfo, tsn, 'video/mp4')[0] and
678 tivo_compatible_audio(vInfo, inFile, tsn, 'video/mp4')[0])
680 def mp4_remux(inFile, basename):
681 outFile = inFile + '.pyTivo-temp'
682 newname = basename + '.pyTivo-temp'
683 if os.path.exists(outFile):
684 return None # ugh!
686 ffmpeg_path = config.get_bin('ffmpeg')
687 fname = unicode(inFile, 'utf-8')
688 oname = unicode(outFile, 'utf-8')
689 if mswindows:
690 fname = fname.encode('iso8859-1')
691 oname = oname.encode('iso8859-1')
693 cmd = [ffmpeg_path, '-i', fname, '-vcodec', 'copy', '-acodec',
694 'copy', '-f', 'mp4', oname]
695 ffmpeg = subprocess.Popen(cmd)
696 debug('remuxing ' + inFile + ' to ' + outFile)
697 if ffmpeg.wait():
698 debug('error during remuxing')
699 os.remove(outFile)
700 return None
702 return newname
704 def tivo_compatible(inFile, tsn='', mime=''):
705 vInfo = video_info(inFile)
707 message = (True, 'all compatible')
708 if not config.get_bin('ffmpeg'):
709 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
710 message = (False, 'no ffmpeg')
711 return message
713 while True:
714 vmessage = tivo_compatible_video(vInfo, tsn, mime)
715 if not vmessage[0]:
716 message = vmessage
717 break
719 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
720 if not amessage[0]:
721 message = amessage
722 break
724 cmessage = tivo_compatible_container(vInfo, inFile, mime)
725 if not cmessage[0]:
726 message = cmessage
728 break
730 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
731 message[1], inFile))
732 return message
734 def video_info(inFile, cache=True):
735 vInfo = dict()
736 fname = unicode(inFile, 'utf-8')
737 mtime = os.stat(fname).st_mtime
738 if cache:
739 if inFile in info_cache and info_cache[inFile][0] == mtime:
740 debug('CACHE HIT! %s' % inFile)
741 return info_cache[inFile][1]
743 vInfo['Supported'] = True
745 ffmpeg_path = config.get_bin('ffmpeg')
746 if not ffmpeg_path:
747 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
748 '.vob', '.tivo']:
749 vInfo['Supported'] = False
750 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
751 'rawmeta': {}})
752 if cache:
753 info_cache[inFile] = (mtime, vInfo)
754 return vInfo
756 if mswindows:
757 fname = fname.encode('iso8859-1')
758 cmd = [ffmpeg_path, '-i', fname]
759 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
760 err_tmp = tempfile.TemporaryFile()
761 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
762 stdin=subprocess.PIPE)
764 # wait configured # of seconds: if ffmpeg is not back give up
765 wait = config.getFFmpegWait()
766 debug('starting ffmpeg, will wait %s seconds for it to complete' % wait)
767 for i in xrange(wait * 20):
768 time.sleep(.05)
769 if not ffmpeg.poll() == None:
770 break
772 if ffmpeg.poll() == None:
773 kill(ffmpeg)
774 vInfo['Supported'] = False
775 if cache:
776 info_cache[inFile] = (mtime, vInfo)
777 return vInfo
779 err_tmp.seek(0)
780 output = err_tmp.read()
781 err_tmp.close()
782 debug('ffmpeg output=%s' % output)
784 attrs = {'container': r'Input #0, ([^,]+),',
785 'vCodec': r'Video: ([^, ]+)', # video codec
786 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
787 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
788 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
789 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
791 for attr in attrs:
792 rezre = re.compile(attrs[attr])
793 x = rezre.search(output)
794 if x:
795 vInfo[attr] = x.group(1)
796 else:
797 if attr in ['container', 'vCodec']:
798 vInfo[attr] = ''
799 vInfo['Supported'] = False
800 else:
801 vInfo[attr] = None
802 debug('failed at ' + attr)
804 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
805 x = rezre.search(output)
806 if x:
807 vInfo['vWidth'] = int(x.group(1))
808 vInfo['vHeight'] = int(x.group(2))
809 else:
810 vInfo['vWidth'] = ''
811 vInfo['vHeight'] = ''
812 vInfo['Supported'] = False
813 debug('failed at vWidth/vHeight')
815 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
816 x = rezre.search(output)
817 if x:
818 vInfo['vFps'] = x.group(1)
819 if '.' not in vInfo['vFps']:
820 vInfo['vFps'] += '.00'
822 # Allow override only if it is mpeg2 and frame rate was doubled
823 # to 59.94
825 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
826 # First look for the build 7215 version
827 rezre = re.compile(r'.*film source: 29.97.*')
828 x = rezre.search(output.lower())
829 if x:
830 debug('film source: 29.97 setting vFps to 29.97')
831 vInfo['vFps'] = '29.97'
832 else:
833 # for build 8047:
834 rezre = re.compile(r'.*frame rate differs from container ' +
835 r'frame rate: 29.97.*')
836 debug('Bug in VideoReDo')
837 x = rezre.search(output.lower())
838 if x:
839 vInfo['vFps'] = '29.97'
840 else:
841 vInfo['vFps'] = ''
842 vInfo['Supported'] = False
843 debug('failed at vFps')
845 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
846 d = durre.search(output)
848 if d:
849 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
850 int(d.group(2)) * 60 +
851 int(d.group(3))) * 1000 +
852 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
853 else:
854 vInfo['millisecs'] = 0
856 # get bitrate of source for tivo compatibility test.
857 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
858 x = rezre.search(output)
859 if x:
860 vInfo['kbps'] = x.group(1)
861 else:
862 # Fallback method of getting video bitrate
863 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
864 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
865 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
866 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
867 x = rezre.search(output)
868 if x:
869 vInfo['kbps'] = x.group(1)
870 else:
871 vInfo['kbps'] = None
872 debug('failed at kbps')
874 # get par.
875 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
876 x = rezre.search(output)
877 if x and x.group(1) != "0" and x.group(2) != "0":
878 vInfo['par1'] = x.group(1) + ':' + x.group(2)
879 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
880 else:
881 vInfo['par1'], vInfo['par2'] = None, None
883 # get dar.
884 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
885 x = rezre.search(output)
886 if x and x.group(1) != "0" and x.group(2) != "0":
887 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
888 else:
889 vInfo['dar1'] = None
891 # get Audio Stream mapping.
892 rezre = re.compile(r'([0-9]+\.[0-9]+)(.*): Audio:(.*)')
893 x = rezre.search(output)
894 amap = []
895 if x:
896 for x in rezre.finditer(output):
897 amap.append(x.groups())
898 else:
899 amap.append(('', ''))
900 debug('failed at mapAudio')
901 vInfo['mapAudio'] = amap
903 vInfo['par'] = None
905 # get Metadata dump (newer ffmpeg).
906 lines = output.split('\n')
907 rawmeta = {}
908 flag = False
910 for line in lines:
911 if line.startswith(' Metadata:'):
912 flag = True
913 else:
914 if flag:
915 if line.startswith(' Duration:'):
916 flag = False
917 else:
918 try:
919 key, value = [x.strip() for x in line.split(':', 1)]
920 try:
921 value = value.decode('utf-8')
922 except:
923 if sys.platform == 'darwin':
924 value = value.decode('macroman')
925 else:
926 value = value.decode('iso8859-1')
927 rawmeta[key] = [value]
928 except:
929 pass
931 vInfo['rawmeta'] = rawmeta
933 data = metadata.from_text(inFile)
934 for key in data:
935 if key.startswith('Override_'):
936 vInfo['Supported'] = True
937 if key.startswith('Override_mapAudio'):
938 audiomap = dict(vInfo['mapAudio'])
939 stream = key.replace('Override_mapAudio', '').strip()
940 if stream in audiomap:
941 newaudiomap = (stream, data[key])
942 audiomap.update([newaudiomap])
943 vInfo['mapAudio'] = sorted(audiomap.items(),
944 key=lambda (k,v): (k,v))
945 elif key.startswith('Override_millisecs'):
946 vInfo[key.replace('Override_', '')] = int(data[key])
947 else:
948 vInfo[key.replace('Override_', '')] = data[key]
950 if cache:
951 info_cache[inFile] = (mtime, vInfo)
952 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
953 return vInfo
955 def audio_check(inFile, tsn):
956 cmd_string = ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
957 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
958 fname = unicode(inFile, 'utf-8')
959 if mswindows:
960 fname = fname.encode('iso8859-1')
961 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
962 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
963 fd, testname = tempfile.mkstemp()
964 testfile = os.fdopen(fd, 'wb')
965 try:
966 shutil.copyfileobj(ffmpeg.stdout, testfile)
967 except:
968 kill(ffmpeg)
969 testfile.close()
970 aKbps = None
971 else:
972 testfile.close()
973 aKbps = video_info(testname, False)['aKbps']
974 os.remove(testname)
975 return aKbps
977 def supported_format(inFile):
978 if video_info(inFile)['Supported']:
979 return True
980 else:
981 debug('FALSE, file not supported %s' % inFile)
982 return False
984 def kill(popen):
985 debug('killing pid=%s' % str(popen.pid))
986 if mswindows:
987 win32kill(popen.pid)
988 else:
989 import os, signal
990 for i in xrange(3):
991 debug('sending SIGTERM to pid: %s' % popen.pid)
992 os.kill(popen.pid, signal.SIGTERM)
993 time.sleep(.5)
994 if popen.poll() is not None:
995 debug('process %s has exited' % popen.pid)
996 break
997 else:
998 while popen.poll() is None:
999 debug('sending SIGKILL to pid: %s' % popen.pid)
1000 os.kill(popen.pid, signal.SIGKILL)
1001 time.sleep(.5)
1003 def win32kill(pid):
1004 import ctypes
1005 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1006 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1007 ctypes.windll.kernel32.CloseHandle(handle)
1009 def gcd(a, b):
1010 while b:
1011 a, b = b, a % b
1012 return a