[4] | 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 ) |
---|