2020/lib/parsers/webm.js (228 lines of code) (raw):

/** * @license * Copyright 2018 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; /** * Helper class for WebM parsing. Takes a DataView containing the elements to * be parsed. Lifted this out of dash-mse-test.appspot.com. * * @param {DataView} elemData The element data view. * @param {number=} opt_start The byte offset of the earliest stream, relative * to an (unspecified) reference point. The current position relative to * this start point can be queried on the element, and the information will * be passed to subelements. * * @constructor * @private */ var WebMElemParser_ = function(elemData, opt_start) { /** * The element data being processed. * * @type {DataView} * @private */ this.elemData_ = elemData; /** * The offset of the next byte in the current element data view. * * @type {number} * @private */ this.pos_ = 0; /** * The start position of the first byte in the data view. * * @type {number} * @private */ this.start_ = opt_start || 0; }; /** * Test if there is data remaining in the stream. * * @return {boolean} True if data remains. */ WebMElemParser_.prototype.atEos = function() { return this.pos_ >= this.elemData_.byteLength; }; /** * Read an element identifier from the stream, advancing the read pointer. * * Note that void elements will automatically be skipped. * * @return {number} The element ID. */ WebMElemParser_.prototype.readId = function() { var id = this.readCodedInt_(false); while (id == 0xec) { this.skipElement(); id = this.readCodedInt_(false); } return id; }; /** * Read a subelement from the stream. Returns a new parser which contains the * subelement's data, and advances the position of the current parser to the * next element at the current level. * * @return {WebMElemParser_} The new parser. */ WebMElemParser_.prototype.readSubElement = function() { var size = this.readCodedInt_(true); // 'size' could be the size of the entire WebM file, which is legal (as long // as the code that uses this doesn't try to read past the end of the data // that's present, which it won't. The 'length' parameter is defined as a // 'long', and the Web IDL spec does integer wraparound by definition when // converting to a 'long', which makes the 'size' param go negative. So we // clamp this before the call. var end = this.elemData_.byteOffset + this.pos_; var length = Math.min(size, this.elemData_.buffer.byteLength - end); var subData = new DataView(this.elemData_.buffer, end, length); var subStart = this.start_ + this.pos_; var parser = new WebMElemParser_(subData, subStart); this.pos_ += size; return parser; }; /** * Peeks at the size of the next element in the stream. * @return {number} The size value. */ WebMElemParser_.prototype.peekSize = function() { var pos = this.pos_; var value = this.readCodedInt_(true); this.pos_ = pos; return value; }; /** * Read an integer element from the stream. * * @return {number} The integer value. */ WebMElemParser_.prototype.readInt = function() { var size = this.readCodedInt_(true); var value = this.readSizedInt_(size); return value; }; /** * Read a floating-point element from the stream. * * @return {number} The integer value. */ WebMElemParser_.prototype.readFloat = function() { var size = this.readCodedInt_(true); var value = this.readSizedFloat_(size); return value; }; /** * Advance the stream past the current element. */ WebMElemParser_.prototype.skipElement = function() { var size = this.readCodedInt_(true); this.pos_ += size; }; /** * Get the position of the current offset with respect to the start offset * supplied to the top-level element at creation. * * @return {number} The offset. */ WebMElemParser_.prototype.getCurrentOffset = function() { return this.start_ + this.pos_; }; /** * Read a WebM-encoded integer. This encoding is used to represent element IDs, * element data sizes, and unsigned integers. Signed integers are not yet * supported. * * @param {boolean} useMask Whether to mask out the EBML Length Descriptor. * @return {number} The value. * @private * @see http://www.matroska.org/technical/specs/index.html */ WebMElemParser_.prototype.readCodedInt_ = function(useMask) { var value = this.readByte_(); if (value == 0x01) { // We run into precision problems in this case, handle it separately. value = 0; for (var i = 0; i < 7; i++) { value = (value * 256) + this.readByte_(); } return value; } var mask = 128; for (var i = 0; i < 6 && mask > value; i++) { value = (value * 256) + this.readByte_(); mask *= 128; } if (useMask) { // Can't use bitwise operations because this value can exceed int31. return value - mask; } else { return value; } }; /** * Read a raw integer with an arbitrary number of bytes. * * @param {number} size Number of bytes to read. * @return {number} The value. * @private */ WebMElemParser_.prototype.readSizedInt_ = function(size) { var value = this.readByte_(); for (var i = 1; i < size; i++) { value = (value << 8) + this.readByte_(); } return value; }; /** * Read a float. * * @param {number} size Number of bytes (4 or 8). * @return {number} The value. * @private */ WebMElemParser_.prototype.readSizedFloat_ = function(size) { var value = 0; if (size == 4) { value = this.elemData_.getFloat32(this.pos_); } else if (size == 8) { value = this.elemData_.getFloat64(this.pos_); } this.pos_ += size; return value; }; /** * Read a single byte from the stream and advance the stream position. * * @return {number} The byte. * @private */ WebMElemParser_.prototype.readByte_ = function() { return this.elemData_.getUint8(this.pos_++); }; /** * Parse a WebM 'CuePoint' element into a SegmentReference. * * @param {WebMElemParser_} parser The parser. * @param {number} timebase The timebase. * @param {number} offset The offset in bytes from the start of the cluster. * @return {Array.<number>} A 2-tuple (first byte offset, start time), or * null if there was an error. * @private */ WebMElemParser_.prototype.readWebMCuePoint_ = function(timebase, offset) { // Assumed structure: 'CueTime' followed by one 'CueTrackPositions'. This is // not intended to be a generalized parser, and will not handle muxed streams. if (this.readId() != 0xb3) { // 'CueTime' element return null; } var time = this.readInt() * timebase; if (this.readId() != 0xb7) { // 'CueTrackPositions' element return null; } var innerParser = this.readSubElement(); var clusterPos = offset; while (!innerParser.atEos()) { var id = innerParser.readId(); if (id == 0xf1) { // 'CueClusterPosition' element clusterPos = innerParser.readInt() + offset; } else { innerParser.skipElement(); } } return [clusterPos, time]; }; // Given a buffer contains the first 32k of a file, return a list of tables // containing 'time', 'duration', 'offset', and 'size' properties for each cue. function parseWebM(data, opt_totalSize) { var dlog = function() { var forward = window.dlog || console.log.bind(console); forward.apply(this, arguments); }; var parser = new WebMElemParser_(new DataView(data)); if (parser.readId() != 0x1a45dfa3) { // 'EBML' element dlog(1, 'SegmentIndex: Invalid EBML ID'); return; } // Skip the EBML header, which must come first. parser.skipElement(); if (parser.readId() != 0x18538067) { // 'Segment' element dlog(1, 'SegmentIndex: Invalid Segment ID'); return; } // Grab the segment size to cap the last segment in the file. var segmentSize = parser.peekSize(); // Discard the segment parser, we're only interested in its contents now parser = parser.readSubElement(); // Capture the offset to the first byte of the contents of the segment to use // as the relative base for 'Cues' elements. // TODO: This assumes single-segment media streams, which may not be // true for live, depending. var segmentOffset = parser.getCurrentOffset(); var cuesDone = false; var needsCluster = false; var hasTiming = false; var id = null; var totalDuration = 0; while ((!cuesDone || !hasTiming) && !parser.atEos()) { id = parser.readId(); switch(id) { case 0x114d9b74: var seekParser = parser.readSubElement(); var cuesPosition = null; while (!seekParser.atEos()) { if (seekParser.readId() == 0x4dbb) { var seekElementParser = seekParser.readSubElement(); if (seekElementParser.readId() != 0x53ab) { dlog(1, 'Seek: Invalid SeekID'); } var seekId = seekElementParser.readSubElement().readId(); if (seekId == 0x1c53bb6b) { if (seekElementParser.readId() != 0x53ac) { dlog(1, 'Seek: Invalid SeekPosition'); } cuesPosition = seekElementParser.readInt(); cuesDone = true; break; } } else { dlog(1, 'Seek: Invalid SeekID'); } } break; case 0x1549a966: // 'Segment' element if (!cuesDone) { // we don't have cues...uh oh, we'll need to manually parse the cluster needsCluster = true; cuesDone = true; } var segmentParser = parser.readSubElement(); var timescaleNum = 1000000; // Default timescale numerator var timescaleDen = 1000000000; // Default timescale denominator while (!segmentParser.atEos()) { id = segmentParser.readId(); if (id == 0x2ad7b1) { // 'TimecodeScale' element timescaleNum = segmentParser.readInt(); } else if (id == 0x2ad7b2) { // 'TimecodeScaleDenominator' element timescaleDen = segmentParser.readInt(); } else if (id == 0x4489) { // 'Duration' element totalDuration = segmentParser.readFloat(); } else { segmentParser.skipElement(); } } var timebase = timescaleNum / timescaleDen; totalDuration *= timebase; hasTiming = true; break; default: parser.skipElement(); break; } } // Done with initialization segment. On to the cues...if there's any. var res = []; if (needsCluster) { while (!parser.atEos()) { if (parser.readId() == 0x1f43b675) { // Subtract 4 bytes for the size of the id. var clusterOffset = parser.getCurrentOffset() - 4; var clusterSize = parser.peekSize(); var clusterParser = parser.readSubElement(); if (clusterParser.readId() != 0xe7) { dlog(1, 'Cluster: Invalid Timecode'); } var timecode = clusterParser.readInt(); res.push({ time: timecode, duration: 0, offset: clusterOffset, size: clusterSize }); if (res.length > 1) { var rLen = res.length; res[rLen - 2].duration = res[rLen - 1].time - res[rLen - 2].time; res[rLen - 2].size = res[rLen - 1].offset - res[rLen - 2].offset; } } else { parser.skipElement(); } } } else { parser = new WebMElemParser_(new DataView(data, segmentOffset + cuesPosition)); if (parser.readId() != 0x1c53bb6b) { // 'Cues' element dlog(1, 'SegmentIndex: Invalid Cues ID'); return; } // As before, we only care about the 'Cues' element contents parser = parser.readSubElement(); while (!parser.atEos()) { id = parser.readId(); if (id == 0xbb) { // 'CuePoint' element var subelem = parser.readSubElement(); var offAndTime = subelem.readWebMCuePoint_(timebase, segmentOffset); res.push({ time: offAndTime[1], duration: 0, offset: offAndTime[0], size: 0 }); if (res.length > 1) { var rLen = res.length; res[rLen - 2].duration = res[rLen - 1].time - res[rLen - 2].time; res[rLen - 2].size = res[rLen - 1].offset - res[rLen - 2].offset; } } else { parser.skipElement(); } } } if (res.length > 0) { res[res.length - 1].duration = totalDuration - res[res.length - 1].time; if (!!opt_totalSize) { res[res.length - 1].size = opt_totalSize - res[res.length - 1].offset; } } return res; }