realigned with wmcbrine's pyTivo code
[pyTivo_dvdvideo.git] / dvdtitlestream.py
blobfe96c7faa7a9d2ffde1b3783be6ea26c57439dc7
1 # Module: dvdtitlestream.py
2 # Author: Eric von Bayer
3 # Contact:
4 # Date: August 18, 2009
5 # Description:
6 # Class that works like a file but is in reality a series of file system
7 # files along with a sector map. This in conjunction with the underlying
8 # composite file will allow linear file access to the convoluted underlying
9 # video stream. Intentionally is a similar API to the file object.
11 # Copyright (c) 2009, Eric von Bayer
12 # All rights reserved.
14 # Redistribution and use in source and binary forms, with or without
15 # modification, are permitted provided that the following conditions are met:
17 # * Redistributions of source code must retain the above copyright notice,
18 # this list of conditions and the following disclaimer.
19 # * Redistributions in binary form must reproduce the above copyright notice,
20 # this list of conditions and the following disclaimer in the documentation
21 # and/or other materials provided with the distribution.
22 # * The names of the contributors may not be used to endorse or promote
23 # products derived from this software without specific prior written
24 # permission.
26 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
30 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
31 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
32 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
33 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
34 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 import os
38 from compositefile import CompositeFile
40 DVD_BLOCK_LEN = 2048L
42 try:
43 os.SEEK_SET
44 except AttributeError:
45 os.SEEK_SET, os.SEEK_CUR, os.SEEK_END = range(3)
47 class DVDTitleStream(object):
48 """Virtual file that is a composite of vob files mapped through a sector list"""
49 def __init__( self, *files ):
50 if len(files) == 1 and isinstance( files[0], DVDTitleStream ):
51 self.__cfile = CompositeFile( files[0].__cfile )
52 self.__slist = files[0].__slist
53 else:
54 self.__cfile = CompositeFile( *files )
55 self.__slist = list()
57 self.__cfile.close()
58 self.__sector_map = None
59 self.__srange = -1
60 self.__sects = 0L
61 self.__off = 0L
62 self.__next_sec_off = 0L
63 self.closed = False
65 def __str__( self ):
66 ser = list()
67 for [s,e] in self.__slist:
68 ser.append( "[%d-%d]" % (s,e) )
69 return "[" + str( self.__cfile ) + "] % " + ",".join( ser )
71 # Build a map of ( virtual offset, real offset )
72 def __map_sectors( self ):
73 if self.__sector_map == None:
74 self.__cfile.open()
75 self.__sector_map = list()
76 off = 0L
77 for (s,e) in self.__slist:
78 off += ( e - s + 1 ) * DVD_BLOCK_LEN
79 self.__sector_map.append( ( off, s * DVD_BLOCK_LEN ) )
80 self.__srange = -1
81 self.__offset = 0L
82 self.__next_sec_off = self.__sector_map[0][0]
83 self.__next_range()
85 # Advance to the next file
86 def __next_range( self ):
88 if not self.closed:
89 # Bump the file number that we're on
90 self.__srange += 1
92 # If there's another sector range, seek to it and get the next offset
93 if self.__srange < len( self.__sector_map ):
94 self.__cfile.seek( self.__sector_map[self.__srange][1] )
95 self.__next_sec_off = self.__sector_map[self.__srange][0]
97 # We're done, close the file
98 else:
99 self.__cfile.close()
100 self.closed = True
102 # Read a slice, return the data and how many bytes remain to be read
103 def __read_slice( self, bytes ):
105 if not self.closed:
106 rem = self.__off + bytes - self.__next_sec_off
108 # If the remaining bytes aren't negative, then read a slice and advance
109 # to the next file.
110 if rem >= 0L:
111 bytes = bytes - rem
112 data = self.__cfile.read( bytes )
113 self.__off += len(data)
114 self.__next_range()
115 return data, rem
117 # Read the bytes all from this file
118 else:
119 data = self.__cfile.read( bytes )
120 self.__off += len(data)
121 return data, 0
122 else:
123 return "", bytes
125 def AddSectors( self, start, end ):
126 self.__sects += (end - start) + 1
127 if len(self.__slist) > 0:
128 if self.__slist[-1][1]+1 == start:
129 self.__slist[-1][1] = end
130 return
131 self.__slist.append( [ start, end ] )
132 self.__sector_map = None
134 def SectorList( self ):
135 return self.__slist
137 def files( self ):
138 return self.__cfile.files()
140 def tell_real( self ):
141 return self.__cfile.tell()
143 def tell( self ):
144 return self.__off
146 # Seek into the composite file
147 def seek( self, off, whence = os.SEEK_SET ):
148 self.__map_sectors()
150 # Calculate the new seek offset
151 if whence == os.SEEK_SET:
152 new_off = off
153 elif whence == os.SEEK_CUR:
154 new_off = self.__off + off
155 elif whence == os.SEEK_END:
156 new_off = self.__sector_map[-1][0] + off
157 else:
158 raise "seek called with an invalid offset type"
160 # Determine which file this seek offset is part of
161 soff = 0
162 srange = 0
163 for ( eoff, roff ) in self.__sector_map:
164 if eoff > new_off:
165 break
166 srange += 1
167 soff = eoff
169 # Make sure this was a valid seek point
170 if eoff <= new_off:
171 raise "seek beyond the bounds of the composite file"
173 # Make sure the correct file is open
174 if srange != self.__srange:
175 self.__next_sec_off = eoff
176 self.__srange = srange
178 # Seek the file handle
179 self.__cfile.seek( roff + new_off - soff )
180 self.__off = new_off
182 # Read from the file
183 def read( self, bytes ):
184 self.__map_sectors()
185 if self.closed == False:
186 data = ""
187 while bytes > 0 and self.closed == False:
188 slice_data, bytes = self.__read_slice( bytes )
189 data += slice_data
190 return data
191 else:
192 return ""
194 def close( self ):
195 if self.__cfile.closed == False:
196 self.__cfile.close()
197 self.closed = True
199 def size( self ):
200 return self.__sects * DVD_BLOCK_LEN