Source: media/manifest_updater.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.media.ManifestUpdater');

goog.require('shaka.media.ManifestInfo');
goog.require('shaka.media.PeriodInfo');
goog.require('shaka.media.StreamInfo');
goog.require('shaka.media.StreamInfoProcessor');
goog.require('shaka.util.TypedBind');



/**
 * Creates a ManifestUpdater.
 * This function takes ownership of |newManifestInfo|.
 *
 * @param {!shaka.media.ManifestInfo} newManifestInfo
 * @struct
 * @constructor
 */
shaka.media.ManifestUpdater = function(newManifestInfo) {
  /** @private {!shaka.media.ManifestInfo} */
  this.newManifestInfo_ = newManifestInfo;
};


/**
 * Destroys this ManifestUpdater.
 * @suppress {checkTypes} to set otherwise non-nullable types to null.
 */
shaka.media.ManifestUpdater.prototype.destroy = function() {
  this.newManifestInfo_.destroy();
  this.newManifestInfo_ = null;
};


/**
 * Updates |currentManifestInfo|.
 *
 * Although this function is asynchronous, all modifications to
 * |currentManifestInfo| occur synchronously immediately before the returned
 * promise resolves.
 *
 * @param {!shaka.media.ManifestInfo} currentManifestInfo
 * @return {!Promise.<!Array.<!shaka.media.StreamInfo>>} A promise to a list of
 *     StreamInfos that were removed from |currentManifestInfo|.
 */
shaka.media.ManifestUpdater.prototype.update = function(currentManifestInfo) {
  // Alias.
  var ManifestUpdater = shaka.media.ManifestUpdater;

  // Pre-create all SegmentIndexes from both manifests. This enables us to
  // update synchronously and in-place safely, i.e., by modifying
  // |currentManifestInfo| directly.
  var promise1 = ManifestUpdater.createSegmentIndexes_(currentManifestInfo);
  var promise2 = ManifestUpdater.createSegmentIndexes_(this.newManifestInfo_);

  return Promise.all([promise1, promise2]).then(shaka.util.TypedBind(this,
      /** @param {!Array} results */
      function(results) {
        /** @type {!Object.<number, !shaka.media.SegmentIndex>} */
        var currentSegmentIndexesByUid = results[0];

        /** @type {!Object.<number, !shaka.media.SegmentIndex>} */
        var newSegmentIndexesByUid = results[1];

        var processor = new shaka.media.StreamInfoProcessor();
        processor.process(this.newManifestInfo_.periodInfos);

        currentManifestInfo.updatePeriod = this.newManifestInfo_.updatePeriod;
        currentManifestInfo.updateUrl =
            this.newManifestInfo_.updateUrl ?
            this.newManifestInfo_.updateUrl.clone() :
            null;
        currentManifestInfo.minBufferTime = this.newManifestInfo_.minBufferTime;

        /** @type {!Array.<!shaka.media.StreamInfo>} */
        var removedStreamInfos = [];

        ManifestUpdater.mergePeriodInfos_(
            currentManifestInfo,
            this.newManifestInfo_,
            currentSegmentIndexesByUid,
            newSegmentIndexesByUid,
            removedStreamInfos);

        // Process |currentManifestInfo| to ensure that the StreamInfos
        // are still sorted by bandwidth.
        processor.process(currentManifestInfo.periodInfos);

        return Promise.resolve(removedStreamInfos);
      })
  );
};


/**
 * Creates every SegmentIndex contained in |manifestInfo|.
 *
 * @param {!shaka.media.ManifestInfo} manifestInfo
 * @return {!Promise.<!Object.<number, !shaka.media.SegmentIndex>>}
 * @private
 */
shaka.media.ManifestUpdater.createSegmentIndexes_ = function(
    manifestInfo) {
  var gather = function(all, part) { return all.concat(part); };
  var streamInfos = manifestInfo.periodInfos
      .map(function(periodInfo) { return periodInfo.streamSetInfos; })
      .reduce(gather, [])
      .map(function(streamSetInfo) { return streamSetInfo.streamInfos; })
      .reduce(gather, []);
  var async = streamInfos.map(
      function(streamInfo) {
        return streamInfo.segmentIndexSource.create();
      });
  return Promise.all(async).then(
      /** @param {!Array.<!shaka.media.SegmentIndex>} segmentIndexes */
      function(segmentIndexes) {
        shaka.asserts.assert(streamInfos.length == segmentIndexes.length);

        /** @type {!Object.<number, !shaka.media.SegmentIndex>} */
        var segmentIndexesByUid = {};

        for (var i = 0; i < streamInfos.length; ++i) {
          segmentIndexesByUid[streamInfos[i].uniqueId] = segmentIndexes[i];
        }

        return Promise.resolve(segmentIndexesByUid);
      }
  );
};


/**
 * Merges PeriodInfos from |newManifestInfo_| into |currentManifestInfo|.
 * Populates |removedStreamInfos| with any StreamInfos from
 * |currentManifestInfo| that have been removed.
 *
 * @param {!shaka.media.ManifestInfo} currentManifestInfo
 * @param {!shaka.media.ManifestInfo} newManifestInfo
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     currentSegmentIndexesByUid
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     newSegmentIndexesByUid
 * @param {!Array.<!shaka.media.StreamInfo>} removedStreamInfos
 * @private
 */
shaka.media.ManifestUpdater.mergePeriodInfos_ = function(
    currentManifestInfo,
    newManifestInfo,
    currentSegmentIndexesByUid,
    newSegmentIndexesByUid,
    removedStreamInfos) {
  /** @type {!shaka.util.MultiMap.<!shaka.media.PeriodInfo>} */
  var currentPeriodInfoMap = new shaka.util.MultiMap();
  currentManifestInfo.periodInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    currentPeriodInfoMap.push(id, info);
  });

  /** @type {!shaka.util.MultiMap.<!shaka.media.PeriodInfo>} */
  var newPeriodInfoMap = new shaka.util.MultiMap();
  newManifestInfo.periodInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    newPeriodInfoMap.push(id, info);
  });

  var keys = currentPeriodInfoMap.keys();
  for (var i = 0; i < keys.length; ++i) {
    var id = keys[i];

    var currentPeriodInfos = currentPeriodInfoMap.get(id);
    shaka.asserts.assert(currentPeriodInfos && currentPeriodInfos.length != 0);
    if (currentPeriodInfos.length > 1) {
      shaka.log.warning('Cannot update Period ' + id + ' because more ' +
                        'than one existing Period has the same ID.');
      continue;
    }

    var newPeriodInfos = newPeriodInfoMap.get(id);
    if (!newPeriodInfos || newPeriodInfos.length == 0) {
      continue;
    } else if (newPeriodInfos.length == 1) {
      shaka.media.ManifestUpdater.mergeStreamSetInfos_(
          currentPeriodInfos[0],
          newPeriodInfos[0],
          currentSegmentIndexesByUid,
          newSegmentIndexesByUid,
          removedStreamInfos);
      currentPeriodInfos[0].duration = newPeriodInfos[0].duration;
    } else {
      shaka.log.warning('Cannot update Period ' + id + ' because more ' +
                        'than one new Period has the same ID.');
    }
  }
};


/**
 * Merges StreamSetInfos from |newPeriodInfo| into |currentPeriodInfo|.
 *
 * @param {!shaka.media.PeriodInfo} currentPeriodInfo
 * @param {!shaka.media.PeriodInfo} newPeriodInfo
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     currentSegmentIndexesByUid
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     newSegmentIndexesByUid
 * @param {!Array.<!shaka.media.StreamInfo>} removedStreamInfos
 * @private
 */
shaka.media.ManifestUpdater.mergeStreamSetInfos_ = function(
    currentPeriodInfo,
    newPeriodInfo,
    currentSegmentIndexesByUid,
    newSegmentIndexesByUid,
    removedStreamInfos) {
  /** @type {!shaka.util.MultiMap.<!shaka.media.StreamSetInfo>} */
  var currentStreamSetInfoMap = new shaka.util.MultiMap();
  currentPeriodInfo.streamSetInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    currentStreamSetInfoMap.push(id, info);
  });

  /** @type {!shaka.util.MultiMap.<!shaka.media.StreamSetInfo>} */
  var newStreamSetInfoMap = new shaka.util.MultiMap();
  newPeriodInfo.streamSetInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    newStreamSetInfoMap.push(id, info);
  });

  var keys = currentStreamSetInfoMap.keys();
  for (var i = 0; i < keys.length; ++i) {
    var id = keys[i];

    var currentStreamSetInfos = currentStreamSetInfoMap.get(id);
    shaka.asserts.assert(currentStreamSetInfos &&
                         currentStreamSetInfos.length != 0);
    if (currentStreamSetInfos.length > 1) {
      shaka.log.warning('Cannot update StreamSet ' + id + ' because more ' +
                        'than one existing StreamSet has the same ID.');
      continue;
    }

    var newStreamSetInfos = newStreamSetInfoMap.get(id);
    if (!newStreamSetInfos || newStreamSetInfos.length == 0) {
      continue;
    } else if (newStreamSetInfos.length == 1) {
      shaka.media.ManifestUpdater.mergeStreamInfos_(
          currentStreamSetInfos[0],
          newStreamSetInfos[0],
          currentSegmentIndexesByUid,
          newSegmentIndexesByUid,
          removedStreamInfos);
    } else {
      shaka.log.warning('Cannot update StreamSet ' + id + ' because more ' +
                        'than one new StreamSet has the same ID.');
    }
  }
};


/**
 * Merges StreamInfos from |newStreamSetInfo| into |currentStreamSetInfo|.
 *
 * @param {!shaka.media.StreamSetInfo} currentStreamSetInfo
 * @param {!shaka.media.StreamSetInfo} newStreamSetInfo
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     currentSegmentIndexesByUid
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     newSegmentIndexesByUid
 * @param {!Array.<!shaka.media.StreamInfo>} removedStreamInfos
 * @private
 */
shaka.media.ManifestUpdater.mergeStreamInfos_ = function(
    currentStreamSetInfo,
    newStreamSetInfo,
    currentSegmentIndexesByUid,
    newSegmentIndexesByUid,
    removedStreamInfos) {
  /** @type {!shaka.util.MultiMap.<!shaka.media.StreamInfo>} */
  var currentStreamInfoMap = new shaka.util.MultiMap();
  currentStreamSetInfo.streamInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    currentStreamInfoMap.push(id, info);
  });

  /** @type {!shaka.util.MultiMap.<!shaka.media.StreamInfo>} */
  var newStreamInfoMap = new shaka.util.MultiMap();
  newStreamSetInfo.streamInfos.forEach(function(info, index) {
    var id = info.id || ('' + index);
    newStreamInfoMap.push(id, info);
  });

  /** @type {!Object.<string, string>} */
  var visitedStreamInfoSet = {};

  var keys = currentStreamInfoMap.keys();
  for (var i = 0; i < keys.length; ++i) {
    var id = keys[i];

    visitedStreamInfoSet[id] = id;

    var currentStreamInfos = currentStreamInfoMap.get(id);
    shaka.asserts.assert(currentStreamInfos && currentStreamInfos.length != 0);
    if (currentStreamInfos.length > 1) {
      shaka.log.warning('Cannot update Stream ' + id + ' because more ' +
                        'than one existing Stream has the same ID.');
      continue;
    }

    var newStreamInfos = newStreamInfoMap.get(id);
    if (!newStreamInfos || newStreamInfos.length == 0) {
      removedStreamInfos.push(currentStreamInfos[0]);
      currentStreamSetInfo.streamInfos.splice(
          currentStreamSetInfo.streamInfos.indexOf(currentStreamInfos[0]), 1);
    } else if (newStreamInfos.length == 1) {
      shaka.media.ManifestUpdater.integrateSegmentIndexes_(
          currentStreamInfos[0],
          newStreamInfos[0],
          currentSegmentIndexesByUid,
          newSegmentIndexesByUid);

      // Transfer ownership of the SegmentInitSource.
      currentStreamInfos[0].segmentInitSource =
          newStreamInfos[0].segmentInitSource;
      newStreamInfos[0].segmentInitSource = null;

      currentStreamInfos[0].timestampOffset = newStreamInfos[0].timestampOffset;
    } else {
      shaka.log.warning('Cannot update Stream ' + id + ' because more ' +
                        'than one new Stream has the same ID.');
    }
  }

  keys = newStreamInfoMap.keys();
  for (var i = 0; i < keys.length; ++i) {
    var id = keys[i];

    if (visitedStreamInfoSet[id]) continue;
    visitedStreamInfoSet[id] = id;

    var newStreamInfos = newStreamInfoMap.get(id);
    shaka.asserts.assert(newStreamInfos && newStreamInfos.length != 0);
    if (newStreamInfos.length > 1) {
      shaka.log.warning('Cannot add Stream ' + id + ' because more ' +
                        'than one new Stream has the same ID.');
    }

    currentStreamSetInfo.streamInfos.push(newStreamInfos[0]);
    shaka.log.info('Added Stream ' + id + '.');
  }
};


/**
 * Integrates |newStreamInfo|'s SegmentIndex into |currentStreamSet|'s
 * SegmentIndex.
 *
 * @param {!shaka.media.StreamInfo} currentStreamInfo
 * @param {!shaka.media.StreamInfo} newStreamInfo
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     currentSegmentIndexesByUid
 * @param {!Object.<number, !shaka.media.SegmentIndex>}
 *     newSegmentIndexesByUid
 * @private
 */
shaka.media.ManifestUpdater.integrateSegmentIndexes_ = function(
    currentStreamInfo,
    newStreamInfo,
    currentSegmentIndexesByUid,
    newSegmentIndexesByUid) {
  var currentSegmentIndex =
      currentSegmentIndexesByUid[currentStreamInfo.uniqueId];
  shaka.asserts.assert(currentSegmentIndex);

  var newSegmentIndex =
      newSegmentIndexesByUid[newStreamInfo.uniqueId];
  shaka.asserts.assert(newSegmentIndex);

  var originalLength = currentSegmentIndex.length();
  if (currentSegmentIndex.integrate(newSegmentIndex)) {
    shaka.log.info('Updated SegmentIndex', currentStreamInfo.uniqueId + ':',
                   originalLength, '->', currentSegmentIndex.length(),
                   'SegmentReference(s).');
  } else {
    shaka.log.warning('Failed to update SegmentIndex',
                      newStreamInfo.uniqueId);
  }
};