| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * @param {MetadataDispatcher} parent Parent object. | |
| 9 * @constructor | |
| 10 */ | |
| 11 function MpegParser(parent) { | |
| 12 MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); | |
| 13 this.mimeType = 'video/mpeg'; | |
| 14 } | |
| 15 | |
| 16 MpegParser.prototype = {__proto__: MetadataParser.prototype}; | |
| 17 | |
| 18 /** | |
| 19 * Size of the atom header. | |
| 20 */ | |
| 21 MpegParser.HEADER_SIZE = 8; | |
| 22 | |
| 23 /** | |
| 24 * @param {ByteReader} br ByteReader instance. | |
| 25 * @param {number=} opt_end End of atom position. | |
| 26 * @return {number} Atom size. | |
| 27 */ | |
| 28 MpegParser.readAtomSize = function(br, opt_end) { | |
| 29 var pos = br.tell(); | |
| 30 | |
| 31 if (opt_end) { | |
| 32 // Assert that opt_end <= buffer end. | |
| 33 // When supplied, opt_end is the end of the enclosing atom and is used to | |
| 34 // check the correct nesting. | |
| 35 br.validateRead(opt_end - pos); | |
| 36 } | |
| 37 | |
| 38 var size = br.readScalar(4, false, opt_end); | |
| 39 | |
| 40 if (size < MpegParser.HEADER_SIZE) | |
| 41 throw 'atom too short (' + size + ') @' + pos; | |
| 42 | |
| 43 if (opt_end && pos + size > opt_end) | |
| 44 throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos; | |
| 45 | |
| 46 return size; | |
| 47 }; | |
| 48 | |
| 49 /** | |
| 50 * @param {ByteReader} br ByteReader instance. | |
| 51 * @param {number=} opt_end End of atom position. | |
| 52 * @return {string} Atom name. | |
| 53 */ | |
| 54 MpegParser.readAtomName = function(br, opt_end) { | |
| 55 return br.readString(4, opt_end).toLowerCase(); | |
| 56 }; | |
| 57 | |
| 58 /** | |
| 59 * @param {Object} metadata Metadata object. | |
| 60 * @return {Object} Root of the parser tree. | |
| 61 */ | |
| 62 MpegParser.createRootParser = function(metadata) { | |
| 63 function findParentAtom(atom, name) { | |
| 64 for (;;) { | |
| 65 atom = atom.parent; | |
| 66 if (!atom) return null; | |
| 67 if (atom.name == name) return atom; | |
| 68 } | |
| 69 } | |
| 70 | |
| 71 function parseFtyp(br, atom) { | |
| 72 metadata.brand = br.readString(4, atom.end); | |
| 73 } | |
| 74 | |
| 75 function parseMvhd(br, atom) { | |
| 76 var version = br.readScalar(4, false, atom.end); | |
| 77 var offset = (version == 0) ? 8 : 16; | |
| 78 br.seek(offset, ByteReader.SEEK_CUR); | |
| 79 var timescale = br.readScalar(4, false, atom.end); | |
| 80 var duration = br.readScalar(4, false, atom.end); | |
| 81 metadata.duration = duration / timescale; | |
| 82 } | |
| 83 | |
| 84 function parseHdlr(br, atom) { | |
| 85 br.seek(8, ByteReader.SEEK_CUR); | |
| 86 findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end); | |
| 87 } | |
| 88 | |
| 89 function parseStsd(br, atom) { | |
| 90 var track = findParentAtom(atom, 'trak'); | |
| 91 if (track && track.trackType == 'vide') { | |
| 92 br.seek(40, ByteReader.SEEK_CUR); | |
| 93 metadata.width = br.readScalar(2, false, atom.end); | |
| 94 metadata.height = br.readScalar(2, false, atom.end); | |
| 95 } | |
| 96 } | |
| 97 | |
| 98 function parseDataString(name, br, atom) { | |
| 99 br.seek(8, ByteReader.SEEK_CUR); | |
| 100 metadata[name] = br.readString(atom.end - br.tell(), atom.end); | |
| 101 } | |
| 102 | |
| 103 function parseCovr(br, atom) { | |
| 104 br.seek(8, ByteReader.SEEK_CUR); | |
| 105 metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end); | |
| 106 } | |
| 107 | |
| 108 // 'meta' atom can occur at one of the several places in the file structure. | |
| 109 var parseMeta = { | |
| 110 ilst: { | |
| 111 '©nam': { data: parseDataString.bind(null, 'title') }, | |
| 112 '©alb': { data: parseDataString.bind(null, 'album') }, | |
| 113 '©art': { data: parseDataString.bind(null, 'artist') }, | |
| 114 'covr': { data: parseCovr } | |
| 115 }, | |
| 116 versioned: true | |
| 117 }; | |
| 118 | |
| 119 // main parser for the entire file structure. | |
| 120 return { | |
| 121 ftyp: parseFtyp, | |
| 122 moov: { | |
| 123 mvhd: parseMvhd, | |
| 124 trak: { | |
| 125 mdia: { | |
| 126 hdlr: parseHdlr, | |
| 127 minf: { | |
| 128 stbl: { | |
| 129 stsd: parseStsd | |
| 130 } | |
| 131 } | |
| 132 }, | |
| 133 meta: parseMeta | |
| 134 }, | |
| 135 udta: { | |
| 136 meta: parseMeta | |
| 137 }, | |
| 138 meta: parseMeta | |
| 139 }, | |
| 140 meta: parseMeta | |
| 141 }; | |
| 142 }; | |
| 143 | |
| 144 /** | |
| 145 * | |
| 146 * @param {File} file File. | |
| 147 * @param {Object} metadata Metadata. | |
| 148 * @param {function(Object)} callback Success callback. | |
| 149 * @param {function} onError Error callback. | |
| 150 */ | |
| 151 MpegParser.prototype.parse = function(file, metadata, callback, onError) { | |
| 152 this.rootParser_ = MpegParser.createRootParser(metadata); | |
| 153 | |
| 154 // Kick off the processing by reading the first atom's header. | |
| 155 this.requestRead(file, 0, MpegParser.HEADER_SIZE, null, | |
| 156 onError, callback.bind(null, metadata)); | |
| 157 }; | |
| 158 | |
| 159 /** | |
| 160 * @param {function(ByteReader, Object)|Object} parser Parser tree node. | |
| 161 * @param {ByteReader} br ByteReader instance. | |
| 162 * @param {Object} atom Atom descriptor. | |
| 163 * @param {number} filePos File position of the atom start. | |
| 164 */ | |
| 165 MpegParser.prototype.applyParser = function(parser, br, atom, filePos) { | |
| 166 if (this.verbose) { | |
| 167 var path = atom.name; | |
| 168 for (var p = atom.parent; p && p.name; p = p.parent) { | |
| 169 path = p.name + '.' + path; | |
| 170 } | |
| 171 | |
| 172 var action; | |
| 173 if (!parser) { | |
| 174 action = 'skipping '; | |
| 175 } else if (parser instanceof Function) { | |
| 176 action = 'parsing '; | |
| 177 } else { | |
| 178 action = 'recursing'; | |
| 179 } | |
| 180 | |
| 181 var start = atom.start - MpegParser.HEADER_SIZE; | |
| 182 this.vlog(path + ': ' + | |
| 183 '@' + (filePos + start) + ':' + (atom.end - start), | |
| 184 action); | |
| 185 } | |
| 186 | |
| 187 if (parser) { | |
| 188 if (parser instanceof Function) { | |
| 189 br.pushSeek(atom.start); | |
| 190 parser(br, atom); | |
| 191 br.popSeek(); | |
| 192 } else { | |
| 193 if (parser.versioned) { | |
| 194 atom.start += 4; | |
| 195 } | |
| 196 this.parseMpegAtomsInRange(parser, br, atom, filePos); | |
| 197 } | |
| 198 } | |
| 199 }; | |
| 200 | |
| 201 /** | |
| 202 * @param {function(ByteReader, Object)|Object} parser Parser tree node. | |
| 203 * @param {ByteReader} br ByteReader instance. | |
| 204 * @param {Object} parentAtom Parent atom descriptor. | |
| 205 * @param {number} filePos File position of the atom start. | |
| 206 */ | |
| 207 MpegParser.prototype.parseMpegAtomsInRange = function( | |
| 208 parser, br, parentAtom, filePos) { | |
| 209 var count = 0; | |
| 210 for (var offset = parentAtom.start; offset != parentAtom.end;) { | |
| 211 if (count++ > 100) // Most likely we are looping through a corrupt file. | |
| 212 throw 'too many child atoms in ' + parentAtom.name + ' @' + offset; | |
| 213 | |
| 214 br.seek(offset); | |
| 215 var size = MpegParser.readAtomSize(br, parentAtom.end); | |
| 216 var name = MpegParser.readAtomName(br, parentAtom.end); | |
| 217 | |
| 218 this.applyParser( | |
| 219 parser[name], | |
| 220 br, | |
| 221 { start: offset + MpegParser.HEADER_SIZE, | |
| 222 end: offset + size, | |
| 223 name: name, | |
| 224 parent: parentAtom | |
| 225 }, | |
| 226 filePos | |
| 227 ); | |
| 228 | |
| 229 offset += size; | |
| 230 } | |
| 231 }; | |
| 232 | |
| 233 /** | |
| 234 * @param {File} file File. | |
| 235 * @param {number} filePos Start position in the file. | |
| 236 * @param {number} size Atom size. | |
| 237 * @param {string} name Atom name. | |
| 238 * @param {function} onError Error callback. | |
| 239 * @param {function} onSuccess Success callback. | |
| 240 */ | |
| 241 MpegParser.prototype.requestRead = function( | |
| 242 file, filePos, size, name, onError, onSuccess) { | |
| 243 var self = this; | |
| 244 var reader = new FileReader(); | |
| 245 reader.onerror = onError; | |
| 246 reader.onload = function(event) { | |
| 247 self.processTopLevelAtom( | |
| 248 reader.result, file, filePos, size, name, onError, onSuccess); | |
| 249 }; | |
| 250 this.vlog('reading @' + filePos + ':' + size); | |
| 251 reader.readAsArrayBuffer(file.slice(filePos, filePos + size)); | |
| 252 }; | |
| 253 | |
| 254 /** | |
| 255 * @param {ArrayBuffer} buf Data buffer. | |
| 256 * @param {File} file File. | |
| 257 * @param {number} filePos Start position in the file. | |
| 258 * @param {number} size Atom size. | |
| 259 * @param {string} name Atom name. | |
| 260 * @param {function} onError Error callback. | |
| 261 * @param {function} onSuccess Success callback. | |
| 262 */ | |
| 263 MpegParser.prototype.processTopLevelAtom = function( | |
| 264 buf, file, filePos, size, name, onError, onSuccess) { | |
| 265 try { | |
| 266 var br = new ByteReader(buf); | |
| 267 | |
| 268 // the header has already been read. | |
| 269 var atomEnd = size - MpegParser.HEADER_SIZE; | |
| 270 | |
| 271 var bufLength = buf.byteLength; | |
| 272 | |
| 273 // Check the available data size. It should be either exactly | |
| 274 // what we requested or HEADER_SIZE bytes less (for the last atom). | |
| 275 if (bufLength != atomEnd && bufLength != size) { | |
| 276 throw 'Read failure @' + filePos + ', ' + | |
| 277 'requested ' + size + ', read ' + bufLength; | |
| 278 } | |
| 279 | |
| 280 // Process the top level atom. | |
| 281 if (name) { // name is null only the first time. | |
| 282 this.applyParser( | |
| 283 this.rootParser_[name], | |
| 284 br, | |
| 285 {start: 0, end: atomEnd, name: name}, | |
| 286 filePos | |
| 287 ); | |
| 288 } | |
| 289 | |
| 290 filePos += bufLength; | |
| 291 if (bufLength == size) { | |
| 292 // The previous read returned everything we asked for, including | |
| 293 // the next atom header at the end of the buffer. | |
| 294 // Parse this header and schedule the next read. | |
| 295 br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END); | |
| 296 var nextSize = MpegParser.readAtomSize(br); | |
| 297 var nextName = MpegParser.readAtomName(br); | |
| 298 | |
| 299 // If we do not have a parser for the next atom, skip the content and | |
| 300 // read only the header (the one after the next). | |
| 301 if (!this.rootParser_[nextName]) { | |
| 302 filePos += nextSize - MpegParser.HEADER_SIZE; | |
| 303 nextSize = MpegParser.HEADER_SIZE; | |
| 304 } | |
| 305 | |
| 306 this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess); | |
| 307 } else { | |
| 308 // The previous read did not return the next atom header, EOF reached. | |
| 309 this.vlog('EOF @' + filePos); | |
| 310 onSuccess(); | |
| 311 } | |
| 312 } catch (e) { | |
| 313 onError(e.toString()); | |
| 314 } | |
| 315 }; | |
| 316 | |
| 317 MetadataDispatcher.registerParserClass(MpegParser); | |
| OLD | NEW |