1 | #if 0 |
---|
2 | # ----------------------------------------------------------------------- |
---|
3 | # ogminfo.py - Ogg Streaming Video Files |
---|
4 | # ----------------------------------------------------------------------- |
---|
5 | # $Id: ogminfo.py,v 1.16 2004/05/20 19:54:52 dischi Exp $ |
---|
6 | # |
---|
7 | # $Log: ogminfo.py,v $ |
---|
8 | # Revision 1.16 2004/05/20 19:54:52 dischi |
---|
9 | # more ogm fixes |
---|
10 | # |
---|
11 | # Revision 1.15 2004/05/20 08:49:29 dischi |
---|
12 | # more ogm fixes, better length calc (again) |
---|
13 | # |
---|
14 | # Revision 1.14 2004/05/18 21:55:52 dischi |
---|
15 | # o get correct length for ogm files |
---|
16 | # o set metadat for the different streams |
---|
17 | # o chapter support |
---|
18 | # |
---|
19 | # Revision 1.13 2003/09/22 16:21:54 the_krow |
---|
20 | # utf-8 comment parsing |
---|
21 | # |
---|
22 | # Revision 1.12 2003/09/09 19:57:08 dischi |
---|
23 | # bad hack to make length oggs work |
---|
24 | # |
---|
25 | # Revision 1.11 2003/09/09 19:32:26 dischi |
---|
26 | # bugfix for finding oggs |
---|
27 | # |
---|
28 | # Revision 1.10 2003/08/04 08:44:51 the_krow |
---|
29 | # Maximum iterations field added as suggested by Magnus in the |
---|
30 | # freevo list. |
---|
31 | # |
---|
32 | # Revision 1.9 2003/07/13 15:20:53 dischi |
---|
33 | # make the module quiet |
---|
34 | # |
---|
35 | # Revision 1.8 2003/07/10 11:16:31 the_krow |
---|
36 | # o Added length calculation for audio only files. |
---|
37 | # o In the future we will use this as a general parser for ogm/ogg |
---|
38 | # |
---|
39 | # Revision 1.7 2003/06/30 13:17:20 the_krow |
---|
40 | # o Refactored mediainfo into factory, synchronizedobject |
---|
41 | # o Parsers now register directly at mmpython not at mmpython.mediainfo |
---|
42 | # o use mmpython.Factory() instead of mmpython.mediainfo.get_singleton() |
---|
43 | # o Bugfix in PNG parser |
---|
44 | # o Renamed disc.AudioInfo into disc.AudioDiscInfo |
---|
45 | # o Renamed disc.DataInfo into disc.DataDiscInfo |
---|
46 | # |
---|
47 | # Revision 1.6 2003/06/29 12:11:16 dischi |
---|
48 | # changed print to _print |
---|
49 | # |
---|
50 | # Revision 1.5 2003/06/23 20:48:11 the_krow |
---|
51 | # width + height fixes for OGM files |
---|
52 | # |
---|
53 | # Revision 1.4 2003/06/23 13:20:51 the_krow |
---|
54 | # basic parsing should now work. |
---|
55 | # |
---|
56 | # |
---|
57 | # |
---|
58 | # ----------------------------------------------------------------------- |
---|
59 | # MMPython - Media Metadata for Python |
---|
60 | # Copyright (C) 2003 Thomas Schueppel, Dirk Meyer |
---|
61 | # |
---|
62 | # This program is free software; you can redistribute it and/or modify |
---|
63 | # it under the terms of the GNU General Public License as published by |
---|
64 | # the Free Software Foundation; either version 2 of the License, or |
---|
65 | # (at your option) any later version. |
---|
66 | # |
---|
67 | # This program is distributed in the hope that it will be useful, but |
---|
68 | # WITHOUT ANY WARRANTY; without even the implied warranty of MER- |
---|
69 | # CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
---|
70 | # Public License for more details. |
---|
71 | # |
---|
72 | # You should have received a copy of the GNU General Public License along |
---|
73 | # with this program; if not, write to the Free Software Foundation, Inc., |
---|
74 | # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
---|
75 | # |
---|
76 | # ----------------------------------------------------------------------- |
---|
77 | #endif |
---|
78 | |
---|
79 | |
---|
80 | from mmpython import mediainfo |
---|
81 | import mmpython |
---|
82 | import struct |
---|
83 | import re |
---|
84 | import stat |
---|
85 | import os |
---|
86 | |
---|
87 | import fourcc |
---|
88 | |
---|
89 | PACKET_TYPE_HEADER = 0x01 |
---|
90 | PACKED_TYPE_METADATA = 0x03 |
---|
91 | PACKED_TYPE_SETUP = 0x05 |
---|
92 | PACKET_TYPE_BITS = 0x07 |
---|
93 | PACKET_IS_SYNCPOINT = 0x08 |
---|
94 | |
---|
95 | #VORBIS_VIDEO_PACKET_INFO = 'video' |
---|
96 | |
---|
97 | STREAM_HEADER_VIDEO = '<4sIQQIIHII' |
---|
98 | STREAM_HEADER_AUDIO = '<4sIQQIIHHHI' |
---|
99 | |
---|
100 | _print = mediainfo._debug |
---|
101 | |
---|
102 | VORBISCOMMENT_tags = { 'title': 'TITLE', |
---|
103 | 'album': 'ALBUM', |
---|
104 | 'artist': 'ARTIST', |
---|
105 | 'comment': 'COMMENT', |
---|
106 | 'date': 'DATE', |
---|
107 | 'encoder': 'ENCODER', |
---|
108 | 'trackno': 'TRACKNUMBER', |
---|
109 | 'language': 'LANGUAGE', |
---|
110 | 'genre': 'GENRE', |
---|
111 | } |
---|
112 | |
---|
113 | MAXITERATIONS = 10 |
---|
114 | |
---|
115 | class OgmInfo(mediainfo.AVInfo): |
---|
116 | def __init__(self, file): |
---|
117 | mediainfo.AVInfo.__init__(self) |
---|
118 | self.samplerate = 1 |
---|
119 | self.all_streams = [] # used to add meta data to streams |
---|
120 | self.all_header = [] |
---|
121 | |
---|
122 | for i in range(MAXITERATIONS): |
---|
123 | granule, nextlen = self._parseOGGS(file) |
---|
124 | if granule == None: |
---|
125 | break |
---|
126 | elif granule > 0: |
---|
127 | # ok, file started |
---|
128 | break |
---|
129 | |
---|
130 | # seek to the end of the stream, to avoid scanning the whole file |
---|
131 | if (os.stat(file.name)[stat.ST_SIZE] > 50000): |
---|
132 | file.seek(os.stat(file.name)[stat.ST_SIZE]-49000) |
---|
133 | |
---|
134 | # read the rest of the file into a buffer |
---|
135 | h = file.read() |
---|
136 | |
---|
137 | # find last OggS to get length info |
---|
138 | if len(h) > 200: |
---|
139 | idx = h.find('OggS') |
---|
140 | pos = -49000 + idx |
---|
141 | if idx: |
---|
142 | file.seek(os.stat(file.name)[stat.ST_SIZE] + pos) |
---|
143 | while 1: |
---|
144 | granule, nextlen = self._parseOGGS(file) |
---|
145 | if not nextlen: |
---|
146 | break |
---|
147 | |
---|
148 | # Copy metadata to the streams |
---|
149 | if len(self.all_header) == len(self.all_streams): |
---|
150 | for i in range(len(self.all_header)): |
---|
151 | # set length |
---|
152 | self.length = max(self.all_streams[i].length, self.length) |
---|
153 | |
---|
154 | # get meta info |
---|
155 | for key in self.all_streams[i].keys: |
---|
156 | if self.all_header[i].has_key(key): |
---|
157 | self.all_streams[i][key] = self.all_header[i][key] |
---|
158 | del self.all_header[i][key] |
---|
159 | if self.all_header[i].has_key(key.upper()): |
---|
160 | self.all_streams[i][key] = self.all_header[i][key.upper()] |
---|
161 | del self.all_header[i][key.upper()] |
---|
162 | |
---|
163 | # Extract subtitles: |
---|
164 | if hasattr(self.all_streams[i], 'type') and \ |
---|
165 | self.all_streams[i].type == 'subtitle': |
---|
166 | self.subtitles.append(self.all_streams[i].language) |
---|
167 | |
---|
168 | # Chapter parser |
---|
169 | if self.all_header[i].has_key('CHAPTER01') and not self.chapters: |
---|
170 | while 1: |
---|
171 | s = 'CHAPTER0%s' % (len(self.chapters) + 1) |
---|
172 | if len(s) < 9: |
---|
173 | s = '0' + s |
---|
174 | if self.all_header[i].has_key(s) and \ |
---|
175 | self.all_header[i].has_key(s + 'NAME'): |
---|
176 | pos = self.all_header[i][s] |
---|
177 | try: |
---|
178 | pos = int(pos) |
---|
179 | except ValueError: |
---|
180 | new_pos = 0 |
---|
181 | for v in pos.split(':'): |
---|
182 | new_pos = new_pos * 60 + float(v) |
---|
183 | pos = int(new_pos) |
---|
184 | |
---|
185 | c = mediainfo.ChapterInfo(self.all_header[i][s + 'NAME'], pos) |
---|
186 | del self.all_header[i][s + 'NAME'] |
---|
187 | del self.all_header[i][s] |
---|
188 | self.chapters.append(c) |
---|
189 | else: |
---|
190 | break |
---|
191 | |
---|
192 | for stream in self.all_streams: |
---|
193 | if not stream.length: |
---|
194 | stream.length = self.length |
---|
195 | |
---|
196 | # Copy Metadata from tables into the main set of attributes |
---|
197 | for header in self.all_header: |
---|
198 | self.appendtable('VORBISCOMMENT', header) |
---|
199 | |
---|
200 | self.tag_map = { ('VORBISCOMMENT', 'en') : VORBISCOMMENT_tags } |
---|
201 | for k in self.tag_map.keys(): |
---|
202 | _print(k) |
---|
203 | map(lambda x:self.setitem(x,self.gettable(k[0],k[1]), |
---|
204 | self.tag_map[k][x]), self.tag_map[k].keys()) |
---|
205 | |
---|
206 | |
---|
207 | def _parseOGGS(self,file): |
---|
208 | h = file.read(27) |
---|
209 | if len(h) == 0: |
---|
210 | # Regular File end |
---|
211 | return None, None |
---|
212 | elif len(h) < 27: |
---|
213 | _print("%d Bytes of Garbage found after End." % len(h)) |
---|
214 | return None, None |
---|
215 | if h[:4] != "OggS": |
---|
216 | self.valid = 0 |
---|
217 | _print("Invalid Ogg") |
---|
218 | return None, None |
---|
219 | |
---|
220 | self.valid = 1 |
---|
221 | version = ord(h[4]) |
---|
222 | if version != 0: |
---|
223 | _print("Unsupported OGG/OGM Version %d." % version) |
---|
224 | return None, None |
---|
225 | head = struct.unpack('<BQIIIB', h[5:]) |
---|
226 | headertype, granulepos, serial, pageseqno, checksum, pageSegCount = head |
---|
227 | |
---|
228 | self.valid = 1 |
---|
229 | self.mime = 'application/ogm' |
---|
230 | self.type = 'OGG Media' |
---|
231 | tab = file.read(pageSegCount) |
---|
232 | nextlen = 0 |
---|
233 | for i in range(len(tab)): |
---|
234 | nextlen += ord(tab[i]) |
---|
235 | else: |
---|
236 | h = file.read(1) |
---|
237 | packettype = ord(h[0]) & PACKET_TYPE_BITS |
---|
238 | if packettype == PACKET_TYPE_HEADER: |
---|
239 | h += file.read(nextlen-1) |
---|
240 | self._parseHeader(h, granulepos) |
---|
241 | elif packettype == PACKED_TYPE_METADATA: |
---|
242 | h += file.read(nextlen-1) |
---|
243 | self._parseMeta(h) |
---|
244 | else: |
---|
245 | file.seek(nextlen-1,1) |
---|
246 | if len(self.all_streams) > serial: |
---|
247 | stream = self.all_streams[serial] |
---|
248 | if hasattr(stream, 'samplerate') and \ |
---|
249 | stream.samplerate: |
---|
250 | stream.length = granulepos / stream.samplerate |
---|
251 | elif hasattr(stream, 'bitrate') and \ |
---|
252 | stream.bitrate: |
---|
253 | stream.length = granulepos / stream.bitrate |
---|
254 | |
---|
255 | return granulepos, nextlen + 27 + pageSegCount |
---|
256 | |
---|
257 | |
---|
258 | def _parseMeta(self,h): |
---|
259 | flags = ord(h[0]) |
---|
260 | headerlen = len(h) |
---|
261 | if headerlen >= 7 and h[1:7] == 'vorbis': |
---|
262 | header = {} |
---|
263 | nextlen, self.encoder = self._extractHeaderString(h[7:]) |
---|
264 | numItems = struct.unpack('<I',h[7+nextlen:7+nextlen+4])[0] |
---|
265 | start = 7+4+nextlen |
---|
266 | for i in range(numItems): |
---|
267 | (nextlen, s) = self._extractHeaderString(h[start:]) |
---|
268 | start += nextlen |
---|
269 | if s: |
---|
270 | a = re.split('=',s) |
---|
271 | header[(a[0]).upper()]=a[1] |
---|
272 | # Put Header fields into info fields |
---|
273 | self.type = 'OGG Vorbis' |
---|
274 | self.subtype = '' |
---|
275 | self.all_header.append(header) |
---|
276 | |
---|
277 | |
---|
278 | def _parseHeader(self,header,granule): |
---|
279 | headerlen = len(header) |
---|
280 | flags = ord(header[0]) |
---|
281 | |
---|
282 | if headerlen >= 30 and header[1:7] == 'vorbis': |
---|
283 | #print("Vorbis Audio Header") |
---|
284 | ai = mediainfo.AudioInfo() |
---|
285 | ai.version, ai.channels, ai.samplerate, bitrate_max, ai.bitrate, \ |
---|
286 | bitrate_min, blocksize, framing = \ |
---|
287 | struct.unpack('<IBIiiiBB',header[7:7+23]) |
---|
288 | ai.codec = 'Vorbis' |
---|
289 | #ai.granule = granule |
---|
290 | #ai.length = granule / ai.samplerate |
---|
291 | self.audio.append(ai) |
---|
292 | self.all_streams.append(ai) |
---|
293 | |
---|
294 | elif headerlen >= 7 and header[1:7] == 'theora': |
---|
295 | #print "Theora Header" |
---|
296 | # Theora Header |
---|
297 | # XXX Finish Me |
---|
298 | vi = mediainfo.VideoInfo() |
---|
299 | vi.codec = 'theora' |
---|
300 | self.video.append(vi) |
---|
301 | self.all_streams.append(vi) |
---|
302 | |
---|
303 | elif headerlen >= 142 and header[1:36] == 'Direct Show Samples embedded in Ogg': |
---|
304 | #print 'Direct Show Samples embedded in Ogg' |
---|
305 | # Old Directshow format |
---|
306 | # XXX Finish Me |
---|
307 | vi = mediainfo.VideoInfo() |
---|
308 | vi.codec = 'dshow' |
---|
309 | self.video.append(vi) |
---|
310 | self.all_streams.append(vi) |
---|
311 | |
---|
312 | elif flags & PACKET_TYPE_BITS == PACKET_TYPE_HEADER and headerlen >= struct.calcsize(STREAM_HEADER_VIDEO)+1: |
---|
313 | #print "New Directshow Format" |
---|
314 | # New Directshow Format |
---|
315 | htype = header[1:9] |
---|
316 | |
---|
317 | if htype[:5] == 'video': |
---|
318 | streamheader = struct.unpack( STREAM_HEADER_VIDEO, header[9:struct.calcsize(STREAM_HEADER_VIDEO)+9] ) |
---|
319 | vi = mediainfo.VideoInfo() |
---|
320 | (type, ssize, timeunit, samplerate, vi.length, buffersize, \ |
---|
321 | vi.bitrate, vi.width, vi.height) = streamheader |
---|
322 | |
---|
323 | vi.width /= 65536 |
---|
324 | vi.height /= 65536 |
---|
325 | # XXX length, bitrate are very wrong |
---|
326 | try: |
---|
327 | vi.codec = fourcc.RIFFCODEC[type] |
---|
328 | except: |
---|
329 | vi.codec = 'Unknown (%s)' % type |
---|
330 | vi.fps = 10000000 / timeunit |
---|
331 | self.video.append(vi) |
---|
332 | self.all_streams.append(vi) |
---|
333 | |
---|
334 | elif htype[:5] == 'audio': |
---|
335 | streamheader = struct.unpack( STREAM_HEADER_AUDIO, header[9:struct.calcsize(STREAM_HEADER_AUDIO)+9] ) |
---|
336 | ai = mediainfo.AudioInfo() |
---|
337 | (type, ssize, timeunit, ai.samplerate, ai.length, buffersize, ai.bitrate, ai.channels, bloc, ai.bitrate) = streamheader |
---|
338 | self.samplerate = ai.samplerate |
---|
339 | _print("Samplerate %d" % self.samplerate) |
---|
340 | self.audio.append(ai) |
---|
341 | self.all_streams.append(ai) |
---|
342 | |
---|
343 | elif htype[:4] == 'text': |
---|
344 | subtitle = mediainfo.MediaInfo() |
---|
345 | subtitle.keys.append('language') |
---|
346 | subtitle.type = 'subtitle' |
---|
347 | subtitle.length = 0 |
---|
348 | self.all_streams.append(subtitle) |
---|
349 | |
---|
350 | else: |
---|
351 | _print("Unknown Header") |
---|
352 | |
---|
353 | |
---|
354 | def _extractHeaderString(self,header): |
---|
355 | len = struct.unpack( '<I', header[:4] )[0] |
---|
356 | try: |
---|
357 | return (len+4,unicode(header[4:4+len], 'utf-8')) |
---|
358 | except: |
---|
359 | return (len+4,None) |
---|
360 | |
---|
361 | |
---|
362 | mmpython.registertype( 'application/ogg', ('ogm', 'ogg',), mediainfo.TYPE_AV, OgmInfo ) |
---|