UNPKG

shaka-player

Version:
530 lines (454 loc) 15.2 kB
/*! @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, }; /** * The nested container box types on the path from the Movie Box down to a * track's Sample Table: 'moov' > 'trak' > 'mdia' > 'minf' > 'stbl'. These are * all simple container boxes and are frequently declared together. * * @const {!Array<string>} */ shaka.util.Mp4Parser.SAMPLE_TABLE_PATH = ['moov', 'trak', 'mdia', 'minf', 'stbl']; /** * The nested container box types of a movie fragment: 'moof' > 'traf'. * * @const {!Array<string>} */ shaka.util.Mp4Parser.FRAGMENT_PATH = ['moof', 'traf']; /** * The AVC (H.264) video sample entry box types, together with their Dolby * Vision variants ('dvav'/'dva1'). * * @const {!Array<string>} * @private */ shaka.util.Mp4Parser.AVC_DV_ = ['avc1', 'avc3', 'dvav', 'dva1']; /** * The HEVC (H.265) video sample entry box types, together with their Dolby * Vision variants ('dvh1'/'dvhe'). * * @const {!Array<string>} * @private */ shaka.util.Mp4Parser.HEVC_DV_ = ['hev1', 'hvc1', 'dvh1', 'dvhe']; /** * The VVC (H.266) video sample entry box types, together with their Dolby * Vision variants ('dvc1'/'dvi1'). * * @const {!Array<string>} * @private */ shaka.util.Mp4Parser.VVC_DV_ = ['vvc1', 'vvi1', 'dvc1', 'dvi1']; /** * The AVC and HEVC video sample entry box types and their Dolby Vision * variants. * * @const {!Array<string>} */ shaka.util.Mp4Parser.AVC_HEVC_DV = [...shaka.util.Mp4Parser.AVC_DV_, ...shaka.util.Mp4Parser.HEVC_DV_]; /** * The AVC, HEVC and VVC video sample entry box types and their Dolby Vision * variants. * * @const {!Array<string>} */ shaka.util.Mp4Parser.AVC_HEVC_VVC_DV = [...shaka.util.Mp4Parser.AVC_HEVC_DV, ...shaka.util.Mp4Parser.VVC_DV_]; /** * The audio sample entry box types that are parsed with a generic handler, * i.e. excluding 'mp4a' (AAC), which is parsed separately: Dolby ('ac-3', * 'ec-3', 'ac-4'), Opus, FLAC and APAC. * * @const {!Array<string>} */ shaka.util.Mp4Parser.GENERIC_AUDIO_SAMPLE_ENTRIES = ['ac-3', 'ec-3', 'ac-4', 'Opus', 'fLaC', 'apac'];