18 logger
= logging
.getLogger('pyTivo.video.transcode')
20 info_cache
= lrucache
.LRUCache(1000)
24 GOOD_MPEG_FPS
= ['23.98', '24.00', '25.00', '29.97',
25 '30.00', '50.00', '59.94', '60.00']
27 BLOCKSIZE
= 512 * 1024
32 # subprocess is broken for me on windows so super hack
33 def patchSubprocess():
34 o
= subprocess
.Popen
._make
_inheritable
36 def _make_inheritable(self
, handle
):
37 if not handle
: return subprocess
.GetCurrentProcess()
38 return o(self
, handle
)
40 subprocess
.Popen
._make
_inheritable
= _make_inheritable
41 mswindows
= (sys
.platform
== "win32")
48 msg
= msg
.decode('utf8')
50 if sys
.platform
== 'darwin':
51 msg
= msg
.decode('macroman')
53 msg
= msg
.decode('cp1252')
56 def transcode(isQuery
, inFile
, outFile
, tsn
='', mime
='', thead
=''):
57 vcodec
= select_videocodec(inFile
, tsn
, mime
)
59 settings
= select_buffsize(tsn
) + vcodec
60 if not vcodec
[1] == 'copy':
61 settings
+= (select_videobr(inFile
, tsn
) +
62 select_maxvideobr(tsn
) +
63 select_videofps(inFile
, tsn
) +
64 select_aspect(inFile
, tsn
))
66 acodec
= select_audiocodec(isQuery
, inFile
, tsn
)
68 if not acodec
[1] == 'copy':
69 settings
+= (select_audiobr(tsn
) +
70 select_audiofr(inFile
, tsn
) +
71 select_audioch(inFile
, tsn
))
73 settings
+= [select_audiolang(inFile
, tsn
),
74 select_ffmpegprams(tsn
)]
76 settings
+= select_format(tsn
, mime
)
78 settings
= ' '.join(settings
).split()
82 ffmpeg_path
= config
.get_bin('ffmpeg')
84 fname
= unicode(inFile
, 'utf-8')
86 fname
= fname
.encode('cp1252')
88 if inFile
[-5:].lower() == '.tivo':
89 tivodecode_path
= config
.get_bin('tivodecode')
90 tivo_mak
= config
.get_server('tivo_mak')
91 tcmd
= [tivodecode_path
, '-m', tivo_mak
, fname
]
92 tivodecode
= subprocess
.Popen(tcmd
, stdout
=subprocess
.PIPE
,
94 if tivo_compatible(inFile
, tsn
)[0]:
98 cmd
= [ffmpeg_path
, '-i', '-'] + settings
99 ffmpeg
= subprocess
.Popen(cmd
, stdin
=tivodecode
.stdout
,
100 stdout
=subprocess
.PIPE
,
101 bufsize
=(512 * 1024))
103 cmd
= [ffmpeg_path
, '-i', fname
] + settings
104 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=(512 * 1024),
105 stdout
=subprocess
.PIPE
)
108 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
111 ffmpeg_procs
[inFile
] = {'process': ffmpeg
, 'start': 0, 'end': 0,
112 'last_read': time
.time(), 'blocks': []}
114 ffmpeg_procs
[inFile
]['blocks'].append(thead
)
116 return resume_transfer(inFile
, outFile
, 0)
118 def is_resumable(inFile
, offset
):
119 if inFile
in ffmpeg_procs
:
120 proc
= ffmpeg_procs
[inFile
]
121 if proc
['start'] <= offset
< proc
['end']:
125 kill(proc
['process'])
128 def resume_transfer(inFile
, outFile
, offset
):
129 proc
= ffmpeg_procs
[inFile
]
130 offset
-= proc
['start']
134 for block
in proc
['blocks']:
138 block
= block
[offset
:]
139 outFile
.write('%x\r\n' % len(block
))
141 outFile
.write('\r\n')
145 except Exception, msg
:
149 proc
['start'] = proc
['end']
152 return count
+ transfer_blocks(inFile
, outFile
)
154 def transfer_blocks(inFile
, outFile
):
155 proc
= ffmpeg_procs
[inFile
]
156 blocks
= proc
['blocks']
161 block
= proc
['process'].stdout
.read(BLOCKSIZE
)
162 proc
['last_read'] = time
.time()
163 except Exception, msg
:
166 kill(proc
['process'])
172 except Exception, msg
:
179 proc
['end'] += len(block
)
180 if len(blocks
) > MAXBLOCKS
:
181 proc
['start'] += len(blocks
[0])
185 outFile
.write('%x\r\n' % len(block
))
187 outFile
.write('\r\n')
189 except Exception, msg
:
195 def reap_process(inFile
):
196 if ffmpeg_procs
and inFile
in ffmpeg_procs
:
197 proc
= ffmpeg_procs
[inFile
]
198 if proc
['last_read'] + TIMEOUT
< time
.time():
199 del ffmpeg_procs
[inFile
]
201 kill(proc
['process'])
203 reaper
= threading
.Timer(TIMEOUT
, reap_process
, (inFile
,))
204 reapers
[inFile
] = reaper
208 del ffmpeg_procs
[inFile
]
209 reapers
[inFile
].cancel()
212 def select_audiocodec(isQuery
, inFile
, tsn
='', mime
=''):
213 if inFile
[-5:].lower() == '.tivo':
214 return ['-c:a', 'copy']
215 vInfo
= video_info(inFile
)
216 codectype
= vInfo
['vCodec']
217 # Default, compatible with all TiVo's
219 if mime
== 'video/mp4':
220 compatiblecodecs
= ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
223 compatiblecodecs
= ('ac3', 'liba52', 'mp2')
225 if vInfo
['aCodec'] in compatiblecodecs
:
226 aKbps
= vInfo
['aKbps']
229 if vInfo
['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
230 # along with the channel check below this should
231 # pass any AAC audio that has undefined 'aKbps' and
232 # is <= 2 channels. Should be TiVo compatible.
235 vInfoQuery
= audio_check(inFile
, tsn
)
236 if vInfoQuery
== None:
240 aKbps
= vInfoQuery
['aKbps']
241 aCh
= vInfoQuery
['aCh']
244 if aKbps
and int(aKbps
) <= config
.getMaxAudioBR(tsn
):
245 # compatible codec and bitrate, do not reencode audio
247 if vInfo
['aCodec'] != 'ac3' and (aCh
== None or aCh
> 2):
249 val
= ['-c:a', codec
]
250 if not (codec
== 'copy' and codectype
== 'mpeg2video'):
251 val
.append('-copyts')
254 def select_audiofr(inFile
, tsn
):
255 freq
= '48000' # default
256 vInfo
= video_info(inFile
)
257 if vInfo
['aFreq'] == '44100':
258 # compatible frequency
259 freq
= vInfo
['aFreq']
262 def select_audioch(inFile
, tsn
):
263 # AC-3 max channels is 5.1
264 if video_info(inFile
)['aCh'] > 6:
265 debug('Too many audio channels for AC-3, using 5.1 instead')
269 def select_audiolang(inFile
, tsn
):
270 vInfo
= video_info(inFile
)
271 audio_lang
= config
.get_tsn('audio_lang', tsn
)
272 debug('audio_lang: %s' % audio_lang
)
273 if vInfo
['mapAudio']:
274 # default to first detected audio stream to begin with
275 stream
= vInfo
['mapAudio'][0][0]
276 debug('set first detected audio stream by default: %s' % stream
)
277 if audio_lang
!= None and vInfo
['mapVideo'] != None:
279 langmatch_prev
= vInfo
['mapAudio'][:]
280 for lang
in audio_lang
.replace(' ', '').lower().split(','):
281 debug('matching lang: %s' % lang
)
282 for s
, l
in langmatch_prev
:
283 if lang
in s
+ l
.replace(' ', '').lower():
284 debug('matched: %s' % s
+ l
.replace(' ', '').lower())
285 langmatch_curr
.append((s
, l
))
286 # if only 1 item matched we're done
287 if len(langmatch_curr
) == 1:
288 stream
= langmatch_curr
[0][0]
289 debug('found exactly one match: %s' % stream
)
291 # if more than 1 item matched copy the curr area to the prev
292 # array we only need to look at the new shorter list from
294 elif len(langmatch_curr
) > 1:
295 langmatch_prev
= langmatch_curr
[:]
296 # default to the first item matched thus far
297 stream
= langmatch_curr
[0][0]
298 debug('remember first match: %s' % stream
)
300 # don't let FFmpeg auto select audio stream, pyTivo defaults to
303 debug('selected audio stream: %s' % stream
)
304 return '-map ' + vInfo
['mapVideo'] + ' -map ' + stream
305 # if no audio is found
306 debug('selected audio stream: None detected')
309 def select_videofps(inFile
, tsn
):
310 vInfo
= video_info(inFile
)
311 fps
= ['-r', '29.97'] # default
312 if config
.isHDtivo(tsn
) and vInfo
['vFps'] in GOOD_MPEG_FPS
:
316 def select_videocodec(inFile
, tsn
, mime
=''):
318 vInfo
= video_info(inFile
)
319 if tivo_compatible_video(vInfo
, tsn
, mime
)[0]:
321 if (mime
== 'video/x-tivo-mpeg-ts'):
322 org_codec
= vInfo
.get('vCodec', '')
323 if org_codec
== 'h264':
324 codec
+= ['-bsf', 'h264_mp4toannexb']
325 elif org_codec
== 'hevc':
326 codec
+= ['-bsf', 'hevc_mp4toannexb']
328 codec
+= ['mpeg2video', '-pix_fmt', 'yuv420p'] # default
331 def select_videobr(inFile
, tsn
, mime
=''):
332 return ['-b:v', str(select_videostr(inFile
, tsn
, mime
) / 1000) + 'k']
334 def select_videostr(inFile
, tsn
, mime
=''):
335 vInfo
= video_info(inFile
)
336 if tivo_compatible_video(vInfo
, tsn
, mime
)[0]:
337 video_str
= int(vInfo
['kbps'])
339 video_str
-= int(vInfo
['aKbps'])
342 video_str
= config
.strtod(config
.getVideoBR(tsn
))
343 if config
.isHDtivo(tsn
) and vInfo
['kbps']:
344 video_str
= max(video_str
, int(vInfo
['kbps']) * 1000)
345 video_str
= int(min(config
.strtod(config
.getMaxVideoBR(tsn
)) * 0.95,
349 def select_audiobr(tsn
):
350 return ['-b:a', config
.getAudioBR(tsn
)]
352 def select_maxvideobr(tsn
):
353 return ['-maxrate', config
.getMaxVideoBR(tsn
)]
355 def select_buffsize(tsn
):
356 return ['-bufsize', config
.getBuffSize(tsn
)]
358 def select_ffmpegprams(tsn
):
359 params
= config
.getFFmpegPrams(tsn
)
364 def select_format(tsn
, mime
):
365 if mime
== 'video/x-tivo-mpeg-ts':
369 return ['-f', fmt
, '-']
371 def pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier
, vInfo
):
372 endHeight
= int(((TIVO_WIDTH
* vInfo
['vHeight']) /
373 vInfo
['vWidth']) * multiplier
)
376 topPadding
= (TIVO_HEIGHT
- endHeight
) / 2
379 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH
,
380 endHeight
, TIVO_WIDTH
, TIVO_HEIGHT
, topPadding
)]
382 def pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier
, vInfo
):
383 endWidth
= int((TIVO_HEIGHT
* vInfo
['vWidth']) /
384 (vInfo
['vHeight'] * multiplier
))
387 leftPadding
= (TIVO_WIDTH
- endWidth
) / 2
390 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth
,
391 TIVO_HEIGHT
, TIVO_WIDTH
, TIVO_HEIGHT
, leftPadding
)]
393 def select_aspect(inFile
, tsn
= ''):
394 TIVO_WIDTH
= config
.getTivoWidth(tsn
)
395 TIVO_HEIGHT
= config
.getTivoHeight(tsn
)
397 vInfo
= video_info(inFile
)
399 debug('tsn: %s' % tsn
)
401 aspect169
= config
.get169Setting(tsn
)
403 debug('aspect169: %s' % aspect169
)
405 optres
= config
.getOptres(tsn
)
407 debug('optres: %s' % optres
)
410 optHeight
= config
.nearestTivoHeight(vInfo
['vHeight'])
411 optWidth
= config
.nearestTivoWidth(vInfo
['vWidth'])
412 if optHeight
< TIVO_HEIGHT
:
413 TIVO_HEIGHT
= optHeight
414 if optWidth
< TIVO_WIDTH
:
415 TIVO_WIDTH
= optWidth
417 if vInfo
.get('par2'):
419 elif vInfo
.get('par'):
420 par2
= float(vInfo
['par'])
425 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
426 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile
, vInfo
['vCodec'],
427 vInfo
['vWidth'], vInfo
['vHeight'], vInfo
['vFps'],
428 vInfo
['millisecs'], TIVO_HEIGHT
, TIVO_WIDTH
))
430 if config
.isHDtivo(tsn
) and not optres
:
434 # adjust for pixel aspect ratio, if set
437 return ['-s', '%dx%d' % (vInfo
['vWidth'],
438 math
.ceil(vInfo
['vHeight'] / npar
))]
440 # FFMPEG expects width to be a multiple of two
441 return ['-s', '%dx%d' % (math
.ceil(vInfo
['vWidth']*npar
/2.0)*2,
444 if vInfo
['vHeight'] <= TIVO_HEIGHT
:
445 # pass all resolutions to S3, except heights greater than
448 # else, resize video.
450 d
= gcd(vInfo
['vHeight'], vInfo
['vWidth'])
451 rheight
, rwidth
= vInfo
['vHeight'] / d
, vInfo
['vWidth'] / d
452 debug('rheight=%s rwidth=%s' % (rheight
, rwidth
))
454 if (rwidth
, rheight
) in [(1, 1)] and vInfo
['par1'] == '8:9':
455 debug('File + PAR is within 4:3.')
456 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
458 elif ((rwidth
, rheight
) in [(4, 3), (10, 11), (15, 11), (59, 54),
459 (59, 72), (59, 36), (59, 54)] or
460 vInfo
['dar1'] == '4:3'):
461 debug('File is within 4:3 list.')
462 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
464 elif (((rwidth
, rheight
) in [(16, 9), (20, 11), (40, 33), (118, 81),
465 (59, 27)] or vInfo
['dar1'] == '16:9')
466 and (aspect169
or config
.get169Letterbox(tsn
))):
467 debug('File is within 16:9 list and 16:9 allowed.')
469 if config
.get169Blacklist(tsn
) or (aspect169
and
470 config
.get169Letterbox(tsn
)):
474 return ['-aspect', aspect
, '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
477 settings
= ['-aspect']
479 multiplier16by9
= (16.0 * TIVO_HEIGHT
) / (9.0 * TIVO_WIDTH
) / par2
480 multiplier4by3
= (4.0 * TIVO_HEIGHT
) / (3.0 * TIVO_WIDTH
) / par2
481 ratio
= vInfo
['vWidth'] * 100 * par2
/ vInfo
['vHeight']
482 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2
, ratio
,
485 # If video is wider than 4:3 add top and bottom padding
487 if ratio
> 133: # Might be 16:9 file, or just need padding on
490 if aspect169
and ratio
> 135: # If file would fall in 4:3
491 # assume it is supposed to be 4:3
493 if (config
.get169Blacklist(tsn
) or
494 config
.get169Letterbox(tsn
)):
495 settings
.append('4:3')
497 settings
.append('16:9')
499 if ratio
> 177: # too short needs padding top and bottom
500 settings
+= pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
,
501 multiplier16by9
, vInfo
)
502 debug(('16:9 aspect allowed, file is wider ' +
503 'than 16:9 padding top and bottom\n%s') %
506 else: # too skinny needs padding on left and right.
507 settings
+= pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
,
508 multiplier16by9
, vInfo
)
509 debug(('16:9 aspect allowed, file is narrower ' +
510 'than 16:9 padding left and right\n%s') %
513 else: # this is a 4:3 file or 16:9 output not allowed
514 if ratio
> 135 and config
.get169Letterbox(tsn
):
515 settings
.append('16:9')
516 multiplier
= multiplier16by9
518 settings
.append('4:3')
519 multiplier
= multiplier4by3
520 settings
+= pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
,
522 debug(('File is wider than 4:3 padding ' +
523 'top and bottom\n%s') % ' '.join(settings
))
525 # If video is taller than 4:3 add left and right padding, this
526 # is rare. All of these files will always be sent in an aspect
527 # ratio of 4:3 since they are so narrow.
530 settings
.append('4:3')
531 settings
+= pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier4by3
, vInfo
)
532 debug('File is taller than 4:3 padding left and right\n%s'
533 % ' '.join(settings
))
537 def tivo_compatible_video(vInfo
, tsn
, mime
=''):
540 codec
= vInfo
.get('vCodec', '')
541 is4k
= config
.is4Ktivo(tsn
) and codec
== 'hevc'
542 if mime
== 'video/mp4':
543 if not (is4k
or codec
== 'h264'):
544 message
= (False, 'vCodec %s not compatible' % codec
)
548 if mime
== 'video/bif':
550 message
= (False, 'vCodec %s not compatible' % codec
)
554 if mime
== 'video/x-tivo-mpeg-ts':
555 if not (is4k
or codec
in ('h264', 'mpeg2video')):
556 message
= (False, 'vCodec %s not compatible' % codec
)
560 if codec
not in ('mpeg2video', 'mpeg1video'):
561 message
= (False, 'vCodec %s not compatible' % codec
)
564 if vInfo
['kbps'] != None:
565 abit
= max('0', vInfo
['aKbps'])
566 if (int(vInfo
['kbps']) - int(abit
) >
567 config
.strtod(config
.getMaxVideoBR(tsn
)) / 1000):
568 message
= (False, '%s kbps exceeds max video bitrate' %
572 message
= (False, '%s kbps not supported' % vInfo
['kbps'])
575 if config
.isHDtivo(tsn
):
576 # HD Tivo detected, skipping remaining tests.
579 if not vInfo
['vFps'] in ['29.97', '59.94']:
580 message
= (False, '%s vFps, should be 29.97' % vInfo
['vFps'])
583 if ((config
.get169Blacklist(tsn
) and not config
.get169Setting(tsn
))
584 or (config
.get169Letterbox(tsn
) and config
.get169Setting(tsn
))):
585 if vInfo
['dar1'] and vInfo
['dar1'] not in ('4:3', '8:9', '880:657'):
586 message
= (False, ('DAR %s not supported ' +
587 'by BLACKLIST_169 tivos') % vInfo
['dar1'])
590 mode
= (vInfo
['vWidth'], vInfo
['vHeight'])
591 if mode
not in [(720, 480), (704, 480), (544, 480),
592 (528, 480), (480, 480), (352, 480), (352, 240)]:
593 message
= (False, '%s x %s not in supported modes' % mode
)
598 def tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
=''):
601 codec
= vInfo
.get('aCodec', '')
604 debug('No audio stream detected')
607 if mime
== 'video/mp4':
608 if codec
not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
610 message
= (False, 'aCodec %s not compatible' % codec
)
612 if vInfo
['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo
['aCh'] == None or vInfo
['aCh'] > 2):
613 message
= (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec
, vInfo
['aCh']))
616 audio_lang
= config
.get_tsn('audio_lang', tsn
)
618 if vInfo
['mapAudio'][0][0] != select_audiolang(inFile
, tsn
)[-3:]:
619 message
= (False, '%s preferred audio track exists' %
623 if mime
== 'video/bif':
625 message
= (False, 'aCodec %s not compatible' % codec
)
629 if inFile
[-5:].lower() == '.tivo':
632 if mime
== 'video/x-tivo-mpeg-ts':
633 if codec
not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
634 message
= (False, 'aCodec %s not compatible' % codec
)
638 if codec
not in ('ac3', 'liba52', 'mp2'):
639 message
= (False, 'aCodec %s not compatible' % codec
)
642 if (not vInfo
['aKbps'] or
643 int(vInfo
['aKbps']) > config
.getMaxAudioBR(tsn
)):
644 message
= (False, '%s kbps exceeds max audio bitrate' %
648 audio_lang
= config
.get_tsn('audio_lang', tsn
)
650 if vInfo
['mapAudio'][0][0] != select_audiolang(inFile
, tsn
)[-3:]:
651 message
= (False, '%s preferred audio track exists' %
657 def tivo_compatible_container(vInfo
, inFile
, mime
=''):
659 container
= vInfo
.get('container', '')
660 if ((mime
== 'video/mp4' and
661 (container
!= 'mov' or inFile
.lower().endswith('.mov'))) or
662 (mime
== 'video/bif' and container
!= 'asf') or
663 (mime
== 'video/x-tivo-mpeg-ts' and container
!= 'mpegts') or
664 (mime
in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
665 (container
!= 'mpeg' or vInfo
['vCodec'] == 'mpeg1video'))):
666 message
= (False, 'container %s not compatible' % container
)
670 def mp4_remuxable(inFile
, tsn
=''):
671 vInfo
= video_info(inFile
)
672 return tivo_compatible_video(vInfo
, tsn
, 'video/mp4')[0]
674 def mp4_remux(inFile
, basename
, tsn
=''):
675 outFile
= inFile
+ '.pyTivo-temp'
676 newname
= basename
+ '.pyTivo-temp'
677 if os
.path
.exists(outFile
):
680 ffmpeg_path
= config
.get_bin('ffmpeg')
681 fname
= unicode(inFile
, 'utf-8')
682 oname
= unicode(outFile
, 'utf-8')
684 fname
= fname
.encode('cp1252')
685 oname
= oname
.encode('cp1252')
687 acodec
= select_audiocodec(False, inFile
, tsn
, 'video/mp4')
688 settings
= select_buffsize(tsn
) + ['-c:v', 'copy'] + acodec
689 if not acodec
[1] == 'copy':
690 settings
+= (select_audiobr(tsn
) +
691 select_audiofr(inFile
, tsn
) +
692 select_audioch(inFile
, tsn
))
693 settings
+= [select_audiolang(inFile
, tsn
),
694 select_ffmpegprams(tsn
),
697 cmd
= [ffmpeg_path
, '-i', fname
] + ' '.join(settings
).split() + [oname
]
699 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
702 ffmpeg
= subprocess
.Popen(cmd
)
703 debug('remuxing ' + inFile
+ ' to ' + outFile
)
705 debug('error during remuxing')
711 def tivo_compatible(inFile
, tsn
='', mime
=''):
712 vInfo
= video_info(inFile
)
714 message
= (True, 'all compatible')
715 if not config
.get_bin('ffmpeg'):
716 if mime
not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
717 message
= (False, 'no ffmpeg')
721 vmessage
= tivo_compatible_video(vInfo
, tsn
, mime
)
726 amessage
= tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
)
731 cmessage
= tivo_compatible_container(vInfo
, inFile
, mime
)
737 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message
[0]],
741 def video_info(inFile
, cache
=True):
743 fname
= unicode(inFile
, 'utf-8')
744 mtime
= os
.path
.getmtime(fname
)
746 if inFile
in info_cache
and info_cache
[inFile
][0] == mtime
:
747 debug('CACHE HIT! %s' % inFile
)
748 return info_cache
[inFile
][1]
750 vInfo
['Supported'] = True
752 ffmpeg_path
= config
.get_bin('ffmpeg')
754 if os
.path
.splitext(inFile
)[1].lower() not in ['.mpg', '.mpeg',
756 vInfo
['Supported'] = False
757 vInfo
.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
760 info_cache
[inFile
] = (mtime
, vInfo
)
764 fname
= fname
.encode('cp1252')
765 cmd
= [ffmpeg_path
, '-i', fname
]
766 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
767 err_tmp
= tempfile
.TemporaryFile()
768 ffmpeg
= subprocess
.Popen(cmd
, stderr
=err_tmp
, stdout
=subprocess
.PIPE
,
769 stdin
=subprocess
.PIPE
)
771 # wait configured # of seconds: if ffmpeg is not back give up
772 limit
= config
.getFFmpegWait()
774 for i
in xrange(limit
* 20):
776 if not ffmpeg
.poll() == None:
779 if ffmpeg
.poll() == None:
781 vInfo
['Supported'] = False
783 info_cache
[inFile
] = (mtime
, vInfo
)
789 output
= err_tmp
.read()
791 debug('ffmpeg output=%s' % output
)
793 attrs
= {'container': r
'Input #0, ([^,]+),',
794 'vCodec': r
'Video: ([^, ]+)', # video codec
795 'aKbps': r
'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
796 'aCodec': r
'.*Audio: ([^, ]+)', # audio codec
797 'aFreq': r
'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
798 'mapVideo': r
'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
801 rezre
= re
.compile(attrs
[attr
])
802 x
= rezre
.search(output
)
804 vInfo
[attr
] = x
.group(1)
806 if attr
in ['container', 'vCodec']:
808 vInfo
['Supported'] = False
811 debug('failed at ' + attr
)
813 rezre
= re
.compile(r
'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
814 x
= rezre
.search(output
)
817 if x
.group(3) == 'stereo':
819 elif x
.group(3) == 'mono':
822 vInfo
['aCh'] = int(x
.group(1)) + int(x
.group(2))
824 vInfo
['aCh'] = int(x
.group(1))
827 debug('failed at aCh')
830 debug('failed at aCh')
832 rezre
= re
.compile(r
'.*Video: .+, (\d+)x(\d+)[, ].*')
833 x
= rezre
.search(output
)
835 vInfo
['vWidth'] = int(x
.group(1))
836 vInfo
['vHeight'] = int(x
.group(2))
839 vInfo
['vHeight'] = ''
840 vInfo
['Supported'] = False
841 debug('failed at vWidth/vHeight')
843 rezre
= re
.compile(r
'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
844 x
= rezre
.search(output
)
846 vInfo
['vFps'] = x
.group(1)
847 if '.' not in vInfo
['vFps']:
848 vInfo
['vFps'] += '.00'
850 # Allow override only if it is mpeg2 and frame rate was doubled
853 if vInfo
['vCodec'] == 'mpeg2video' and vInfo
['vFps'] != '29.97':
854 # First look for the build 7215 version
855 rezre
= re
.compile(r
'.*film source: 29.97.*')
856 x
= rezre
.search(output
.lower())
858 debug('film source: 29.97 setting vFps to 29.97')
859 vInfo
['vFps'] = '29.97'
862 rezre
= re
.compile(r
'.*frame rate differs from container ' +
863 r
'frame rate: 29.97.*')
864 debug('Bug in VideoReDo')
865 x
= rezre
.search(output
.lower())
867 vInfo
['vFps'] = '29.97'
870 vInfo
['Supported'] = False
871 debug('failed at vFps')
873 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
874 d
= durre
.search(output
)
877 vInfo
['millisecs'] = ((int(d
.group(1)) * 3600 +
878 int(d
.group(2)) * 60 +
879 int(d
.group(3))) * 1000 +
880 int(d
.group(4)) * (10 ** (3 - len(d
.group(4)))))
882 vInfo
['millisecs'] = 0
884 # get bitrate of source for tivo compatibility test.
885 rezre
= re
.compile(r
'.*bitrate: (.+) (?:kb/s).*')
886 x
= rezre
.search(output
)
888 vInfo
['kbps'] = x
.group(1)
890 # Fallback method of getting video bitrate
891 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
892 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
893 rezre
= re
.compile(r
'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
894 r
'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
895 x
= rezre
.search(output
)
897 vInfo
['kbps'] = x
.group(1)
900 debug('failed at kbps')
903 rezre
= re
.compile(r
'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
904 x
= rezre
.search(output
)
905 if x
and x
.group(1) != "0" and x
.group(2) != "0":
906 vInfo
['par1'] = x
.group(1) + ':' + x
.group(2)
907 vInfo
['par2'] = float(x
.group(1)) / float(x
.group(2))
909 vInfo
['par1'], vInfo
['par2'] = None, None
912 rezre
= re
.compile(r
'.*Video: .+DAR ([0-9]+):([0-9]+).*')
913 x
= rezre
.search(output
)
914 if x
and x
.group(1) != "0" and x
.group(2) != "0":
915 vInfo
['dar1'] = x
.group(1) + ':' + x
.group(2)
919 # get Audio Stream mapping.
920 rezre
= re
.compile(r
'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
921 x
= rezre
.search(output
)
924 for x
in rezre
.finditer(output
):
925 amap
.append((x
.group(1), x
.group(2) + x
.group(3)))
927 amap
.append(('', ''))
928 debug('failed at mapAudio')
929 vInfo
['mapAudio'] = amap
933 # get Metadata dump (newer ffmpeg).
934 lines
= output
.split('\n')
939 if line
.startswith(' Metadata:'):
943 if line
.startswith(' Duration:'):
947 key
, value
= [x
.strip() for x
in line
.split(':', 1)]
949 value
= value
.decode('utf-8')
951 if sys
.platform
== 'darwin':
952 value
= value
.decode('macroman')
954 value
= value
.decode('cp1252')
955 rawmeta
[key
] = [value
]
959 vInfo
['rawmeta'] = rawmeta
961 data
= metadata
.from_text(inFile
)
963 if key
.startswith('Override_'):
964 vInfo
['Supported'] = True
965 if key
.startswith('Override_mapAudio'):
966 audiomap
= dict(vInfo
['mapAudio'])
967 newmap
= shlex
.split(data
[key
])
968 audiomap
.update(zip(newmap
[::2], newmap
[1::2]))
969 vInfo
['mapAudio'] = sorted(audiomap
.items(),
970 key
=lambda (k
,v
): (k
,v
))
971 elif key
.startswith('Override_millisecs'):
972 vInfo
[key
.replace('Override_', '')] = int(data
[key
])
974 vInfo
[key
.replace('Override_', '')] = data
[key
]
977 info_cache
[inFile
] = (mtime
, vInfo
)
978 debug("; ".join(["%s=%s" % (k
, v
) for k
, v
in vInfo
.items()]))
981 def audio_check(inFile
, tsn
):
982 cmd_string
= ('-y -c:v mpeg2video -r 29.97 -b:v 1000k -c:a copy ' +
983 select_audiolang(inFile
, tsn
) + ' -t 00:00:01 -f vob -')
984 fname
= unicode(inFile
, 'utf-8')
986 fname
= fname
.encode('cp1252')
987 cmd
= [config
.get_bin('ffmpeg'), '-i', fname
] + cmd_string
.split()
988 ffmpeg
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
989 fd
, testname
= tempfile
.mkstemp()
990 testfile
= os
.fdopen(fd
, 'wb')
992 shutil
.copyfileobj(ffmpeg
.stdout
, testfile
)
999 vInfo
= video_info(testname
, False)
1003 def supported_format(inFile
):
1004 if video_info(inFile
)['Supported']:
1007 debug('FALSE, file not supported %s' % inFile
)
1011 debug('killing pid=%s' % str(popen
.pid
))
1013 win32kill(popen
.pid
)
1017 debug('sending SIGTERM to pid: %s' % popen
.pid
)
1018 os
.kill(popen
.pid
, signal
.SIGTERM
)
1020 if popen
.poll() is not None:
1021 debug('process %s has exited' % popen
.pid
)
1024 while popen
.poll() is None:
1025 debug('sending SIGKILL to pid: %s' % popen
.pid
)
1026 os
.kill(popen
.pid
, signal
.SIGKILL
)
1031 handle
= ctypes
.windll
.kernel32
.OpenProcess(1, False, pid
)
1032 ctypes
.windll
.kernel32
.TerminateProcess(handle
, -1)
1033 ctypes
.windll
.kernel32
.CloseHandle(handle
)