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;