/**
* @license
* Copyright 2016 Google Inc.
*
* 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.
*/
goog.provide('shaka.media.WebmSegmentIndexParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.EbmlElement');
goog.require('shaka.util.EbmlParser');
goog.require('shaka.util.Error');
/**
* Creates a WebM Cues element parser.
*
* @constructor
* @struct
*/
shaka.media.WebmSegmentIndexParser = function() {};
/** @const {number} */
shaka.media.WebmSegmentIndexParser.EBML_ID = 0x1a45dfa3;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.SEGMENT_ID = 0x18538067;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.INFO_ID = 0x1549a966;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID = 0x2ad7b1;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.DURATION_ID = 0x4489;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUES_ID = 0x1c53bb6b;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_POINT_ID = 0xbb;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_TIME_ID = 0xb3;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID = 0xb7;
/** @const {number} */
shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1;
/**
* Parses SegmentReferences from a WebM container.
* @param {!ArrayBuffer} cuesData The WebM container's "Cueing Data" section.
* @param {!ArrayBuffer} initData The WebM container's headers.
* @param {!Array.<string>} uris The possible locations of the WebM file that
* contains the segments.
* @param {number} scaledPresentationTimeOffset
* @return {!Array.<!shaka.media.SegmentReference>}
* @throws {shaka.util.Error}
* @see http://www.matroska.org/technical/specs/index.html
* @see http://www.webmproject.org/docs/container/
*/
shaka.media.WebmSegmentIndexParser.prototype.parse = function(
cuesData, initData, uris, scaledPresentationTimeOffset) {
let tuple = this.parseWebmContainer_(initData);
let parser = new shaka.util.EbmlParser(new DataView(cuesData));
let cuesElement = parser.parseElement();
if (cuesElement.id != shaka.media.WebmSegmentIndexParser.CUES_ID) {
shaka.log.error('Not a Cues element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUES_ELEMENT_MISSING);
}
return this.parseCues_(
cuesElement, tuple.segmentOffset, tuple.timecodeScale, tuple.duration,
uris, scaledPresentationTimeOffset);
};
/**
* Parses a WebM container to get the segment's offset, timecode scale, and
* duration.
*
* @param {!ArrayBuffer} initData
* @return {{segmentOffset: number, timecodeScale: number, duration: number}}
* The segment's offset in bytes, the segment's timecode scale in seconds,
* and the duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseWebmContainer_ = function(
initData) {
let parser = new shaka.util.EbmlParser(new DataView(initData));
// Check that the WebM container data starts with the EBML header, but
// skip its contents.
let ebmlElement = parser.parseElement();
if (ebmlElement.id != shaka.media.WebmSegmentIndexParser.EBML_ID) {
shaka.log.error('Not an EBML element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_EBML_HEADER_ELEMENT_MISSING);
}
let segmentElement = parser.parseElement();
if (segmentElement.id != shaka.media.WebmSegmentIndexParser.SEGMENT_ID) {
shaka.log.error('Not a Segment element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_SEGMENT_ELEMENT_MISSING);
}
// This value is used as the initial offset to the first referenced segment.
let segmentOffset = segmentElement.getOffset();
// Parse the Segment element to get the segment info.
let segmentInfo = this.parseSegment_(segmentElement);
return {
segmentOffset: segmentOffset,
timecodeScale: segmentInfo.timecodeScale,
duration: segmentInfo.duration,
};
};
/**
* Parses a WebM Info element to get the segment's timecode scale and duration.
* @param {!shaka.util.EbmlElement} segmentElement
* @return {{timecodeScale: number, duration: number}} The segment's timecode
* scale in seconds and duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseSegment_ = function(
segmentElement) {
let parser = segmentElement.createParser();
// Find the Info element.
let infoElement = null;
while (parser.hasMoreData()) {
let elem = parser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.INFO_ID) {
continue;
}
infoElement = elem;
break;
}
if (!infoElement) {
shaka.log.error('Not an Info element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_INFO_ELEMENT_MISSING);
}
return this.parseInfo_(infoElement);
};
/**
* Parses a WebM Info element to get the segment's timecode scale and duration.
* @param {!shaka.util.EbmlElement} infoElement
* @return {{timecodeScale: number, duration: number}} The segment's timecode
* scale in seconds and duration in seconds.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseInfo_ = function(
infoElement) {
let parser = infoElement.createParser();
// The timecode scale factor in units of [nanoseconds / T], where [T] are the
// units used to express all other time values in the WebM container. By
// default it's assumed that [T] == [milliseconds].
let timecodeScaleNanoseconds = 1000000;
/** @type {?number} */
let durationScale = null;
while (parser.hasMoreData()) {
let elem = parser.parseElement();
if (elem.id == shaka.media.WebmSegmentIndexParser.TIMECODE_SCALE_ID) {
timecodeScaleNanoseconds = elem.getUint();
} else if (elem.id == shaka.media.WebmSegmentIndexParser.DURATION_ID) {
durationScale = elem.getFloat();
}
}
if (durationScale == null) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_DURATION_ELEMENT_MISSING);
}
// The timecode scale factor in units of [seconds / T].
let timecodeScale = timecodeScaleNanoseconds / 1000000000;
// The duration is stored in units of [T]
let durationSeconds = durationScale * timecodeScale;
return {timecodeScale: timecodeScale, duration: durationSeconds};
};
/**
* Parses a WebM CuesElement.
* @param {!shaka.util.EbmlElement} cuesElement
* @param {number} segmentOffset
* @param {number} timecodeScale
* @param {number} duration
* @param {!Array.<string>} uris
* @param {number} scaledPresentationTimeOffset
* @return {!Array.<!shaka.media.SegmentReference>}
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseCues_ = function(
cuesElement, segmentOffset, timecodeScale, duration, uris,
scaledPresentationTimeOffset) {
let references = [];
let getUris = function() { return uris; };
let parser = cuesElement.createParser();
let lastTime = null;
let lastOffset = null;
while (parser.hasMoreData()) {
let elem = parser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_POINT_ID) {
continue;
}
let tuple = this.parseCuePoint_(elem);
if (!tuple) {
continue;
}
// Substract the presentation time offset from the unscaled time
let currentTime = timecodeScale * tuple.unscaledTime;
let currentOffset = segmentOffset + tuple.relativeOffset;
if (lastTime != null) {
goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
references.push(
new shaka.media.SegmentReference(
references.length,
lastTime - scaledPresentationTimeOffset,
currentTime - scaledPresentationTimeOffset,
getUris,
lastOffset, currentOffset - 1));
}
lastTime = currentTime;
lastOffset = currentOffset;
}
if (lastTime != null) {
goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
references.push(
new shaka.media.SegmentReference(
references.length,
lastTime - scaledPresentationTimeOffset,
duration - scaledPresentationTimeOffset,
getUris,
lastOffset, null));
}
return references;
};
/**
* Parses a WebM CuePointElement to get an "unadjusted" segment reference.
* @param {shaka.util.EbmlElement} cuePointElement
* @return {{unscaledTime: number, relativeOffset: number}} The referenced
* segment's start time in units of [T] (see parseInfo_()), and the
* referenced segment's offset in bytes, relative to a WebM Segment
* element.
* @throws {shaka.util.Error}
* @private
*/
shaka.media.WebmSegmentIndexParser.prototype.parseCuePoint_ = function(
cuePointElement) {
let parser = cuePointElement.createParser();
// Parse CueTime element.
let cueTimeElement = parser.parseElement();
if (cueTimeElement.id != shaka.media.WebmSegmentIndexParser.CUE_TIME_ID) {
shaka.log.warning('Not a CueTime element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUE_TIME_ELEMENT_MISSING);
}
let unscaledTime = cueTimeElement.getUint();
// Parse CueTrackPositions element.
let cueTrackPositionsElement = parser.parseElement();
if (cueTrackPositionsElement.id !=
shaka.media.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID) {
shaka.log.warning('Not a CueTrackPositions element.');
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.WEBM_CUE_TRACK_POSITIONS_ELEMENT_MISSING);
}
let cueTrackParser = cueTrackPositionsElement.createParser();
let relativeOffset = 0;
while (cueTrackParser.hasMoreData()) {
let elem = cueTrackParser.parseElement();
if (elem.id != shaka.media.WebmSegmentIndexParser.CUE_CLUSTER_POSITION) {
continue;
}
relativeOffset = elem.getUint();
break;
}
return {unscaledTime: unscaledTime, relativeOffset: relativeOffset};
};