Source: dash/mpd_processor.js

/**
 * @license
 * Copyright 2015 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.MpdProcessor');

goog.require('goog.Uri');
goog.require('shaka.asserts');
goog.require('shaka.dash.ContainerSegmentIndexSource');
goog.require('shaka.dash.DurationSegmentIndexSource');
goog.require('shaka.dash.ListSegmentIndexSource');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.dash.TimelineSegmentIndexSource');
goog.require('shaka.dash.mpd');
goog.require('shaka.features');
goog.require('shaka.log');
goog.require('shaka.media.PeriodInfo');
goog.require('shaka.media.SegmentInitSource');
goog.require('shaka.media.StreamInfo');
goog.require('shaka.media.StreamSetInfo');
goog.require('shaka.media.TextSegmentIndexSource');
goog.require('shaka.util.FailoverUri');



/**
 * Creates an MpdProcessor, which validates MPDs, calculates start, duration,
 * and other missing attributes, removes invalid Periods, AdaptationSets, and
 * Representations, and ultimately generates a ManifestInfo.
 *
 * @param {?shaka.player.DashVideoSource.ContentProtectionCallback}
 *     interpretContentProtection
 *
 * @constructor
 * @struct
 */
shaka.dash.MpdProcessor = function(interpretContentProtection) {
  /** @private {?shaka.player.DashVideoSource.ContentProtectionCallback} */
  this.interpretContentProtection_ = interpretContentProtection;
};


/**
 * The default value, in seconds, for MPD@minBufferTime if this attribute is
 * missing.
 * @const {number}
 */
shaka.dash.MpdProcessor.DEFAULT_MIN_BUFFER_TIME = 5.0;


/**
 * Processes the given MPD.
 * This function modifies |mpd| but does not take ownership of it.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {!shaka.media.ManifestInfo}
 */
shaka.dash.MpdProcessor.prototype.process = function(mpd, networkCallback) {
  var manifestCreationTime = shaka.util.Clock.now() / 1000.0;

  this.validateSegmentInfo_(mpd);
  this.calculateDurations_(mpd);
  this.filterPeriods_(mpd);

  if ((mpd.type == 'dynamic') && (mpd.availabilityStartTime == null)) {
    // Assume broadcasting just started.
    shaka.log.warning(
        'The MPD is \'dynamic\' but @availabilityStartTime is not specified:',
        'treating @availabilityStartTime as if it were the current time.');
    mpd.availabilityStartTime = manifestCreationTime;
  }

  return this.createManifestInfo_(mpd, manifestCreationTime, networkCallback);
};


/**
 * Ensures that each Representation has either a SegmentBase, SegmentList, or
 * SegmentTemplate.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @private
 */
shaka.dash.MpdProcessor.prototype.validateSegmentInfo_ = function(mpd) {
  for (var i = 0; i < mpd.periods.length; ++i) {
    var period = mpd.periods[i];
    for (var j = 0; j < period.adaptationSets.length; ++j) {
      var adaptationSet = period.adaptationSets[j];
      if (adaptationSet.contentType == 'text') continue;

      for (var k = 0; k < adaptationSet.representations.length; ++k) {
        var representation = adaptationSet.representations[k];

        var n = 0;
        n += representation.segmentBase ? 1 : 0;
        n += representation.segmentList ? 1 : 0;
        n += representation.segmentTemplate ? 1 : 0;

        if (n == 0) {
          shaka.log.warning(
              'Representation does not contain any segment information:',
              'the Representation must contain one of SegmentBase,',
              'SegmentList, or SegmentTemplate.',
              representation);
          adaptationSet.representations.splice(k, 1);
          --k;
        } else if (n != 1) {
          shaka.log.warning(
              'Representation contains multiple segment information sources:',
              'the Representation should only contain one of SegmentBase,',
              'SegmentList, or SegmentTemplate.',
              representation);
          if (representation.segmentBase) {
            shaka.log.info('Using SegmentBase by default.');
            representation.segmentList = null;
            representation.segmentTemplate = null;
          } else if (representation.segmentList) {
            shaka.log.info('Using SegmentList by default.');
            representation.segmentTemplate = null;
          } else {
            shaka.asserts.unreachable();
          }
        }
      }  // for k
    }
  }
};


/**
 * Attempts to calculate each Period's start attribute and duration attribute,
 * and attempts to calcuate the MPD's mediaPresentationDuration attribute.
 *
 * @see ISO/IEC 23009-1:2014 section 5.3.2.1
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @private
 */
shaka.dash.MpdProcessor.prototype.calculateDurations_ = function(mpd) {
  if (!mpd.periods.length) {
    return;
  }

  if (mpd.periods[0].start == null) {
    mpd.periods[0].start = 0;
  }

  // If it's zero or truthy, it's set.  This means null and NaN are not set.
  var isSet = function(x) { return x == 0 || !!x; };

  // Ignore @mediaPresentationDuration if the MPD is dynamic.
  // TODO: Consider using @mediaPresentationDuration or other duration
  // attributes for signalling the end of a live stream.
  if (mpd.type == 'dynamic') {
    mpd.mediaPresentationDuration = null;
  }

  // If there is only one Period then infer its duration.
  if (isSet(mpd.mediaPresentationDuration) &&
      (mpd.periods.length == 1) &&
      !isSet(mpd.periods[0].duration)) {
    mpd.periods[0].duration = mpd.mediaPresentationDuration;
  }

  var totalDuration = 0;

  // True if |totalDuration| includes all periods, false if it only includes up
  // to the last Period in which a start time and duration could be
  // ascertained.
  var totalDurationIncludesAllPeriods = true;

  for (var i = 0; i < mpd.periods.length; ++i) {
    var previousPeriod = mpd.periods[i - 1];
    var period = mpd.periods[i];

    // "The Period extends until the Period.start of the next Period, or until
    // the end of the Media Presentation in the case of the last Period."
    var nextPeriod = mpd.periods[i + 1] ||
                     { start: mpd.mediaPresentationDuration };

    // "If the 'start' attribute is absent, but the previous period contains a
    // 'duration' attribute, the start time of the new Period is the sum of the
    // start time of the previous period Period.start and the value of the
    // attribute 'duration' of the previous Period."
    if (!isSet(period.start) &&
        previousPeriod &&
        isSet(previousPeriod.start) &&
        isSet(previousPeriod.duration)) {
      period.start = previousPeriod.start + previousPeriod.duration;
    }

    // "The difference between the start time of a Period and the start time
    // of the following Period is the duration of the media content represented
    // by this Period."
    if (!isSet(period.duration) && isSet(nextPeriod.start)) {
      period.duration = nextPeriod.start - period.start;
    }

    if ((period.start != null) && (period.duration != null)) {
      totalDuration += period.duration;
    } else {
      totalDurationIncludesAllPeriods = false;
    }
  }

  // "The Media Presentation Duration is provided either as the value of MPD
  // 'mediaPresentationDuration' attribute if present, or as the sum of
  // Period.start + Period.duration of the last Period."
  if (isSet(mpd.mediaPresentationDuration)) {
    if (mpd.mediaPresentationDuration != totalDuration) {
      shaka.log.warning(
          '@mediaPresentationDuration does not match the total duration of all',
          'Periods.');
      // Assume mpd.mediaPresentationDuration is correct;
      // |totalDurationIncludesAllPeriods| may be false.
    }
  } else {
    var finalPeriod = mpd.periods[mpd.periods.length - 1];
    if (totalDurationIncludesAllPeriods) {
      shaka.asserts.assert(isSet(finalPeriod.start) &&
                           isSet(finalPeriod.duration));
      shaka.asserts.assert(totalDuration ==
                           finalPeriod.start + finalPeriod.duration);
      mpd.mediaPresentationDuration = totalDuration;
    } else {
      if (isSet(finalPeriod.start) && isSet(finalPeriod.duration)) {
        shaka.log.warning(
            'Some Periods may not have valid start times or durations.');
        mpd.mediaPresentationDuration =
            finalPeriod.start + finalPeriod.duration;
      } else {
        // Fallback to what we were able to compute.
        if (mpd.type != 'dynamic') {
          shaka.log.warning(
              'Some Periods may not have valid start times or durations;',
              '@mediaPresentationDuration may not include the duration of all',
              'periods.');
          mpd.mediaPresentationDuration = totalDuration;
        }
      }
    }
  }
};


/**
 * Removes invalid Representations from |mpd|.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @private
 */
shaka.dash.MpdProcessor.prototype.filterPeriods_ = function(mpd) {
  for (var i = 0; i < mpd.periods.length; ++i) {
    var period = mpd.periods[i];
    for (var j = 0; j < period.adaptationSets.length; ++j) {
      var adaptationSet = period.adaptationSets[j];
      this.filterAdaptationSet_(adaptationSet);
      if (adaptationSet.representations.length == 0) {
        // Drop any AdaptationSet that is empty.
        // An error has already been logged.
        period.adaptationSets.splice(j, 1);
        --j;
      }
    }
  }
};


/**
 * Removes any Representation from the given AdaptationSet that has a different
 * MIME type than the MIME type of the first Representation of the
 * AdaptationSet.
 *
 * @param {!shaka.dash.mpd.AdaptationSet} adaptationSet
 * @private
 */
shaka.dash.MpdProcessor.prototype.filterAdaptationSet_ = function(
    adaptationSet) {
  var desiredMimeType = null;

  for (var i = 0; i < adaptationSet.representations.length; ++i) {
    var representation = adaptationSet.representations[i];
    var mimeType = representation.mimeType || '';

    if (!desiredMimeType) {
      desiredMimeType = mimeType;
    } else if (mimeType != desiredMimeType) {
      shaka.log.warning(
          'Representation does not have the same MIME type as other',
          'Representations within its AdaptationSet.',
          adaptationSet.representations[i]);
      adaptationSet.representations.splice(i, 1);
      --i;
    }
  }
};


/**
 * Creates a ManifestInfo from |mpd|.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {!shaka.media.ManifestInfo}
 * @private
 */
shaka.dash.MpdProcessor.prototype.createManifestInfo_ = function(
    mpd, manifestCreationTime, networkCallback) {
  var manifestInfo = new shaka.media.ManifestInfo();

  if (mpd.type == 'dynamic') {
    manifestInfo.live = true;
    manifestInfo.updatePeriod = mpd.minUpdatePeriod;

    // Prefer the URL specified by the Location element.
    manifestInfo.updateUrl = new shaka.util.FailoverUri(
        networkCallback, mpd.updateLocation || mpd.url);

    manifestInfo.availabilityStartTime = mpd.availabilityStartTime;
  }

  manifestInfo.minBufferTime = mpd.minBufferTime ||
      shaka.dash.MpdProcessor.DEFAULT_MIN_BUFFER_TIME;

  for (var i = 0; i < mpd.periods.length; ++i) {
    var period = mpd.periods[i];

    if (period.start == null) {
      shaka.log.warning(
          'Skipping Period', i + 1, 'and any subsequent Periods:',
          'Period', i + 1, 'does not have a valid start time.',
          period);
      break;
    }

    var periodInfo = this.createPeriodInfo_(
        mpd, period, manifestCreationTime, networkCallback);
    manifestInfo.periodInfos.push(periodInfo);
  }

  return manifestInfo;
};


/**
 * Creates a PeriodInfo.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {!shaka.media.PeriodInfo}
 * @private
 */
shaka.dash.MpdProcessor.prototype.createPeriodInfo_ = function(
    mpd, period, manifestCreationTime, networkCallback) {
  shaka.asserts.assert(period.start != null);

  var periodInfo = new shaka.media.PeriodInfo();
  periodInfo.id = period.id;
  periodInfo.start = /** @type {number} */(period.start);
  periodInfo.duration = period.duration;

  // First group AdaptationSets by type.
  var setsByType = new shaka.util.MultiMap();
  period.adaptationSets.forEach(
      function(set) { setsByType.push(set.contentType || '', set); });

  var setTypes = setsByType.keys();
  for (var typeIdx = 0; typeIdx < setTypes.length; ++typeIdx) {
    var type = setTypes[typeIdx];

    // Then group AdaptationSets of the same type by group.
    var setsByGroup = new shaka.util.MultiMap();
    setsByType.get(type).forEach(
        function(set) { setsByGroup.push(set.group, set); });

    var setGroups = setsByGroup.keys();
    for (var groupIdx = 0; groupIdx < setGroups.length; ++groupIdx) {
      var group = setGroups[groupIdx];
      shaka.asserts.assert(group != null);

      // Finally group AdaptationSets of the same type and group by language,
      // then squash them into the same StreamSetInfo.
      var setsByLang = new shaka.util.MultiMap();
      setsByGroup.get(group).forEach(
          function(set) { setsByLang.push(set.lang, set); });

      var setLangs = setsByLang.keys();
      for (var langIdx = 0; langIdx < setLangs.length; ++langIdx) {
        var lang = setLangs[langIdx];

        var sets = /** @type {!Array.<shaka.dash.mpd.AdaptationSet>} */(
            setsByLang.get(lang));
        var streamSetInfo = this.createStreamSetInfo_(
            mpd, period, sets, manifestCreationTime, networkCallback);
        periodInfo.streamSetInfos.push(streamSetInfo);
      }  // for langIdx
    }  // for groupIdx
  }  // for typeIdx

  return periodInfo;
};


/**
 * Creates a StreamSetInfo from AdaptationSets of the same type, group, and
 * language.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {Array.<!shaka.dash.mpd.AdaptationSet>} adaptationSets
 *     AdaptationSets from the same group.
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {!shaka.media.StreamSetInfo}
 * @private
 */
shaka.dash.MpdProcessor.prototype.createStreamSetInfo_ = function(
    mpd, period, adaptationSets, manifestCreationTime, networkCallback) {
  shaka.asserts.assert(adaptationSets.length > 0);
  shaka.asserts.assert(adaptationSets.every(function(set) {
    return (set.group == adaptationSets[0].group) &&
           ((set.lang || '') == (adaptationSets[0].lang || '')) &&
           ((set.contentType || '') == (adaptationSets[0].contentType || ''));
  }));

  var streamSetInfo = new shaka.media.StreamSetInfo();

  // Construct a StreamSetInfo ID from the AdaptationSet IDs. Consider the
  // entire StreamSetInfo unidentifiable if an AdaptationSet (in the group)
  // does not have an ID. StreamSetInfo IDs must remain identical between
  // manifest updates, see shaka.media.ManifestUpdater.
  var identifiable = adaptationSets
      .filter(function(set) { return set.id != null; });
  if (identifiable.length == adaptationSets.length) {
    streamSetInfo.id = identifiable
        .map(function(set) { return set.id; })
        .sort()
        .reduce(function(all, part) { return all + ',' + part; });
  }

  streamSetInfo.lang = adaptationSets[0].lang || '';
  streamSetInfo.contentType = adaptationSets[0].contentType || '';
  streamSetInfo.main = adaptationSets.some(
      function(set) { return set.main; });

  // Maps each StreamInfo to its AdaptationSet, which we use below to enable
  // certain StreamInfos.
  var streamInfoUidToAdaptationSet = {};

  for (var i = 0; i < adaptationSets.length; ++i) {
    var adaptationSet = adaptationSets[i];

    for (var j = 0; j < adaptationSet.representations.length; ++j) {
      var representation = adaptationSet.representations[j];
      var drmInfos = this.getDrmInfos_(representation);

      // Update the common DrmInfos.
      var commonDrmInfos = streamSetInfo.drmInfos.slice(0);
      this.updateCommonDrmInfos_(drmInfos, commonDrmInfos);
      if (commonDrmInfos.length == 0 && streamSetInfo.drmInfos.length > 0) {
        shaka.log.warning(
            'Representation does not contain any ContentProtection elements',
            'that are compatible with other Representations within its',
            'AdaptationSet or AdaptationSet group.',
            representation);
        continue;
      }

      var streamInfo = this.createStreamInfo_(
          mpd, period, representation, manifestCreationTime, networkCallback);
      if (!streamInfo) {
        // An error has already been logged.
        continue;
      }

      streamSetInfo.streamInfos.push(streamInfo);
      streamSetInfo.drmInfos = commonDrmInfos;

      streamInfoUidToAdaptationSet[streamInfo.uniqueId] = adaptationSet;
    }  // for j
  }  // for i

  // If the streams are unencrypted then we don't require a key system.
  var unencrypted = streamSetInfo.drmInfos.some(
      function(drmInfo) { return drmInfo.keySystem == ''; });
  if (unencrypted) {
    streamSetInfo.streamInfos.forEach(
        function(streamInfo) { streamInfo.allowedByKeySystem = true; });
    return streamSetInfo;
  }

  // If the streams are encrypted then assume the key system can probably
  // decrypt the lowest quality streams.  This enables the VideoSource to
  // select at least one stream for stream startup without having to query the
  // key system first.
  //
  // The lowest quality streams are the streams that are contained within
  // the AdaptationSet which contains THE lowest quality stream.
  //
  // For example, given an MPD that looks like
  // <AdaptationSet id="A1" group="1">
  //   <Representation id="R1" width="360" height="240" />
  //   <Representation id="R2" width="640" height="480" />
  // </AdaptationSet>
  // <AdaptationSet id="A2' group="1">
  //   <ContentProtection cenc:default_KID="01234567890ABCDEF" />
  //   <Representation id="R3" width="1280" height="720" />
  //   <Representation id="R4" width="1920" height="1080" />
  // </AdaptationSet>
  // the lowest quality streams are R1 and R2.
  var lowestQualitySet = this.findLowestQualityAdaptationSet_(adaptationSets);

  for (var i = 0; i < streamSetInfo.streamInfos.length; ++i) {
    var streamInfo = streamSetInfo.streamInfos[i];
    var adaptationSet = streamInfoUidToAdaptationSet[streamInfo.uniqueId];
    if (adaptationSet == lowestQualitySet) {
      shaka.log.v1(
          'Assuming the key system can decrypt stream', streamInfo.id + '.');
      streamInfo.allowedByKeySystem = true;
    }
  }

  return streamSetInfo;
};


/**
 * Returns the AdaptationSet that contains the lowest quality Representation.
 *
 * @param {!Array.<!shaka.dash.mpd.AdaptationSet>} adaptationSets
 * @return {shaka.dash.mpd.AdaptationSet}
 * @private
 */
shaka.dash.MpdProcessor.prototype.findLowestQualityAdaptationSet_ = function(
    adaptationSets) {
  var lowestQuality = null;
  var lowestQualitySet = null;

  for (var i = 0; i < adaptationSets.length; ++i) {
    var adaptationSet = adaptationSets[i];

    for (var j = 0; j < adaptationSet.representations.length; ++j) {
      var representation = adaptationSet.representations[j];

      var quality = (representation.width || 1) *
                    (representation.height || 1) *
                    (representation.bandwidth || 1);
      if ((lowestQuality == null) || (quality < lowestQuality)) {
        lowestQuality = quality;
        lowestQualitySet = adaptationSet;
      }
    }
  }

  return lowestQualitySet;
};


/**
 * Sets |commonDrmInfos| to |drmInfos| if |commonDrmInfos| is empty; otherwise,
 * sets |commonDrmInfos| to the intersection between |commonDrmInfos| and
 * |drmInfos|.
 *
 * @param {!Array.<!shaka.player.DrmInfo>} drmInfos
 * @param {!Array.<!shaka.player.DrmInfo>} commonDrmInfos
 *
 * @private
 */
shaka.dash.MpdProcessor.prototype.updateCommonDrmInfos_ = function(
    drmInfos, commonDrmInfos) {
  if (commonDrmInfos.length == 0) {
    Array.prototype.push.apply(commonDrmInfos, drmInfos);
    return;
  }

  for (var i = 0; i < commonDrmInfos.length; ++i) {
    var found = false;
    for (var j = 0; j < drmInfos.length; ++j) {
      if (commonDrmInfos[i].isCompatible(drmInfos[j])) {
        found = true;
        commonDrmInfos[i].addInitDatas(drmInfos[j].initDatas);
        break;
      }
    }
    if (!found) {
      commonDrmInfos.splice(i, 1);
      --i;
    }
  }
};


/**
 * Gets the application provided DrmInfos for the given Representation.
 *
 * @param {!shaka.dash.mpd.Representation} representation
 * @return {!Array.<!shaka.player.DrmInfo>} The application provided
 *     DrmInfos. A dummy DrmInfo, which has an empty |keySystem| string,
 *     is used for unencrypted content.
 * @private
 */
shaka.dash.MpdProcessor.prototype.getDrmInfos_ = function(representation) {
  var drmInfos = [];
  if (representation.contentProtections.length == 0) {
    // Return a single item which indicates that the content is unencrypted.
    drmInfos.push(new shaka.player.DrmInfo());
  } else if (this.interpretContentProtection_) {
    for (var i = 0; i < representation.contentProtections.length; ++i) {
      var contentProtection = representation.contentProtections[i];
      drmInfos.push.apply(drmInfos, this.createDrmInfos_(contentProtection));
    }
  }
  return drmInfos;
};


/**
 * @param {!shaka.dash.mpd.ContentProtection} contentProtection
 * @return {!Array.<!shaka.player.DrmInfo>}
 * @private
 */
shaka.dash.MpdProcessor.prototype.createDrmInfos_ = function(
    contentProtection) {
  var drmInfos = [];

  var newStyle = this.interpretContentProtection_.length == 2;
  if (!newStyle) {
    shaka.log.error(
        'You must use the new-style ContentProtection interpretation API.',
        'See shaka.player.DashVideoSource.ContentProtectionCallback.');
    return [];
  }

  var schemeId = contentProtection.schemeIdUri || '';
  var element = /** @type {!Node} */ (contentProtection.element);
  var configs = this.interpretContentProtection_(schemeId, element);
  if (!configs) {
    return [];
  }

  if (!(configs instanceof Array)) {
    shaka.log.error(
        'ContentProtection interpretation callback must return',
        'an array of shaka.player.DrmInfo.Config objects.');
    return [];
  }

  for (var i = 0; i < configs.length; ++i) {
    var drmInfo = shaka.player.DrmInfo.createFromConfig(configs[i]);

    if (drmInfo.initDatas.length == 0 &&
        contentProtection.pssh &&
        contentProtection.pssh.psshBox) {
      var initData = {
        'initData': contentProtection.pssh.psshBox,
        'initDataType': 'cenc'
      };
      drmInfo.addInitDatas([initData]);
    }

    drmInfos.push(drmInfo);
  }

  return drmInfos;
};


/**
 * Creates a StreamInfo from the given Representation.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.StreamInfo} The new StreamInfo on success; otherwise,
 *     return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.createStreamInfo_ = function(
    mpd, period, representation, manifestCreationTime, networkCallback) {
  if (!representation.baseUrl || representation.baseUrl.length === 0) {
    shaka.log.warning(
        'Representation does not contain sufficient segment information:',
        'the Representation must contain a BaseURL.',
        representation);
    return null;
  }

  var streamInfo = null;
  var timescale = 1;
  var presentationTimeOffset = 0;

  if (representation.segmentBase) {
    streamInfo = this.createStreamInfoFromSegmentBase_(
        mpd, period, representation, manifestCreationTime, networkCallback);
    timescale = representation.segmentBase.timescale;
    presentationTimeOffset = representation.segmentBase.presentationTimeOffset;
  } else if (representation.segmentList) {
    streamInfo = this.createStreamInfoFromSegmentList_(
        mpd, period, representation, manifestCreationTime, networkCallback);
    timescale = representation.segmentList.timescale;
    presentationTimeOffset = representation.segmentList.presentationTimeOffset;
  } else if (representation.segmentTemplate) {
    streamInfo = this.createStreamInfoFromSegmentTemplate_(
        mpd, period, representation, manifestCreationTime, networkCallback);
    timescale = representation.segmentTemplate.timescale;
    presentationTimeOffset =
        representation.segmentTemplate.presentationTimeOffset;
  } else if (representation.mimeType.split('/')[0] == 'text') {
    streamInfo = new shaka.media.StreamInfo();
    streamInfo.segmentIndexSource = new shaka.media.TextSegmentIndexSource(
        new shaka.util.FailoverUri(networkCallback, representation.baseUrl));
  } else {
    shaka.asserts.unreachable();
  }

  if (!streamInfo) {
    // An error has already been logged.
    return null;
  }

  streamInfo.id = representation.id;

  if (presentationTimeOffset) {
    // Each timestamp within each media segment is relative to the start of the
    // Period minus @presentationTimeOffset. So to align the start of the first
    // segment to the start of the Period we must apply an offset of -1 *
    // @presentationTimeOffset seconds to each timestamp within each media
    // segment.
    streamInfo.timestampOffset = -1 * presentationTimeOffset / timescale;
  }

  streamInfo.bandwidth = representation.bandwidth;
  streamInfo.width = representation.width;
  streamInfo.height = representation.height;
  streamInfo.mimeType = representation.mimeType || '';
  streamInfo.codecs = representation.codecs || '';

  for (var i = 0; i < representation.contentProtections.length; ++i) {
    // Since we don't know which key system we will use yet, map each
    // cenc:default_KID that the manifest specifies.
    var contentProtection = representation.contentProtections[i];
    if (contentProtection.defaultKeyId) {
      streamInfo.keyIds.push(contentProtection.defaultKeyId);
    }
  }

  return streamInfo;
};


/**
 * Creates a StreamInfo from a SegmentBase.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.StreamInfo} A streamInfo on success; otherwise,
 *     return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentBase_ = function(
    mpd, period, representation, manifestCreationTime, networkCallback) {
  shaka.asserts.assert(representation.segmentBase);
  shaka.asserts.assert(representation.segmentBase.timescale > 0);

  if (!shaka.features.Containers) {
    shaka.log.error('Parsing of containers not supported in this build.');
    return null;
  }

  // Determine the container type.
  var containerType = representation.mimeType.split('/')[1];
  if ((containerType != 'mp4') && (containerType != 'webm')) {
    shaka.log.warning(
        'SegmentBase specifies an unsupported container type.',
        representation);
    return null;
  }

  var segmentBase = representation.segmentBase;

  if ((containerType == 'webm') && !segmentBase.initialization) {
    shaka.log.warning(
        'SegmentBase does not contain sufficient segment information:',
        'the SegmentBase uses a WebM container,',
        'but does not contain an Initialization element.',
        segmentBase);
    return null;
  }

  var hasSegmentIndexMetadata =
      segmentBase.indexRange ||
      (segmentBase.representationIndex &&
       segmentBase.representationIndex.range);
  if (!hasSegmentIndexMetadata) {
    shaka.log.warning(
        'SegmentBase does not contain sufficient segment information:',
        'the SegmentBase does not contain @indexRange',
        'or a RepresentationIndex element.',
        segmentBase);
    return null;
  }

  // If a RepresentationIndex does not exist then fallback to @indexRange.
  var representationIndex = segmentBase.representationIndex;
  if (!representationIndex) {
    representationIndex = new shaka.dash.mpd.RepresentationIndex();
    representationIndex.url = representation.baseUrl;
    representationIndex.range = segmentBase.indexRange ?
                                segmentBase.indexRange.clone() :
                                null;
  }

  var indexMetadata = this.createSegmentMetadata_(
      representationIndex, networkCallback);
  var initMetadata =
      segmentBase.initialization ?
      this.createSegmentMetadata_(segmentBase.initialization, networkCallback) :
      null;

  var segmentIndexSource =
      new shaka.dash.ContainerSegmentIndexSource(
          mpd,
          period,
          containerType,
          indexMetadata,
          initMetadata,
          manifestCreationTime,
          networkCallback);
  var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);

  var streamInfo = new shaka.media.StreamInfo();
  streamInfo.segmentIndexSource = segmentIndexSource;
  streamInfo.segmentInitSource = segmentInitSource;

  return streamInfo;
};


/**
 * Creates a StreamInfo from a SegmentList.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise,
 *     return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentList_ =
    function(mpd, period, representation, manifestCreationTime,
             networkCallback) {
  shaka.asserts.assert(representation.segmentList);

  if (!shaka.features.Live) {
    shaka.log.error('SegmentList requires Live, which is disabled.');
    return null;
  }

  var segmentList = representation.segmentList;

  if (!segmentList.segmentDuration && !segmentList.timeline &&
      (segmentList.segmentUrls.length > 1)) {
    shaka.log.warning(
        'SegmentList does not contain sufficient segment information:',
        'the SegmentList specifies multiple segments,',
        'but does not specify a segment duration or timeline.',
        segmentList);
    return null;
  }

  if (!segmentList.segmentDuration && !period.duration &&
      !segmentList.timeline && (segmentList.segmentUrls.length == 1)) {
    shaka.log.warning(
        'SegmentList does not contain sufficient segment information:',
        'the SegmentList specifies one segment,',
        'but does not specify a segment duration, period duration,',
        'or timeline.',
        segmentList);
    return null;
  }

  if (segmentList.timeline && segmentList.timeline.timePoints.length === 0) {
    shaka.log.warning(
        'SegmentList does not contain sufficient segment information:',
        'the SegmentList has an empty timeline.',
        segmentList);
    return null;
  }

  var initMetadata =
      segmentList.initialization ?
      this.createSegmentMetadata_(segmentList.initialization, networkCallback) :
      null;

  var segmentIndexSource =
      new shaka.dash.ListSegmentIndexSource(
          mpd, period, representation, manifestCreationTime, networkCallback);
  var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);

  var streamInfo = new shaka.media.StreamInfo();
  streamInfo.segmentIndexSource = segmentIndexSource;
  streamInfo.segmentInitSource = segmentInitSource;

  return streamInfo;
};


/**
 * Creates a StreamInfo from a SegmentTemplate
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.StreamInfo} A StreamInfo on success; otherwise,
 *     return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.createStreamInfoFromSegmentTemplate_ =
    function(mpd, period, representation, manifestCreationTime,
             networkCallback) {
  shaka.asserts.assert(representation.segmentTemplate);

  if (!shaka.features.Live) {
    shaka.log.error('SegmentTemplate requires Live, which is disabled.');
    return null;
  }

  var segmentTemplate = /** @type {!shaka.dash.mpd.SegmentTemplate} */ (
      representation.segmentTemplate);

  if (!this.validateSegmentTemplate_(segmentTemplate)) {
    // An error has already been logged.
    return null;
  }

  // Generate an Initialization.
  var initialization = null;
  if (segmentTemplate.initializationUrlTemplate) {
    initialization = this.generateInitialization_(representation);
    if (!initialization) {
      // An error has already been logged.
      return null;
    }
  }

  var initMetadata =
      initialization ? this.createSegmentMetadata_(
          initialization, networkCallback) : null;

  var segmentIndexSource = this.makeSegmentIndexSourceViaSegmentTemplate_(
      mpd, period, representation, manifestCreationTime, initMetadata,
      networkCallback);
  if (!segmentIndexSource) {
    // An error has already been logged.
    return null;
  }

  var segmentInitSource = new shaka.media.SegmentInitSource(initMetadata);

  var streamInfo = new shaka.media.StreamInfo();
  streamInfo.segmentIndexSource = segmentIndexSource;
  streamInfo.segmentInitSource = segmentInitSource;

  return streamInfo;
};


/**
 * Ensures that |segmentTemplate| has either an index URL template, a
 * SegmentTimeline, or a segment duration.
 *
 * @param {!shaka.dash.mpd.SegmentTemplate} segmentTemplate
 * @return {boolean}
 * @private
 */
shaka.dash.MpdProcessor.prototype.validateSegmentTemplate_ = function(
    segmentTemplate) {
  var n = 0;
  n += segmentTemplate.indexUrlTemplate ? 1 : 0;
  n += segmentTemplate.timeline ? 1 : 0;
  n += segmentTemplate.segmentDuration ? 1 : 0;

  if (n == 0) {
    shaka.log.warning(
        'SegmentTemplate does not contain any segment information:',
        'the SegmentTemplate must contain either an index URL template',
        'a SegmentTimeline, or a segment duration.',
        segmentTemplate);
    return false;
  } else if (n != 1) {
    shaka.log.warning(
        'SegmentTemplate containes multiple segment information sources:',
        'the SegmentTemplate should only contain an index URL template,',
        'a SegmentTimeline or a segment duration.',
        segmentTemplate);
    if (segmentTemplate.indexUrlTemplate) {
      shaka.log.info('Using the index URL template by default.');
      segmentTemplate.timeline = null;
      segmentTemplate.segmentDuration = null;
    } else if (segmentTemplate.timeline) {
      shaka.log.info('Using the SegmentTimeline by default.');
      segmentTemplate.segmentDuration = null;
    } else {
      shaka.asserts.unreachable();
    }
  }

  return true;
};


/**
 * Creates an ISegmentIndexSource from a SegmentTemplate.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri} initMetadata
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success;
 *     otherwise, return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaSegmentTemplate_ =
    function(mpd, period, representation, manifestCreationTime, initMetadata,
             networkCallback) {
  shaka.asserts.assert(representation.segmentTemplate);

  var segmentTemplate = representation.segmentTemplate;

  if (segmentTemplate.indexUrlTemplate) {
    return this.makeSegmentIndexSourceViaIndexUrlTemplate_(
        mpd, period, representation, manifestCreationTime, initMetadata,
        networkCallback);
  }

  if (!segmentTemplate.mediaUrlTemplate) {
    shaka.log.warning(
        'SegmentTemplate does not contain sufficient segment information:',
        'the SegmentTemplate\'s media URL template is missing.',
        representation);
    return null;
  }

  if (segmentTemplate.timeline) {
    return new shaka.dash.TimelineSegmentIndexSource(
        mpd, period, representation, manifestCreationTime, networkCallback);
  } else if (segmentTemplate.segmentDuration) {
    if ((mpd.type != 'dynamic') && (period.duration == null)) {
      shaka.log.warning(
          'SegmentTemplate does not contain sufficient segment information:',
          'the Period\'s duration is not known.',
          representation);
      return null;
    }
    return new shaka.dash.DurationSegmentIndexSource(
        mpd, period, representation, manifestCreationTime, networkCallback);
  }

  shaka.asserts.unreachable();
  return null;
};


/**
 * Creates an ISegmentIndexSource from a SegmentTemplate with an index URL
 * template.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri} initMetadata
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {shaka.media.ISegmentIndexSource} A SegmentIndexSource on success;
 *     otherwise, return null.
 * @private
 */
shaka.dash.MpdProcessor.prototype.makeSegmentIndexSourceViaIndexUrlTemplate_ =
    function(mpd, period, representation, manifestCreationTime, initMetadata,
             networkCallback) {
  shaka.asserts.assert(representation.segmentTemplate);
  shaka.asserts.assert(representation.segmentTemplate.indexUrlTemplate);

  if (!shaka.features.Containers) {
    shaka.log.error('Parsing of containers not supported in this build.');
    return null;
  }

  // Determine the container type.
  var containerType = representation.mimeType.split('/')[1];
  if ((containerType != 'mp4') && (containerType != 'webm')) {
    shaka.log.warning(
        'SegmentTemplate specifies an unsupported container type.',
        representation);
    return null;
  }

  var segmentTemplate = representation.segmentTemplate;

  if ((containerType == 'webm') && !initMetadata) {
    shaka.log.warning(
        'SegmentTemplate does not contain sufficient segment information:',
        'the SegmentTemplate uses a WebM container,',
        'but does not contain an initialization URL template.',
        segmentTemplate);
    return null;
  }

  // Generate the media URL.
  var mediaUrl = shaka.dash.MpdUtils.createFromTemplate(
      networkCallback, representation, 1, 0, 0, null);
  if (!mediaUrl) {
    // An error has already been logged.
    return null;
  }

  // Generate a RepresentationIndex.
  var representationIndex = this.generateRepresentationIndex_(representation);
  if (!representationIndex) {
    // An error has already been logged.
    return null;
  }

  var indexMetadata = this.createSegmentMetadata_(
      representationIndex, networkCallback);

  var segmentIndexSource =
      new shaka.dash.ContainerSegmentIndexSource(
          mpd,
          period,
          containerType,
          indexMetadata,
          initMetadata,
          manifestCreationTime,
          networkCallback);

  return segmentIndexSource;
};


/**
 * Generates a RepresentationIndex from a SegmentTemplate.
 *
 * @param {!shaka.dash.mpd.Representation} representation
 * @return {shaka.dash.mpd.RepresentationIndex} A RepresentationIndex on
 *     success; otherwise, return null if no index URL template exists or an
 *     error occurred.
 * @private
 */
shaka.dash.MpdProcessor.prototype.generateRepresentationIndex_ = function(
    representation) {
  shaka.asserts.assert(representation.segmentTemplate);
  var urlTemplate = representation.segmentTemplate.indexUrlTemplate;
  if (!urlTemplate) return null;
  return this.generateUrlTypeObject_(
      representation, urlTemplate, shaka.dash.mpd.RepresentationIndex);
};


/**
 * Generates an Initialization from a SegmentTemplate.
 *
 * @param {!shaka.dash.mpd.Representation} representation
 * @return {shaka.dash.mpd.Initialization} An Initialization on success;
 *     otherwise return null if no initialization URL template exists or an
 *     error occurred.
 * @private
 */
shaka.dash.MpdProcessor.prototype.generateInitialization_ = function(
    representation) {
  shaka.asserts.assert(representation.segmentTemplate);
  var urlTemplate = representation.segmentTemplate.initializationUrlTemplate;
  if (!urlTemplate) return null;
  return this.generateUrlTypeObject_(
      representation, urlTemplate, shaka.dash.mpd.Initialization);
};


/**
 * Generates either an Initialization or a RepresentationIndex.
 *
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {string} urlTemplate
 * @param {!function(new:T)} constructor
 * @return {T}
 * @template T
 * @private
 */
shaka.dash.MpdProcessor.prototype.generateUrlTypeObject_ = function(
    representation, urlTemplate, constructor) {
  shaka.asserts.assert(representation.segmentTemplate);
  var segmentTemplate = representation.segmentTemplate;

  // $Number$ and $Time$ cannot be present in an initialization URL template.
  var filledUrlTemplate = shaka.dash.MpdUtils.fillUrlTemplate(
      urlTemplate,
      representation.id,
      null,
      representation.bandwidth,
      null);

  if (!filledUrlTemplate) {
    // An error has already been logged.
    return null;
  }

  /**
   * @type {!shaka.dash.mpd.RepresentationIndex|
   *        !shaka.dash.mpd.Initialization}
   */
  var urlTypeObject = new constructor();
  urlTypeObject.url = shaka.util.FailoverUri.resolve(
      representation.baseUrl, filledUrlTemplate);
  return urlTypeObject;
};


/**
 * Creates a SegmentMetadata from either a RepresentationIndex or an
 * Initialization.
 *
 * @param {!shaka.dash.mpd.RepresentationIndex|
 *         !shaka.dash.mpd.Initialization} urlTypeObject
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @return {!shaka.util.FailoverUri}
 * @private
 */
shaka.dash.MpdProcessor.prototype.createSegmentMetadata_ = function(
    urlTypeObject, networkCallback) {
  var url = urlTypeObject.url;

  var startByte = 0;
  var endByte = null;
  if (urlTypeObject.range) {
    startByte = urlTypeObject.range.begin;
    endByte = urlTypeObject.range.end;
  }

  return new shaka.util.FailoverUri(networkCallback, url, startByte, endByte);
};