Source: lib/offline/manifest_converter.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.offline.ManifestConverter');

goog.require('goog.asserts');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.ManifestParser');
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.MimeUtils');


/**
 * 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 |shaka.extern.ManifestDB| object to a |shaka.extern.Manifest|
   * object.
   *
   * @param {shaka.extern.ManifestDB} manifestDB
   * @return {shaka.extern.Manifest}
   */
  fromManifestDB(manifestDB) {
    const timeline = new shaka.media.PresentationTimeline(null, 0);
    timeline.setDuration(manifestDB.duration);

    /** @type {!Array.<shaka.extern.StreamDB>} */
    const audioStreams =
        manifestDB.streams.filter((streamDB) => this.isAudio_(streamDB));

    /** @type {!Array.<shaka.extern.StreamDB>} */
    const videoStreams =
        manifestDB.streams.filter((streamDB) => this.isVideo_(streamDB));

    /** @type {!Map.<number, shaka.extern.Variant>} */
    const variants = this.createVariants(audioStreams, videoStreams, timeline);

    /** @type {!Array.<shaka.extern.Stream>} */
    const textStreams =
        manifestDB.streams.filter((streamDB) => this.isText_(streamDB))
            .map((streamDB) => this.fromStreamDB_(streamDB, timeline));

    /** @type {!Array.<shaka.extern.Stream>} */
    const imageStreams =
        manifestDB.streams.filter((streamDB) => this.isImage_(streamDB))
            .map((streamDB) => this.fromStreamDB_(streamDB, timeline));

    const drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : [];
    if (manifestDB.drmInfo) {
      for (const variant of variants.values()) {
        if (variant.audio && variant.audio.encrypted) {
          variant.audio.drmInfos = drmInfos;
        }
        if (variant.video && variant.video.encrypted) {
          variant.video.drmInfos = drmInfos;
        }
      }
    }

    return {
      presentationTimeline: timeline,
      minBufferTime: 2,
      offlineSessionIds: manifestDB.sessionIds,
      variants: Array.from(variants.values()),
      textStreams: textStreams,
      imageStreams: imageStreams,
      sequenceMode: manifestDB.sequenceMode || false,
      ignoreManifestTimestampsInSegmentsMode: false,
      type: manifestDB.type || shaka.media.ManifestParser.UNKNOWN,
      serviceDescription: null,
      nextUrl: null,
      periodCount: 1,
      gapCount: 0,
      isLowLatency: false,
      startTime: null,
    };
  }

  /**
   * Recreates Variants from audio and video StreamDB collections.
   *
   * @param {!Array.<!shaka.extern.StreamDB>} audios
   * @param {!Array.<!shaka.extern.StreamDB>} videos
   * @param {shaka.media.PresentationTimeline} timeline
   * @return {!Map.<number, !shaka.extern.Variant>}
   */
  createVariants(audios, videos, timeline) {
    // Get all the variant ids from all audio and video streams.
    /** @type {!Set.<number>} */
    const variantIds = new Set();
    for (const streamDB of audios) {
      for (const id of streamDB.variantIds) {
        variantIds.add(id);
      }
    }
    for (const streamDB of videos) {
      for (const id of streamDB.variantIds) {
        variantIds.add(id);
      }
    }

    /** @type {!Map.<number, shaka.extern.Variant>} */
    const variantMap = new Map();
    for (const id of variantIds) {
      variantMap.set(id, this.createEmptyVariant_(id));
    }

    // Assign each audio stream to its variants.
    for (const audio of audios) {
      /** @type {shaka.extern.Stream} */
      const stream = this.fromStreamDB_(audio, timeline);

      for (const variantId of audio.variantIds) {
        const variant = variantMap.get(variantId);

        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.
    for (const video of videos) {
      /** @type {shaka.extern.Stream} */
      const stream = this.fromStreamDB_(video, timeline);

      for (const variantId of video.variantIds) {
        const variant = variantMap.get(variantId);

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

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

    return variantMap;
  }

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

    timeline.notifySegments(segments);

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

    /** @type {shaka.extern.Stream} */
    const stream = {
      id: streamDB.id,
      originalId: streamDB.originalId,
      groupId: streamDB.groupId,
      createSegmentIndex: () => Promise.resolve(),
      segmentIndex,
      mimeType: streamDB.mimeType,
      codecs: streamDB.codecs,
      width: streamDB.width || undefined,
      height: streamDB.height || undefined,
      frameRate: streamDB.frameRate,
      pixelAspectRatio: streamDB.pixelAspectRatio,
      hdr: streamDB.hdr,
      colorGamut: streamDB.colorGamut,
      videoLayout: streamDB.videoLayout,
      kind: streamDB.kind,
      encrypted: streamDB.encrypted,
      drmInfos: [],
      keyIds: streamDB.keyIds,
      language: streamDB.language,
      originalLanguage: streamDB.originalLanguage || null,
      label: streamDB.label,
      type: streamDB.type,
      primary: streamDB.primary,
      trickModeVideo: null,
      emsgSchemeIdUris: null,
      roles: streamDB.roles,
      forced: streamDB.forced,
      channelsCount: streamDB.channelsCount,
      audioSamplingRate: streamDB.audioSamplingRate,
      spatialAudio: streamDB.spatialAudio,
      closedCaptions: streamDB.closedCaptions,
      tilesLayout: streamDB.tilesLayout,
      accessibilityPurpose: null,
      external: streamDB.external,
      fastSwitching: streamDB.fastSwitching,
      fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
          streamDB.mimeType, streamDB.codecs)]),
    };

    return stream;
  }

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

    const initSegmentReference = segmentDB.initSegmentKey != null ?
        this.fromInitSegmentDB_(segmentDB.initSegmentKey) : null;

    const ref = new shaka.media.SegmentReference(
        segmentDB.startTime,
        segmentDB.endTime,
        () => [uri.toString()],
        /* startByte= */ 0,
        /* endByte= */ null,
        initSegmentReference,
        segmentDB.timestampOffset,
        segmentDB.appendWindowStart,
        segmentDB.appendWindowEnd,
        /* partialReferences= */ [],
        segmentDB.tilesLayout || '');
    ref.mimeType = segmentDB.mimeType || streamDB.mimeType || '';
    ref.codecs = segmentDB.codecs || streamDB.codecs || '';
    return ref;
  }

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

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

  /**
   * @param {shaka.extern.StreamDB} streamDB
   * @return {boolean}
   * @private
   */
  isAudio_(streamDB) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return streamDB.type == ContentType.AUDIO;
  }

  /**
   * @param {shaka.extern.StreamDB} streamDB
   * @return {boolean}
   * @private
   */
  isVideo_(streamDB) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return streamDB.type == ContentType.VIDEO;
  }

  /**
   * @param {shaka.extern.StreamDB} streamDB
   * @return {boolean}
   * @private
   */
  isText_(streamDB) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return streamDB.type == ContentType.TEXT;
  }

  /**
   * @param {shaka.extern.StreamDB} streamDB
   * @return {boolean}
   * @private
   */
  isImage_(streamDB) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    return streamDB.type == ContentType.IMAGE;
  }

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