realigned with wmcbrine's pyTivo code
[pyTivo_dvdvideo.git] / transcode.py
blob526a0657b4553357e0d4650595089bb16444dd8c
1 # Module: dvdfolder.py
2 # Author: Eric von Bayer
3 # Updated By: Luke Broadbent
4 # Contact:
5 # Date: June 5, 2012
6 # Description:
7 # Routines for transcoding DVD vob files to mpeg file the TiVo can play.
8 # This is closely aligned with transcode.py from the video plugin (from
9 # wmcbrine's branch).
11 # Copyright (c) 2009, Eric von Bayer
12 # All rights reserved.
14 # Redistribution and use in source and binary forms, with or without
15 # modification, are permitted provided that the following conditions are met:
17 # * Redistributions of source code must retain the above copyright notice,
18 # this list of conditions and the following disclaimer.
19 # * Redistributions in binary form must reproduce the above copyright notice,
20 # this list of conditions and the following disclaimer in the documentation
21 # and/or other materials provided with the distribution.
22 # * The names of the contributors may not be used to endorse or promote
23 # products derived from this software without specific prior written
24 # permission.
26 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
30 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
31 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
32 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
33 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
34 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 import logging
38 import math
39 import os
40 import re
41 import shlex
42 import shutil
43 import subprocess
44 import sys
45 import tempfile
46 import threading
47 import time
49 import lrucache
51 import config
52 import metadata
54 import vobstream
56 logger = logging.getLogger('pyTivo.video.transcode')
58 info_cache = lrucache.LRUCache(1000)
59 ffmpeg_procs = {}
60 reapers = {}
62 GOOD_MPEG_FPS = ['23.98', '24.00', '25.00', '29.97',
63 '30.00', '50.00', '59.94', '60.00']
65 BLOCKSIZE = 512 * 1024
66 MAXBLOCKS = 2
67 TIMEOUT = 600
69 # XXX BIG HACK
70 # subprocess is broken for me on windows so super hack
71 def patchSubprocess():
72 o = subprocess.Popen._make_inheritable
74 def _make_inheritable(self, handle):
75 if not handle: return subprocess.GetCurrentProcess()
76 return o(self, handle)
78 subprocess.Popen._make_inheritable = _make_inheritable
79 mswindows = (sys.platform == "win32")
80 if mswindows:
81 patchSubprocess()
83 def debug(msg):
84 if type(msg) == str:
85 try:
86 msg = msg.decode('utf8')
87 except:
88 if sys.platform == 'darwin':
89 msg = msg.decode('macroman')
90 else:
91 msg = msg.decode('cp1252')
92 logger.debug(msg)
94 def transcode(isQuery, inFile, outFile, tsn='', mime='', thead=''):
95 vcodec = select_videocodec(inFile, tsn, mime)
97 settings = select_buffsize(tsn) + vcodec
98 if not vcodec[1] == 'copy':
99 settings += (select_videobr(inFile, tsn) +
100 select_maxvideobr(tsn) +
101 select_videofps(inFile, tsn) +
102 select_aspect(inFile, tsn))
104 acodec = select_audiocodec(isQuery, inFile, tsn)
105 settings += acodec
106 if not acodec[1] == 'copy':
107 settings += (select_audiobr(tsn) +
108 select_audiofr(inFile, tsn) +
109 select_audioch(inFile, tsn))
111 settings += [select_audiolang(inFile, tsn),
112 select_ffmpegprams(tsn)]
114 settings += select_format(tsn, mime)
116 settings = ' '.join(settings).split()
117 vfilter = select_videofilter(inFile)
118 if vfilter:
119 settings = vfilter + settings
121 if isQuery:
122 return settings
124 ffmpeg_path = config.get_bin('ffmpeg')
126 fname = unicode(inFile, 'utf-8')
127 if mswindows:
128 fname = fname.encode('cp1252')
130 if inFile[-5:].lower() == '.tivo':
131 tivodecode_path = config.get_bin('tivodecode')
132 tivo_mak = config.get_server('tivo_mak')
133 tcmd = [tivodecode_path, '-m', tivo_mak, fname]
134 tivodecode = subprocess.Popen(tcmd, stdout=subprocess.PIPE,
135 bufsize=(512 * 1024))
136 if tivo_compatible(inFile, tsn)[0]:
137 cmd = ''
138 ffmpeg = tivodecode
139 else:
140 cmd = [ffmpeg_path, '-i', '-'] + settings
141 ffmpeg = subprocess.Popen(cmd, stdin=tivodecode.stdout,
142 stdout=subprocess.PIPE,
143 bufsize=(512 * 1024))
144 elif vobstream.is_dvd(inFile):
145 cmd = [ffmpeg_path, '-i', '-'] + settings
146 ffmpeg = subprocess.Popen(cmd, stdin=subprocess.PIPE,
147 stdout=subprocess.PIPE,
148 bufsize= BLOCKSIZE * MAXBLOCKS )
149 proc = vobstream.vobstream(False, inFile, ffmpeg, BLOCKSIZE)
150 else:
151 cmd = [ffmpeg_path, '-i', fname] + settings
152 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024),
153 stdout=subprocess.PIPE)
155 if cmd:
156 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
157 debug(' '.join(cmd))
159 ffmpeg_procs[inFile] = {'process': ffmpeg, 'start': 0, 'end': 0,
160 'last_read': time.time(), 'blocks': []}
161 if thead:
162 ffmpeg_procs[inFile]['blocks'].append(thead)
163 if vobstream.is_dvd(inFile):
164 ffmpeg_procs[inFile]['stream'] = proc['stream']
165 ffmpeg_procs[inFile]['thread'] = proc['thread']
166 ffmpeg_procs[inFile]['event'] = proc['event']
168 reap_process(inFile)
169 return resume_transfer(inFile, outFile, 0)
171 def is_resumable(inFile, offset):
172 if inFile in ffmpeg_procs:
173 proc = ffmpeg_procs[inFile]
174 if proc['start'] <= offset < proc['end']:
175 return True
176 else:
177 cleanup(inFile)
178 kill(proc['process'])
179 return False
181 def resume_transfer(inFile, outFile, offset):
182 proc = ffmpeg_procs[inFile]
183 offset -= proc['start']
184 count = 0
186 try:
187 for block in proc['blocks']:
188 length = len(block)
189 if offset < length:
190 if offset > 0:
191 block = block[offset:]
192 outFile.write('%x\r\n' % len(block))
193 outFile.write(block)
194 outFile.write('\r\n')
195 count += len(block)
196 offset -= length
197 outFile.flush()
198 except Exception, msg:
199 logger.info(msg)
200 return count
202 proc['start'] = proc['end']
203 proc['blocks'] = []
205 return count + transfer_blocks(inFile, outFile)
207 def transfer_blocks(inFile, outFile):
208 proc = ffmpeg_procs[inFile]
209 blocks = proc['blocks']
210 count = 0
212 while True:
213 try:
214 block = proc['process'].stdout.read(BLOCKSIZE)
215 proc['last_read'] = time.time()
216 except Exception, msg:
217 logger.info(msg)
218 cleanup(inFile)
219 kill(proc['process'])
220 break
222 if not block:
223 try:
224 outFile.flush()
225 except Exception, msg:
226 logger.info(msg)
227 else:
228 cleanup(inFile)
229 break
231 blocks.append(block)
232 proc['end'] += len(block)
233 if len(blocks) > MAXBLOCKS:
234 proc['start'] += len(blocks[0])
235 blocks.pop(0)
237 try:
238 outFile.write('%x\r\n' % len(block))
239 outFile.write(block)
240 outFile.write('\r\n')
241 count += len(block)
242 except Exception, msg:
243 logger.info(msg)
244 break
246 return count
248 def reap_process(inFile):
249 if ffmpeg_procs and inFile in ffmpeg_procs:
250 proc = ffmpeg_procs[inFile]
251 if proc['last_read'] + TIMEOUT < time.time():
252 del ffmpeg_procs[inFile]
253 del reapers[inFile]
254 kill(proc['process'])
255 else:
256 reaper = threading.Timer(TIMEOUT, reap_process, (inFile,))
257 reapers[inFile] = reaper
258 reaper.start()
260 def cleanup(inFile):
261 if vobstream.is_dvd(inFile):
262 proc = ffmpeg_procs[inFile]
263 kill(proc['process'])
264 proc['process'].wait()
266 # Tell thread to break out of loop
267 proc['event'].set()
268 proc['thread'].join()
270 del ffmpeg_procs[inFile]
271 reapers[inFile].cancel()
272 del reapers[inFile]
274 def select_audiocodec(isQuery, inFile, tsn='', mime=''):
275 if inFile[-5:].lower() == '.tivo':
276 return ['-c:a', 'copy']
277 vInfo = video_info(inFile)
278 codectype = vInfo['vCodec']
279 # Default, compatible with all TiVo's
280 codec = 'ac3'
281 if mime == 'video/mp4':
282 compatiblecodecs = ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
283 'ac3', 'liba52')
284 else:
285 compatiblecodecs = ('ac3', 'liba52', 'mp2')
287 if vInfo['aCodec'] in compatiblecodecs:
288 aKbps = vInfo['aKbps']
289 aCh = vInfo['aCh']
290 if aKbps == None:
291 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
292 # along with the channel check below this should
293 # pass any AAC audio that has undefined 'aKbps' and
294 # is <= 2 channels. Should be TiVo compatible.
295 codec = 'copy'
296 elif not isQuery:
297 vInfoQuery = audio_check(inFile, tsn)
298 if vInfoQuery == None:
299 aKbps = None
300 aCh = None
301 else:
302 aKbps = vInfoQuery['aKbps']
303 aCh = vInfoQuery['aCh']
304 else:
305 codec = 'TBA'
306 if aKbps and int(aKbps) <= config.getMaxAudioBR(tsn):
307 # compatible codec and bitrate, do not reencode audio
308 codec = 'copy'
309 if vInfo['aCodec'] != 'ac3' and (aCh == None or aCh > 2):
310 codec = 'ac3'
311 val = ['-c:a', codec]
312 if not (codec == 'copy' and codectype == 'mpeg2video'):
313 val.append('-copyts')
314 return val
316 def select_audiofr(inFile, tsn):
317 freq = '48000' # default
318 vInfo = video_info(inFile)
319 if vInfo['aFreq'] == '44100':
320 # compatible frequency
321 freq = vInfo['aFreq']
322 return ['-ar', freq]
324 def select_audioch(inFile, tsn):
325 # AC-3 max channels is 5.1
326 if video_info(inFile)['aCh'] > 6:
327 debug('Too many audio channels for AC-3, using 5.1 instead')
328 return ['-ac', '6']
329 return []
331 def select_audiolang(inFile, tsn):
332 vInfo = video_info(inFile)
333 audio_lang = config.get_tsn('audio_lang', tsn)
334 debug('audio_lang: %s' % audio_lang)
335 if vInfo['mapAudio']:
336 # default to first detected audio stream to begin with
337 stream = vInfo['mapAudio'][0][0]
338 debug('set first detected audio stream by default: %s' % stream)
339 if audio_lang != None and vInfo['mapVideo'] != None:
340 langmatch_curr = []
341 langmatch_prev = vInfo['mapAudio'][:]
342 for lang in audio_lang.replace(' ', '').lower().split(','):
343 debug('matching lang: %s' % lang)
344 for s, l in langmatch_prev:
345 if lang in s + l.replace(' ', '').lower():
346 debug('matched: %s' % s + l.replace(' ', '').lower())
347 langmatch_curr.append((s, l))
348 # if only 1 item matched we're done
349 if len(langmatch_curr) == 1:
350 stream = langmatch_curr[0][0]
351 debug('found exactly one match: %s' % stream)
352 break
353 # if more than 1 item matched copy the curr area to the prev
354 # array we only need to look at the new shorter list from
355 # now on
356 elif len(langmatch_curr) > 1:
357 langmatch_prev = langmatch_curr[:]
358 # default to the first item matched thus far
359 stream = langmatch_curr[0][0]
360 debug('remember first match: %s' % stream)
361 langmatch_curr = []
362 # don't let FFmpeg auto select audio stream, pyTivo defaults to
363 # first detected
364 if stream:
365 debug('selected audio stream: %s' % stream)
366 return '-map ' + vInfo['mapVideo'] + ' -map ' + stream
367 # if no audio is found
368 debug('selected audio stream: None detected')
369 return ''
371 def select_videofps(inFile, tsn):
372 vInfo = video_info(inFile)
373 fps = ['-r', '29.97'] # default
374 if config.isHDtivo(tsn) and vInfo['vFps'] in GOOD_MPEG_FPS:
375 fps = []
376 return fps
378 def select_videocodec(inFile, tsn, mime=''):
379 codec = ['-c:v']
380 vInfo = video_info(inFile)
381 if tivo_compatible_video(vInfo, tsn, mime)[0]:
382 codec.append('copy')
383 if (mime == 'video/x-tivo-mpeg-ts'):
384 org_codec = vInfo.get('vCodec', '')
385 if org_codec == 'h264':
386 codec += ['-bsf', 'h264_mp4toannexb']
387 elif org_codec == 'hevc':
388 codec += ['-bsf', 'hevc_mp4toannexb']
389 else:
390 codec += ['mpeg2video', '-pix_fmt', 'yuv420p'] # default
391 return codec
393 def select_videofilter(inFile):
394 vInfo = video_info(inFile)
395 #legacy subtitle support to be removed
396 subtitles = vInfo.get('subtitles')
397 if subtitles:
398 return ['-vf', subtitles]
400 subfile = vInfo.get('subFile')
402 #first select a subFile in the metadata.txt file
403 #then select the subFile with the same filename as the video file (video file: video.mpg, subFile: video.mpg.srt)
404 if subfile and os.path.exists(os.path.join(os.path.split(inFile)[0], subfile)):
405 subfile = os.path.join(os.path.split(inFile)[0], subfile)
406 elif os.path.exists(os.path.join(os.path.split(inFile)[0], os.path.basename(inFile) + '.srt')):
407 subfile = os.path.join(os.path.split(inFile)[0], os.path.basename(inFile) + '.srt')
408 elif os.path.exists(os.path.join(os.path.split(inFile)[0], os.path.basename(inFile) + '.ass')):
409 subfile = os.path.join(os.path.split(inFile)[0], os.path.basename(inFile) + '.ass')
411 if subfile:
412 if subfile == inFile:
413 #TODO need to look at handling when there is more than one subtitle track
414 subType = vInfo.get('subType')
415 else:
416 sInfo = video_info(subfile)
417 subType = sInfo.get('subType')
418 logger.info('sInfo: %s' % sInfo)
420 #escape ffmpeg special characters
421 #not sure how to escape ' - currently can't support files with this character in the path
422 subfile_escape = re.sub(r'([\\\[\]\:@;,])', r'\\\1', subfile)
424 if subType == 'subrip':
425 vfilter = ['-vf', 'subtitles=\\\'%s\\\'' % subfile_escape]
426 logger.info('video filter: %s' % vfilter)
427 return vfilter
428 elif subType == 'ass':
429 vfilter = ['-vf', 'ass=\\\'%s\\\'' % subfile_escape]
430 logger.info('video filter: %s' % vfilter)
431 return vfilter
433 return False
435 def select_videobr(inFile, tsn, mime=''):
436 return ['-b:v', str(select_videostr(inFile, tsn, mime) / 1000) + 'k']
438 def select_videostr(inFile, tsn, mime=''):
439 vInfo = video_info(inFile)
440 if tivo_compatible_video(vInfo, tsn, mime)[0]:
441 video_str = int(vInfo['kbps'])
442 if vInfo['aKbps']:
443 video_str -= int(vInfo['aKbps'])
444 video_str *= 1000
445 else:
446 video_str = config.strtod(config.getVideoBR(tsn))
447 if config.isHDtivo(tsn) and vInfo['kbps']:
448 video_str = max(video_str, int(vInfo['kbps']) * 1000)
449 video_str = int(min(config.strtod(config.getMaxVideoBR(tsn)) * 0.95,
450 video_str))
451 return video_str
453 def select_audiobr(tsn):
454 return ['-b:a', config.getAudioBR(tsn)]
456 def select_maxvideobr(tsn):
457 return ['-maxrate', config.getMaxVideoBR(tsn)]
459 def select_buffsize(tsn):
460 return ['-bufsize', config.getBuffSize(tsn)]
462 def select_ffmpegprams(tsn):
463 params = config.getFFmpegPrams(tsn)
464 if not params:
465 params = ''
466 return params
468 def select_format(tsn, mime):
469 if mime == 'video/x-tivo-mpeg-ts':
470 fmt = 'mpegts'
471 else:
472 fmt = 'vob'
473 return ['-f', fmt, '-']
475 def pad_TB(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
476 endHeight = int(((TIVO_WIDTH * vInfo['vHeight']) /
477 vInfo['vWidth']) * multiplier)
478 if endHeight % 2:
479 endHeight -= 1
480 topPadding = (TIVO_HEIGHT - endHeight) / 2
481 if topPadding % 2:
482 topPadding -= 1
483 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH,
484 endHeight, TIVO_WIDTH, TIVO_HEIGHT, topPadding)]
486 def pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier, vInfo):
487 endWidth = int((TIVO_HEIGHT * vInfo['vWidth']) /
488 (vInfo['vHeight'] * multiplier))
489 if endWidth % 2:
490 endWidth -= 1
491 leftPadding = (TIVO_WIDTH - endWidth) / 2
492 if leftPadding % 2:
493 leftPadding -= 1
494 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth,
495 TIVO_HEIGHT, TIVO_WIDTH, TIVO_HEIGHT, leftPadding)]
497 def select_aspect(inFile, tsn = ''):
498 TIVO_WIDTH = config.getTivoWidth(tsn)
499 TIVO_HEIGHT = config.getTivoHeight(tsn)
501 vInfo = video_info(inFile)
503 debug('tsn: %s' % tsn)
505 aspect169 = config.get169Setting(tsn)
507 debug('aspect169: %s' % aspect169)
509 optres = config.getOptres(tsn)
511 debug('optres: %s' % optres)
513 if optres:
514 optHeight = config.nearestTivoHeight(vInfo['vHeight'])
515 optWidth = config.nearestTivoWidth(vInfo['vWidth'])
516 if optHeight < TIVO_HEIGHT:
517 TIVO_HEIGHT = optHeight
518 if optWidth < TIVO_WIDTH:
519 TIVO_WIDTH = optWidth
521 if vInfo.get('par2'):
522 par2 = vInfo['par2']
523 elif vInfo.get('par'):
524 par2 = float(vInfo['par'])
525 else:
526 # Assume PAR = 1.0
527 par2 = 1.0
529 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
530 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile, vInfo['vCodec'],
531 vInfo['vWidth'], vInfo['vHeight'], vInfo['vFps'],
532 vInfo['millisecs'], TIVO_HEIGHT, TIVO_WIDTH))
534 if config.isHDtivo(tsn) and not optres:
535 if vInfo['par']:
536 npar = par2
538 # adjust for pixel aspect ratio, if set
540 if npar < 1.0:
541 return ['-s', '%dx%d' % (vInfo['vWidth'],
542 math.ceil(vInfo['vHeight'] / npar))]
543 elif npar > 1.0:
544 # FFMPEG expects width to be a multiple of two
545 return ['-s', '%dx%d' % (math.ceil(vInfo['vWidth']*npar/2.0)*2,
546 vInfo['vHeight'])]
548 if vInfo['vHeight'] <= TIVO_HEIGHT:
549 # pass all resolutions to S3, except heights greater than
550 # conf height
551 return []
552 # else, resize video.
554 d = gcd(vInfo['vHeight'], vInfo['vWidth'])
555 rheight, rwidth = vInfo['vHeight'] / d, vInfo['vWidth'] / d
556 debug('rheight=%s rwidth=%s' % (rheight, rwidth))
558 if (rwidth, rheight) in [(1, 1)] and vInfo['par1'] == '8:9':
559 debug('File + PAR is within 4:3.')
560 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
562 elif ((rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54),
563 (59, 72), (59, 36), (59, 54)] or
564 vInfo['dar1'] == '4:3'):
565 debug('File is within 4:3 list.')
566 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
568 elif (((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81),
569 (59, 27)] or vInfo['dar1'] == '16:9')
570 and (aspect169 or config.get169Letterbox(tsn))):
571 debug('File is within 16:9 list and 16:9 allowed.')
573 if config.get169Blacklist(tsn) or (aspect169 and
574 config.get169Letterbox(tsn)):
575 aspect = '4:3'
576 else:
577 aspect = '16:9'
578 return ['-aspect', aspect, '-s', '%sx%s' % (TIVO_WIDTH, TIVO_HEIGHT)]
580 else:
581 settings = ['-aspect']
583 multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) / par2
584 multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) / par2
585 ratio = vInfo['vWidth'] * 100 * par2 / vInfo['vHeight']
586 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2, ratio,
587 multiplier4by3))
589 # If video is wider than 4:3 add top and bottom padding
591 if ratio > 133: # Might be 16:9 file, or just need padding on
592 # top and bottom
594 if aspect169 and ratio > 135: # If file would fall in 4:3
595 # assume it is supposed to be 4:3
597 if (config.get169Blacklist(tsn) or
598 config.get169Letterbox(tsn)):
599 settings.append('4:3')
600 else:
601 settings.append('16:9')
603 if ratio > 177: # too short needs padding top and bottom
604 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
605 multiplier16by9, vInfo)
606 debug(('16:9 aspect allowed, file is wider ' +
607 'than 16:9 padding top and bottom\n%s') %
608 ' '.join(settings))
610 else: # too skinny needs padding on left and right.
611 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT,
612 multiplier16by9, vInfo)
613 debug(('16:9 aspect allowed, file is narrower ' +
614 'than 16:9 padding left and right\n%s') %
615 ' '.join(settings))
617 else: # this is a 4:3 file or 16:9 output not allowed
618 if ratio > 135 and config.get169Letterbox(tsn):
619 settings.append('16:9')
620 multiplier = multiplier16by9
621 else:
622 settings.append('4:3')
623 multiplier = multiplier4by3
624 settings += pad_TB(TIVO_WIDTH, TIVO_HEIGHT,
625 multiplier, vInfo)
626 debug(('File is wider than 4:3 padding ' +
627 'top and bottom\n%s') % ' '.join(settings))
629 # If video is taller than 4:3 add left and right padding, this
630 # is rare. All of these files will always be sent in an aspect
631 # ratio of 4:3 since they are so narrow.
633 else:
634 settings.append('4:3')
635 settings += pad_LR(TIVO_WIDTH, TIVO_HEIGHT, multiplier4by3, vInfo)
636 debug('File is taller than 4:3 padding left and right\n%s'
637 % ' '.join(settings))
639 return settings
641 def tivo_compatible_video(vInfo, tsn, mime=''):
642 message = (True, '')
643 while True:
644 if select_videofilter(vInfo.get('fname')):
645 message = (False, 'Subtitle hard-coding requires reencoding')
647 break
649 codec = vInfo.get('vCodec', '')
650 is4k = config.is4Ktivo(tsn) and codec == 'hevc'
651 if mime == 'video/mp4':
652 if not (is4k or codec == 'h264'):
653 message = (False, 'vCodec %s not compatible' % codec)
655 break
657 if mime == 'video/bif':
658 if codec != 'vc1':
659 message = (False, 'vCodec %s not compatible' % codec)
661 break
663 if mime == 'video/x-tivo-mpeg-ts':
664 if not (is4k or codec in ('h264', 'mpeg2video')):
665 message = (False, 'vCodec %s not compatible' % codec)
667 break
669 if codec not in ('mpeg2video', 'mpeg1video'):
670 message = (False, 'vCodec %s not compatible' % codec)
671 break
673 if vInfo['kbps'] != None:
674 abit = max('0', vInfo['aKbps'])
675 if (int(vInfo['kbps']) - int(abit) >
676 config.strtod(config.getMaxVideoBR(tsn)) / 1000):
677 message = (False, '%s kbps exceeds max video bitrate' %
678 vInfo['kbps'])
679 break
680 else:
681 message = (False, '%s kbps not supported' % vInfo['kbps'])
682 break
684 if config.isHDtivo(tsn):
685 # HD Tivo detected, skipping remaining tests.
686 break
688 if not vInfo['vFps'] in ['29.97', '59.94']:
689 message = (False, '%s vFps, should be 29.97' % vInfo['vFps'])
690 break
692 if ((config.get169Blacklist(tsn) and not config.get169Setting(tsn))
693 or (config.get169Letterbox(tsn) and config.get169Setting(tsn))):
694 if vInfo['dar1'] and vInfo['dar1'] not in ('4:3', '8:9', '880:657'):
695 message = (False, ('DAR %s not supported ' +
696 'by BLACKLIST_169 tivos') % vInfo['dar1'])
697 break
699 mode = (vInfo['vWidth'], vInfo['vHeight'])
700 if mode not in [(720, 480), (704, 480), (544, 480),
701 (528, 480), (480, 480), (352, 480), (352, 240)]:
702 message = (False, '%s x %s not in supported modes' % mode)
703 break
705 return message
707 def tivo_compatible_audio(vInfo, inFile, tsn, mime=''):
708 message = (True, '')
709 while True:
710 codec = vInfo.get('aCodec', '')
712 if codec == None:
713 debug('No audio stream detected')
714 break
716 if mime == 'video/mp4':
717 if codec not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
718 'ac3', 'liba52'):
719 message = (False, 'aCodec %s not compatible' % codec)
720 break
721 if vInfo['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo['aCh'] == None or vInfo['aCh'] > 2):
722 message = (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec, vInfo['aCh']))
723 break
725 audio_lang = config.get_tsn('audio_lang', tsn)
726 if audio_lang:
727 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
728 message = (False, '%s preferred audio track exists' %
729 audio_lang)
730 break
732 if mime == 'video/bif':
733 if codec != 'wmav2':
734 message = (False, 'aCodec %s not compatible' % codec)
736 break
738 if inFile[-5:].lower() == '.tivo':
739 break
741 if mime == 'video/x-tivo-mpeg-ts':
742 if codec not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
743 message = (False, 'aCodec %s not compatible' % codec)
745 break
747 if codec not in ('ac3', 'liba52', 'mp2'):
748 message = (False, 'aCodec %s not compatible' % codec)
749 break
751 if (not vInfo['aKbps'] or
752 int(vInfo['aKbps']) > config.getMaxAudioBR(tsn)):
753 message = (False, '%s kbps exceeds max audio bitrate' %
754 vInfo['aKbps'])
755 break
757 audio_lang = config.get_tsn('audio_lang', tsn)
758 if audio_lang:
759 if vInfo['mapAudio'][0][0] != select_audiolang(inFile, tsn)[-3:]:
760 message = (False, '%s preferred audio track exists' %
761 audio_lang)
762 break
764 return message
766 def tivo_compatible_container(vInfo, inFile, mime=''):
767 message = (True, '')
768 container = vInfo.get('container', '')
769 if ((mime == 'video/mp4' and
770 (container != 'mov' or inFile.lower().endswith('.mov'))) or
771 (mime == 'video/bif' and container != 'asf') or
772 (mime == 'video/x-tivo-mpeg-ts' and container != 'mpegts') or
773 (mime in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
774 (container != 'mpeg' or vInfo['vCodec'] == 'mpeg1video'))):
775 message = (False, 'container %s not compatible' % container)
777 return message
779 def mp4_remuxable(inFile, tsn=''):
780 vInfo = video_info(inFile)
781 return tivo_compatible_video(vInfo, tsn, 'video/mp4')[0]
783 def mp4_remux(inFile, basename, tsn='', temp_share_path=''):
784 outFile = inFile + '.pyTivo-temp'
785 newname = basename + '.pyTivo-temp'
787 if temp_share_path:
788 newname = os.path.splitext(os.path.split(basename)[1])[0] + '.mp4.pyTivo-temp'
789 outFile = os.path.join(temp_share_path, newname)
791 if os.path.exists(outFile):
792 return None # ugh!
794 ffmpeg_path = config.get_bin('ffmpeg')
795 fname = unicode(inFile, 'utf-8')
796 oname = unicode(outFile, 'utf-8')
797 if mswindows:
798 fname = fname.encode('cp1252')
799 oname = oname.encode('cp1252')
801 acodec = select_audiocodec(False, inFile, tsn, 'video/mp4')
802 settings = select_buffsize(tsn) + ['-c:v', 'copy'] + acodec
803 if not acodec[1] == 'copy':
804 settings += (select_audiobr(tsn) +
805 select_audiofr(inFile, tsn) +
806 select_audioch(inFile, tsn))
807 settings += [select_audiolang(inFile, tsn),
808 select_ffmpegprams(tsn),
809 '-f', 'mp4']
811 cmd = [ffmpeg_path, '-i', fname] + ' '.join(settings).split() + [oname]
813 debug('transcoding to tivo model ' + tsn[:3] + ' using ffmpeg command:')
814 debug(' '.join(cmd))
816 ffmpeg = subprocess.Popen(cmd)
817 debug('remuxing ' + inFile + ' to ' + outFile)
818 if ffmpeg.wait():
819 debug('error during remuxing')
820 os.remove(outFile)
821 return None
823 return newname
825 def tivo_compatible(inFile, tsn='', mime=''):
826 vInfo = video_info(inFile)
828 message = (True, 'all compatible')
829 if not config.get_bin('ffmpeg'):
830 if mime not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
831 message = (False, 'no ffmpeg')
832 return message
834 while True:
835 vmessage = tivo_compatible_video(vInfo, tsn, mime)
836 if not vmessage[0]:
837 message = vmessage
838 break
840 amessage = tivo_compatible_audio(vInfo, inFile, tsn, mime)
841 if not amessage[0]:
842 message = amessage
843 break
845 cmessage = tivo_compatible_container(vInfo, inFile, mime)
846 if not cmessage[0]:
847 message = cmessage
848 break
850 dmessage = (False, 'All DVD Video must be re-encapsulated')
851 if vobstream.is_dvd(inFile):
852 message = dmessage
854 break
856 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message[0]],
857 message[1], inFile))
858 return message
860 def video_info(inFile, cache=True):
861 vInfo = dict()
862 fname = unicode(inFile, 'utf-8')
863 #mtime = os.path.getmtime(fname)
864 if vobstream.is_dvd(inFile):
865 is_dvd = True
866 mtime = os.stat(os.path.dirname(fname)).st_mtime
867 else:
868 is_dvd = False
869 mtime = os.path.getmtime(fname)
871 if cache:
872 if inFile in info_cache and info_cache[inFile][0] == mtime:
873 debug('CACHE HIT! %s' % inFile)
874 return info_cache[inFile][1]
876 vInfo['fname'] = inFile
877 vInfo['Supported'] = True
879 ffmpeg_path = config.get_bin('ffmpeg')
880 if not ffmpeg_path:
881 if os.path.splitext(inFile)[1].lower() not in ['.mpg', '.mpeg',
882 '.vob', '.tivo', '.ts']:
883 vInfo['Supported'] = False
884 vInfo.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
885 'rawmeta': {}})
886 if cache:
887 info_cache[inFile] = (mtime, vInfo)
888 return vInfo
890 if mswindows:
891 fname = fname.encode('cp1252')
892 #cmd = [ffmpeg_path, '-i', fname]
893 if is_dvd:
894 cmd = [ffmpeg_path, '-i', '-']
895 else:
896 cmd = [ffmpeg_path, '-i', fname]
897 debug('cmd: %s' % cmd)
898 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
899 err_tmp = tempfile.TemporaryFile()
900 ffmpeg = subprocess.Popen(cmd, stderr=err_tmp, stdout=subprocess.PIPE,
901 stdin=subprocess.PIPE)
902 if is_dvd:
903 vobstream.vobstream(True, inFile, ffmpeg, BLOCKSIZE)
905 # wait configured # of seconds: if ffmpeg is not back give up
906 limit = config.getFFmpegWait()
907 if limit:
908 for i in xrange(limit * 20):
909 time.sleep(.05)
910 if not ffmpeg.poll() == None:
911 break
913 if ffmpeg.poll() == None:
914 kill(ffmpeg)
915 vInfo['Supported'] = False
916 if cache:
917 info_cache[inFile] = (mtime, vInfo)
918 return vInfo
919 else:
920 ffmpeg.wait()
922 err_tmp.seek(0)
923 output = err_tmp.read()
924 err_tmp.close()
925 debug('ffmpeg output=%s' % output)
927 attrs = {'container': r'Input #0, ([^,]+),',
928 'vCodec': r'Video: ([^, ]+)', # video codec
929 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
930 'aCodec': r'.*Audio: ([^, ]+)', # audio codec
931 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
932 'subType': r'.*Subtitle: ([a-zA-Z]+)', # subtitle type
933 'mapVideo': r'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
935 for attr in attrs:
936 rezre = re.compile(attrs[attr])
937 x = rezre.search(output)
938 if x:
939 vInfo[attr] = x.group(1)
940 else:
941 if attr in ['container', 'vCodec']:
942 vInfo[attr] = ''
943 vInfo['Supported'] = False
944 else:
945 vInfo[attr] = None
946 debug('failed at ' + attr)
948 rezre = re.compile(r'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
949 x = rezre.search(output)
950 if x:
951 if x.group(3):
952 if x.group(3) == 'stereo':
953 vInfo['aCh'] = 2
954 elif x.group(3) == 'mono':
955 vInfo['aCh'] = 1
956 elif x.group(2):
957 vInfo['aCh'] = int(x.group(1)) + int(x.group(2))
958 elif x.group(1):
959 vInfo['aCh'] = int(x.group(1))
960 else:
961 vInfo['aCh'] = None
962 debug('failed at aCh')
963 else:
964 vInfo['aCh'] = None
965 debug('failed at aCh')
967 rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
968 x = rezre.search(output)
969 if x:
970 vInfo['vWidth'] = int(x.group(1))
971 vInfo['vHeight'] = int(x.group(2))
972 else:
973 vInfo['vWidth'] = ''
974 vInfo['vHeight'] = ''
975 vInfo['Supported'] = False
976 debug('failed at vWidth/vHeight')
978 rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
979 x = rezre.search(output)
980 if x:
981 vInfo['vFps'] = x.group(1)
982 if '.' not in vInfo['vFps']:
983 vInfo['vFps'] += '.00'
985 # Allow override only if it is mpeg2 and frame rate was doubled
986 # to 59.94
988 if vInfo['vCodec'] == 'mpeg2video' and vInfo['vFps'] != '29.97':
989 # First look for the build 7215 version
990 rezre = re.compile(r'.*film source: 29.97.*')
991 x = rezre.search(output.lower())
992 if x:
993 debug('film source: 29.97 setting vFps to 29.97')
994 vInfo['vFps'] = '29.97'
995 else:
996 # for build 8047:
997 rezre = re.compile(r'.*frame rate differs from container ' +
998 r'frame rate: 29.97.*')
999 debug('Bug in VideoReDo')
1000 x = rezre.search(output.lower())
1001 if x:
1002 vInfo['vFps'] = '29.97'
1003 else:
1004 vInfo['vFps'] = ''
1005 vInfo['Supported'] = False
1006 debug('failed at vFps')
1008 durre = re.compile(r'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
1009 d = durre.search(output)
1011 if d:
1012 vInfo['millisecs'] = ((int(d.group(1)) * 3600 +
1013 int(d.group(2)) * 60 +
1014 int(d.group(3))) * 1000 +
1015 int(d.group(4)) * (10 ** (3 - len(d.group(4)))))
1016 else:
1017 vInfo['millisecs'] = 0
1019 if is_dvd:
1020 vInfo['millisecs'] = vobstream.duration(inFile)
1022 # get bitrate of source for tivo compatibility test.
1023 rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
1024 x = rezre.search(output)
1025 if x:
1026 vInfo['kbps'] = x.group(1)
1027 else:
1028 # Fallback method of getting video bitrate
1029 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
1030 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
1031 rezre = re.compile(r'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
1032 r'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
1033 x = rezre.search(output)
1034 if x:
1035 vInfo['kbps'] = x.group(1)
1036 else:
1037 vInfo['kbps'] = None
1038 debug('failed at kbps')
1040 # get par.
1041 rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
1042 x = rezre.search(output)
1043 if x and x.group(1) != "0" and x.group(2) != "0":
1044 vInfo['par1'] = x.group(1) + ':' + x.group(2)
1045 vInfo['par2'] = float(x.group(1)) / float(x.group(2))
1046 else:
1047 vInfo['par1'], vInfo['par2'] = None, None
1049 # get dar.
1050 rezre = re.compile(r'.*Video: .+DAR ([0-9]+):([0-9]+).*')
1051 x = rezre.search(output)
1052 if x and x.group(1) != "0" and x.group(2) != "0":
1053 vInfo['dar1'] = x.group(1) + ':' + x.group(2)
1054 else:
1055 vInfo['dar1'] = None
1057 # get Audio Stream mapping.
1058 rezre = re.compile(r'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
1059 x = rezre.search(output)
1060 amap = []
1061 if x:
1062 for x in rezre.finditer(output):
1063 amap.append((x.group(1), x.group(2) + x.group(3)))
1064 else:
1065 amap.append(('', ''))
1066 debug('failed at mapAudio')
1067 vInfo['mapAudio'] = amap
1069 vInfo['par'] = None
1071 # get Metadata dump (newer ffmpeg).
1072 lines = output.split('\n')
1073 rawmeta = {}
1074 flag = False
1076 for line in lines:
1077 if line.startswith(' Metadata:'):
1078 flag = True
1079 else:
1080 if flag:
1081 if line.startswith(' Duration:'):
1082 flag = False
1083 else:
1084 try:
1085 key, value = [x.strip() for x in line.split(':', 1)]
1086 try:
1087 value = value.decode('utf-8')
1088 except:
1089 if sys.platform == 'darwin':
1090 value = value.decode('macroman')
1091 else:
1092 value = value.decode('cp1252')
1093 rawmeta[key] = [value]
1094 except:
1095 pass
1097 vInfo['rawmeta'] = rawmeta
1099 data = metadata.from_text(inFile)
1100 for key in data:
1101 if key.startswith('Override_'):
1102 vInfo['Supported'] = True
1103 if key.startswith('Override_mapAudio'):
1104 audiomap = dict(vInfo['mapAudio'])
1105 newmap = shlex.split(data[key])
1106 audiomap.update(zip(newmap[::2], newmap[1::2]))
1107 vInfo['mapAudio'] = sorted(audiomap.items(),
1108 key=lambda (k,v): (k,v))
1109 elif key.startswith('Override_millisecs'):
1110 vInfo[key.replace('Override_', '')] = int(data[key])
1111 else:
1112 vInfo[key.replace('Override_', '')] = data[key]
1113 #legacy subtitle support to be removed
1114 elif key.lower() == 'subtitles':
1115 vInfo['subtitles'] = data[key]
1116 elif key.lower() == 'subfile':
1117 vInfo['subFile'] = data[key]
1119 if cache:
1120 info_cache[inFile] = (mtime, vInfo)
1121 debug("; ".join(["%s=%s" % (k, v) for k, v in vInfo.items()]))
1122 return vInfo
1124 def audio_check(inFile, tsn):
1125 cmd_string = ('-y -c:v mpeg2video -r 29.97 -b:v 1000k -c:a copy ' +
1126 select_audiolang(inFile, tsn) + ' -t 00:00:01 -f vob -')
1127 fname = unicode(inFile, 'utf-8')
1128 if mswindows:
1129 fname = fname.encode('cp1252')
1130 cmd = [config.get_bin('ffmpeg'), '-i', fname] + cmd_string.split()
1131 ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
1132 fd, testname = tempfile.mkstemp()
1133 testfile = os.fdopen(fd, 'wb')
1134 try:
1135 shutil.copyfileobj(ffmpeg.stdout, testfile)
1136 except:
1137 kill(ffmpeg)
1138 testfile.close()
1139 vInfo = None
1140 else:
1141 testfile.close()
1142 vInfo = video_info(testname, False)
1143 os.remove(testname)
1144 return vInfo
1146 def supported_format(inFile):
1147 if video_info(inFile)['Supported']:
1148 return True
1149 else:
1150 debug('FALSE, file not supported %s' % inFile)
1151 return False
1153 def kill(popen):
1154 debug('killing pid=%s' % str(popen.pid))
1155 if mswindows:
1156 win32kill(popen.pid)
1157 else:
1158 import os, signal
1159 for i in xrange(3):
1160 debug('sending SIGTERM to pid: %s' % popen.pid)
1161 os.kill(popen.pid, signal.SIGTERM)
1162 time.sleep(.5)
1163 if popen.poll() is not None:
1164 debug('process %s has exited' % popen.pid)
1165 break
1166 else:
1167 while popen.poll() is None:
1168 debug('sending SIGKILL to pid: %s' % popen.pid)
1169 os.kill(popen.pid, signal.SIGKILL)
1170 time.sleep(.5)
1172 def win32kill(pid):
1173 import ctypes
1174 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
1175 ctypes.windll.kernel32.TerminateProcess(handle, -1)
1176 ctypes.windll.kernel32.CloseHandle(handle)
1178 def gcd(a, b):
1179 while b:
1180 a, b = b, a % b
1181 return a
1183 def dvd_size( full_path ):
1184 return vobstream.size( full_path )
1186 def is_dvd( full_path ):
1187 return vobstream.is_dvd( full_path)