// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @param {MetadataDispatcher} parent Parent object. * @constructor */ function MpegParser(parent) { MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); this.mimeType = 'video/mpeg'; } MpegParser.prototype = {__proto__: MetadataParser.prototype}; /** * Size of the atom header. */ MpegParser.HEADER_SIZE = 8; /** * @param {ByteReader} br ByteReader instance. * @param {number=} opt_end End of atom position. * @return {number} Atom size. */ MpegParser.readAtomSize = function(br, opt_end) { var pos = br.tell(); if (opt_end) { // Assert that opt_end <= buffer end. // When supplied, opt_end is the end of the enclosing atom and is used to // check the correct nesting. br.validateRead(opt_end - pos); } var size = br.readScalar(4, false, opt_end); if (size < MpegParser.HEADER_SIZE) throw 'atom too short (' + size + ') @' + pos; if (opt_end && pos + size > opt_end) throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos; return size; }; /** * @param {ByteReader} br ByteReader instance. * @param {number=} opt_end End of atom position. * @return {string} Atom name. */ MpegParser.readAtomName = function(br, opt_end) { return br.readString(4, opt_end).toLowerCase(); }; /** * @param {Object} metadata Metadata object. * @return {Object} Root of the parser tree. */ MpegParser.createRootParser = function(metadata) { function findParentAtom(atom, name) { for (;;) { atom = atom.parent; if (!atom) return null; if (atom.name == name) return atom; } } function parseFtyp(br, atom) { metadata.brand = br.readString(4, atom.end); } function parseMvhd(br, atom) { var version = br.readScalar(4, false, atom.end); var offset = (version == 0) ? 8 : 16; br.seek(offset, ByteReader.SEEK_CUR); var timescale = br.readScalar(4, false, atom.end); var duration = br.readScalar(4, false, atom.end); metadata.duration = duration / timescale; } function parseHdlr(br, atom) { br.seek(8, ByteReader.SEEK_CUR); findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end); } function parseStsd(br, atom) { var track = findParentAtom(atom, 'trak'); if (track && track.trackType == 'vide') { br.seek(40, ByteReader.SEEK_CUR); metadata.width = br.readScalar(2, false, atom.end); metadata.height = br.readScalar(2, false, atom.end); } } function parseDataString(name, br, atom) { br.seek(8, ByteReader.SEEK_CUR); metadata[name] = br.readString(atom.end - br.tell(), atom.end); } function parseCovr(br, atom) { br.seek(8, ByteReader.SEEK_CUR); metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end); } // 'meta' atom can occur at one of the several places in the file structure. var parseMeta = { ilst: { '©nam': { data: parseDataString.bind(null, 'title') }, '©alb': { data: parseDataString.bind(null, 'album') }, '©art': { data: parseDataString.bind(null, 'artist') }, 'covr': { data: parseCovr } }, versioned: true }; // main parser for the entire file structure. return { ftyp: parseFtyp, moov: { mvhd: parseMvhd, trak: { mdia: { hdlr: parseHdlr, minf: { stbl: { stsd: parseStsd } } }, meta: parseMeta }, udta: { meta: parseMeta }, meta: parseMeta }, meta: parseMeta }; }; /** * * @param {File} file File. * @param {Object} metadata Metadata. * @param {function(Object)} callback Success callback. * @param {function} onError Error callback. */ MpegParser.prototype.parse = function(file, metadata, callback, onError) { this.rootParser_ = MpegParser.createRootParser(metadata); // Kick off the processing by reading the first atom's header. this.requestRead(file, 0, MpegParser.HEADER_SIZE, null, onError, callback.bind(null, metadata)); }; /** * @param {function(ByteReader, Object)|Object} parser Parser tree node. * @param {ByteReader} br ByteReader instance. * @param {Object} atom Atom descriptor. * @param {number} filePos File position of the atom start. */ MpegParser.prototype.applyParser = function(parser, br, atom, filePos) { if (this.verbose) { var path = atom.name; for (var p = atom.parent; p && p.name; p = p.parent) { path = p.name + '.' + path; } var action; if (!parser) { action = 'skipping '; } else if (parser instanceof Function) { action = 'parsing '; } else { action = 'recursing'; } var start = atom.start - MpegParser.HEADER_SIZE; this.vlog(path + ': ' + '@' + (filePos + start) + ':' + (atom.end - start), action); } if (parser) { if (parser instanceof Function) { br.pushSeek(atom.start); parser(br, atom); br.popSeek(); } else { if (parser.versioned) { atom.start += 4; } this.parseMpegAtomsInRange(parser, br, atom, filePos); } } }; /** * @param {function(ByteReader, Object)|Object} parser Parser tree node. * @param {ByteReader} br ByteReader instance. * @param {Object} parentAtom Parent atom descriptor. * @param {number} filePos File position of the atom start. */ MpegParser.prototype.parseMpegAtomsInRange = function( parser, br, parentAtom, filePos) { var count = 0; for (var offset = parentAtom.start; offset != parentAtom.end;) { if (count++ > 100) // Most likely we are looping through a corrupt file. throw 'too many child atoms in ' + parentAtom.name + ' @' + offset; br.seek(offset); var size = MpegParser.readAtomSize(br, parentAtom.end); var name = MpegParser.readAtomName(br, parentAtom.end); this.applyParser( parser[name], br, { start: offset + MpegParser.HEADER_SIZE, end: offset + size, name: name, parent: parentAtom }, filePos ); offset += size; } }; /** * @param {File} file File. * @param {number} filePos Start position in the file. * @param {number} size Atom size. * @param {string} name Atom name. * @param {function} onError Error callback. * @param {function} onSuccess Success callback. */ MpegParser.prototype.requestRead = function( file, filePos, size, name, onError, onSuccess) { var self = this; var reader = new FileReader(); reader.onerror = onError; reader.onload = function(event) { self.processTopLevelAtom( reader.result, file, filePos, size, name, onError, onSuccess); }; this.vlog('reading @' + filePos + ':' + size); reader.readAsArrayBuffer(file.slice(filePos, filePos + size)); }; /** * @param {ArrayBuffer} buf Data buffer. * @param {File} file File. * @param {number} filePos Start position in the file. * @param {number} size Atom size. * @param {string} name Atom name. * @param {function} onError Error callback. * @param {function} onSuccess Success callback. */ MpegParser.prototype.processTopLevelAtom = function( buf, file, filePos, size, name, onError, onSuccess) { try { var br = new ByteReader(buf); // the header has already been read. var atomEnd = size - MpegParser.HEADER_SIZE; var bufLength = buf.byteLength; // Check the available data size. It should be either exactly // what we requested or HEADER_SIZE bytes less (for the last atom). if (bufLength != atomEnd && bufLength != size) { throw 'Read failure @' + filePos + ', ' + 'requested ' + size + ', read ' + bufLength; } // Process the top level atom. if (name) { // name is null only the first time. this.applyParser( this.rootParser_[name], br, {start: 0, end: atomEnd, name: name}, filePos ); } filePos += bufLength; if (bufLength == size) { // The previous read returned everything we asked for, including // the next atom header at the end of the buffer. // Parse this header and schedule the next read. br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END); var nextSize = MpegParser.readAtomSize(br); var nextName = MpegParser.readAtomName(br); // If we do not have a parser for the next atom, skip the content and // read only the header (the one after the next). if (!this.rootParser_[nextName]) { filePos += nextSize - MpegParser.HEADER_SIZE; nextSize = MpegParser.HEADER_SIZE; } this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess); } else { // The previous read did not return the next atom header, EOF reached. this.vlog('EOF @' + filePos); onSuccess(); } } catch (e) { onError(e.toString()); } }; MetadataDispatcher.registerParserClass(MpegParser);