instrumentation: add next-share/
[cs-p2p-next.git] / instrumentation / next-share / BaseLib / Video / Ogg.py
1 # Written by Arno Bakker \r
2 # see LICENSE.txt for license information\r
3 \r
4 """ Utility functions for (live) streams in Ogg container format.\r
5     \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
10 """ \r
11 \r
12 import sys\r
13 import os\r
14 from cStringIO import StringIO\r
15 \r
16 DEBUG = False\r
17 \r
18 \r
19 def is_ogg(name):\r
20     return name.endswith('.ogg') or name.endswith('.ogv') or name.endswith('ogm') or name.endswith('oga') or name.endswith('ogx')\r
21 \r
22 \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
29     """\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
41     \r
42     header_size = ord(number_page_segments)+27\r
43     segment_size = 0\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
47 \r
48     if capture_pattern != "OggS":\r
49         raise ValueError("Header does not start with OggS")\r
50     # TODO: calc CRC\r
51     if page_size > 65307:\r
52         raise ValueError("Page too big")\r
53 \r
54     if DEBUG:\r
55         print >>sys.stderr,"ogg: type",ord(header_type_flag)\r
56 \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
59 \r
60     if checkcrc:\r
61         import binascii\r
62         import socket \r
63 \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
66         \r
67         newcrc = ogg_crc(crcpage) \r
68         newcrcnbo = socket.htonl(newcrc) & 0xffffffff\r
69         newcrcstr = "%08x" % newcrcnbo\r
70         \r
71         oldcrcstr = binascii.hexlify(CRC_checksum)\r
72         if DEBUG:\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
76     \r
77     # BOS or header page\r
78     header_type = body[0]\r
79     isheader = False\r
80     if header_type == '\x01' or header_type == '\x03' or header_type == '\x05':\r
81         isheader = True\r
82         vorbis_grab_header(StringIO(body))\r
83     elif header_type == '\x80' or header_type == '\x81' or header_type == '\x82':\r
84         isheader = True\r
85         theora_grab_header(StringIO(body))\r
86     elif header_type == '\x7F':\r
87         isheader = True\r
88         flac_grab_header(StringIO(body))\r
89         \r
90     return (isheader,header,body)\r
91     \r
92     \r
93 def vorbis_grab_header(input):\r
94     if DEBUG:\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
103         \r
104 \r
105 def theora_grab_header(input):\r
106     if DEBUG:\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
115 \r
116 \r
117 def flac_grab_header(input):\r
118     if DEBUG:\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
123 \r
124 \r
125 """\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
130 """\r
131 \r
132 def makeCRCTable( idx ):\r
133     r = idx << 24\r
134     for i in range( 8 ):\r
135         if r & 0x80000000 != 0:\r
136             r = ((r & 0x7fffffff) << 1) ^ 0x04c11db7\r
137         else:\r
138             r = ((r & 0x7fffffff) << 1)\r
139 \r
140     return r\r
141 \r
142 CRCTable = [ makeCRCTable( i ) for i in range( 256 ) ]\r
143 \r
144 def ogg_crc( src ):\r
145     crc = 0\r
146     for c in src:\r
147         crc = ((crc & 0xffffff) << 8) ^ CRCTable[(crc >> 24) ^ ord(c)]\r
148     return crc\r
149 \r
150 # End-of-Fujii code.\r
151 \r
152 \r
153 \r
154 \r
155 \r
156     \r
157 OGGMAGIC_TDEF = 0\r
158 OGGMAGIC_FIRSTPAGE = 1\r
159 OGGMAGIC_REST_OF_INPUT = 2\r
160     \r
161 class OggMagicLiveStream:\r
162     \r
163     def __init__(self,tdef,input):\r
164         \r
165         self.tdef = tdef\r
166         self.input = input\r
167         self.firstpagestream = None\r
168 \r
169         self.mode = OGGMAGIC_TDEF\r
170         self.find_first_page()\r
171 \r
172     def find_first_page(self):\r
173         # Read max Ogg page size bytes + some, must contain page starter\r
174         nwant = 65307 + 4\r
175         firstpagedata = ''\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
182             \r
183         self.firstpagestream = StringIO(firstpagedata)\r
184         \r
185         while True:\r
186             char = self.firstpagestream.read(1)\r
187             if len(char) == 0:\r
188                 break\r
189             if char == 'O':\r
190                 rest = self.firstpagestream.read(3)\r
191                 if rest == 'ggS':\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
199                     break\r
200                 else:\r
201                     self.firstpagestream.seek(-3,os.SEEK_CUR)\r
202                 \r
203         if len(char) == 0:\r
204             raise ValueError("OggMagicLiveStream: could not find start-of-page in P2P-stream")\r
205             \r
206     def read(self,numbytes=None):\r
207         """\r
208         When read return:\r
209         1. Ogg header pages from TorrentDef\r
210         3. self.firstpagestream till EOF\r
211         4. self.input till EOF\r
212         """\r
213         #print >>sys.stderr,"OggMagicLiveStream: read",numbytes\r
214         \r
215         if numbytes is None:\r
216             raise ValueError("OggMagicLiveStream: don't support read all")\r
217             \r
218         if self.mode == OGGMAGIC_TDEF:\r
219             data = self.tdef.get_live_ogg_headers()\r
220             if DEBUG:\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
225             return data\r
226         elif self.mode == OGGMAGIC_FIRSTPAGE:\r
227             data = self.firstpagestream.read(numbytes)\r
228             if DEBUG:\r
229                 print >>sys.stderr,"OggMagicLiveStream: Writing 1st remain",len(data)\r
230             if len(data) == 0:\r
231                 self.mode = OGGMAGIC_REST_OF_INPUT\r
232                 return self.input.read(numbytes)\r
233             else:\r
234                 return data\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
238             return data\r
239             \r
240     def seek(self,offset,whence=None):\r
241         print >>sys.stderr,"OggMagicLiveStream: SEEK CALLED",offset,whence\r
242         if offset == 0:\r
243             if self.mode != OGGMAGIC_TDEF:\r
244                 self.mode = OGGMAGIC_TDEF\r
245                 self.find_first_page()\r
246         else:\r
247             raise ValueError("OggMagicLiveStream doens't support seeking other than to beginning")\r
248 \r
249     def close(self):\r
250         self.input.close()\r
251     \r
252     def available(self):\r
253         return -1\r
254 \r
255 \r
256     \r
257     \r
258 if __name__ == "__main__":\r
259     \r
260     header_pages = []\r
261     f = open("libre.ogg","rb")\r
262     while True:\r
263         (isheader,header,body) = ogg_grab_page(f)\r
264         if not isheader:\r
265             break\r
266         else:\r
267             header_pages.append((header,body))\r
268     f.close()\r
269     \r
270     \r
271     g = open("stroom.ogg","rb")\r
272     while True:\r
273         char = g.read(1)\r
274         if len(char) == 0:\r
275             break\r
276         if char == 'O':\r
277             rest = g.read(3)\r
278             if rest == 'ggS':\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
283                 break\r
284             else:\r
285                 g.seek(-3,os.SEEK_CUR)\r
286             \r
287     if len(char) > 0:\r
288         # Not EOF\r
289         h = open("new.ogg","wb")\r
290         for header,body in header_pages:\r
291             h.write(header)\r
292             h.write(body)\r
293         h.write(pheader)\r
294         h.write(pbody)\r
295         while True:\r
296             data = g.read(65536)\r
297             if len(data) == 0:\r
298                 break\r
299             else:\r
300                 h.write(data)\r
301         h.close()\r
302     g.close()\r
303     \r