2 # Author: Eric von Bayer
3 # Updated By: Luke Broadbent
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
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
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.
56 logger
= logging
.getLogger('pyTivo.video.transcode')
58 info_cache
= lrucache
.LRUCache(1000)
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
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")
86 msg
= msg
.decode('utf8')
88 if sys
.platform
== 'darwin':
89 msg
= msg
.decode('macroman')
91 msg
= msg
.decode('cp1252')
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
)
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
)
119 settings
= vfilter
+ settings
124 ffmpeg_path
= config
.get_bin('ffmpeg')
126 fname
= unicode(inFile
, 'utf-8')
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]:
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
)
151 cmd
= [ffmpeg_path
, '-i', fname
] + settings
152 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=(512 * 1024),
153 stdout
=subprocess
.PIPE
)
156 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
159 ffmpeg_procs
[inFile
] = {'process': ffmpeg
, 'start': 0, 'end': 0,
160 'last_read': time
.time(), 'blocks': []}
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']
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']:
178 kill(proc
['process'])
181 def resume_transfer(inFile
, outFile
, offset
):
182 proc
= ffmpeg_procs
[inFile
]
183 offset
-= proc
['start']
187 for block
in proc
['blocks']:
191 block
= block
[offset
:]
192 outFile
.write('%x\r\n' % len(block
))
194 outFile
.write('\r\n')
198 except Exception, msg
:
202 proc
['start'] = proc
['end']
205 return count
+ transfer_blocks(inFile
, outFile
)
207 def transfer_blocks(inFile
, outFile
):
208 proc
= ffmpeg_procs
[inFile
]
209 blocks
= proc
['blocks']
214 block
= proc
['process'].stdout
.read(BLOCKSIZE
)
215 proc
['last_read'] = time
.time()
216 except Exception, msg
:
219 kill(proc
['process'])
225 except Exception, msg
:
232 proc
['end'] += len(block
)
233 if len(blocks
) > MAXBLOCKS
:
234 proc
['start'] += len(blocks
[0])
238 outFile
.write('%x\r\n' % len(block
))
240 outFile
.write('\r\n')
242 except Exception, msg
:
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
]
254 kill(proc
['process'])
256 reaper
= threading
.Timer(TIMEOUT
, reap_process
, (inFile
,))
257 reapers
[inFile
] = reaper
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
268 proc
['thread'].join()
270 del ffmpeg_procs
[inFile
]
271 reapers
[inFile
].cancel()
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
281 if mime
== 'video/mp4':
282 compatiblecodecs
= ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
285 compatiblecodecs
= ('ac3', 'liba52', 'mp2')
287 if vInfo
['aCodec'] in compatiblecodecs
:
288 aKbps
= vInfo
['aKbps']
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.
297 vInfoQuery
= audio_check(inFile
, tsn
)
298 if vInfoQuery
== None:
302 aKbps
= vInfoQuery
['aKbps']
303 aCh
= vInfoQuery
['aCh']
306 if aKbps
and int(aKbps
) <= config
.getMaxAudioBR(tsn
):
307 # compatible codec and bitrate, do not reencode audio
309 if vInfo
['aCodec'] != 'ac3' and (aCh
== None or aCh
> 2):
311 val
= ['-c:a', codec
]
312 if not (codec
== 'copy' and codectype
== 'mpeg2video'):
313 val
.append('-copyts')
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']
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')
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:
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
)
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
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
)
362 # don't let FFmpeg auto select audio stream, pyTivo defaults to
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')
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
:
378 def select_videocodec(inFile
, tsn
, mime
=''):
380 vInfo
= video_info(inFile
)
381 if tivo_compatible_video(vInfo
, tsn
, mime
)[0]:
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']
390 codec
+= ['mpeg2video', '-pix_fmt', 'yuv420p'] # default
393 def select_videofilter(inFile
):
394 vInfo
= video_info(inFile
)
395 #legacy subtitle support to be removed
396 subtitles
= vInfo
.get('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')
412 if subfile
== inFile
:
413 #TODO need to look at handling when there is more than one subtitle track
414 subType
= vInfo
.get('subType')
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
)
428 elif subType
== 'ass':
429 vfilter
= ['-vf', 'ass=\\\'%s\\\'' % subfile_escape
]
430 logger
.info('video filter: %s' % vfilter
)
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'])
443 video_str
-= int(vInfo
['aKbps'])
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,
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
)
468 def select_format(tsn
, mime
):
469 if mime
== 'video/x-tivo-mpeg-ts':
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
)
480 topPadding
= (TIVO_HEIGHT
- endHeight
) / 2
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
))
491 leftPadding
= (TIVO_WIDTH
- endWidth
) / 2
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
)
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'):
523 elif vInfo
.get('par'):
524 par2
= float(vInfo
['par'])
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
:
538 # adjust for pixel aspect ratio, if set
541 return ['-s', '%dx%d' % (vInfo
['vWidth'],
542 math
.ceil(vInfo
['vHeight'] / npar
))]
544 # FFMPEG expects width to be a multiple of two
545 return ['-s', '%dx%d' % (math
.ceil(vInfo
['vWidth']*npar
/2.0)*2,
548 if vInfo
['vHeight'] <= TIVO_HEIGHT
:
549 # pass all resolutions to S3, except heights greater than
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
)):
578 return ['-aspect', aspect
, '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
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
,
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
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')
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') %
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') %
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
622 settings
.append('4:3')
623 multiplier
= multiplier4by3
624 settings
+= pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
,
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.
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
))
641 def tivo_compatible_video(vInfo
, tsn
, mime
=''):
644 if select_videofilter(vInfo
.get('fname')):
645 message
= (False, 'Subtitle hard-coding requires reencoding')
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
)
657 if mime
== 'video/bif':
659 message
= (False, 'vCodec %s not compatible' % codec
)
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
)
669 if codec
not in ('mpeg2video', 'mpeg1video'):
670 message
= (False, 'vCodec %s not compatible' % codec
)
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' %
681 message
= (False, '%s kbps not supported' % vInfo
['kbps'])
684 if config
.isHDtivo(tsn
):
685 # HD Tivo detected, skipping remaining tests.
688 if not vInfo
['vFps'] in ['29.97', '59.94']:
689 message
= (False, '%s vFps, should be 29.97' % vInfo
['vFps'])
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'])
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
)
707 def tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
=''):
710 codec
= vInfo
.get('aCodec', '')
713 debug('No audio stream detected')
716 if mime
== 'video/mp4':
717 if codec
not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
719 message
= (False, 'aCodec %s not compatible' % codec
)
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']))
725 audio_lang
= config
.get_tsn('audio_lang', tsn
)
727 if vInfo
['mapAudio'][0][0] != select_audiolang(inFile
, tsn
)[-3:]:
728 message
= (False, '%s preferred audio track exists' %
732 if mime
== 'video/bif':
734 message
= (False, 'aCodec %s not compatible' % codec
)
738 if inFile
[-5:].lower() == '.tivo':
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
)
747 if codec
not in ('ac3', 'liba52', 'mp2'):
748 message
= (False, 'aCodec %s not compatible' % codec
)
751 if (not vInfo
['aKbps'] or
752 int(vInfo
['aKbps']) > config
.getMaxAudioBR(tsn
)):
753 message
= (False, '%s kbps exceeds max audio bitrate' %
757 audio_lang
= config
.get_tsn('audio_lang', tsn
)
759 if vInfo
['mapAudio'][0][0] != select_audiolang(inFile
, tsn
)[-3:]:
760 message
= (False, '%s preferred audio track exists' %
766 def tivo_compatible_container(vInfo
, inFile
, mime
=''):
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
)
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'
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
):
794 ffmpeg_path
= config
.get_bin('ffmpeg')
795 fname
= unicode(inFile
, 'utf-8')
796 oname
= unicode(outFile
, 'utf-8')
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
),
811 cmd
= [ffmpeg_path
, '-i', fname
] + ' '.join(settings
).split() + [oname
]
813 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
816 ffmpeg
= subprocess
.Popen(cmd
)
817 debug('remuxing ' + inFile
+ ' to ' + outFile
)
819 debug('error during remuxing')
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')
835 vmessage
= tivo_compatible_video(vInfo
, tsn
, mime
)
840 amessage
= tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
)
845 cmessage
= tivo_compatible_container(vInfo
, inFile
, mime
)
850 dmessage
= (False, 'All DVD Video must be re-encapsulated')
851 if vobstream
.is_dvd(inFile
):
856 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message
[0]],
860 def video_info(inFile
, cache
=True):
862 fname
= unicode(inFile
, 'utf-8')
863 #mtime = os.path.getmtime(fname)
864 if vobstream
.is_dvd(inFile
):
866 mtime
= os
.stat(os
.path
.dirname(fname
)).st_mtime
869 mtime
= os
.path
.getmtime(fname
)
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')
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,
887 info_cache
[inFile
] = (mtime
, vInfo
)
891 fname
= fname
.encode('cp1252')
892 #cmd = [ffmpeg_path, '-i', fname]
894 cmd
= [ffmpeg_path
, '-i', '-']
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
)
903 vobstream
.vobstream(True, inFile
, ffmpeg
, BLOCKSIZE
)
905 # wait configured # of seconds: if ffmpeg is not back give up
906 limit
= config
.getFFmpegWait()
908 for i
in xrange(limit
* 20):
910 if not ffmpeg
.poll() == None:
913 if ffmpeg
.poll() == None:
915 vInfo
['Supported'] = False
917 info_cache
[inFile
] = (mtime
, vInfo
)
923 output
= err_tmp
.read()
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
936 rezre
= re
.compile(attrs
[attr
])
937 x
= rezre
.search(output
)
939 vInfo
[attr
] = x
.group(1)
941 if attr
in ['container', 'vCodec']:
943 vInfo
['Supported'] = False
946 debug('failed at ' + attr
)
948 rezre
= re
.compile(r
'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
949 x
= rezre
.search(output
)
952 if x
.group(3) == 'stereo':
954 elif x
.group(3) == 'mono':
957 vInfo
['aCh'] = int(x
.group(1)) + int(x
.group(2))
959 vInfo
['aCh'] = int(x
.group(1))
962 debug('failed at aCh')
965 debug('failed at aCh')
967 rezre
= re
.compile(r
'.*Video: .+, (\d+)x(\d+)[, ].*')
968 x
= rezre
.search(output
)
970 vInfo
['vWidth'] = int(x
.group(1))
971 vInfo
['vHeight'] = int(x
.group(2))
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
)
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
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())
993 debug('film source: 29.97 setting vFps to 29.97')
994 vInfo
['vFps'] = '29.97'
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())
1002 vInfo
['vFps'] = '29.97'
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
)
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)))))
1017 vInfo
['millisecs'] = 0
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
)
1026 vInfo
['kbps'] = x
.group(1)
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
)
1035 vInfo
['kbps'] = x
.group(1)
1037 vInfo
['kbps'] = None
1038 debug('failed at kbps')
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))
1047 vInfo
['par1'], vInfo
['par2'] = None, None
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)
1055 vInfo
['dar1'] = None
1057 # get Audio Stream mapping.
1058 rezre
= re
.compile(r
'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
1059 x
= rezre
.search(output
)
1062 for x
in rezre
.finditer(output
):
1063 amap
.append((x
.group(1), x
.group(2) + x
.group(3)))
1065 amap
.append(('', ''))
1066 debug('failed at mapAudio')
1067 vInfo
['mapAudio'] = amap
1071 # get Metadata dump (newer ffmpeg).
1072 lines
= output
.split('\n')
1077 if line
.startswith(' Metadata:'):
1081 if line
.startswith(' Duration:'):
1085 key
, value
= [x
.strip() for x
in line
.split(':', 1)]
1087 value
= value
.decode('utf-8')
1089 if sys
.platform
== 'darwin':
1090 value
= value
.decode('macroman')
1092 value
= value
.decode('cp1252')
1093 rawmeta
[key
] = [value
]
1097 vInfo
['rawmeta'] = rawmeta
1099 data
= metadata
.from_text(inFile
)
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
])
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
]
1120 info_cache
[inFile
] = (mtime
, vInfo
)
1121 debug("; ".join(["%s=%s" % (k
, v
) for k
, v
in vInfo
.items()]))
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')
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')
1135 shutil
.copyfileobj(ffmpeg
.stdout
, testfile
)
1142 vInfo
= video_info(testname
, False)
1146 def supported_format(inFile
):
1147 if video_info(inFile
)['Supported']:
1150 debug('FALSE, file not supported %s' % inFile
)
1154 debug('killing pid=%s' % str(popen
.pid
))
1156 win32kill(popen
.pid
)
1160 debug('sending SIGTERM to pid: %s' % popen
.pid
)
1161 os
.kill(popen
.pid
, signal
.SIGTERM
)
1163 if popen
.poll() is not None:
1164 debug('process %s has exited' % popen
.pid
)
1167 while popen
.poll() is None:
1168 debug('sending SIGKILL to pid: %s' % popen
.pid
)
1169 os
.kill(popen
.pid
, signal
.SIGKILL
)
1174 handle
= ctypes
.windll
.kernel32
.OpenProcess(1, False, pid
)
1175 ctypes
.windll
.kernel32
.TerminateProcess(handle
, -1)
1176 ctypes
.windll
.kernel32
.CloseHandle(handle
)
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
)