3 from threading
import Thread
4 from dvdtitlestream
import DVDTitleStream
23 logger
= logging
.getLogger('pyTivo.dvdvideo.vobstream')
25 info_cache
= lrucache
.LRUCache(1000)
29 BLOCKSIZE
= 512 * 1024
35 # subprocess is broken for me on windows so super hack
36 def patchSubprocess():
37 o
= subprocess
.Popen
._make
_inheritable
39 def _make_inheritable(self
, handle
):
40 if not handle
: return subprocess
.GetCurrentProcess()
41 return o(self
, handle
)
43 subprocess
.Popen
._make
_inheritable
= _make_inheritable
45 mswindows
= (sys
.platform
== "win32")
52 msg
= msg
.decode('utf8')
54 if sys
.platform
== 'darwin':
55 msg
= msg
.decode('macroman')
57 msg
= msg
.decode('iso8859-1')
60 def WriteSectorStreamToSubprocess( fhin
, sub
, event
, blocksize
):
62 # Write all the data till either end is closed or done
63 while not event
.isSet():
65 # Read in the block and escape if we got nothing
66 data
= fhin
.read( blocksize
)
70 if sub
.poll() != None and sub
.stdin
!= None:
73 # Write the data and flush it
75 sub
.stdin
.write( data
)
80 # We got less data so we must be at the end
81 if len(data
) < blocksize
:
84 # Close the input if it's not already closed
88 # Close the output if it's not already closed
89 if sub
.stdin
!= None and not sub
.stdin
.closed
:
92 def vobstream(isQuery
, inFile
, outFile
, tsn
=''):
93 settings
= {'TBD': 'TBD'}
98 ffmpeg_path
= config
.get_bin('ffmpeg')
100 dvd
= virtualdvd
.VirtualDVD( inFile
)
101 title
= dvd
.FileTitle()
102 ts
= DVDTitleStream( title
.Stream() )
104 vinfo
= video_info( inFile
)
105 vmap
= vinfo
['mapVideo'].replace( '.', ':' )
106 amap
= vinfo
['mapAudio'].replace( '.', ':' )
109 sp
= subprocess
.Popen( [ ffmpeg_path
, '-i', '-', \
110 '-map', vmap
, '-map', amap
,
111 '-acodec', 'copy', '-vcodec', 'copy', '-f', 'vob', '-' ], \
112 stdout
= subprocess
.PIPE
, \
113 stdin
= subprocess
.PIPE
, \
114 bufsize
= BLOCKSIZE
* MAXBLOCKS
)
116 # Make an event to shutdown the thread
117 sde
= threading
.Event()
120 # Stream data to the subprocess
121 t
= Thread( target
=WriteSectorStreamToSubprocess
, args
=(ts
,sp
,sde
,BLOCKSIZE
) )
124 mpgcat_procs
[inFile
] = {'stream': ts
, 'start': 0, 'end': 0, \
125 'thread': t
, 'process':sp
, 'event':sde
, \
126 'last_read': time
.time(), 'blocks': []}
129 transfer_blocks(inFile
, outFile
)
131 def is_resumable(inFile
, offset
):
132 if inFile
in mpgcat_procs
:
133 proc
= mpgcat_procs
[inFile
]
134 if proc
['start'] <= offset
< proc
['end']:
140 def resume_transfer(inFile
, outFile
, offset
):
141 proc
= mpgcat_procs
[inFile
]
142 offset
-= proc
['start']
144 for block
in proc
['blocks']:
148 block
= block
[offset
:]
149 outFile
.write('%x\r\n' % len(block
))
151 outFile
.write('\r\n')
154 except Exception, msg
:
157 proc
['start'] = proc
['end']
160 transfer_blocks(inFile
, outFile
)
162 def transfer_blocks(inFile
, outFile
):
163 proc
= mpgcat_procs
[inFile
]
164 blocks
= proc
['blocks']
169 block
= proc
['process'].stdout
.read(BLOCKSIZE
)
171 block
= proc
['stream'].read(BLOCKSIZE
)
172 proc
['last_read'] = time
.time()
173 except Exception, msg
:
178 if not block
or len(block
) == 0:
181 proc
['stream'].close()
182 except Exception, msg
:
189 proc
['end'] += len(block
)
190 if len(blocks
) > MAXBLOCKS
:
191 proc
['start'] += len(blocks
[0])
195 outFile
.write('%x\r\n' % len(block
))
197 outFile
.write('\r\n')
198 except Exception, msg
:
202 def reap_process(inFile
):
203 if inFile
in mpgcat_procs
:
204 proc
= mpgcat_procs
[inFile
]
205 if proc
['last_read'] + TIMEOUT
< time
.time():
209 reaper
= threading
.Timer(TIMEOUT
, reap_process
, (inFile
,))
210 reapers
[inFile
] = reaper
215 # Don't fear the reaper
217 reapers
[inFile
].cancel()
223 kill(mpgcat_procs
[inFile
]['process'])
224 mpgcat_procs
[inFile
]['process'].wait()
226 # Tell thread to break out of loop
227 mpgcat_procs
[inFile
]['event'].set()
228 mpgcat_procs
[inFile
]['thread'].join()
230 del mpgcat_procs
[inFile
]
232 def supported_format( inFile
):
233 dvd
= virtualdvd
.VirtualDVD( inFile
)
234 return dvd
.Valid() and dvd
.file_id
!= -1
238 dvd
= virtualdvd
.VirtualDVD( inFile
)
239 return dvd
.FileTitle().Size()
243 def video_info(inFile
, audio_spec
= "", cache
=True):
246 mtime
= os
.stat(inFile
).st_mtime
251 if inFile
in info_cache
and info_cache
[inFile
][0] == mtime
:
252 debug('CACHE HIT! %s' % inFile
)
253 return info_cache
[inFile
][1]
255 dvd
= virtualdvd
.VirtualDVD( inFile
)
256 if not dvd
.Valid() or dvd
.file_id
== -1:
257 debug('Not a valid dvd file')
260 ffmpeg_path
= config
.get_bin('ffmpeg')
262 title
= dvd
.FileTitle()
263 sid
= title
.FindBestAudioStreamID( audio_spec
)
264 ts
= DVDTitleStream( title
.Stream() )
267 # Make a subprocess to get the information from a stream
268 proc
= subprocess
.Popen( [ ffmpeg_path
, '-i', '-' ], \
269 stdout
=subprocess
.PIPE
, \
270 stdin
=subprocess
.PIPE
, \
271 stderr
=subprocess
.STDOUT
, \
272 bufsize
=BLOCKSIZE
* MAXBLOCKS
)
274 # Make an event to shutdown the thread
275 sde
= threading
.Event()
278 # Stream data to the subprocess
279 t
= Thread( target
=WriteSectorStreamToSubprocess
, args
=(ts
,proc
,sde
,BLOCKSIZE
) )
282 # Readin the output from the subprocess
286 # Don't throw on any IO errors
288 data
= proc
.stdout
.read( BLOCKSIZE
)
292 # If we're blank, then the data stream is empty
299 # Shutdown the helper threads/processes
304 # Close the title stream
307 #print "VOB Info:", output
308 vInfo
['mapAudio'] = ''
310 attrs
= {'container': r
'Input #0, ([^,]+),',
311 'vCodec': r
'.*Video: ([^,]+),.*', # video codec
312 'aKbps': r
'.*Audio: .+, (.+) (?:kb/s).*', # audio bitrate
313 'aCodec': r
'.*Audio: ([^,]+),.*', # audio codec
314 'aFreq': r
'.*Audio: .+, (.+) (?:Hz).*', # audio frequency
315 'mapVideo': r
'([0-9]+\.[0-9]+).*: Video:.*', # video mapping
316 'mapAudio': r
'([0-9]+\.[0-9]+)\[0x%02x\]: Audio:.*' % sid
} # Audio mapping
319 rezre
= re
.compile(attrs
[attr
])
320 x
= rezre
.search(output
)
322 #print attr, attrs[attr], x.group(1)
323 vInfo
[attr
] = x
.group(1)
325 #print attr, attrs[attr], '(None)'
326 if attr
in ['container', 'vCodec']:
328 vInfo
['Supported'] = False
331 #print '***************** failed at ' + attr + ' : ' + attrs[attr]
332 debug('failed at ' + attr
)
334 # Get the Pixel Aspect Ratio
335 rezre
= re
.compile(r
'.*Video: .+PAR ([0-9]+):([0-9]+) DAR [0-9:]+.*')
336 x
= rezre
.search(output
)
337 if x
and x
.group(1) != "0" and x
.group(2) != "0":
338 vInfo
['par1'] = x
.group(1) + ':' + x
.group(2)
339 vInfo
['par2'] = float(x
.group(1)) / float(x
.group(2))
341 vInfo
['par1'], vInfo
['par2'] = None, None
343 # Get the Display Aspect Ratio
344 rezre
= re
.compile(r
'.*Video: .+DAR ([0-9]+):([0-9]+).*')
345 x
= rezre
.search(output
)
346 if x
and x
.group(1) != "0" and x
.group(2) != "0":
347 vInfo
['dar1'] = x
.group(1) + ':' + x
.group(2)
351 # Get the video dimensions
352 rezre
= re
.compile(r
'.*Video: .+, (\d+)x(\d+)[, ].*')
353 x
= rezre
.search(output
)
355 vInfo
['vWidth'] = int(x
.group(1))
356 vInfo
['vHeight'] = int(x
.group(2))
359 vInfo
['vHeight'] = ''
360 vInfo
['Supported'] = False
361 debug('failed at vWidth/vHeight')
363 vInfo
['millisecs'] = title
.Time().MSecs()
364 vInfo
['Supported'] = True
367 info_cache
[inFile
] = (mtime
, vInfo
)
372 debug('killing pid=%s' % str(popen
.pid
))
378 debug('sending SIGTERM to pid: %s' % popen
.pid
)
379 os
.kill(popen
.pid
, signal
.SIGTERM
)
381 if popen
.poll() is not None:
382 debug('process %s has exited' % popen
.pid
)
385 while popen
.poll() is None:
386 debug('sending SIGKILL to pid: %s' % popen
.pid
)
387 os
.kill(popen
.pid
, signal
.SIGKILL
)
392 handle
= ctypes
.windll
.kernel32
.OpenProcess(1, False, pid
)
393 ctypes
.windll
.kernel32
.TerminateProcess(handle
, -1)
394 ctypes
.windll
.kernel32
.CloseHandle(handle
)