From 615d8c4a4feba5ba2c3c423144a0df81c4a8b03a Mon Sep 17 00:00:00 2001 From: Eric von Bayer Date: Tue, 20 Oct 2009 15:26:22 -0700 Subject: [PATCH] Audio specification plus better error checking. See the pyTivo.conf.dist for info on the audio_spec option. --- plugins/dvdvideo/dvdfolder.py | 76 ++++++++++++++++++++++++++++++++++++++----- plugins/dvdvideo/dvdvideo.py | 25 +++++++++----- plugins/dvdvideo/vobstream.py | 75 +++++++++++++++++++++++++++++------------- pyTivo.conf.dist | 5 ++- 4 files changed, 140 insertions(+), 41 deletions(-) mode change 100644 => 100755 pyTivo.conf.dist diff --git a/plugins/dvdvideo/dvdfolder.py b/plugins/dvdvideo/dvdfolder.py index 2ad4da6..ca32c61 100755 --- a/plugins/dvdvideo/dvdfolder.py +++ b/plugins/dvdvideo/dvdfolder.py @@ -56,11 +56,29 @@ PATTERN_VTS_IFO = re.compile( r"(?i)VTS_([0-9]{2})_0.IFO$" ) PATTERN_VTS_VOB = re.compile( r"(?i)VTS_([0-9]{2})_([0-9]).VOB$" ) def FindDOSFilename( path, dosname ): - if os.path.isdir( path ): + try: + if os.path.isdir( path ): for f in os.listdir( path ): if f.upper() == dosname: return os.path.join( path, f ) - return None + except: + return None + +def MatchAudioAttr( audio_attr, lang, chan ): + if lang != '*' and lang.lower() != audio_attr.LanguageCode().lower(): + #print "Failed based on language" + return False + elif chan != '*' and int( chan ) != audio_attr.Channels(): + #print "Failed based on channels" + return False + elif audio_attr.Coding() != "AC3": + #print "Failed based on coding" + return False + elif audio_attr.CodeExtensionValue() > 1: + #print "Failed based on extension", audio_attr.CodeExtensionValue() + return False + else: + return True def BCD2Dec( bcd ): return int( str( "%X" % bcd ) ) @@ -287,6 +305,9 @@ class IFOAudioAttrs(DVDFileHandle): def CodeExtension( self ): return [ "Unspecified", "Normal", "For the Blind", "Director's Comments", \ "Alternate Director's Comments" ] [ self.__code_ext ] + + def CodeExtensionValue( self ): + return self.__code_ext def StreamID( self ): return self.__stream_id @@ -321,7 +342,7 @@ class IFOAVAttrs(DVDFileHandle): def AudioList( self ): return self.__audio - + ################################# IFOFile ###################################### class IFOVMGFile(DVDFileHandle): @@ -453,12 +474,12 @@ class IFOVTSFile(DVDFileHandle): handle = open( filename, "rb" ) DVDFileHandle.__init__( self, handle, 0x0 ) if not self.IsOpen(): - raise + raise DVDFormatError( "Can't open VTS info file" ) # Make sure we're a VMG IFO file id = self.Read( 12 ) if id != "DVDVIDEO-VTS": - raise + raise DVDFormatError( "Expected a VTS info file" ) # Get the VOB, IFO, and BUP sectors self.Seek( 0x0C ) @@ -523,6 +544,18 @@ class IFOVTSFile(DVDFileHandle): # Read in the playback time info['playtime'] = IFOPlaybackTime( handle ) + # Skip the prohibited ops + self.Skip(4) + + # Read the list of valid audio streams + astrs = list() + for num in range(8): + strnum = self.ReadU8() + self.Skip(1) + if strnum & 0x80: + astrs.append( strnum & 0x7 ) + info[ 'audio_stream_nums' ] = astrs + # Default these to False and mark true if we find a cell # that matches. info['ilvu'] = False @@ -553,8 +586,11 @@ class IFOVTSFile(DVDFileHandle): if i == 0: ts.AddSectors( s, e ) else: - for [sr,er] in ComputeRealSectors( s, e, *ts.files() ): - ts.AddSectors( sr, er ) + try: + for [sr,er] in ComputeRealSectors( s, e, *ts.files() ): + ts.AddSectors( sr, er ) + except: + raise DVDFormatError( "Error processing ILVU block within title set "+self.__num+", program chain "+info["number"] ) info['stream'] = ts @@ -631,6 +667,11 @@ class DVDTitle(object): raise DVDFormatError( "Title number: %d - PGC number %d in VTS %d is out of range (%d)" % ( tnum, self.__tinfo['vts_pgc_num'], self.__tinfo['vts_num'], self.__vts.NumPGCs() ) ) self.__valid = True + + self.__audio_streams = list() + vts_audio_streams = self.__vts.Title().AudioList() + for asnum in self.__pgcinfo['audio_stream_nums']: + self.__audio_streams.append( vts_audio_streams[asnum] ) except: raise @@ -656,6 +697,25 @@ class DVDTitle(object): def HasInterleaved( self ): return self.__pgcinfo['ilvu'] + def AudioStreams( self ): + return self.__audio_streams + + def FindBestAudioStreamID( self, spec ): + #print "FindBestAudioStreamID( "+spec+" )" + parts = spec.split( ',' ) + for part in parts: + #print "Part:", part + elems = part.split( ':', 1 ) + + if len(elems) >= 2: + for stream in self.__audio_streams: + #print elems[0], elems[1], "==?", stream.LanguageCode(),stream.Channels(), "(0x%02x)" % stream.StreamID() + if MatchAudioAttr( stream, elems[0], elems[1] ): + return stream.StreamID() + + #print "Defaulted to", self.__audio_streams[0].LanguageCode(),self.__audio_streams[0].Channels(), "(0x%02x)" % self.__audio_streams[0].StreamID() + return self.__audio_streams[0].StreamID() + def Stream( self ): return self.__pgcinfo['stream'] @@ -666,7 +726,7 @@ class DVDTitle(object): return self.__pgcinfo['playtime'] -################################# DVDFolder #################################### +###########################s###### DVDFolder #################################### class DVDFolder(object): """DVD Folder along with routines to read contents""" diff --git a/plugins/dvdvideo/dvdvideo.py b/plugins/dvdvideo/dvdvideo.py index 3301df2..34246b1 100755 --- a/plugins/dvdvideo/dvdvideo.py +++ b/plugins/dvdvideo/dvdvideo.py @@ -164,9 +164,6 @@ class DVDVideo(Plugin): logger.info(msg) logger.debug("Finished outputing video") - def __duration(self, full_path): - return vobstream.video_info(full_path)['millisecs'] - def __total_items(self, full_path): count = 0 try: @@ -190,9 +187,10 @@ class DVDVideo(Plugin): # Size is estimated by taking audio and video bit rate adding 2% return vobstream.size( full_path ) - def metadata_full(self, full_path, tsn='', mime=''): + def metadata_full(self, full_path, tsn='', mime='', audio_spec=''): data = {} - vInfo = vobstream.video_info(full_path) + + vInfo = vobstream.video_info( full_path, audio_spec=audio_spec ) if ((int(vInfo['vHeight']) >= 720 and config.getTivoHeight >= 720) or @@ -256,7 +254,7 @@ class DVDVideo(Plugin): ) now = datetime.utcnow() - duration = self.__duration(full_path) + duration = int( vInfo['millisecs'] ) duration_delta = timedelta(milliseconds = duration) min = duration_delta.seconds / 60 sec = duration_delta.seconds % 60 @@ -288,6 +286,7 @@ class DVDVideo(Plugin): container = handler.server.containers[cname] force_alpha = container.get('force_alpha', 'False').lower() == 'true' + audio_spec = container.get('audio_spec', '') # Patch in our virtual filesystem if it exists while 1: @@ -337,7 +336,7 @@ class DVDVideo(Plugin): if len(files) == 1 or f.name in vobstream.info_cache: video['valid'] = vobstream.supported_format(f.name) if video['valid']: - video.update(self.metadata_full(f.name, tsn)) + video.update(self.metadata_full(f.name, tsn, audio_spec=audio_spec)) elif dvd != None and dvd.Valid(): video['valid'] = True video['title'] = dvd.DVDTitleName() @@ -379,13 +378,18 @@ class DVDVideo(Plugin): #print "************************************* In TVBus" tsn = handler.headers.getheader('tsn', '') f = query['File'][0] + subcname = query['Container'][0] + cname = subcname.split('/')[0] + container_obj = handler.server.containers[cname] + audio_spec = container_obj.get('audio_spec', '') + path = self.get_local_path(handler, query) file_path = path + os.path.normpath(f) file_info = VideoDetails() file_info['valid'] = vobstream.supported_format(file_path) if file_info['valid']: - file_info.update(self.metadata_full(file_path, tsn)) + file_info.update(self.metadata_full(file_path, tsn, audio_spec=audio_spec)) t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode) t.video = file_info @@ -413,6 +417,9 @@ class DVDVideo(Plugin): break container = quote(query['Container'][0].split('/')[0]) + container_obj = handler.server.containers[container] + audio_spec = container_obj.get('audio_spec', '') + ip = config.get_ip() port = config.getPort() @@ -436,7 +443,7 @@ class DVDVideo(Plugin): mime = 'video/mpeg' if file_info['valid']: - file_info.update(self.metadata_full(file_path, tsn, mime)) + file_info.update(self.metadata_full(file_path, tsn, mime, audio_spec=audio_spec)) url = baseurl + '/%s%s' % (container, quote(f)) diff --git a/plugins/dvdvideo/vobstream.py b/plugins/dvdvideo/vobstream.py index eb2cd47..4c33320 100755 --- a/plugins/dvdvideo/vobstream.py +++ b/plugins/dvdvideo/vobstream.py @@ -57,10 +57,10 @@ def debug(msg): msg = msg.decode('iso8859-1') logger.debug(msg) -def WriteSectorStreamToSubprocess( fhin, sub, blocksize ): +def WriteSectorStreamToSubprocess( fhin, sub, event, blocksize ): # Write all the data till either end is closed or done - while 1: + while not event.isSet(): # Read in the block and escape if we got nothing data = fhin.read( blocksize ) @@ -101,20 +101,30 @@ def vobstream(isQuery, inFile, outFile, tsn=''): title = dvd.FileTitle() ts = DVDTitleStream( title.Stream() ) + vinfo = video_info( inFile ) + vmap = vinfo['mapVideo'].replace( '.', ':' ) + amap = vinfo['mapAudio'].replace( '.', ':' ) + if USE_FFMPEG: sp = subprocess.Popen( [ ffmpeg_path, '-i', '-', \ + '-map', vmap, '-map', amap, '-acodec', 'copy', '-vcodec', 'copy', '-f', 'vob', '-' ], \ stdout = subprocess.PIPE, \ stdin = subprocess.PIPE, \ bufsize = BLOCKSIZE * MAXBLOCKS ) + # Make an event to shutdown the thread + sde = threading.Event() + sde.clear() + # Stream data to the subprocess - t = Thread( target=WriteSectorStreamToSubprocess, args=(ts,sp,BLOCKSIZE,) ) + t = Thread( target=WriteSectorStreamToSubprocess, args=(ts,sp,sde,BLOCKSIZE) ) t.start() - + mpgcat_procs[inFile] = {'stream': ts, 'start': 0, 'end': 0, \ - 'thread': t, 'process':sp, \ + 'thread': t, 'process':sp, 'event':sde, \ 'last_read': time.time(), 'blocks': []} + reap_process(inFile) transfer_blocks(inFile, outFile) @@ -125,9 +135,6 @@ def is_resumable(inFile, offset): return True else: cleanup(inFile) - if USE_FFMPEG: - proc['thread'].exit() - kill(proc['process']) return False def resume_transfer(inFile, outFile, offset): @@ -166,9 +173,6 @@ def transfer_blocks(inFile, outFile): except Exception, msg: logger.info(msg) cleanup(inFile) - if USE_FFMPEG: - kill(proc['process']) - proc['thread'].exit() break if not block or len(block) == 0: @@ -199,22 +203,32 @@ def reap_process(inFile): if inFile in mpgcat_procs: proc = mpgcat_procs[inFile] if proc['last_read'] + TIMEOUT < time.time(): - mpgcat_procs[inFile]['stream'].close() - del mpgcat_procs[inFile] - del reapers[inFile] - if USE_FFMPEG: - proc['thread'].exit() - kill(proc['process']) + cleanup(inFile) + else: reaper = threading.Timer(TIMEOUT, reap_process, (inFile,)) reapers[inFile] = reaper reaper.start() def cleanup(inFile): - del mpgcat_procs[inFile] - reapers[inFile].cancel() - del reapers[inFile] + # Don't fear the reaper + try: + reapers[inFile].cancel() + del reapers[inFile] + except: + pass + + if USE_FFMPEG: + kill(mpgcat_procs[inFile]['process']) + mpgcat_procs[inFile]['process'].wait() + + # Tell thread to break out of loop + mpgcat_procs[inFile]['event'].set() + mpgcat_procs[inFile]['thread'].join() + + del mpgcat_procs[inFile] + def supported_format( inFile ): dvd = virtualdvd.VirtualDVD( inFile ) return dvd.Valid() and dvd.file_id != -1 @@ -226,7 +240,7 @@ def size(inFile): except: return 0 -def video_info(inFile, cache=True): +def video_info(inFile, audio_spec = "", cache=True): vInfo = dict() try: mtime = os.stat(inFile).st_mtime @@ -246,6 +260,7 @@ def video_info(inFile, cache=True): ffmpeg_path = config.get_bin('ffmpeg') title = dvd.FileTitle() + sid = title.FindBestAudioStreamID( audio_spec ) ts = DVDTitleStream( title.Stream() ) ts.seek(0) @@ -255,9 +270,13 @@ def video_info(inFile, cache=True): stdin=subprocess.PIPE, \ stderr=subprocess.STDOUT, \ bufsize=BLOCKSIZE * MAXBLOCKS ) + + # Make an event to shutdown the thread + sde = threading.Event() + sde.clear() # Stream data to the subprocess - t = Thread( target=WriteSectorStreamToSubprocess, args=(ts,proc,BLOCKSIZE,) ) + t = Thread( target=WriteSectorStreamToSubprocess, args=(ts,proc,sde,BLOCKSIZE) ) t.start() # Readin the output from the subprocess @@ -276,30 +295,40 @@ def video_info(inFile, cache=True): # append the output output += data + + # Shutdown the helper threads/processes + sde.set() + proc.wait() + t.join() # Close the title stream ts.close() #print "VOB Info:", output + vInfo['mapAudio'] = '' attrs = {'container': r'Input #0, ([^,]+),', 'vCodec': r'.*Video: ([^,]+),.*', # video codec 'aKbps': r'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate 'aCodec': r'.*Audio: ([^,]+),.*', # audio codec 'aFreq': r'.*Audio: .+, (.+) (?:Hz).*', # audio frequency - 'mapVideo': r'([0-9]+\.[0-9]+).*: Video:.*'} # video mapping + 'mapVideo': r'([0-9]+\.[0-9]+).*: Video:.*', # video mapping + 'mapAudio': r'([0-9]+\.[0-9]+)\[0x%02x\]: Audio:.*' % sid } # Audio mapping for attr in attrs: rezre = re.compile(attrs[attr]) x = rezre.search(output) if x: + #print attr, attrs[attr], x.group(1) vInfo[attr] = x.group(1) else: + #print attr, attrs[attr], '(None)' if attr in ['container', 'vCodec']: vInfo[attr] = '' vInfo['Supported'] = False else: vInfo[attr] = None + #print '***************** failed at ' + attr + ' : ' + attrs[attr] debug('failed at ' + attr) # Get the Pixel Aspect Ratio diff --git a/pyTivo.conf.dist b/pyTivo.conf.dist old mode 100644 new mode 100755 index 5b1d916..e3a36d7 --- a/pyTivo.conf.dist +++ b/pyTivo.conf.dist @@ -94,11 +94,14 @@ path=/home/armooo/Videos # VIDEO_TS (this is only needed if you want descriptions, actors, etc). It # pretends the folder that contains a VIDEO_TS is the name of the movie, it # makes episode names for all the titles over 10s underneath of it. Only type -# and path parameters are required. +# and path parameters are required. The audio spec defines a priority order +# for the preferred audio stream, it it built up of comma seperated elemnts of +# the form <2 letter language code>:. # #[DVDs] #type=dvdvideo #path= #fast_index=false #title_min=10.0 +#audio_spec=en:8,en:6,*:6,fr:* #force_alpha=false -- 2.11.4.GIT