Source: lib/media/adaptation_set_criteria.js

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

goog.provide('shaka.media.AdaptationSetCriteria');
goog.provide('shaka.media.ExampleBasedCriteria');
goog.provide('shaka.media.PreferenceBasedCriteria');

goog.require('shaka.config.CodecSwitchingStrategy');
goog.require('shaka.log');
goog.require('shaka.media.AdaptationSet');
goog.require('shaka.media.Capabilities');
goog.require('shaka.util.LanguageUtils');


/**
 * An adaptation set criteria is a unit of logic that can take a set of
 * variants and return a subset of variants that should (and can) be
 * adapted between.
 *
 * @interface
 */
shaka.media.AdaptationSetCriteria = class {
  /**
   * Take a set of variants, and return a subset of variants that can be
   * adapted between.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @return {!shaka.media.AdaptationSet}
   */
  create(variants) {}
};


/**
 * @implements {shaka.media.AdaptationSetCriteria}
 * @final
 */
shaka.media.ExampleBasedCriteria = class {
  /**
   * @param {shaka.extern.Variant} example
   * @param {shaka.config.CodecSwitchingStrategy} codecSwitchingStrategy
   * @param {boolean} enableAudioGroups
   */
  constructor(example, codecSwitchingStrategy, enableAudioGroups) {
    // We can't know if role and label are really important, so we don't use
    // role and label for this.
    const role = '';
    const audioLabel = '';
    const videoLabel = '';
    const hdrLevel = example.video && example.video.hdr ?
        example.video.hdr : '';
    const spatialAudio = example.audio && example.audio.spatialAudio ?
        example.audio.spatialAudio : false;
    const videoLayout = example.video && example.video.videoLayout ?
        example.video.videoLayout : '';
    const channelCount = example.audio && example.audio.channelsCount ?
        example.audio.channelsCount : 0;
    const audioCodec = example.audio && example.audio.codecs ?
        example.audio.codecs : '';

    /** @private {!shaka.media.AdaptationSetCriteria} */
    this.preferenceBasedCriteria_ = new shaka.media.PreferenceBasedCriteria(
        example.language, role, channelCount, hdrLevel, spatialAudio,
        videoLayout, audioLabel, videoLabel,
        codecSwitchingStrategy, enableAudioGroups, audioCodec);
  }

  /** @override */
  create(variants) {
    return this.preferenceBasedCriteria_.create(variants);
  }
};


/**
 * @implements {shaka.media.AdaptationSetCriteria}
 * @final
 */
shaka.media.PreferenceBasedCriteria = class {
  /**
   * @param {string} language
   * @param {string} role
   * @param {number} channelCount
   * @param {string} hdrLevel
   * @param {boolean} spatialAudio
   * @param {string} videoLayout
   * @param {string} audioLabel
   * @param {string} videoLabel
   * @param {shaka.config.CodecSwitchingStrategy} codecSwitchingStrategy
   * @param {boolean} enableAudioGroups
   * @param {string} audioCodec
   */
  constructor(language, role, channelCount, hdrLevel, spatialAudio,
      videoLayout, audioLabel, videoLabel, codecSwitchingStrategy,
      enableAudioGroups, audioCodec) {
    /** @private {string} */
    this.language_ = language;
    /** @private {string} */
    this.role_ = role;
    /** @private {number} */
    this.channelCount_ = channelCount;
    /** @private {string} */
    this.hdrLevel_ = hdrLevel;
    /** @private {boolean} */
    this.spatialAudio_ = spatialAudio;
    /** @private {string} */
    this.videoLayout_ = videoLayout;
    /** @private {string} */
    this.audioLabel_ = audioLabel;
    /** @private {string} */
    this.videoLabel_ = videoLabel;
    /** @private {shaka.config.CodecSwitchingStrategy} */
    this.codecSwitchingStrategy_ = codecSwitchingStrategy;
    /** @private {boolean} */
    this.enableAudioGroups_ = enableAudioGroups;
    /** @private {string} */
    this.audioCodec_ = audioCodec;
  }

  /** @override */
  create(variants) {
    const Class = shaka.media.PreferenceBasedCriteria;

    let current = [];

    const byLanguage = Class.filterByLanguage_(variants, this.language_);
    const byPrimary = variants.filter((variant) => variant.primary);

    if (byLanguage.length) {
      current = byLanguage;
    } else if (byPrimary.length) {
      current = byPrimary;
    } else {
      current = variants;
    }

    // Now refine the choice based on role preference.  Even the empty string
    // works here, and will match variants without any roles.
    const byRole = Class.filterVariantsByRole_(current, this.role_);
    if (byRole.length) {
      current = byRole;
    } else {
      shaka.log.warning('No exact match for variant role could be found.');
    }

    if (this.videoLayout_) {
      const byVideoLayout = Class.filterVariantsByVideoLayout_(
          current, this.videoLayout_);
      if (byVideoLayout.length) {
        current = byVideoLayout;
      } else {
        shaka.log.warning(
            'No exact match for the video layout could be found.');
      }
    }

    if (this.hdrLevel_) {
      const byHdrLevel = Class.filterVariantsByHDRLevel_(
          current, this.hdrLevel_);
      if (byHdrLevel.length) {
        current = byHdrLevel;
      } else {
        shaka.log.warning(
            'No exact match for the hdr level could be found.');
      }
    }

    if (this.channelCount_) {
      const byChannel = Class.filterVariantsByAudioChannelCount_(
          current, this.channelCount_);
      if (byChannel.length) {
        current = byChannel;
      } else {
        shaka.log.warning(
            'No exact match for the channel count could be found.');
      }
    }

    if (this.audioLabel_) {
      const byLabel = Class.filterVariantsByAudioLabel_(
          current, this.audioLabel_);
      if (byLabel.length) {
        current = byLabel;
      } else {
        shaka.log.warning('No exact match for audio label could be found.');
      }
    }

    if (this.videoLabel_) {
      const byLabel = Class.filterVariantsByVideoLabel_(
          current, this.videoLabel_);
      if (byLabel.length) {
        current = byLabel;
      } else {
        shaka.log.warning('No exact match for video label could be found.');
      }
    }

    const bySpatialAudio = Class.filterVariantsBySpatialAudio_(
        current, this.spatialAudio_);
    if (bySpatialAudio.length) {
      current = bySpatialAudio;
    } else {
      shaka.log.warning('No exact match for spatial audio could be found.');
    }

    if (this.audioCodec_) {
      const byAudioCodec = Class.filterVariantsByAudioCodec_(
          current, this.audioCodec_);
      if (byAudioCodec.length) {
        current = byAudioCodec;
      } else {
        shaka.log.warning('No exact match for audio codec could be found.');
      }
    }

    const supportsSmoothCodecTransitions = this.codecSwitchingStrategy_ ==
      shaka.config.CodecSwitchingStrategy.SMOOTH &&
        shaka.media.Capabilities.isChangeTypeSupported();

    return new shaka.media.AdaptationSet(current[0], current,
        !supportsSmoothCodecTransitions, this.enableAudioGroups_);
  }

  /**
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} preferredLanguage
   * @return {!Array.<shaka.extern.Variant>}
   * @private
   */
  static filterByLanguage_(variants, preferredLanguage) {
    const LanguageUtils = shaka.util.LanguageUtils;

    /** @type {string} */
    const preferredLocale = LanguageUtils.normalize(preferredLanguage);

    /** @type {?string} */
    const closestLocale = LanguageUtils.findClosestLocale(
        preferredLocale,
        variants.map((variant) => LanguageUtils.getLocaleForVariant(variant)));

    // There were no locales close to what we preferred.
    if (!closestLocale) {
      return [];
    }

    // Find the variants that use the closest variant.
    return variants.filter((variant) => {
      return closestLocale == LanguageUtils.getLocaleForVariant(variant);
    });
  }

  /**
   * Filter Variants by role.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} preferredRole
   * @return {!Array.<shaka.extern.Variant>}
   * @private
   */
  static filterVariantsByRole_(variants, preferredRole) {
    return variants.filter((variant) => {
      if (!variant.audio) {
        return false;
      }

      if (preferredRole) {
        return variant.audio.roles.includes(preferredRole);
      } else {
        return variant.audio.roles.length == 0;
      }
    });
  }

  /**
   * Filter Variants by audio label.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} preferredLabel
   * @return {!Array.<shaka.extern.Variant>}
   * @private
   */
  static filterVariantsByAudioLabel_(variants, preferredLabel) {
    return variants.filter((variant) => {
      if (!variant.audio || !variant.audio.label) {
        return false;
      }

      const label1 = variant.audio.label.toLowerCase();
      const label2 = preferredLabel.toLowerCase();
      return label1 == label2;
    });
  }

  /**
   * Filter Variants by video label.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} preferredLabel
   * @return {!Array.<shaka.extern.Variant>}
   * @private
   */
  static filterVariantsByVideoLabel_(variants, preferredLabel) {
    return variants.filter((variant) => {
      if (!variant.video || !variant.video.label) {
        return false;
      }

      const label1 = variant.video.label.toLowerCase();
      const label2 = preferredLabel.toLowerCase();
      return label1 == label2;
    });
  }

  /**
   * Filter Variants by channelCount.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {number} channelCount
   * @return {!Array.<shaka.extern.Variant>}
   * @private
   */
  static filterVariantsByAudioChannelCount_(variants, channelCount) {
    return variants.filter((variant) => {
      if (variant.audio && variant.audio.channelsCount &&
          variant.audio.channelsCount != channelCount) {
        return false;
      }
      return true;
    });
  }

  /**
   * Filters variants according to the given hdr level config.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} hdrLevel
   * @private
   */
  static filterVariantsByHDRLevel_(variants, hdrLevel) {
    if (hdrLevel == 'AUTO') {
      // Auto detect the ideal HDR level.
      if (window.matchMedia('(color-gamut: p3)').matches) {
        const someHLG = variants.some((variant) => {
          if (variant.video && variant.video.hdr &&
              variant.video.hdr == 'HLG') {
            return true;
          }
          return false;
        });
        hdrLevel = someHLG ? 'HLG' : 'PQ';
      } else {
        hdrLevel = 'SDR';
      }
    }
    return variants.filter((variant) => {
      if (variant.video && variant.video.hdr && variant.video.hdr != hdrLevel) {
        return false;
      }
      return true;
    });
  }


  /**
   * Filters variants according to the given video layout config.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {string} videoLayout
   * @private
   */
  static filterVariantsByVideoLayout_(variants, videoLayout) {
    return variants.filter((variant) => {
      if (variant.video && variant.video.videoLayout &&
          variant.video.videoLayout != videoLayout) {
        return false;
      }
      return true;
    });
  }


  /**
   * Filters variants according to the given spatial audio config.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {boolean} spatialAudio
   * @private
   */
  static filterVariantsBySpatialAudio_(variants, spatialAudio) {
    return variants.filter((variant) => {
      if (variant.audio && variant.audio.spatialAudio != spatialAudio) {
        return false;
      }
      return true;
    });
  }


  /**
   * Filters variants according to the given audio codec.
   *
   * @param {!Array<shaka.extern.Variant>} variants
   * @param {string} audioCodec
   * @private
   */
  static filterVariantsByAudioCodec_(variants, audioCodec) {
    return variants.filter((variant) => {
      if (variant.audio && variant.audio.codecs != audioCodec) {
        return false;
      }
      return true;
    });
  }
};