mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
450 lines
13 KiB
JavaScript
450 lines
13 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.util.Mp4Parser');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.util.DataViewReader');
|
|
|
|
|
|
/**
|
|
* @export
|
|
*/
|
|
shaka.util.Mp4Parser = class {
|
|
constructor() {
|
|
/** @private {!Map<number, shaka.util.Mp4Parser.BoxType_>} */
|
|
this.headers_ = new Map();
|
|
|
|
/** @private {!Map<number, !shaka.util.Mp4Parser.CallbackType>} */
|
|
this.boxDefinitions_ = new Map();
|
|
|
|
/** @private {boolean} */
|
|
this.done_ = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Declare a box type as a Box.
|
|
*
|
|
* @param {string} type
|
|
* @param {!shaka.util.Mp4Parser.CallbackType} definition
|
|
* @return {!shaka.util.Mp4Parser}
|
|
* @export
|
|
*/
|
|
box(type, definition) {
|
|
const typeCode = shaka.util.Mp4Parser.typeFromString_(type);
|
|
this.headers_.set(typeCode, shaka.util.Mp4Parser.BoxType_.BASIC_BOX);
|
|
this.boxDefinitions_.set(typeCode, definition);
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Declare multiple box types as Basic Boxes.
|
|
*
|
|
* @param {!Array<string>} types
|
|
* @param {!shaka.util.Mp4Parser.CallbackType} definition
|
|
* @return {!shaka.util.Mp4Parser}
|
|
* @export
|
|
*/
|
|
boxes(types, definition) {
|
|
for (const type of types) {
|
|
this.box(type, definition);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Declare a box type as a Full Box.
|
|
*
|
|
* @param {string} type
|
|
* @param {!shaka.util.Mp4Parser.CallbackType} definition
|
|
* @return {!shaka.util.Mp4Parser}
|
|
* @export
|
|
*/
|
|
fullBox(type, definition) {
|
|
const typeCode = shaka.util.Mp4Parser.typeFromString_(type);
|
|
this.headers_.set(typeCode, shaka.util.Mp4Parser.BoxType_.FULL_BOX);
|
|
this.boxDefinitions_.set(typeCode, definition);
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Stop parsing. Useful for extracting information from partial segments and
|
|
* avoiding an out-of-bounds error once you find what you are looking for.
|
|
*
|
|
* @export
|
|
*/
|
|
stop() {
|
|
this.done_ = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse the given data using the added callbacks.
|
|
*
|
|
* @param {!BufferSource} data
|
|
* @param {boolean=} partialOkay If true, allow reading partial payloads
|
|
* from some boxes. If the goal is a child box, we can sometimes find it
|
|
* without enough data to find all child boxes.
|
|
* @param {boolean=} stopOnPartial If true, stop reading if an incomplete
|
|
* box is detected.
|
|
* @export
|
|
*/
|
|
parse(data, partialOkay, stopOnPartial) {
|
|
const reader = new shaka.util.DataViewReader(
|
|
data, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
|
|
|
|
this.done_ = false;
|
|
while (reader.hasMoreData() && !this.done_) {
|
|
this.parseNext(0, reader, partialOkay, stopOnPartial);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse the next box on the current level.
|
|
*
|
|
* @param {number} absStart The absolute start position in the original
|
|
* byte array.
|
|
* @param {!shaka.util.DataViewReader} reader
|
|
* @param {boolean=} partialOkay If true, allow reading partial payloads
|
|
* from some boxes. If the goal is a child box, we can sometimes find it
|
|
* without enough data to find all child boxes.
|
|
* @param {boolean=} stopOnPartial If true, stop reading if an incomplete
|
|
* box is detected.
|
|
* @export
|
|
*/
|
|
parseNext(absStart, reader, partialOkay, stopOnPartial) {
|
|
const start = reader.getPosition();
|
|
|
|
// size(4 bytes) + type(4 bytes) = 8 bytes
|
|
if (stopOnPartial && start + 8 > reader.getLength()) {
|
|
this.done_ = true;
|
|
return;
|
|
}
|
|
|
|
let size = reader.readUint32();
|
|
const type = reader.readUint32();
|
|
const name = shaka.util.Mp4Parser.typeToString(type);
|
|
let has64BitSize = false;
|
|
shaka.log.v2('Parsing MP4 box', name);
|
|
|
|
switch (size) {
|
|
case 0:
|
|
size = reader.getLength() - start;
|
|
break;
|
|
case 1:
|
|
if (stopOnPartial && reader.getPosition() + 8 > reader.getLength()) {
|
|
this.done_ = true;
|
|
return;
|
|
}
|
|
size = reader.readUint64();
|
|
has64BitSize = true;
|
|
break;
|
|
}
|
|
|
|
const boxDefinition = this.boxDefinitions_.get(type);
|
|
|
|
if (boxDefinition) {
|
|
let version = null;
|
|
let flags = null;
|
|
|
|
if (this.headers_.get(type) == shaka.util.Mp4Parser.BoxType_.FULL_BOX) {
|
|
if (stopOnPartial && reader.getPosition() + 4 > reader.getLength()) {
|
|
this.done_ = true;
|
|
return;
|
|
}
|
|
const versionAndFlags = reader.readUint32();
|
|
version = versionAndFlags >>> 24;
|
|
flags = versionAndFlags & 0xFFFFFF;
|
|
}
|
|
|
|
// Read the whole payload so that the current level can be safely read
|
|
// regardless of how the payload is parsed.
|
|
let end = start + size;
|
|
if (partialOkay && end > reader.getLength()) {
|
|
// For partial reads, truncate the payload if we must.
|
|
end = reader.getLength();
|
|
}
|
|
|
|
if (stopOnPartial && end > reader.getLength()) {
|
|
this.done_ = true;
|
|
return;
|
|
}
|
|
const payloadSize = end - reader.getPosition();
|
|
const payload = (payloadSize > 0) ?
|
|
// This is a view made available to the next parser. Don't clone.
|
|
reader.readBytes(payloadSize, /* clone= */ false) :
|
|
new Uint8Array(0);
|
|
|
|
const payloadReader = new shaka.util.DataViewReader(
|
|
payload, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
|
|
|
|
/** @type {shaka.extern.ParsedBox} */
|
|
const box = {
|
|
name,
|
|
parser: this,
|
|
partialOkay: partialOkay || false,
|
|
stopOnPartial: stopOnPartial || false,
|
|
version,
|
|
flags,
|
|
reader: payloadReader,
|
|
size,
|
|
start: start + absStart,
|
|
has64BitSize,
|
|
};
|
|
|
|
boxDefinition(box);
|
|
} else {
|
|
// Move the read head to be at the end of the box.
|
|
// If the box is longer than the remaining parts of the file, e.g. the
|
|
// mp4 is improperly formatted, or this was a partial range request that
|
|
// ended in the middle of a box, just skip to the end.
|
|
const skipLength = Math.min(
|
|
start + size - reader.getPosition(),
|
|
reader.getLength() - reader.getPosition());
|
|
reader.skip(skipLength);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A callback that tells the Mp4 parser to treat the body of a box as a series
|
|
* of boxes. The number of boxes is limited by the size of the parent box.
|
|
*
|
|
* @param {!shaka.extern.ParsedBox} box
|
|
* @export
|
|
*/
|
|
static children(box) {
|
|
// The "reader" starts at the payload, so we need to add the header to the
|
|
// start position. The header size varies.
|
|
const headerSize = shaka.util.Mp4Parser.headerSize(box);
|
|
while (box.reader.hasMoreData() && !box.parser.done_) {
|
|
box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
|
|
box.stopOnPartial);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A callback that tells the Mp4 parser to treat the body of a box as a sample
|
|
* description. A sample description box has a fixed number of children. The
|
|
* number of children is represented by a 4 byte unsigned integer. Each child
|
|
* is a box.
|
|
*
|
|
* @param {!shaka.extern.ParsedBox} box
|
|
* @export
|
|
*/
|
|
static sampleDescription(box) {
|
|
// The "reader" starts at the payload, so we need to add the header to the
|
|
// start position. The header size varies.
|
|
const headerSize = shaka.util.Mp4Parser.headerSize(box);
|
|
const count = box.reader.readUint32();
|
|
for (let i = 0; i < count; i++) {
|
|
box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
|
|
box.stopOnPartial);
|
|
if (box.parser.done_) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A callback that tells the Mp4 parser to treat the body of a box as a visual
|
|
* sample entry. A visual sample entry has some fixed-sized fields
|
|
* describing the video codec parameters, followed by an arbitrary number of
|
|
* appended children. Each child is a box.
|
|
*
|
|
* @param {!shaka.extern.ParsedBox} box
|
|
* @export
|
|
*/
|
|
static visualSampleEntry(box) {
|
|
// The "reader" starts at the payload, so we need to add the header to the
|
|
// start position. The header size varies.
|
|
const headerSize = shaka.util.Mp4Parser.headerSize(box);
|
|
|
|
// Skip 6 reserved bytes.
|
|
// Skip 2-byte data reference index.
|
|
// Skip 16 more reserved bytes.
|
|
// Skip 4 bytes for width/height.
|
|
// Skip 8 bytes for horizontal/vertical resolution.
|
|
// Skip 4 more reserved bytes (0)
|
|
// Skip 2-byte frame count.
|
|
// Skip 32-byte compressor name (length byte, then name, then 0-padding).
|
|
// Skip 2-byte depth.
|
|
// Skip 2 more reserved bytes (0xff)
|
|
// 78 bytes total.
|
|
// See also https://github.com/shaka-project/shaka-packager/blob/d5ca6e84/packager/media/formats/mp4/box_definitions.cc#L1544
|
|
box.reader.skip(78);
|
|
|
|
while (box.reader.hasMoreData() && !box.parser.done_) {
|
|
box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
|
|
box.stopOnPartial);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A callback that tells the Mp4 parser to treat the body of a box as a audio
|
|
* sample entry. A audio sample entry has some fixed-sized fields
|
|
* describing the audio codec parameters, followed by an arbitrary number of
|
|
* appended children. Each child is a box.
|
|
*
|
|
* @param {!shaka.extern.ParsedBox} box
|
|
* @export
|
|
*/
|
|
static audioSampleEntry(box) {
|
|
// The "reader" starts at the payload, so we need to add the header to the
|
|
// start position. The header size varies.
|
|
const headerSize = shaka.util.Mp4Parser.headerSize(box);
|
|
|
|
// 6 bytes reserved
|
|
// 2 bytes data reference index
|
|
box.reader.skip(8);
|
|
// 2 bytes version
|
|
const version = box.reader.readUint16();
|
|
// 2 bytes revision (0, could be ignored)
|
|
// 4 bytes reserved
|
|
box.reader.skip(6);
|
|
|
|
if (version == 2) {
|
|
// 16 bytes hard-coded values with no comments
|
|
// 8 bytes sample rate
|
|
// 4 bytes channel count
|
|
// 4 bytes hard-coded values with no comments
|
|
// 4 bytes bits per sample
|
|
// 4 bytes lpcm flags
|
|
// 4 bytes sample size
|
|
// 4 bytes samples per packet
|
|
box.reader.skip(48);
|
|
} else {
|
|
// 2 bytes channel count
|
|
// 2 bytes bits per sample
|
|
// 2 bytes compression ID
|
|
// 2 bytes packet size
|
|
// 2 bytes sample rate
|
|
// 2 byte reserved
|
|
box.reader.skip(12);
|
|
}
|
|
|
|
if (version == 1) {
|
|
// 4 bytes samples per packet
|
|
// 4 bytes bytes per packet
|
|
// 4 bytes bytes per frame
|
|
// 4 bytes bytes per sample
|
|
box.reader.skip(16);
|
|
}
|
|
|
|
while (box.reader.hasMoreData() && !box.parser.done_) {
|
|
box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay,
|
|
box.stopOnPartial);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a callback that tells the Mp4 parser to treat the body of a box as a
|
|
* binary blob and to parse the body's contents using the provided callback.
|
|
*
|
|
* @param {function(!Uint8Array)} callback
|
|
* @param {boolean} clone True to clone the data into a new buffer, false to
|
|
* create a view on the existing buffer. Creating a view on the existing
|
|
* buffer will keep the entire buffer in memory so long as the view is
|
|
* reachable. Use false for temporary values, and true for values that
|
|
* need to outlive the underlying buffer.
|
|
* @return {!shaka.util.Mp4Parser.CallbackType}
|
|
* @export
|
|
*/
|
|
static allData(callback, clone) {
|
|
return (box) => {
|
|
const all = box.reader.getLength() - box.reader.getPosition();
|
|
callback(box.reader.readBytes(all, clone));
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert an ascii string name to the integer type for a box.
|
|
*
|
|
* @param {string} name The name of the box. The name must be four
|
|
* characters long.
|
|
* @return {number}
|
|
* @private
|
|
*/
|
|
static typeFromString_(name) {
|
|
goog.asserts.assert(
|
|
name.length == 4,
|
|
'Mp4 box names must be 4 characters long');
|
|
|
|
let code = 0;
|
|
for (const chr of name) {
|
|
code = (code << 8) | chr.charCodeAt(0);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert an integer type from a box into an ascii string name.
|
|
* Useful for debugging.
|
|
*
|
|
* @param {number} type The type of the box, a uint32.
|
|
* @return {string}
|
|
* @export
|
|
*/
|
|
static typeToString(type) {
|
|
const name = String.fromCharCode(
|
|
(type >> 24) & 0xff,
|
|
(type >> 16) & 0xff,
|
|
(type >> 8) & 0xff,
|
|
type & 0xff);
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Find the header size of the box.
|
|
* Useful for modifying boxes in place or finding the exact offset of a field.
|
|
*
|
|
* @param {shaka.extern.ParsedBox} box
|
|
* @return {number}
|
|
* @export
|
|
*/
|
|
static headerSize(box) {
|
|
const basicHeaderSize = 8;
|
|
const _64BitFieldSize = box.has64BitSize ? 8 : 0;
|
|
const versionAndFlagsSize = box.flags != null ? 4 : 0;
|
|
return basicHeaderSize + _64BitFieldSize + versionAndFlagsSize;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {function(!shaka.extern.ParsedBox)}
|
|
* @exportInterface
|
|
*/
|
|
shaka.util.Mp4Parser.CallbackType;
|
|
|
|
|
|
/**
|
|
* An enum used to track the type of box so that the correct values can be
|
|
* read from the header.
|
|
*
|
|
* @enum {number}
|
|
* @private
|
|
*/
|
|
shaka.util.Mp4Parser.BoxType_ = {
|
|
BASIC_BOX: 0,
|
|
FULL_BOX: 1,
|
|
};
|
|
|
|
|