17 logger
= logging
.getLogger('pyTivo.video.transcode')
19 info_cache
= lrucache
.LRUCache(1000)
23 GOOD_MPEG_FPS
= ['23.98', '24.00', '25.00', '29.97',
24 '30.00', '50.00', '59.94', '60.00']
26 BLOCKSIZE
= 512 * 1024
37 # subprocess is broken for me on windows so super hack
38 def patchSubprocess():
39 o
= subprocess
.Popen
._make
_inheritable
41 def _make_inheritable(self
, handle
):
42 if not handle
: return subprocess
.GetCurrentProcess()
43 return o(self
, handle
)
45 subprocess
.Popen
._make
_inheritable
= _make_inheritable
46 mswindows
= (sys
.platform
== "win32")
53 msg
= msg
.decode('utf8')
55 if sys
.platform
== 'darwin':
56 msg
= msg
.decode('macroman')
58 msg
= msg
.decode('iso8859-1')
61 def transcode(isQuery
, inFile
, outFile
, tsn
='', mime
='', thead
=''):
62 settings
= {'video_codec': select_videocodec(inFile
, tsn
, mime
),
63 'video_br': select_videobr(inFile
, tsn
),
64 'video_fps': select_videofps(inFile
, tsn
),
65 'max_video_br': select_maxvideobr(tsn
),
66 'buff_size': select_buffsize(tsn
),
67 'aspect_ratio': ' '.join(select_aspect(inFile
, tsn
)),
68 'audio_br': select_audiobr(tsn
),
69 'audio_fr': select_audiofr(inFile
, tsn
),
70 'audio_ch': select_audioch(inFile
, tsn
),
71 'audio_codec': select_audiocodec(isQuery
, inFile
, tsn
),
72 'audio_lang': select_audiolang(inFile
, tsn
),
73 'ffmpeg_pram': select_ffmpegprams(tsn
),
74 'format': select_format(tsn
, mime
)}
79 ffmpeg_path
= config
.get_bin('ffmpeg')
80 cmd_string
= config
.getFFmpegTemplate(tsn
) % settings
81 fname
= unicode(inFile
, 'utf-8')
83 fname
= fname
.encode('iso8859-1')
85 if inFile
[-5:].lower() == '.tivo':
86 tivodecode_path
= config
.get_bin('tivodecode')
87 tivo_mak
= config
.get_server('tivo_mak')
88 tcmd
= [tivodecode_path
, '-m', tivo_mak
, fname
]
89 tivodecode
= subprocess
.Popen(tcmd
, stdout
=subprocess
.PIPE
,
91 if tivo_compatible(inFile
, tsn
)[0]:
95 cmd
= [ffmpeg_path
, '-i', '-'] + cmd_string
.split()
96 ffmpeg
= subprocess
.Popen(cmd
, stdin
=tivodecode
.stdout
,
97 stdout
=subprocess
.PIPE
,
100 cmd
= [ffmpeg_path
, '-i', fname
] + cmd_string
.split()
101 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=(512 * 1024),
102 stdout
=subprocess
.PIPE
)
105 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
108 ffmpeg_procs
[inFile
] = {'process': ffmpeg
, 'start': 0, 'end': 0,
109 'last_read': time
.time(), 'blocks': []}
111 ffmpeg_procs
[inFile
]['blocks'].append(thead
)
113 return resume_transfer(inFile
, outFile
, 0)
115 def is_resumable(inFile
, offset
):
116 if inFile
in ffmpeg_procs
:
117 proc
= ffmpeg_procs
[inFile
]
118 if proc
['start'] <= offset
< proc
['end']:
122 kill(proc
['process'])
125 def resume_transfer(inFile
, outFile
, offset
):
126 proc
= ffmpeg_procs
[inFile
]
127 offset
-= proc
['start']
131 for block
in proc
['blocks']:
135 block
= block
[offset
:]
136 outFile
.write('%x\r\n' % len(block
))
138 outFile
.write('\r\n')
142 except Exception, msg
:
146 proc
['start'] = proc
['end']
149 return count
+ transfer_blocks(inFile
, outFile
)
151 def transfer_blocks(inFile
, outFile
):
152 proc
= ffmpeg_procs
[inFile
]
153 blocks
= proc
['blocks']
158 block
= proc
['process'].stdout
.read(BLOCKSIZE
)
159 proc
['last_read'] = time
.time()
160 except Exception, msg
:
163 kill(proc
['process'])
169 except Exception, msg
:
176 proc
['end'] += len(block
)
177 if len(blocks
) > MAXBLOCKS
:
178 proc
['start'] += len(blocks
[0])
182 outFile
.write('%x\r\n' % len(block
))
184 outFile
.write('\r\n')
186 except Exception, msg
:
192 def reap_process(inFile
):
193 if ffmpeg_procs
and inFile
in ffmpeg_procs
:
194 proc
= ffmpeg_procs
[inFile
]
195 if proc
['last_read'] + TIMEOUT
< time
.time():
196 del ffmpeg_procs
[inFile
]
198 kill(proc
['process'])
200 reaper
= threading
.Timer(TIMEOUT
, reap_process
, (inFile
,))
201 reapers
[inFile
] = reaper
205 del ffmpeg_procs
[inFile
]
206 reapers
[inFile
].cancel()
209 def select_audiocodec(isQuery
, inFile
, tsn
='', mime
=''):
210 if inFile
[-5:].lower() == '.tivo':
211 return '-acodec copy'
212 vInfo
= video_info(inFile
)
213 codectype
= vInfo
['vCodec']
214 # Default, compatible with all TiVo's
216 if mime
== 'video/mp4':
217 compatiblecodecs
= ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
220 compatiblecodecs
= ('ac3', 'liba52', 'mp2')
222 if vInfo
['aCodec'] in compatiblecodecs
:
223 aKbps
= vInfo
['aKbps']
226 if vInfo
['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac'):
227 # along with the channel check below this should
228 # pass any AAC audio that has undefined 'aKbps' and
229 # is <= 2 channels. Should be TiVo compatible.
232 vInfoQuery
= audio_check(inFile
, tsn
)
233 if vInfoQuery
== None:
237 aKbps
= vInfoQuery
['aKbps']
238 aCh
= vInfoQuery
['aCh']
241 if aKbps
and int(aKbps
) <= config
.getMaxAudioBR(tsn
):
242 # compatible codec and bitrate, do not reencode audio
244 if vInfo
['aCodec'] != 'ac3' and (aCh
== None or aCh
> 2):
246 copy_flag
= config
.get_tsn('copy_ts', tsn
)
248 if ((codec
== 'copy' and codectype
== 'mpeg2video' and not copy_flag
) or
249 (copy_flag
and copy_flag
.lower() == 'false')):
251 return '-acodec ' + codec
+ copyts
253 def select_audiofr(inFile
, tsn
):
254 freq
= '48000' # default
255 vInfo
= video_info(inFile
)
256 if vInfo
['aFreq'] == '44100':
257 # compatible frequency
258 freq
= vInfo
['aFreq']
259 audio_fr
= config
.get_tsn('audio_fr', tsn
)
264 def select_audioch(inFile
, tsn
):
265 ch
= config
.get_tsn('audio_ch', tsn
)
268 # AC-3 max channels is 5.1
269 if video_info(inFile
)['aCh'] > 6:
270 debug('Too many audio channels for AC-3, using 5.1 instead')
272 elif video_info(inFile
)['aCh']:
273 return '-ac %i' % video_info(inFile
)['aCh']
276 def select_audiolang(inFile
, tsn
):
277 vInfo
= video_info(inFile
)
278 audio_lang
= config
.get_tsn('audio_lang', tsn
)
279 debug('audio_lang: %s' % audio_lang
)
280 if vInfo
['mapAudio']:
281 # default to first detected audio stream to begin with
282 stream
= vInfo
['mapAudio'][0][0]
283 if audio_lang
!= None and vInfo
['mapVideo'] != None:
285 langmatch_prev
= vInfo
['mapAudio'][:]
286 for lang
in audio_lang
.replace(' ', '').lower().split(','):
287 for s
, l
in langmatch_prev
:
288 if lang
in s
+ l
.replace(' ', '').lower():
289 langmatch_curr
.append((s
, l
))
291 # if only 1 item matched we're done
292 if len(langmatch_curr
) == 1:
294 # if more than 1 item matched copy the curr area to the prev
295 # array we only need to look at the new shorter list from
297 elif len(langmatch_curr
) > 1:
298 langmatch_prev
= langmatch_curr
[:]
300 # if we drop out of the loop with more than 1 item default to
302 if len(langmatch_curr
) > 1:
303 stream
= langmatch_curr
[0][0]
304 # don't let FFmpeg auto select audio stream, pyTivo defaults to
307 debug('selected audio stream: %s' % stream
)
308 return '-map ' + vInfo
['mapVideo'] + ' -map ' + stream
309 # if no audio is found
310 debug('selected audio stream: None detected')
313 def select_videofps(inFile
, tsn
):
314 vInfo
= video_info(inFile
)
315 fps
= '-r 29.97' # default
316 if config
.isHDtivo(tsn
) and vInfo
['vFps'] in GOOD_MPEG_FPS
:
318 video_fps
= config
.get_tsn('video_fps', tsn
)
319 if video_fps
!= None:
320 fps
= '-r ' + video_fps
323 def select_videocodec(inFile
, tsn
, mime
=''):
324 vInfo
= video_info(inFile
)
325 if tivo_compatible_video(vInfo
, tsn
, mime
)[0]:
327 if (mime
== 'video/x-tivo-mpeg-ts' and
328 vInfo
.get('vCodec', '') == 'h264'):
329 codec
+= ' -bsf h264_mp4toannexb'
331 codec
= 'mpeg2video' # default
332 return '-vcodec ' + codec
334 def select_videobr(inFile
, tsn
, mime
=''):
335 return '-b ' + str(select_videostr(inFile
, tsn
, mime
) / 1000) + 'k'
337 def select_videostr(inFile
, tsn
, mime
=''):
338 vInfo
= video_info(inFile
)
339 if tivo_compatible_video(vInfo
, tsn
, mime
)[0]:
340 video_str
= int(vInfo
['kbps'])
342 video_str
-= int(vInfo
['aKbps'])
345 video_str
= config
.strtod(config
.getVideoBR(tsn
))
346 if config
.isHDtivo(tsn
) and vInfo
['kbps']:
347 video_str
= max(video_str
, int(vInfo
['kbps']) * 1000)
348 video_str
= int(min(config
.strtod(config
.getMaxVideoBR(tsn
)) * 0.95,
352 def select_audiobr(tsn
):
353 return '-ab ' + config
.getAudioBR(tsn
)
355 def select_maxvideobr(tsn
):
356 return '-maxrate ' + config
.getMaxVideoBR(tsn
)
358 def select_buffsize(tsn
):
359 return '-bufsize ' + config
.getBuffSize(tsn
)
361 def select_ffmpegprams(tsn
):
362 params
= config
.getFFmpegPrams(tsn
)
367 def select_format(tsn
, mime
):
368 if mime
== 'video/x-tivo-mpeg-ts':
372 return '-f %s -' % fmt
376 if pad_style
== UNSET
:
378 filters
= tempfile
.TemporaryFile()
379 cmd
= [config
.get_bin('ffmpeg'), '-filters']
380 ffmpeg
= subprocess
.Popen(cmd
, stdout
=filters
, stderr
=subprocess
.PIPE
)
384 if line
.startswith('pad'):
388 return pad_style
== NEW_PAD
390 def pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier
, vInfo
):
391 endHeight
= int(((TIVO_WIDTH
* vInfo
['vHeight']) /
392 vInfo
['vWidth']) * multiplier
)
395 topPadding
= (TIVO_HEIGHT
- endHeight
) / 2
400 return ['-vf', 'scale=%d:%d,pad=%d:%d:0:%d' % (TIVO_WIDTH
,
401 endHeight
, TIVO_WIDTH
, TIVO_HEIGHT
, topPadding
)]
403 bottomPadding
= (TIVO_HEIGHT
- endHeight
) - topPadding
404 return ['-s', '%sx%s' % (TIVO_WIDTH
, endHeight
),
405 '-padtop', str(topPadding
),
406 '-padbottom', str(bottomPadding
)]
408 def pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier
, vInfo
):
409 endWidth
= int((TIVO_HEIGHT
* vInfo
['vWidth']) /
410 (vInfo
['vHeight'] * multiplier
))
413 leftPadding
= (TIVO_WIDTH
- endWidth
) / 2
418 return ['-vf', 'scale=%d:%d,pad=%d:%d:%d:0' % (endWidth
,
419 TIVO_HEIGHT
, TIVO_WIDTH
, TIVO_HEIGHT
, leftPadding
)]
421 rightPadding
= (TIVO_WIDTH
- endWidth
) - leftPadding
422 return ['-s', '%sx%s' % (endWidth
, TIVO_HEIGHT
),
423 '-padleft', str(leftPadding
),
424 '-padright', str(rightPadding
)]
426 def select_aspect(inFile
, tsn
= ''):
427 TIVO_WIDTH
= config
.getTivoWidth(tsn
)
428 TIVO_HEIGHT
= config
.getTivoHeight(tsn
)
430 vInfo
= video_info(inFile
)
432 debug('tsn: %s' % tsn
)
434 aspect169
= config
.get169Setting(tsn
)
436 debug('aspect169: %s' % aspect169
)
438 optres
= config
.getOptres(tsn
)
440 debug('optres: %s' % optres
)
443 optHeight
= config
.nearestTivoHeight(vInfo
['vHeight'])
444 optWidth
= config
.nearestTivoWidth(vInfo
['vWidth'])
445 if optHeight
< TIVO_HEIGHT
:
446 TIVO_HEIGHT
= optHeight
447 if optWidth
< TIVO_WIDTH
:
448 TIVO_WIDTH
= optWidth
450 if vInfo
.get('par2'):
452 elif vInfo
.get('par'):
453 par2
= float(vInfo
['par'])
458 debug(('File=%s vCodec=%s vWidth=%s vHeight=%s vFps=%s millisecs=%s ' +
459 'TIVO_HEIGHT=%s TIVO_WIDTH=%s') % (inFile
, vInfo
['vCodec'],
460 vInfo
['vWidth'], vInfo
['vHeight'], vInfo
['vFps'],
461 vInfo
['millisecs'], TIVO_HEIGHT
, TIVO_WIDTH
))
463 if config
.isHDtivo(tsn
) and not optres
:
467 # adjust for pixel aspect ratio, if set
470 return ['-s', '%dx%d' % (vInfo
['vWidth'],
471 math
.ceil(vInfo
['vHeight'] / npar
))]
473 # FFMPEG expects width to be a multiple of two
474 return ['-s', '%dx%d' % (math
.ceil(vInfo
['vWidth']*npar
/2.0)*2,
477 if vInfo
['vHeight'] <= TIVO_HEIGHT
:
478 # pass all resolutions to S3, except heights greater than
481 # else, resize video.
483 d
= gcd(vInfo
['vHeight'], vInfo
['vWidth'])
484 rheight
, rwidth
= vInfo
['vHeight'] / d
, vInfo
['vWidth'] / d
485 debug('rheight=%s rwidth=%s' % (rheight
, rwidth
))
487 if (rwidth
, rheight
) in [(1, 1)] and vInfo
['par1'] == '8:9':
488 debug('File + PAR is within 4:3.')
489 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
491 elif ((rwidth
, rheight
) in [(4, 3), (10, 11), (15, 11), (59, 54),
492 (59, 72), (59, 36), (59, 54)] or
493 vInfo
['dar1'] == '4:3'):
494 debug('File is within 4:3 list.')
495 return ['-aspect', '4:3', '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
497 elif (((rwidth
, rheight
) in [(16, 9), (20, 11), (40, 33), (118, 81),
498 (59, 27)] or vInfo
['dar1'] == '16:9')
499 and (aspect169
or config
.get169Letterbox(tsn
))):
500 debug('File is within 16:9 list and 16:9 allowed.')
502 if config
.get169Blacklist(tsn
) or (aspect169
and
503 config
.get169Letterbox(tsn
)):
507 return ['-aspect', aspect
, '-s', '%sx%s' % (TIVO_WIDTH
, TIVO_HEIGHT
)]
510 settings
= ['-aspect']
512 multiplier16by9
= (16.0 * TIVO_HEIGHT
) / (9.0 * TIVO_WIDTH
) / par2
513 multiplier4by3
= (4.0 * TIVO_HEIGHT
) / (3.0 * TIVO_WIDTH
) / par2
514 ratio
= vInfo
['vWidth'] * 100 * par2
/ vInfo
['vHeight']
515 debug('par2=%.3f ratio=%.3f mult4by3=%.3f' % (par2
, ratio
,
518 # If video is wider than 4:3 add top and bottom padding
520 if ratio
> 133: # Might be 16:9 file, or just need padding on
523 if aspect169
and ratio
> 135: # If file would fall in 4:3
524 # assume it is supposed to be 4:3
526 if (config
.get169Blacklist(tsn
) or
527 config
.get169Letterbox(tsn
)):
528 settings
.append('4:3')
530 settings
.append('16:9')
532 if ratio
> 177: # too short needs padding top and bottom
533 settings
+= pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
,
534 multiplier16by9
, vInfo
)
535 debug(('16:9 aspect allowed, file is wider ' +
536 'than 16:9 padding top and bottom\n%s') %
539 else: # too skinny needs padding on left and right.
540 settings
+= pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
,
541 multiplier16by9
, vInfo
)
542 debug(('16:9 aspect allowed, file is narrower ' +
543 'than 16:9 padding left and right\n%s') %
546 else: # this is a 4:3 file or 16:9 output not allowed
547 if ratio
> 135 and config
.get169Letterbox(tsn
):
548 settings
.append('16:9')
549 multiplier
= multiplier16by9
551 settings
.append('4:3')
552 multiplier
= multiplier4by3
553 settings
+= pad_TB(TIVO_WIDTH
, TIVO_HEIGHT
,
555 debug(('File is wider than 4:3 padding ' +
556 'top and bottom\n%s') % ' '.join(settings
))
558 # If video is taller than 4:3 add left and right padding, this
559 # is rare. All of these files will always be sent in an aspect
560 # ratio of 4:3 since they are so narrow.
563 settings
.append('4:3')
564 settings
+= pad_LR(TIVO_WIDTH
, TIVO_HEIGHT
, multiplier4by3
, vInfo
)
565 debug('File is taller than 4:3 padding left and right\n%s'
566 % ' '.join(settings
))
570 def tivo_compatible_video(vInfo
, tsn
, mime
=''):
573 codec
= vInfo
.get('vCodec', '')
574 if mime
== 'video/mp4':
576 message
= (False, 'vCodec %s not compatible' % codec
)
580 if mime
== 'video/bif':
582 message
= (False, 'vCodec %s not compatible' % codec
)
586 if mime
== 'video/x-tivo-mpeg-ts':
587 if codec
not in ('h264', 'mpeg2video'):
588 message
= (False, 'vCodec %s not compatible' % codec
)
592 if codec
not in ('mpeg2video', 'mpeg1video'):
593 message
= (False, 'vCodec %s not compatible' % codec
)
596 if vInfo
['kbps'] != None:
597 abit
= max('0', vInfo
['aKbps'])
598 if (int(vInfo
['kbps']) - int(abit
) >
599 config
.strtod(config
.getMaxVideoBR(tsn
)) / 1000):
600 message
= (False, '%s kbps exceeds max video bitrate' %
604 message
= (False, '%s kbps not supported' % vInfo
['kbps'])
607 if config
.isHDtivo(tsn
):
608 # HD Tivo detected, skipping remaining tests.
611 if not vInfo
['vFps'] in ['29.97', '59.94']:
612 message
= (False, '%s vFps, should be 29.97' % vInfo
['vFps'])
615 if ((config
.get169Blacklist(tsn
) and not config
.get169Setting(tsn
))
616 or (config
.get169Letterbox(tsn
) and config
.get169Setting(tsn
))):
617 if vInfo
['dar1'] and vInfo
['dar1'] not in ('4:3', '8:9', '880:657'):
618 message
= (False, ('DAR %s not supported ' +
619 'by BLACKLIST_169 tivos') % vInfo
['dar1'])
622 mode
= (vInfo
['vWidth'], vInfo
['vHeight'])
623 if mode
not in [(720, 480), (704, 480), (544, 480),
624 (528, 480), (480, 480), (352, 480), (352, 240)]:
625 message
= (False, '%s x %s not in supported modes' % mode
)
630 def tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
=''):
633 codec
= vInfo
.get('aCodec', '')
636 debug('No audio stream detected')
639 if mime
== 'video/mp4':
640 if codec
not in ('mpeg4aac', 'libfaad', 'mp4a', 'aac',
642 message
= (False, 'aCodec %s not compatible' % codec
)
644 if vInfo
['aCodec'] in ('mpeg4aac', 'libfaad', 'mp4a', 'aac') and (vInfo
['aCh'] == None or vInfo
['aCh'] > 2):
645 message
= (False, 'aCodec %s is only supported with 2 or less channels, the track has %s channels' % (codec
, vInfo
['aCh']))
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' %
655 if mime
== 'video/bif':
657 message
= (False, 'aCodec %s not compatible' % codec
)
661 if inFile
[-5:].lower() == '.tivo':
664 if mime
== 'video/x-tivo-mpeg-ts':
665 if codec
not in ('ac3', 'liba52', 'mp2', 'aac_latm'):
666 message
= (False, 'aCodec %s not compatible' % codec
)
670 if codec
not in ('ac3', 'liba52', 'mp2'):
671 message
= (False, 'aCodec %s not compatible' % codec
)
674 if (not vInfo
['aKbps'] or
675 int(vInfo
['aKbps']) > config
.getMaxAudioBR(tsn
)):
676 message
= (False, '%s kbps exceeds max audio bitrate' %
680 audio_lang
= config
.get_tsn('audio_lang', tsn
)
682 if vInfo
['mapAudio'][0][0] != select_audiolang(inFile
, tsn
)[-3:]:
683 message
= (False, '%s preferred audio track exists' %
689 def tivo_compatible_container(vInfo
, inFile
, mime
=''):
691 container
= vInfo
.get('container', '')
692 if ((mime
== 'video/mp4' and
693 (container
!= 'mov' or inFile
.lower().endswith('.mov'))) or
694 (mime
== 'video/bif' and container
!= 'asf') or
695 (mime
== 'video/x-tivo-mpeg-ts' and container
!= 'mpegts') or
696 (mime
in ['video/x-tivo-mpeg', 'video/mpeg', ''] and
697 (container
!= 'mpeg' or vInfo
['vCodec'] == 'mpeg1video'))):
698 message
= (False, 'container %s not compatible' % container
)
702 def mp4_remuxable(inFile
, tsn
=''):
703 vInfo
= video_info(inFile
)
704 return tivo_compatible_video(vInfo
, tsn
, 'video/mp4')[0]
706 def mp4_remux(inFile
, basename
, tsn
='', temp_share_path
=''):
707 outFile
= inFile
+ '.pyTivo-temp'
708 newname
= basename
+ '.pyTivo-temp'
711 newname
= os
.path
.splitext(os
.path
.split(basename
)[1])[0] + '.mp4.pyTivo-temp'
712 outFile
= os
.path
.join(temp_share_path
, newname
)
714 if os
.path
.exists(outFile
):
717 ffmpeg_path
= config
.get_bin('ffmpeg')
718 fname
= unicode(inFile
, 'utf-8')
719 oname
= unicode(outFile
, 'utf-8')
721 fname
= fname
.encode('iso8859-1')
722 oname
= oname
.encode('iso8859-1')
724 settings
= {'video_codec': '-vcodec copy',
725 'video_br': select_videobr(inFile
, tsn
),
726 'video_fps': select_videofps(inFile
, tsn
),
727 'max_video_br': select_maxvideobr(tsn
),
728 'buff_size': select_buffsize(tsn
),
729 'aspect_ratio': ' '.join(select_aspect(inFile
, tsn
)),
730 'audio_br': select_audiobr(tsn
),
731 'audio_fr': select_audiofr(inFile
, tsn
),
732 'audio_ch': select_audioch(inFile
, tsn
),
733 'audio_codec': select_audiocodec(False, inFile
, tsn
, 'video/mp4'),
734 'audio_lang': select_audiolang(inFile
, tsn
),
735 'ffmpeg_pram': select_ffmpegprams(tsn
),
738 cmd_string
= config
.getFFmpegTemplate(tsn
) % settings
739 cmd
= [ffmpeg_path
, '-i', fname
] + cmd_string
.split() + [oname
]
741 debug('transcoding to tivo model ' + tsn
[:3] + ' using ffmpeg command:')
744 ffmpeg
= subprocess
.Popen(cmd
)
745 debug('remuxing ' + inFile
+ ' to ' + outFile
)
747 debug('error during remuxing')
753 def tivo_compatible(inFile
, tsn
='', mime
=''):
754 vInfo
= video_info(inFile
)
756 message
= (True, 'all compatible')
757 if not config
.get_bin('ffmpeg'):
758 if mime
not in ['video/x-tivo-mpeg', 'video/mpeg', '']:
759 message
= (False, 'no ffmpeg')
763 vmessage
= tivo_compatible_video(vInfo
, tsn
, mime
)
768 amessage
= tivo_compatible_audio(vInfo
, inFile
, tsn
, mime
)
773 cmessage
= tivo_compatible_container(vInfo
, inFile
, mime
)
779 debug('TRANSCODE=%s, %s, %s' % (['YES', 'NO'][message
[0]],
783 def video_info(inFile
, cache
=True):
785 fname
= unicode(inFile
, 'utf-8')
786 mtime
= os
.stat(fname
).st_mtime
788 if inFile
in info_cache
and info_cache
[inFile
][0] == mtime
:
789 debug('CACHE HIT! %s' % inFile
)
790 return info_cache
[inFile
][1]
792 vInfo
['Supported'] = True
794 ffmpeg_path
= config
.get_bin('ffmpeg')
796 if os
.path
.splitext(inFile
)[1].lower() not in ['.mpg', '.mpeg',
798 vInfo
['Supported'] = False
799 vInfo
.update({'millisecs': 0, 'vWidth': 704, 'vHeight': 480,
802 info_cache
[inFile
] = (mtime
, vInfo
)
806 fname
= fname
.encode('iso8859-1')
807 cmd
= [ffmpeg_path
, '-i', fname
]
808 # Windows and other OS buffer 4096 and ffmpeg can output more than that.
809 err_tmp
= tempfile
.TemporaryFile()
810 ffmpeg
= subprocess
.Popen(cmd
, stderr
=err_tmp
, stdout
=subprocess
.PIPE
,
811 stdin
=subprocess
.PIPE
)
813 # wait configured # of seconds: if ffmpeg is not back give up
814 limit
= config
.getFFmpegWait()
816 for i
in xrange(limit
* 20):
818 if not ffmpeg
.poll() == None:
821 if ffmpeg
.poll() == None:
823 vInfo
['Supported'] = False
825 info_cache
[inFile
] = (mtime
, vInfo
)
831 output
= err_tmp
.read()
833 debug('ffmpeg output=%s' % output
)
835 attrs
= {'container': r
'Input #0, ([^,]+),',
836 'vCodec': r
'Video: ([^, ]+)', # video codec
837 'aKbps': r
'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
838 'aCodec': r
'.*Audio: ([^, ]+)', # audio codec
839 'aFreq': r
'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
840 'mapVideo': r
'([0-9]+[.:]+[0-9]+).*: Video:.*'} # video mapping
843 rezre
= re
.compile(attrs
[attr
])
844 x
= rezre
.search(output
)
846 vInfo
[attr
] = x
.group(1)
848 if attr
in ['container', 'vCodec']:
850 vInfo
['Supported'] = False
853 debug('failed at ' + attr
)
855 rezre
= re
.compile(r
'.*Audio: .+, (?:(\d+)(?:(?:\.(\d).*)?(?: channels.*)?)|(stereo|mono)),.*')
856 x
= rezre
.search(output
)
859 if x
.group(3) == 'stereo':
861 elif x
.group(3) == 'mono':
864 vInfo
['aCh'] = int(x
.group(1)) + int(x
.group(2))
866 vInfo
['aCh'] = int(x
.group(1))
869 debug('failed at aCh')
872 debug('failed at aCh')
874 rezre
= re
.compile(r
'.*Video: .+, (\d+)x(\d+)[, ].*')
875 x
= rezre
.search(output
)
877 vInfo
['vWidth'] = int(x
.group(1))
878 vInfo
['vHeight'] = int(x
.group(2))
881 vInfo
['vHeight'] = ''
882 vInfo
['Supported'] = False
883 debug('failed at vWidth/vHeight')
885 rezre
= re
.compile(r
'.*Video: .+, (.+) (?:fps|tb\(r\)|tbr).*')
886 x
= rezre
.search(output
)
888 vInfo
['vFps'] = x
.group(1)
889 if '.' not in vInfo
['vFps']:
890 vInfo
['vFps'] += '.00'
892 # Allow override only if it is mpeg2 and frame rate was doubled
895 if vInfo
['vCodec'] == 'mpeg2video' and vInfo
['vFps'] != '29.97':
896 # First look for the build 7215 version
897 rezre
= re
.compile(r
'.*film source: 29.97.*')
898 x
= rezre
.search(output
.lower())
900 debug('film source: 29.97 setting vFps to 29.97')
901 vInfo
['vFps'] = '29.97'
904 rezre
= re
.compile(r
'.*frame rate differs from container ' +
905 r
'frame rate: 29.97.*')
906 debug('Bug in VideoReDo')
907 x
= rezre
.search(output
.lower())
909 vInfo
['vFps'] = '29.97'
912 vInfo
['Supported'] = False
913 debug('failed at vFps')
915 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),')
916 d
= durre
.search(output
)
919 vInfo
['millisecs'] = ((int(d
.group(1)) * 3600 +
920 int(d
.group(2)) * 60 +
921 int(d
.group(3))) * 1000 +
922 int(d
.group(4)) * (10 ** (3 - len(d
.group(4)))))
924 vInfo
['millisecs'] = 0
926 # get bitrate of source for tivo compatibility test.
927 rezre
= re
.compile(r
'.*bitrate: (.+) (?:kb/s).*')
928 x
= rezre
.search(output
)
930 vInfo
['kbps'] = x
.group(1)
932 # Fallback method of getting video bitrate
933 # Sample line: Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p,
934 # 720x480 [PAR 32:27 DAR 16:9], 9800 kb/s, 59.94 tb(r)
935 rezre
= re
.compile(r
'.*Stream #0\.0\[.*\]: Video: mpeg2video, ' +
936 r
'\S+, \S+ \[.*\], (\d+) (?:kb/s).*')
937 x
= rezre
.search(output
)
939 vInfo
['kbps'] = x
.group(1)
942 debug('failed at kbps')
945 rezre
= re
.compile(r
'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
946 x
= rezre
.search(output
)
947 if x
and x
.group(1) != "0" and x
.group(2) != "0":
948 vInfo
['par1'] = x
.group(1) + ':' + x
.group(2)
949 vInfo
['par2'] = float(x
.group(1)) / float(x
.group(2))
951 vInfo
['par1'], vInfo
['par2'] = None, None
954 rezre
= re
.compile(r
'.*Video: .+DAR ([0-9]+):([0-9]+).*')
955 x
= rezre
.search(output
)
956 if x
and x
.group(1) != "0" and x
.group(2) != "0":
957 vInfo
['dar1'] = x
.group(1) + ':' + x
.group(2)
961 # get Audio Stream mapping.
962 rezre
= re
.compile(r
'([0-9]+[.:]+[0-9]+)(.*): Audio:(.*)')
963 x
= rezre
.search(output
)
966 for x
in rezre
.finditer(output
):
967 amap
.append((x
.group(1), x
.group(2) + x
.group(3)))
969 amap
.append(('', ''))
970 debug('failed at mapAudio')
971 vInfo
['mapAudio'] = amap
975 # get Metadata dump (newer ffmpeg).
976 lines
= output
.split('\n')
981 if line
.startswith(' Metadata:'):
985 if line
.startswith(' Duration:'):
989 key
, value
= [x
.strip() for x
in line
.split(':', 1)]
991 value
= value
.decode('utf-8')
993 if sys
.platform
== 'darwin':
994 value
= value
.decode('macroman')
996 value
= value
.decode('iso8859-1')
997 rawmeta
[key
] = [value
]
1001 vInfo
['rawmeta'] = rawmeta
1003 data
= metadata
.from_text(inFile
)
1005 if key
.startswith('Override_'):
1006 vInfo
['Supported'] = True
1007 if key
.startswith('Override_mapAudio'):
1008 audiomap
= dict(vInfo
['mapAudio'])
1009 stream
= key
.replace('Override_mapAudio', '').strip()
1010 if stream
in audiomap
:
1011 newaudiomap
= (stream
, data
[key
])
1012 audiomap
.update([newaudiomap
])
1013 vInfo
['mapAudio'] = sorted(audiomap
.items(),
1014 key
=lambda (k
,v
): (k
,v
))
1015 elif key
.startswith('Override_millisecs'):
1016 vInfo
[key
.replace('Override_', '')] = int(data
[key
])
1018 vInfo
[key
.replace('Override_', '')] = data
[key
]
1021 info_cache
[inFile
] = (mtime
, vInfo
)
1022 debug("; ".join(["%s=%s" % (k
, v
) for k
, v
in vInfo
.items()]))
1025 def audio_check(inFile
, tsn
):
1026 cmd_string
= ('-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy ' +
1027 select_audiolang(inFile
, tsn
) + ' -t 00:00:01 -f vob -')
1028 fname
= unicode(inFile
, 'utf-8')
1030 fname
= fname
.encode('iso8859-1')
1031 cmd
= [config
.get_bin('ffmpeg'), '-i', fname
] + cmd_string
.split()
1032 ffmpeg
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
1033 fd
, testname
= tempfile
.mkstemp()
1034 testfile
= os
.fdopen(fd
, 'wb')
1036 shutil
.copyfileobj(ffmpeg
.stdout
, testfile
)
1043 vInfo
= video_info(testname
, False)
1047 def supported_format(inFile
):
1048 if video_info(inFile
)['Supported']:
1051 debug('FALSE, file not supported %s' % inFile
)
1055 debug('killing pid=%s' % str(popen
.pid
))
1057 win32kill(popen
.pid
)
1061 debug('sending SIGTERM to pid: %s' % popen
.pid
)
1062 os
.kill(popen
.pid
, signal
.SIGTERM
)
1064 if popen
.poll() is not None:
1065 debug('process %s has exited' % popen
.pid
)
1068 while popen
.poll() is None:
1069 debug('sending SIGKILL to pid: %s' % popen
.pid
)
1070 os
.kill(popen
.pid
, signal
.SIGKILL
)
1075 handle
= ctypes
.windll
.kernel32
.OpenProcess(1, False, pid
)
1076 ctypes
.windll
.kernel32
.TerminateProcess(handle
, -1)
1077 ctypes
.windll
.kernel32
.CloseHandle(handle
)