shaka-player
Version:
DASH/EME video player library
350 lines (282 loc) • 11.4 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.dash.WebmSegmentIndexParser');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.EbmlElement');
goog.require('shaka.dash.EbmlParser');
goog.require('shaka.util.Error');
shaka.dash.WebmSegmentIndexParser = class {
/**
* Parses SegmentReferences from a WebM container.
* @param {BufferSource} cuesData The WebM container's "Cueing Data" section.
* @param {BufferSource} initData The WebM container's headers.
* @param {!Array<string>} uris The possible locations of the WebM file that
* contains the segments.
* @param {shaka.media.InitSegmentReference} initSegmentReference
* @param {number} timestampOffset
* @param {number} appendWindowStart
* @param {number} appendWindowEnd
* @return {!Array<!shaka.media.SegmentReference>}
* @see http://www.matroska.org/technical/specs/index.html
* @see http://www.webmproject.org/docs/container/
*/
static parse(
cuesData, initData, uris, initSegmentReference, timestampOffset,
appendWindowStart, appendWindowEnd) {
const tuple =
shaka.dash.WebmSegmentIndexParser.parseWebmContainer_(initData);
const parser = new shaka.dash.EbmlParser(cuesData);
const cuesElement = parser.parseElement();
if (cuesElement.id != shaka.dash.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 shaka.dash.WebmSegmentIndexParser.parseCues_(
cuesElement, tuple.segmentOffset, tuple.timecodeScale, tuple.duration,
uris, initSegmentReference, timestampOffset, appendWindowStart,
appendWindowEnd);
}
/**
* Parses a WebM container to get the segment's offset, timecode scale, and
* duration.
*
* @param {BufferSource} 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.
* @private
*/
static parseWebmContainer_(initData) {
const parser = new shaka.dash.EbmlParser(initData);
// Check that the WebM container data starts with the EBML header, but
// skip its contents.
const ebmlElement = parser.parseElement();
if (ebmlElement.id != shaka.dash.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);
}
const segmentElement = parser.parseElement();
if (segmentElement.id != shaka.dash.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.
const segmentOffset = segmentElement.getOffset();
// Parse the Segment element to get the segment info.
const segmentInfo = shaka.dash.WebmSegmentIndexParser.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.
* @private
*/
static parseSegment_(segmentElement) {
const parser = segmentElement.createParser();
// Find the Info element.
let infoElement = null;
while (parser.hasMoreData()) {
const elem = parser.parseElement();
if (elem.id != shaka.dash.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 shaka.dash.WebmSegmentIndexParser.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.
* @private
*/
static parseInfo_(infoElement) {
const 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()) {
const elem = parser.parseElement();
if (elem.id == shaka.dash.WebmSegmentIndexParser.TIMECODE_SCALE_ID) {
timecodeScaleNanoseconds = elem.getUint();
} else if (elem.id == shaka.dash.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].
const timecodeScale = timecodeScaleNanoseconds / 1000000000;
// The duration is stored in units of [T]
const 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 {shaka.media.InitSegmentReference} initSegmentReference
* @param {number} timestampOffset
* @param {number} appendWindowStart
* @param {number} appendWindowEnd
* @return {!Array<!shaka.media.SegmentReference>}
* @private
*/
static parseCues_(cuesElement, segmentOffset, timecodeScale, duration,
uris, initSegmentReference, timestampOffset, appendWindowStart,
appendWindowEnd) {
const references = [];
const getUris = () => uris;
const parser = cuesElement.createParser();
let lastTime = null;
let lastOffset = null;
while (parser.hasMoreData()) {
const elem = parser.parseElement();
if (elem.id != shaka.dash.WebmSegmentIndexParser.CUE_POINT_ID) {
continue;
}
const tuple = shaka.dash.WebmSegmentIndexParser.parseCuePoint_(elem);
if (!tuple) {
continue;
}
// Subtract the presentation time offset from the unscaled time
const currentTime = timecodeScale * tuple.unscaledTime;
const currentOffset = segmentOffset + tuple.relativeOffset;
if (lastTime != null) {
goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
references.push(
new shaka.media.SegmentReference(
lastTime + timestampOffset,
currentTime + timestampOffset,
getUris,
/* startByte= */ lastOffset, /* endByte= */ currentOffset - 1,
initSegmentReference,
timestampOffset,
appendWindowStart,
appendWindowEnd));
}
lastTime = currentTime;
lastOffset = currentOffset;
}
if (lastTime != null) {
goog.asserts.assert(lastOffset != null, 'last offset cannot be null');
references.push(
new shaka.media.SegmentReference(
lastTime + timestampOffset,
duration + timestampOffset,
getUris,
/* startByte= */ lastOffset, /* endByte= */ null,
initSegmentReference,
timestampOffset,
appendWindowStart,
appendWindowEnd));
}
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.
* @private
*/
static parseCuePoint_(cuePointElement) {
const parser = cuePointElement.createParser();
// Parse CueTime element.
const cueTimeElement = parser.parseElement();
if (cueTimeElement.id != shaka.dash.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);
}
const unscaledTime = cueTimeElement.getUint();
// Parse CueTrackPositions element.
const cueTrackPositionsElement = parser.parseElement();
if (cueTrackPositionsElement.id !=
shaka.dash.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);
}
const cueTrackParser = cueTrackPositionsElement.createParser();
let relativeOffset = 0;
while (cueTrackParser.hasMoreData()) {
const elem = cueTrackParser.parseElement();
if (elem.id != shaka.dash.WebmSegmentIndexParser.CUE_CLUSTER_POSITION) {
continue;
}
relativeOffset = elem.getUint();
break;
}
return {unscaledTime: unscaledTime, relativeOffset: relativeOffset};
}
};
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.EBML_ID = 0x1a45dfa3;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.SEGMENT_ID = 0x18538067;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.INFO_ID = 0x1549a966;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.TIMECODE_SCALE_ID = 0x2ad7b1;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.DURATION_ID = 0x4489;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.CUES_ID = 0x1c53bb6b;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.CUE_POINT_ID = 0xbb;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.CUE_TIME_ID = 0xb3;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID = 0xb7;
/** @const {number} */
shaka.dash.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1;