Source: dataProvider/model/MediaComposition.js

import Drm from '../../utils/Drm.js';
import * as SRGQuality from '../../utils/SRGQuality.js';
import SRGStreamType from '../../utils/SRGStreamType.js';
import Image from '../../utils/Image.js';
import BlockingReason from '../../utils/BlockingReason.js';
import Utils from '../../utils/Utils.js';

/**
 * @class MediaComposition
 */
class MediaComposition {
  static cleanCache(mc) {
    const cleanedMc = Object.assign({}, mc);
    cleanedMc.cachedMainChapter = undefined;
    cleanedMc.cachedSegments = undefined;
    return cleanedMc;
  }

  /**
   * Find a chapter by his URN
   * @param {String} urn
   * @returns {Object} chapter
   */
  findChapterByUrn(urn) {
    if (this.chapterList) {
      const [chapter] = this.chapterList.filter(element => element.urn === urn);

      return chapter;
    }

    return undefined;
  }

  /**
   * Find a drm by resource
   * @param {Resource} resource
   * @returns {Array} of drm
   */
  static findDrmListByResource(resource) {
    return resource.drmList;
  }

  /**
   * Filter the main resource by quality
   * @param {String} quality is set to HD by default
   * @return {Array} filtered resources by quality
   */
  filterMainResourcesByQuality(quality = SRGQuality.types.HD) {
    return this.getMainResources()
      .filter(
        resource => resource.quality === quality,
      );
  }

  /**
   * Find resource list by URN
   * @param {String} urn
   * @returns {Array|undefined} of resources
   */
  findResourceListByUrn(urn) {
    const chapterByUrn = this.findChapterByUrn(urn);

    if (chapterByUrn) {
      return chapterByUrn.resourceList || [];
    }

    return undefined;
  }

  /**
   * Get the main chapter's image URL decorated with default width and format.
   *
   * @returns {String|undefined} image URL.
   */
  getMainChapterImageUrl() {
    const mainChapter = this.getMainChapter();

    if (mainChapter && mainChapter.imageUrl) {
      return Image.scale({
        url: mainChapter.imageUrl,
      });
    }

    return undefined;
  }

  /**
   * Chapters as provided by the server
   *
   * @returns {Array} of chapters
   */
  getChapters() {
    return this.chapterList;
  }

  /**
   * Block reason for main chapter. This also uses current date for STARTDATE.
   * @returns {undefined|String} undefined if main chapter is not blocked
   * @see BlockingReason
   */
  getMainBlockReason() {
    const mainChapter = this.getMainChapter();

    if (!mainChapter) {
      return undefined;
    }

    let { blockReason } = mainChapter;
    if (!blockReason && new Date() < this.getMainValidFromDate()) {
      blockReason = BlockingReason.STARTDATE;
    }
    return blockReason;
  }

  /**
   * Compute a date from which this content is valid. Always return a date object.
   * @returns {Date} date specified in media composition or EPOCH when no date present.
   */
  getMainValidFromDate() {
    const mainChapter = this.getMainChapter();

    if (mainChapter) {
      const { validFrom } = mainChapter;

      if (validFrom) {
        return new Date(validFrom);
      }
    }

    return new Date(0);
  }

  /**
   * Get the mediaComposition's main chapter
   */
  getMainChapter() {
    if (!this.cachedMainChapter) {
      this.cachedMainChapter = this.findChapterByUrn(this.chapterUrn);
    }

    if (!this.cachedMainChapter && this.chapterList && this.chapterList.length > 0) {
      // Fallback to fix missing chapterUrn in mediaComposition (should not happen)
      [this.cachedMainChapter] = this.chapterList;
    }

    return this.cachedMainChapter;
  }

  /**
   * Get videojs formatted main resources
   * @param {String} preferredQuality set the preferred quality SD, HD, HQ
   * @see SRGQuality all available qualities
   * @param {String} preferredStreamType set the preferred stream type ON_DEMAND, LIVE, DVR
   * @see SRGStreamType all available stream types
   * @returns {Array} array of sources
   */
  getMainResources(preferredQuality = undefined, preferredStreamType = undefined) {
    const resourceList = this.getResourceList();
    if (!resourceList || !resourceList.length) {
      return undefined;
    }
    const resources = resourceList && resourceList.map((resource) => {
      const { keySystems } = Drm.buildKeySystems(resource.drmList);
      const [...keySystemOptions] = Drm.buildKeySystemOptions(resource.drmList);
      const {
        analyticsData,
        analyticsMetadata,
        dvr,
        live,
        mimeType: type,
        presentation,
        quality,
        streaming,
        streamOffset,
        tokenType,
        url: src,
      } = resource;

      const mergedAnalyticsData = this.getMergedAnalyticsData(analyticsData);
      const mergedAnalyticsMetadata = this.getMergedAnalyticsMetadata(analyticsMetadata);
      const mainSegment = this.findMainSegment();
      const {
        id, eventData, imageCopyright, mediaType, vendor: bu,
      } = this.getMainChapter();
      let pendingSeek;
      if (this.pendingSeek) {
        // eslint-disable-next-line prefer-destructuring
        pendingSeek = this.pendingSeek;
      } else if (mainSegment) {
        pendingSeek = Utils.millisecondsToSeconds(mainSegment.markIn);
      } else {
        pendingSeek = undefined;
      }

      return {
        analyticsData: mergedAnalyticsData,
        analyticsMetadata: mergedAnalyticsMetadata,
        bu,
        eventData,
        id,
        imageCopyright,
        keySystems,
        keySystemOptions,
        mediaType,
        presentation,
        quality,
        src,
        streaming,
        streamOffset,
        streamType: SRGStreamType.evaluate(dvr, live),
        tokenType,
        type,
        pendingSegment: mainSegment,
        pendingSeek,
        playbackSettings: this.playbackSettings,
      };
    });

    let orderedResources = MediaComposition.orderMainResourcesByQuality(
      resources,
      preferredQuality,
    );
    orderedResources = MediaComposition.orderMainResourcesByStreamType(
      orderedResources,
      preferredStreamType,
    );

    return orderedResources;
  }

  /**
   * Get merged analytics data
   * @returns {Object}
   */
  getMergedAnalyticsData(analyticsData) {
    return {
      ...this.analyticsData,
      ...this.getMainChapter().analyticsData,
      ...analyticsData,
    };
  }

  /**
   * Get merged analytics metadata
   * @returns {Object}
   */
  getMergedAnalyticsMetadata(analyticsMetadata) {
    return {
      ...this.analyticsMetadata,
      ...this.getMainChapter().analyticsMetadata,
      ...analyticsMetadata,
    };
  }

  /**
   * Filter external text tracks that are already available internally.
   *
   * __Rules:__
   * 1. TTML format is filtered
   *
   * 2. If both are empty that means only internal text tracks will be displayed
   * to the user as they are automatically loaded by the player.
   *
   * 3. If subtitleInformationList is missing from the MediaComposition and subtitleList
   * is available but the media contains internal text tracks that are also available internaly.
   * It will result on a duplication client side.
   *
   * 4. If subtitleList and subtitleInformationList a merge between both will be operated,
   * removing the external text tracks already available internaly.
   *
   * @param {Object} logger
   *
   * @returns {Array} external text tracks
   */
  getFilteredExternalSubtitles(logger = console.log /* eslint-disable-line no-console */) {
    const { subtitleList } = this.getMainChapter();
    const [resourceWithSubtitles] = this.getResourceList()
      .filter(resource => resource.subtitleInformationList);
    const { subtitleInformationList } = { ...resourceWithSubtitles };

    if ((!subtitleList && !subtitleInformationList) || (!subtitleList && subtitleInformationList)) {
      return [];
    }

    if (subtitleList && !subtitleInformationList) {
      logger('MediaComposition: No subtitleInformationList found');
      // TTML format is not used anymore
      return subtitleList.filter(subtitle => subtitle.format !== 'TTML');
    }

    const subtitles = subtitleList.filter((subtitle) => {
      const addSubtitle = !subtitleInformationList
        .find(subtitleInformation => (
          subtitleInformation.locale === subtitle.locale
          && subtitle.type === subtitleInformation.type));

      return subtitle.format !== 'TTML' && addSubtitle;
    });

    return subtitles;
  }

  /**
   * Get the chapter's resource list
   * @returns {Array} of resources
   */
  getResourceList() {
    const mainChapter = this.getMainChapter();
    return (mainChapter && mainChapter.resourceList) || [];
  }

  /**
   * Get segments of the main chapter ordered by markIn
   * @returns {Array} of segments
   */
  getMainSegments() {
    const mainChapter = this.getMainChapter();
    if (!this.cachedSegments && mainChapter && mainChapter.segmentList) {
      this.cachedSegments = mainChapter.segmentList;
    }

    return this.cachedSegments || [];
  }

  /**
   * Return segment from main chapter following segmentUrn in mediaComposition.
   * @returns {undefined|*}
   */
  findMainSegment() {
    if (this.segmentUrn) {
      const { segmentList } = this.getMainChapter();

      if (segmentList) {
        const [segment] = segmentList.filter(element => element.urn === this.segmentUrn);
        return segment;
      }
    }
    return undefined;
  }

  /**
   * Get subdivisions to be displayed for this MediaComposition.
   * They can be segments or chapters.
   *
   * @returns {Array} of subdivisions
   */
  getSubdivisions() {
    const chapters = this.getChapters();
    const mainChapter = this.getMainChapter();
    const subdivisions = [];
    const displayableMainSegments = this.getMainSegments().filter(s => s.displayable);

    if (chapters.length === 1 && displayableMainSegments.length === 0) {
      return [];
    }

    chapters.forEach((chapter) => {
      const isSegment = chapter.urn === mainChapter.urn && displayableMainSegments.length >= 1;

      if (isSegment) {
        subdivisions.push(...displayableMainSegments);
      } else {
        subdivisions.push(chapter);
      }
    });

    return subdivisions;
  }

  /**
   * Order the resources by a preferred quality
   * FYI: The [...resourcesToOrder] avoid to mutate the parameter with the sort method
   * @param {Array} resourcesToOrder array to order
   * @param {String} quality, the default preferred quality is HD
   * @see SRGQuality all available qualities
   * @returns {Array} resources ordered by a preferred quality
   */
  static orderMainResourcesByQuality(
    resourcesToOrder,
    quality = SRGQuality.types.HD,
  ) {
    const preferredQuality = SRGQuality.list.filter(q => quality !== q);
    preferredQuality.push(quality);

    return [...resourcesToOrder].sort((a, b) => {
      const aq = preferredQuality.indexOf(a.quality);
      const bq = preferredQuality.indexOf(b.quality);

      return bq - aq;
    });
  }

  /**
   * Order the stream types by a preferred type
   * FYI: The [...resourcesToOrder] avoid to mutate the parameter with the sort method
   * @param {Array} resourcesToOrder array to order
   * @param {String} streamType, the default preferred stream type is DVR
   * @see SRGStreamType all available stream types
   * @returns {Array} resources ordered by a preferred stream type
   */
  static orderMainResourcesByStreamType(
    resourcesToOrder,
    streamType = SRGStreamType.types.DVR,
  ) {
    const preferredType = SRGStreamType.list.filter(s => streamType !== s);
    preferredType.push(streamType);

    return [...resourcesToOrder].sort((a, b) => {
      const as = preferredType.indexOf(a.streamType);
      const bs = preferredType.indexOf(b.streamType);

      return bs - as;
    });
  }
}

export default MediaComposition;