root/plugins/video/transcode.py

Revision 9567aeae71db6082ee952d3bf002c3897db5363d, 19.9 kB (checked in by Jason Michalski <armooo@armooo.net>, 9 months ago)

Fixed logging typeo

  • Property mode set to 100644
Line 
1 import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache, math
2 import config
3 import logging
4
5 logger = logging.getLogger('pyTivo.video.transcode')
6
7 info_cache = lrucache.LRUCache(1000)
8 videotest = os.path.join(os.path.dirname(__file__), 'videotest.mpg')
9
10 BAD_MPEG_FPS = ['15.00']
11
12 def ffmpeg_path():
13     return config.get('Server', 'ffmpeg')
14
15 # XXX BIG HACK
16 # subprocess is broken for me on windows so super hack
17 def patchSubprocess():
18     o = subprocess.Popen._make_inheritable
19
20     def _make_inheritable(self, handle):
21         if not handle: return subprocess.GetCurrentProcess()
22         return o(self, handle)
23
24     subprocess.Popen._make_inheritable = _make_inheritable
25 mswindows = (sys.platform == "win32")
26 if mswindows:
27     patchSubprocess()
28
29 def output_video(inFile, outFile, tsn=''):
30     if tivo_compatable(inFile, tsn):
31         logger.debug('%s is tivo compatible' % inFile)
32         f = file(inFile, 'rb')
33         shutil.copyfileobj(f, outFile)
34         f.close()
35     else:
36         logger.debug('%s is not tivo compatible' % inFile)
37         transcode(inFile, outFile, tsn)
38
39 def transcode(inFile, outFile, tsn=''):
40
41     settings = {}
42     settings['video_codec'] = select_videocodec(tsn)
43     settings['video_br'] = select_videobr(tsn)
44     settings['video_fps'] = select_videofps(inFile, tsn)
45     settings['max_video_br'] = select_maxvideobr()
46     settings['buff_size'] = select_buffsize()
47     settings['aspect_ratio'] = ' '.join(select_aspect(inFile, tsn))
48     settings['audio_br'] = select_audiobr(tsn)
49     settings['audio_fr'] = select_audiofr(inFile, tsn)
50     settings['audio_ch'] = select_audioch(tsn)
51     settings['audio_codec'] = select_audiocodec(inFile, tsn)
52     settings['ffmpeg_pram'] = select_ffmpegprams(tsn)
53     settings['format'] = select_format(tsn)
54
55     cmd_string = config.getFFmpegTemplate(tsn) % settings
56
57     cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
58     logging.debug('transcoding to tivo model '+tsn[:3]+' using ffmpeg command:')
59     logging.debug(' '.join(cmd))
60     ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
61     try:
62         shutil.copyfileobj(ffmpeg.stdout, outFile)
63     except:
64         kill(ffmpeg.pid)
65
66 def select_audiocodec(inFile, tsn = ''):
67     # Default, compatible with all TiVo's
68     codec = 'ac3'
69     if config.getAudioCodec(tsn) == None:
70         type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(inFile)
71         if acodec in ('ac3', 'liba52', 'mp2'):
72             if akbps == None:
73                 cmd_string = '-y -vcodec mpeg2video -r 29.97 -b 1000k -acodec copy -t 00:00:01 -f vob -'
74                 if video_check(inFile, cmd_string):
75                     type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(videotest)
76             if not akbps == None and int(akbps) <= config.getMaxAudioBR(tsn):
77                 # compatible codec and bitrate, do not reencode audio
78                 codec = 'copy'
79     else:
80         codec = config.getAudioCodec(tsn)
81     return '-acodec '+codec
82
83 def select_audiofr(inFile, tsn):
84     freq = '48000'  #default
85     type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(inFile)
86     if not afreq == None and afreq in ('44100', '48000'):
87         # compatible frequency
88         freq = afreq
89     if config.getAudioFR(tsn) != None:
90         freq = config.getAudioFR(tsn)
91     return '-ar '+freq
92
93 def select_audioch(tsn):
94     if config.getAudioCH(tsn) != None:
95         return '-ac '+config.getAudioCH(tsn)
96     return ''
97
98 def select_videofps(inFile, tsn):
99     type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(inFile)
100     vfps = '-r 29.97'  #default
101     if config.isHDtivo(tsn) and fps not in BAD_MPEG_FPS:
102         vfps = ' '
103     if config.getVideoFPS(tsn) != None:
104         vfps = '-r '+config.getVideoFPS(tsn)
105     return vfps
106
107 def select_videocodec(tsn):
108     vcodec = 'mpeg2video'  #default
109     if config.getVideoCodec(tsn) != None:
110         vcodec = config.getVideoCodec(tsn)
111     return '-vcodec '+vcodec
112
113 def select_videobr(tsn):
114     return '-b '+config.getVideoBR(tsn)
115
116 def select_audiobr(tsn):
117     return '-ab '+config.getAudioBR(tsn)
118
119 def select_maxvideobr():
120     return '-maxrate '+config.getMaxVideoBR()
121
122 def select_buffsize():
123     return '-bufsize '+config.getBuffSize()
124
125 def select_ffmpegprams(tsn):
126     if config.getFFmpegPrams(tsn) != None:
127         return config.getFFmpegPrams(tsn)
128     return ''
129
130 def select_format(tsn):
131     fmt = 'vob'
132     if config.getFormat(tsn) != None:
133         fmt = config.getFormat(tsn)
134     return '-f '+fmt+' -'
135
136 def select_aspect(inFile, tsn = ''):
137     TIVO_WIDTH = config.getTivoWidth(tsn)
138     TIVO_HEIGHT = config.getTivoHeight(tsn)
139
140     type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(inFile)
141
142     logging.debug('tsn: %s' % tsn)
143
144     aspect169 = config.get169Setting(tsn)
145
146     logging.debug('aspect169:%s' % aspect169)
147
148     optres = config.getOptres(tsn)
149
150     logging.debug('optres:%s' % optres)
151
152     if optres:
153         optHeight = config.nearestTivoHeight(height)
154         optWidth = config.nearestTivoWidth(width)
155         if optHeight < TIVO_HEIGHT:
156             TIVO_HEIGHT = optHeight
157         if optWidth < TIVO_WIDTH:
158             TIVO_WIDTH = optWidth
159
160     d = gcd(height,width)
161     ratio = (width*100)/height
162     rheight, rwidth = height/d, width/d
163
164     logger.debug('File=%s Type=%s width=%s height=%s fps=%s millisecs=%s ratio=%s rheight=%s rwidth=%s TIVO_HEIGHT=%sTIVO_WIDTH=%s' % (inFile, type, width, height, fps, millisecs, ratio, rheight, rwidth, TIVO_HEIGHT, TIVO_WIDTH))
165
166     multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH)
167     multiplier4by3  =  (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH)
168
169     if config.isHDtivo(tsn) and optres:
170         if config.getPixelAR(0):
171             if vpar == None:
172                 npar = config.getPixelAR(1)
173             else:
174                 npar = vpar
175             # adjust for pixel aspect ratio, if set, because TiVo expects square pixels
176             if npar<1.0:
177                 return ['-s', str(width) + 'x' + str(int(math.ceil(height/npar)))]
178             elif npar>1.0:
179                 # FFMPEG expects width to be a multiple of two
180                 return ['-s', str(int(math.ceil(width*npar/2.0)*2)) + 'x' + str(height)]
181         if height <= TIVO_HEIGHT:
182             # pass all resolutions to S3, except heights greater than conf height
183             return []
184         # else, resize video.
185     if (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]:
186         logger.debug('File is within 4:3 list.')
187         return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
188     elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169:
189         logger.debug('File is within 16:9 list and 16:9 allowed.')
190         return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)]
191     else:
192         settings = []
193         #If video is wider than 4:3 add top and bottom padding
194         if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom
195             if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3
196                 if (ratio > 177):#too short needs padding top and bottom
197                     endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9)
198                     settings.append('-aspect')
199                     settings.append('16:9')
200                     if endHeight % 2:
201                         endHeight -= 1
202                     if endHeight < TIVO_HEIGHT * 0.99:
203                         settings.append('-s')
204                         settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
205
206                         topPadding = ((TIVO_HEIGHT - endHeight)/2)
207                         if topPadding % 2:
208                             topPadding -= 1
209
210                         settings.append('-padtop')
211                         settings.append(str(topPadding))
212                         bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
213                         settings.append('-padbottom')
214                         settings.append(str(bottomPadding))
215                     else:   #if only very small amount of padding needed, then just stretch it
216                         settings.append('-s')
217                         settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
218                     logger.debug('16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n%s' % ' '.join(settings))
219                 else: #too skinny needs padding on left and right.
220                     endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9))
221                     settings.append('-aspect')
222                     settings.append('16:9')
223                     if endWidth % 2:
224                         endWidth -= 1
225                     if endWidth < (TIVO_WIDTH-10):
226                         settings.append('-s')
227                         settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
228
229                         leftPadding = ((TIVO_WIDTH - endWidth)/2)
230                         if leftPadding % 2:
231                             leftPadding -= 1
232
233                         settings.append('-padleft')
234                         settings.append(str(leftPadding))
235                         rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
236                         settings.append('-padright')
237                         settings.append(str(rightPadding))
238                     else: #if only very small amount of padding needed, then just stretch it
239                         settings.append('-s')
240                         settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
241                     logger.debug('16:9 aspect allowed, file is narrower than 16:9 padding left and right\n%s' % ' '.join(settings))
242             else: #this is a 4:3 file or 16:9 output not allowed
243                 settings.append('-aspect')
244                 settings.append('4:3')
245                 endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3)
246                 if endHeight % 2:
247                     endHeight -= 1
248                 if endHeight < TIVO_HEIGHT * 0.99:
249                     settings.append('-s')
250                     settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight))
251
252                     topPadding = ((TIVO_HEIGHT - endHeight)/2)
253                     if topPadding % 2:
254                         topPadding -= 1
255
256                     settings.append('-padtop')
257                     settings.append(str(topPadding))
258                     bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding
259                     settings.append('-padbottom')
260                     settings.append(str(bottomPadding))
261                 else:   #if only very small amount of padding needed, then just stretch it
262                     settings.append('-s')
263                     settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
264                 logging.debug('File is wider than 4:3 padding top and bottom\n%s'' '.join(settings))
265
266             return settings
267         #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in
268         #an aspect ratio of 4:3 since they are so narrow.
269         else:
270             endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3))
271             settings.append('-aspect')
272             settings.append('4:3')
273             if endWidth % 2:
274                 endWidth -= 1
275             if endWidth < (TIVO_WIDTH * 0.99):
276                 settings.append('-s')
277                 settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT))
278
279                 leftPadding = ((TIVO_WIDTH - endWidth)/2)
280                 if leftPadding % 2:
281                     leftPadding -= 1
282
283                 settings.append('-padleft')
284                 settings.append(str(leftPadding))
285                 rightPadding = (TIVO_WIDTH - endWidth) - leftPadding
286                 settings.append('-padright')
287                 settings.append(str(rightPadding))
288             else: #if only very small amount of padding needed, then just stretch it
289                 settings.append('-s')
290                 settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT))
291
292             logger.debug_write('File is taller than 4:3 padding left and right\n%s' % ' '.join(settings))
293
294             return settings
295
296 def tivo_compatable(inFile, tsn = ''):
297     supportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]]
298     type, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar =  video_info(inFile)
299     #print type, width, height, fps, millisecs, kbps, akbps, acodec
300
301     if (inFile[-5:]).lower() == '.tivo':
302         logger.debug('TRUE, ends with .tivo. %s' % inFile)
303         return True
304
305     if not type == 'mpeg2video':
306         #print 'Not Tivo Codec'
307         logger.debug('FALSE, type %s not mpeg2video. %s' % (type, inFile))
308         return False
309
310     if os.path.splitext(inFile)[-1].lower() in ('.ts', '.mpv'):
311         logger.debug('FALSE, ext %s not tivo compatible. %s' % (os.path.splitext(inFile)[-1], inFile))
312         return False
313
314     if acodec == 'dca':
315         logger.debug('FALSE, acodec %s not supported. %s' % (acodec, inFile))
316         return False
317
318     if acodec != None:
319         if not akbps or int(akbps) > config.getMaxAudioBR(tsn):
320             logger.debug('FALSE, %s kbps exceeds max audio bitrate. %s' % (akbps, inFile))
321             return False
322
323     if kbps != None:
324         abit = max('0', akbps)
325         if int(kbps)-int(abit) > config.strtod(config.getMaxVideoBR())/1000:
326             logger.debug('FALSE, %s kbps exceeds max video bitrate. %s' % (kbps, inFile))
327             return False
328     else:
329         logger.debug('FALSE, %s kbps not supported. %s' % (kbps, inFile))
330         return False
331
332     if config.isHDtivo(tsn):
333         if vpar != 1.0:
334             if config.getPixelAR(0):
335                 if vpar != None or config.getPixelAR(1) != 1.0:
336                     logger.debug('FALSE, %s not correct PAR, %s' % (vpar, inFile))
337                     return False
338         logger.debug('TRUE, HD Tivo detected, skipping remaining tests %s' % inFile)
339         return True
340
341     if not fps == '29.97':
342         #print 'Not Tivo fps'
343         logger.debug('FALSE, %s fps, should be 29.97. %s' % (fps, inFile))
344         return False
345
346     for mode in supportedModes:
347         if (mode[0], mode[1]) == (width, height):
348             logger.debug('TRUE, %s x %s is valid. %s' % (width, height, inFile))
349             return True
350     #print 'Not Tivo dimensions'
351     logger.debug('FALSE, %s x %s not in supported modes. %s' % (width, height, inFile))
352     return False
353
354 def video_info(inFile):
355     mtime = os.stat(inFile).st_mtime
356     if inFile != videotest:
357         if inFile in info_cache and info_cache[inFile][0] == mtime:
358             logging.debug('CACHE HIT! %s' % inFile)
359             return info_cache[inFile][1]
360
361     if (inFile[-5:]).lower() == '.tivo':
362         info_cache[inFile] = (mtime, (True, True, True, True, True, True, True, True, True, True))
363         logger.debug('VALID, ends in .tivo. %s' % inFile)
364         return True, True, True, True, True, True, True, True, True, True
365
366     cmd = [ffmpeg_path(), '-i', inFile ]
367     ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
368
369     # wait 10 sec if ffmpeg is not back give up
370     for i in xrange(200):
371         time.sleep(.05)
372         if not ffmpeg.poll() == None:
373             break
374
375     if ffmpeg.poll() == None:
376         kill(ffmpeg.pid)
377         info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None, None))
378         return None, None, None, None, None, None, None, None, None, None
379
380     output = ffmpeg.stderr.read()
381     logging.debug('ffmpeg output=%s' % output)
382
383     rezre = re.compile(r'.*Video: ([^,]+),.*')
384     x = rezre.search(output)
385     if x:
386         codec = x.group(1)
387     else:
388         info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None, None))
389         logging.debug('failed at video codec')
390         return None, None, None, None, None, None, None, None, None, None
391
392     rezre = re.compile(r'.*Video: .+, (\d+)x(\d+)[, ].*')
393     x = rezre.search(output)
394     if x:
395         width = int(x.group(1))
396         height = int(x.group(2))
397     else:
398         info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None, None))
399         logger.debug('failed at width/height')
400         return None, None, None, None, None, None, None, None, None, None
401
402     rezre = re.compile(r'.*Video: .+, (.+) (?:fps|tb).*')
403     x = rezre.search(output)
404     if x:
405         fps = x.group(1)
406     else:
407         info_cache[inFile] = (mtime, (None, None, None, None, None, None, None, None, None, None))
408         logging.debug('failed at fps')
409         return None, None, None, None, None, None, None, None, None, None
410
411     # Allow override only if it is mpeg2 and frame rate was doubled to 59.94
412     if (not fps == '29.97') and (codec == 'mpeg2video'):
413         # First look for the build 7215 version
414         rezre = re.compile(r'.*film source: 29.97.*')
415         x = rezre.search(output.lower() )
416         if x:
417             logger.debug('film source: 29.97 setting fps to 29.97')
418             fps = '29.97'
419         else:
420             # for build 8047:
421             rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*')
422             logger.debug('Bug in VideoReDo')
423             x = rezre.search(output.lower() )
424             if x:
425                 fps = '29.97'
426
427     durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),')
428     d = durre.search(output)
429     if d:
430         millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100)
431     else:
432         millisecs = 0
433
434     #get bitrate of source for tivo compatibility test.
435     rezre = re.compile(r'.*bitrate: (.+) (?:kb/s).*')
436     x = rezre.search(output)
437     if x:
438         kbps = x.group(1)
439     else:
440         kbps = None
441         logger.debug('failed at kbps')
442
443     #get audio bitrate of source for tivo compatibility test.
444     rezre = re.compile(r'.*Audio: .+, (.+) (?:kb/s).*')
445     x = rezre.search(output)
446     if x:
447         akbps = x.group(1)
448     else:
449         akbps = None
450         logger.debug('failed at akbps')
451
452     #get audio codec of source for tivo compatibility test.
453     rezre = re.compile(r'.*Audio: ([^,]+),.*')
454     x = rezre.search(output)
455     if x:
456         acodec = x.group(1)
457     else:
458         acodec = None
459         logger.debug('failed at acodec')
460
461     #get audio frequency of source for tivo compatibility test.
462     rezre = re.compile(r'.*Audio: .+, (.+) (?:Hz).*')
463     x = rezre.search(output)
464     if x:
465         afreq = x.group(1)
466     else:
467         afreq = None
468         logger.debug('failed at afreq')
469
470     #get par.
471     rezre = re.compile(r'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
472     x = rezre.search(output)
473     if x and x.group(1)!="0" and x.group(2)!="0":
474         vpar = float(x.group(1))/float(x.group(2))
475     else:
476         vpar = None
477
478     info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar))
479     logger.debug('Codec=%s width=%s height=%s fps=%s millisecs=%s kbps=%s akbps=%s acodec=%s afreq=%s par=%s' %
480         (codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar))
481     return codec, width, height, fps, millisecs, kbps, akbps, acodec, afreq, vpar
482
483 def video_check(inFile, cmd_string):
484     cmd = [ffmpeg_path(), '-i', inFile] + cmd_string.split()
485     ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE)
486     try:
487         shutil.copyfileobj(ffmpeg.stdout, open(videotest, 'wb'))
488         return True
489     except:
490         kill(ffmpeg.pid)
491         return False
492
493 def supported_format(inFile):
494     if video_info(inFile)[0]:
495         return True
496     else:
497         logger.debug('FALSE, file not supported %s' % inFile)
498         return False
499
500 def kill(pid):
501     logger.debug('killing pid=%s' % str(pid))
502     if mswindows:
503         win32kill(pid)
504     else:
505         import os, signal
506         os.kill(pid, signal.SIGTERM)
507
508 def win32kill(pid):
509         import ctypes
510         handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
511         ctypes.windll.kernel32.TerminateProcess(handle, -1)
512         ctypes.windll.kernel32.CloseHandle(handle)
513
514 def gcd(a,b):
515     while b:
516         a, b = b, a % b
517     return a
518
Note: See TracBrowser for help on using the browser.