Source: lib/offline/manifest_converter.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.offline.ManifestConverter');

goog.require('goog.asserts');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.offline.OfflineUri');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MapUtils');


/**
 * Utility class for converting database manifest objects back to normal
 * player-ready objects. Used by the offline system to convert on-disk
 * objects back to the in-memory objects.
 */
shaka.offline.ManifestConverter = class {

  /**
   * Create a new manifest converter. Need to know the mechanism and cell that
   * the manifest is from so that all segments paths can be created.
   *
   * @param {string} mechanism
   * @param {string} cell
   */
  constructor(mechanism, cell) {
    /** @private {string} */
    this.mechanism_ = mechanism;
    /** @private {string} */
    this.cell_ = cell;
  }

  /**
   * Convert a |shakaExtern.ManifestDB| object to a |shakaExtern.Manifest|
   * object.
   *
   * @param {shakaExtern.ManifestDB} manifestDB
   * @return {shakaExtern.Manifest}
   */
  fromManifestDB(manifestDB) {
    let timeline = new shaka.media.PresentationTimeline(null, 0);
    timeline.setDuration(manifestDB.duration);

    let periods = manifestDB.periods.map((period) =>
        this.fromPeriodDB(period, timeline));

    let drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : [];
    if (manifestDB.drmInfo) {
      periods.forEach((period) => {
        period.variants.forEach((variant) => { variant.drmInfos = drmInfos; });
      });
    }

    return {
      presentationTimeline: timeline,
      minBufferTime: 2,
      offlineSessionIds: manifestDB.sessionIds,
      periods: periods
    };
  }

  /**
   * Create a period object from a database period.
   *
   * @param {shakaExtern.PeriodDB} period
   * @param {shaka.media.PresentationTimeline} timeline
   * @return {shakaExtern.Period}
   */
  fromPeriodDB(period, timeline) {
    /** @type {!Array.<shakaExtern.StreamDB>} */
    let audioStreams = period.streams.filter((stream) => this.isAudio_(stream));
    /** @type {!Array.<shakaExtern.StreamDB>} */
    let videoStreams = period.streams.filter((stream) => this.isVideo_(stream));

    /** @type {!Array.<shakaExtern.Variant>} */
    let variants = this.createVariants(audioStreams, videoStreams);

    /** @type {!Array.<shakaExtern.Stream>} */
    let textStreams = period.streams
        .filter((stream) => this.isText_(stream))
        .map((stream) => this.fromStreamDB_(stream));

    period.streams.forEach((stream, i) => {
      /** @type {!Array.<!shaka.media.SegmentReference>} */
      let refs = stream.segments.map((segment, index) => {
        return this.fromSegmentDB_(index, segment);
      });

      timeline.notifySegments(refs, i == 0);
    });

    return {
      startTime: period.startTime,
      variants: variants,
      textStreams: textStreams
    };
  }

  /**
   * Recreates Variants from audio and video StreamDB collections.
   *
   * @param {!Array.<!shakaExtern.StreamDB>} audios
   * @param {!Array.<!shakaExtern.StreamDB>} videos
   * @return {!Array.<!shakaExtern.Variant>}
   */
  createVariants(audios, videos) {
    const MapUtils = shaka.util.MapUtils;

    // Create a variant for each variant id.
    /** @type {!Object.<number, shakaExtern.Variant>} */
    let variantMap = {};

    let allStreams = [];
    allStreams.push.apply(allStreams, audios);
    allStreams.push.apply(allStreams, videos);

    // Create a variant for each variant id across all the streams.
    allStreams.forEach((stream) => {
      stream.variantIds.forEach((id) => {
        variantMap[id] = variantMap[id] || this.createEmptyVariant_(id);
      });
    });

    // Assign each audio stream to its variants.
    audios.forEach((audio) => {
      /** @type {shakaExtern.Stream} */
      let stream = this.fromStreamDB_(audio);

      audio.variantIds.forEach((id) => {
        let variant = variantMap[id];

        goog.asserts.assert(
            !variant.audio, 'A variant should only have one audio stream');

        variant.language = stream.language;
        variant.primary = variant.primary || stream.primary;
        variant.audio = stream;
      });
    });

    // Assign each video stream to its variants.
    videos.forEach((video) => {
      /** @type {shakaExtern.Stream} */
      let stream = this.fromStreamDB_(video);

      video.variantIds.forEach((id) => {
        let variant = variantMap[id];

        goog.asserts.assert(
            !variant.video, 'A variant should only have one video stream');

        variant.primary = variant.primary || stream.primary;
        variant.video = stream;
      });
    });

    return MapUtils.values(variantMap);
  }

  /**
   * @param {shakaExtern.StreamDB} streamDB
   * @return {shakaExtern.Stream}
   * @private
   */
  fromStreamDB_(streamDB) {
    /** @type {!Array.<!shaka.media.SegmentReference>} */
    let segments = streamDB.segments.map((segment, index) =>
        this.fromSegmentDB_(index, segment));

    /** @type {!shaka.media.SegmentIndex} */
    let segmentIndex = new shaka.media.SegmentIndex(segments);

    /** @type {shakaExtern.Stream} */
    let stream = {
      id: streamDB.id,
      createSegmentIndex: () => Promise.resolve(),
      findSegmentPosition: (index) => segmentIndex.find(index),
      getSegmentReference: (index) => segmentIndex.get(index),
      initSegmentReference: null,
      presentationTimeOffset: streamDB.presentationTimeOffset,
      mimeType: streamDB.mimeType,
      codecs: streamDB.codecs,
      width: streamDB.width || undefined,
      height: streamDB.height || undefined,
      frameRate: streamDB.frameRate || undefined,
      kind: streamDB.kind,
      encrypted: streamDB.encrypted,
      keyId: streamDB.keyId,
      language: streamDB.language,
      label: streamDB.label || null,
      type: streamDB.contentType,
      primary: streamDB.primary,
      trickModeVideo: null,
      // TODO(modmaker): Store offline?
      containsEmsgBoxes: false,
      roles: [],
      channelsCount: null
    };

    if (streamDB.initSegmentKey != null) {
      stream.initSegmentReference =
          this.fromInitSegmentDB_(streamDB.initSegmentKey);
    }

    return stream;
  }

  /**
   * @param {number} index
   * @param {shakaExtern.SegmentDB} segmentDB
   * @return {!shaka.media.SegmentReference}
   * @private
   */
  fromSegmentDB_(index, segmentDB) {
    /** @type {!shaka.offline.OfflineUri} */
    let uri = shaka.offline.OfflineUri.segment(
        this.mechanism_, this.cell_, segmentDB.dataKey);

    return new shaka.media.SegmentReference(
        index,
        segmentDB.startTime,
        segmentDB.endTime,
        () => [uri.toString()],
        0 /* startByte */,
        null /*  endByte */);
  }

  /**
   * @param {number} key
   * @return {!shaka.media.InitSegmentReference}
   * @private
   */
  fromInitSegmentDB_(key) {
    /** @type {!shaka.offline.OfflineUri} */
    let uri = shaka.offline.OfflineUri.segment(
        this.mechanism_, this.cell_, key);

    return new shaka.media.InitSegmentReference(
        () => [uri.toString()],
        0 /* startBytes*/,
        null /* endBytes */);
  }

  /**
   * @param {shakaExtern.StreamDB} stream
   * @return {boolean}
   * @private
   */
  isAudio_(stream) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return stream.contentType == ContentType.AUDIO;
  }

  /**
   * @param {shakaExtern.StreamDB} stream
   * @return {boolean}
   * @private
   */
  isVideo_(stream) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return stream.contentType == ContentType.VIDEO;
  }

  /**
   * @param {shakaExtern.StreamDB} stream
   * @return {boolean}
   * @private
   */
  isText_(stream) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return stream.contentType == ContentType.TEXT;
  }

  /**
   * Creates an empty Variant.
   *
   * @param {number} id
   * @return {!shakaExtern.Variant}
   * @private
   */
  createEmptyVariant_(id) {
    return {
      id: id,
      language: '',
      primary: false,
      audio: null,
      video: null,
      bandwidth: 0,
      drmInfos: [],
      allowedByApplication: true,
      allowedByKeySystem: true
    };
  }
};