blob: 03637cff6ad5909773a354e9218af691c43ec128 [file] [log] [blame]
// 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);