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 var rootParser = MpegParser.createRootParser(metadata); 153 154 // Kick off the processing by reading the first atom's header. 155 this.requestRead(rootParser, 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 {Object} rootParser Parser definition. 235 * @param {File} file File. 236 * @param {number} filePos Start position in the file. 237 * @param {number} size Atom size. 238 * @param {string} name Atom name. 239 * @param {function} onError Error callback. 240 * @param {function} onSuccess Success callback. 241 */ 242MpegParser.prototype.requestRead = function( 243 rootParser, file, filePos, size, name, onError, onSuccess) { 244 var self = this; 245 var reader = new FileReader(); 246 reader.onerror = onError; 247 reader.onload = function(event) { 248 self.processTopLevelAtom( 249 reader.result, rootParser, file, filePos, size, name, 250 onError, onSuccess); 251 }; 252 this.vlog('reading @' + filePos + ':' + size); 253 reader.readAsArrayBuffer(file.slice(filePos, filePos + size)); 254}; 255 256/** 257 * @param {ArrayBuffer} buf Data buffer. 258 * @param {Object} rootParser Parser definition. 259 * @param {File} file File. 260 * @param {number} filePos Start position in the file. 261 * @param {number} size Atom size. 262 * @param {string} name Atom name. 263 * @param {function} onError Error callback. 264 * @param {function} onSuccess Success callback. 265 */ 266MpegParser.prototype.processTopLevelAtom = function( 267 buf, rootParser, file, filePos, size, name, onError, onSuccess) { 268 try { 269 var br = new ByteReader(buf); 270 271 // the header has already been read. 272 var atomEnd = size - MpegParser.HEADER_SIZE; 273 274 var bufLength = buf.byteLength; 275 276 // Check the available data size. It should be either exactly 277 // what we requested or HEADER_SIZE bytes less (for the last atom). 278 if (bufLength != atomEnd && bufLength != size) { 279 throw 'Read failure @' + filePos + ', ' + 280 'requested ' + size + ', read ' + bufLength; 281 } 282 283 // Process the top level atom. 284 if (name) { // name is null only the first time. 285 this.applyParser( 286 rootParser[name], 287 br, 288 {start: 0, end: atomEnd, name: name}, 289 filePos 290 ); 291 } 292 293 filePos += bufLength; 294 if (bufLength == size) { 295 // The previous read returned everything we asked for, including 296 // the next atom header at the end of the buffer. 297 // Parse this header and schedule the next read. 298 br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END); 299 var nextSize = MpegParser.readAtomSize(br); 300 var nextName = MpegParser.readAtomName(br); 301 302 // If we do not have a parser for the next atom, skip the content and 303 // read only the header (the one after the next). 304 if (!rootParser[nextName]) { 305 filePos += nextSize - MpegParser.HEADER_SIZE; 306 nextSize = MpegParser.HEADER_SIZE; 307 } 308 309 this.requestRead(rootParser, file, filePos, nextSize, nextName, 310 onError, onSuccess); 311 } else { 312 // The previous read did not return the next atom header, EOF reached. 313 this.vlog('EOF @' + filePos); 314 onSuccess(); 315 } 316 } catch (e) { 317 onError(e.toString()); 318 } 319}; 320 321MetadataDispatcher.registerParserClass(MpegParser); 322