| // 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); |