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 */ 11function MpegParser(parent) { 12 MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); 13 this.mimeType = 'video/mpeg'; 14} 15 16MpegParser.prototype = {__proto__: MetadataParser.prototype}; 17 18/** 19 * Size of the atom header. 20 */ 21MpegParser.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 */ 28MpegParser.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 */ 54MpegParser.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 */ 62MpegParser.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 */ 151MpegParser.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 */ 165MpegParser.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 */ 207MpegParser.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 */ 241MpegParser.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 */ 263MpegParser.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 317MetadataDispatcher.registerParserClass(MpegParser); 318