Source: lib/dash/segment_base.js

/**
 * @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.dash.SegmentBase');

goog.require('goog.asserts');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.log');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.Mp4SegmentIndexParser');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.WebmSegmentIndexParser');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.XmlUtils');


/**
 * @namespace shaka.dash.SegmentBase
 * @summary A set of functions for parsing SegmentBase elements.
 */


/**
 * Creates an init segment reference from a Context object.
 *
 * @param {shaka.dash.DashParser.Context} context
 * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback
 * @return {shaka.media.InitSegmentReference}
 */
shaka.dash.SegmentBase.createInitSegment = function(context, callback) {
  const MpdUtils = shaka.dash.MpdUtils;
  const XmlUtils = shaka.util.XmlUtils;
  const ManifestParserUtils = shaka.util.ManifestParserUtils;

  let initialization =
      MpdUtils.inheritChild(context, callback, 'Initialization');
  if (!initialization) {
    return null;
  }

  let resolvedUris = context.representation.baseUris;
  let uri = initialization.getAttribute('sourceURL');
  if (uri) {
    resolvedUris =
        ManifestParserUtils.resolveUris(context.representation.baseUris, [uri]);
  }

  let startByte = 0;
  let endByte = null;
  let range = XmlUtils.parseAttr(initialization, 'range', XmlUtils.parseRange);
  if (range) {
    startByte = range.start;
    endByte = range.end;
  }

  let getUris = function() { return resolvedUris; };
  return new shaka.media.InitSegmentReference(getUris, startByte, endByte);
};


/**
 * Creates a new Stream object.
 *
 * @param {shaka.dash.DashParser.Context} context
 * @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
 * @throws shaka.util.Error When there is a parsing error.
 * @return {shaka.dash.DashParser.StreamInfo}
 */
shaka.dash.SegmentBase.createStream = function(context, requestInitSegment) {
  goog.asserts.assert(context.representation.segmentBase,
                      'Should only be called with SegmentBase');
  // Since SegmentBase does not need updates, simply treat any call as
  // the initial parse.
  const MpdUtils = shaka.dash.MpdUtils;
  const SegmentBase = shaka.dash.SegmentBase;
  const XmlUtils = shaka.util.XmlUtils;

  let unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(
      context, SegmentBase.fromInheritance_, 'presentationTimeOffset')) || 0;

  let timescaleStr = MpdUtils.inheritAttribute(
      context, SegmentBase.fromInheritance_, 'timescale');
  let timescale = 1;
  if (timescaleStr) {
    timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1;
  }

  let scaledPresentationTimeOffset =
      (unscaledPresentationTimeOffset / timescale) || 0;

  let init =
      SegmentBase.createInitSegment(context, SegmentBase.fromInheritance_);
  let index = SegmentBase.createSegmentIndex_(
      context, requestInitSegment, init, scaledPresentationTimeOffset);

  return {
    createSegmentIndex: index.createSegmentIndex,
    findSegmentPosition: index.findSegmentPosition,
    getSegmentReference: index.getSegmentReference,
    initSegmentReference: init,
    scaledPresentationTimeOffset: scaledPresentationTimeOffset,
  };
};


/**
 * Creates segment index info for the given info.
 *
 * @param {shaka.dash.DashParser.Context} context
 * @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
 * @param {shaka.media.InitSegmentReference} init
 * @param {!Array.<string>} uris
 * @param {number} startByte
 * @param {?number} endByte
 * @param {string} containerType
 * @param {number} scaledPresentationTimeOffset
 * @return {shaka.dash.DashParser.SegmentIndexFunctions}
 */
shaka.dash.SegmentBase.createSegmentIndexFromUris = function(
    context, requestInitSegment, init, uris,
    startByte, endByte, containerType, scaledPresentationTimeOffset) {
  let presentationTimeline = context.presentationTimeline;
  let fitLast = !context.dynamic || !context.periodInfo.isLastPeriod;
  let periodStart = context.periodInfo.start;
  let periodDuration = context.periodInfo.duration;

  // Create a local variable to bind to so we can set to null to help the GC.
  let localRequest = requestInitSegment;
  let segmentIndex = null;
  let create = function() {
    let async = [
      localRequest(uris, startByte, endByte),
      containerType == 'webm' ?
          localRequest(init.getUris(), init.startByte, init.endByte) :
          null,
    ];

    localRequest = null;
    return Promise.all(async).then(function(results) {
      let indexData = results[0];
      let initData = results[1] || null;
      let references = null;

      if (containerType == 'mp4') {
        // eslint-disable-next-line new-cap
        references = shaka.media.Mp4SegmentIndexParser(
            indexData, startByte, uris, scaledPresentationTimeOffset);
      } else {
        goog.asserts.assert(initData, 'WebM requires init data');
        let parser = new shaka.media.WebmSegmentIndexParser();
        references = parser.parse(indexData, initData, uris,
            scaledPresentationTimeOffset);
      }

      presentationTimeline.notifySegments(references, periodStart);

      // Since containers are never updated, we don't need to store the
      // segmentIndex in the map.
      goog.asserts.assert(!segmentIndex,
                          'Should not call createSegmentIndex twice');

      segmentIndex = new shaka.media.SegmentIndex(references);
      if (fitLast) {
        segmentIndex.fit(periodDuration);
      }
    });
  };
  let get = function(i) {
    goog.asserts.assert(segmentIndex, 'Must call createSegmentIndex first');
    return segmentIndex.get(i);
  };
  let find = function(t) {
    goog.asserts.assert(segmentIndex, 'Must call createSegmentIndex first');
    return segmentIndex.find(t);
  };

  return {
    createSegmentIndex: create,
    findSegmentPosition: find,
    getSegmentReference: get,
  };
};


/**
 * @param {?shaka.dash.DashParser.InheritanceFrame} frame
 * @return {Element}
 * @private
 */
shaka.dash.SegmentBase.fromInheritance_ = function(frame) {
  return frame.segmentBase;
};


/**
 * Creates segment index info from a Context object.
 *
 * @param {shaka.dash.DashParser.Context} context
 * @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
 * @param {shaka.media.InitSegmentReference} init
 * @param {number} scaledPresentationTimeOffset
 * @return {shaka.dash.DashParser.SegmentIndexFunctions}
 * @throws shaka.util.Error When there is a parsing error.
 * @private
 */
shaka.dash.SegmentBase.createSegmentIndex_ = function(
    context, requestInitSegment, init, scaledPresentationTimeOffset) {
  const MpdUtils = shaka.dash.MpdUtils;
  const SegmentBase = shaka.dash.SegmentBase;
  const XmlUtils = shaka.util.XmlUtils;
  const ManifestParserUtils = shaka.util.ManifestParserUtils;
  const ContentType = shaka.util.ManifestParserUtils.ContentType;

  let contentType = context.representation.contentType;
  let containerType = context.representation.mimeType.split('/')[1];
  if (contentType != ContentType.TEXT && containerType != 'mp4' &&
      containerType != 'webm') {
    shaka.log.error(
        'SegmentBase specifies an unsupported container type.',
        context.representation);
    throw new shaka.util.Error(
        shaka.util.Error.Severity.CRITICAL,
        shaka.util.Error.Category.MANIFEST,
        shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER);
  }

  if ((containerType == 'webm') && !init) {
    shaka.log.error(
        'SegmentBase does not contain sufficient segment information:',
        'the SegmentBase uses a WebM container,',
        'but does not contain an Initialization element.',
        context.representation);
    throw new shaka.util.Error(
        shaka.util.Error.Severity.CRITICAL,
        shaka.util.Error.Category.MANIFEST,
        shaka.util.Error.Code.DASH_WEBM_MISSING_INIT);
  }

  let representationIndex = MpdUtils.inheritChild(
      context, SegmentBase.fromInheritance_, 'RepresentationIndex');
  let indexRangeElem = MpdUtils.inheritAttribute(
      context, SegmentBase.fromInheritance_, 'indexRange');

  let indexUris = context.representation.baseUris;
  let indexRange = XmlUtils.parseRange(indexRangeElem || '');
  if (representationIndex) {
    let representationUri = representationIndex.getAttribute('sourceURL');
    if (representationUri) {
      indexUris = ManifestParserUtils.resolveUris(
          context.representation.baseUris, [representationUri]);
    }

    indexRange = XmlUtils.parseAttr(
        representationIndex, 'range', XmlUtils.parseRange, indexRange);
  }

  if (!indexRange) {
    shaka.log.error(
        'SegmentBase does not contain sufficient segment information:',
        'the SegmentBase does not contain @indexRange',
        'or a RepresentationIndex element.',
        context.representation);
    throw new shaka.util.Error(
        shaka.util.Error.Severity.CRITICAL,
        shaka.util.Error.Category.MANIFEST,
        shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  }

  return shaka.dash.SegmentBase.createSegmentIndexFromUris(
      context, requestInitSegment, init, indexUris, indexRange.start,
      indexRange.end, containerType, scaledPresentationTimeOffset);
};