1 # Written by Arno Bakker
\r
2 # see LICENSE.txt for license information
\r
4 """ Utility functions for (live) streams in Ogg container format.
\r
6 See: http://www.ietf.org/rfc/rfc3533.txt
\r
7 http://www.theora.org/doc/Theora.pdf (Aug 5, 2009)
\r
8 http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html (Feb 3, 2010)
\r
9 http://flac.sourceforge.net/ogg_mapping.html
\r
14 from cStringIO import StringIO
\r
20 return name.endswith('.ogg') or name.endswith('.ogv') or name.endswith('ogm') or name.endswith('oga') or name.endswith('ogx')
\r
23 def ogg_grab_page(input,checkcrc=False):
\r
24 """ Read a Ogg Version 0 page.
\r
25 @param input An input stream object.
\r
26 @param checkcrc Whether to check the page's CRC or not.
\r
27 @return (isheader,header,body) tuples.
\r
28 isheader is True if the page is a BOS or comment or setup header page.
\r
30 # TODO: make resistant against streams that return less than req. bytes
\r
31 # TODO: add identifiers for other codecs to recognize their headers
\r
32 capture_pattern = input.read(4)
\r
33 stream_structure_version = input.read(1)
\r
34 header_type_flag = input.read(1)
\r
35 granule_position = input.read(8)
\r
36 bitstream_serial_number = input.read(4)
\r
37 page_sequence_number = input.read(4)
\r
38 CRC_checksum = input.read(4)
\r
39 number_page_segments = input.read(1)
\r
40 segment_table = input.read(ord(number_page_segments))
\r
42 header_size = ord(number_page_segments)+27
\r
44 for i in range(0,ord(number_page_segments)):
\r
45 segment_size += ord(segment_table[i])
\r
46 page_size = header_size + segment_size
\r
48 if capture_pattern != "OggS":
\r
49 raise ValueError("Header does not start with OggS")
\r
51 if page_size > 65307:
\r
52 raise ValueError("Page too big")
\r
55 print >>sys.stderr,"ogg: type",ord(header_type_flag)
\r
57 header = capture_pattern+stream_structure_version+header_type_flag+granule_position+bitstream_serial_number+page_sequence_number+CRC_checksum+number_page_segments+segment_table
\r
58 body = input.read(page_size - header_size)
\r
64 crcheader = capture_pattern+stream_structure_version+header_type_flag+granule_position+bitstream_serial_number+page_sequence_number+'\x00\x00\x00\x00'+number_page_segments+segment_table
\r
65 crcpage = crcheader+body
\r
67 newcrc = ogg_crc(crcpage)
\r
68 newcrcnbo = socket.htonl(newcrc) & 0xffffffff
\r
69 newcrcstr = "%08x" % newcrcnbo
\r
71 oldcrcstr = binascii.hexlify(CRC_checksum)
\r
73 print >>sys.stderr,"ogg: CRC exp",oldcrcstr,"got",newcrcstr
\r
74 if oldcrcstr != newcrcstr:
\r
75 raise ValueError("Page fails CRC check")
\r
77 # BOS or header page
\r
78 header_type = body[0]
\r
80 if header_type == '\x01' or header_type == '\x03' or header_type == '\x05':
\r
82 vorbis_grab_header(StringIO(body))
\r
83 elif header_type == '\x80' or header_type == '\x81' or header_type == '\x82':
\r
85 theora_grab_header(StringIO(body))
\r
86 elif header_type == '\x7F':
\r
88 flac_grab_header(StringIO(body))
\r
90 return (isheader,header,body)
\r
93 def vorbis_grab_header(input):
\r
95 header_type = input.read(1)
\r
96 if header_type == '\x01':
\r
97 codec = input.read(6)
\r
98 print >>sys.stderr,"ogg: Got vorbis ident header",codec
\r
99 elif header_type == '\x03':
\r
100 print >>sys.stderr,"ogg: Got vorbis comment header"
\r
101 elif header_type == '\x05':
\r
102 print >>sys.stderr,"ogg: Got vorbis setup header"
\r
105 def theora_grab_header(input):
\r
107 header_type = input.read(1)
\r
108 if header_type == '\x80':
\r
109 codec = input.read(6)
\r
110 print >>sys.stderr,"ogg: Got theora ident header",codec
\r
111 elif header_type == '\x81':
\r
112 print >>sys.stderr,"ogg: Got theora comment header"
\r
113 elif header_type == '\x82':
\r
114 print >>sys.stderr,"ogg: Got theora setup header"
\r
117 def flac_grab_header(input):
\r
119 header_type = input.read(1)
\r
120 if header_type == '\x7f':
\r
121 codec = input.read(4)
\r
122 print >>sys.stderr,"ogg: Got flac ident header",codec
\r
126 Ogg apparently uses a non-standard CRC, see http://www.xiph.org/ogg/doc/framing.html
\r
127 The following code is from
\r
128 http://mimosa-pudica.net/src/oggcut.py
\r
129 by y.fujii <y-fujii at mimosa-pudica.net>, public domain
\r
132 def makeCRCTable( idx ):
\r
134 for i in range( 8 ):
\r
135 if r & 0x80000000 != 0:
\r
136 r = ((r & 0x7fffffff) << 1) ^ 0x04c11db7
\r
138 r = ((r & 0x7fffffff) << 1)
\r
142 CRCTable = [ makeCRCTable( i ) for i in range( 256 ) ]
\r
144 def ogg_crc( src ):
\r
147 crc = ((crc & 0xffffff) << 8) ^ CRCTable[(crc >> 24) ^ ord(c)]
\r
150 # End-of-Fujii code.
\r
158 OGGMAGIC_FIRSTPAGE = 1
\r
159 OGGMAGIC_REST_OF_INPUT = 2
\r
161 class OggMagicLiveStream:
\r
163 def __init__(self,tdef,input):
\r
167 self.firstpagestream = None
\r
169 self.mode = OGGMAGIC_TDEF
\r
170 self.find_first_page()
\r
172 def find_first_page(self):
\r
173 # Read max Ogg page size bytes + some, must contain page starter
\r
176 while len(firstpagedata) < nwant: # Max Ogg page size
\r
177 print >>sys.stderr,"OggMagicLiveStream: Reading first page, avail",self.input.available()
\r
178 data = self.input.read(nwant)
\r
179 firstpagedata += data
\r
180 if len(data) == 0 and len(firstpagedata < nwant):
\r
181 raise ValueError("OggMagicLiveStream: Could not get max. page bytes")
\r
183 self.firstpagestream = StringIO(firstpagedata)
\r
186 char = self.firstpagestream.read(1)
\r
190 rest = self.firstpagestream.read(3)
\r
192 # Found page boundary
\r
193 print >>sys.stderr,"OggMagicLiveStream: Found page"
\r
194 self.firstpagestream.seek(-4,os.SEEK_CUR)
\r
195 # For real reliability we should parse the page here
\r
196 # and look further if the "OggS" was just video data.
\r
197 # I'm now counting on the Ogg player to do that.
\r
198 # (need better parser than this code to be able to do that)
\r
201 self.firstpagestream.seek(-3,os.SEEK_CUR)
\r
204 raise ValueError("OggMagicLiveStream: could not find start-of-page in P2P-stream")
\r
206 def read(self,numbytes=None):
\r
209 1. Ogg header pages from TorrentDef
\r
210 3. self.firstpagestream till EOF
\r
211 4. self.input till EOF
\r
213 #print >>sys.stderr,"OggMagicLiveStream: read",numbytes
\r
215 if numbytes is None:
\r
216 raise ValueError("OggMagicLiveStream: don't support read all")
\r
218 if self.mode == OGGMAGIC_TDEF:
\r
219 data = self.tdef.get_live_ogg_headers()
\r
221 print >>sys.stderr,"OggMagicLiveStream: Writing TDEF",len(data)
\r
222 if len(data) > numbytes:
\r
223 raise ValueError("OggMagicLiveStream: Not implemented, Ogg headers too big, need more code")
\r
224 self.mode = OGGMAGIC_FIRSTPAGE
\r
226 elif self.mode == OGGMAGIC_FIRSTPAGE:
\r
227 data = self.firstpagestream.read(numbytes)
\r
229 print >>sys.stderr,"OggMagicLiveStream: Writing 1st remain",len(data)
\r
231 self.mode = OGGMAGIC_REST_OF_INPUT
\r
232 return self.input.read(numbytes)
\r
235 elif self.mode == OGGMAGIC_REST_OF_INPUT:
\r
236 data = self.input.read(numbytes)
\r
237 #print >>sys.stderr,"OggMagicLiveStream: Writing input",len(data)
\r
240 def seek(self,offset,whence=None):
\r
241 print >>sys.stderr,"OggMagicLiveStream: SEEK CALLED",offset,whence
\r
243 if self.mode != OGGMAGIC_TDEF:
\r
244 self.mode = OGGMAGIC_TDEF
\r
245 self.find_first_page()
\r
247 raise ValueError("OggMagicLiveStream doens't support seeking other than to beginning")
\r
252 def available(self):
\r
258 if __name__ == "__main__":
\r
261 f = open("libre.ogg","rb")
\r
263 (isheader,header,body) = ogg_grab_page(f)
\r
267 header_pages.append((header,body))
\r
271 g = open("stroom.ogg","rb")
\r
279 # Found page boundary
\r
280 print >>sys.stderr,"Found page"
\r
281 g.seek(-4,os.SEEK_CUR)
\r
282 (isheader,pheader,pbody) = ogg_grab_page(g)
\r
285 g.seek(-3,os.SEEK_CUR)
\r
289 h = open("new.ogg","wb")
\r
290 for header,body in header_pages:
\r
296 data = g.read(65536)
\r