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

goog.provide('shaka.util.StreamUtils');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.MultiMap');
goog.require('shaka.util.Platform');
goog.requireType('shaka.media.DrmEngine');


/**
 * @summary A set of utility functions for dealing with Streams and Manifests.
 */
shaka.util.StreamUtils = class {
  /**
   * In case of multiple usable codecs, choose one based on lowest average
   * bandwidth and filter out the rest.
   * Also filters out variants that have too many audio channels.
   * @param {!shaka.extern.Manifest} manifest
   * @param {!Array.<string>} preferredVideoCodecs
   * @param {!Array.<string>} preferredAudioCodecs
   * @param {number} preferredAudioChannelCount
   * @param {!Array.<string>} preferredDecodingAttributes
   */
  static chooseCodecsAndFilterManifest(manifest, preferredVideoCodecs,
      preferredAudioCodecs, preferredAudioChannelCount,
      preferredDecodingAttributes) {
    const StreamUtils = shaka.util.StreamUtils;

    let variants = manifest.variants;
    // To start, choose the codecs based on configured preferences if available.
    if (preferredVideoCodecs.length || preferredAudioCodecs.length) {
      variants = StreamUtils.choosePreferredCodecs(variants,
          preferredVideoCodecs, preferredAudioCodecs);
    }

    // Consider a subset of variants based on audio channel
    // preferences.
    // For some content (#1013), surround-sound variants will use a different
    // codec than stereo variants, so it is important to choose codecs **after**
    // considering the audio channel config.
    variants = StreamUtils.filterVariantsByAudioChannelCount(
        variants, preferredAudioChannelCount);

    // Now organize variants into buckets by codecs.
    /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
    let variantsByCodecs = StreamUtils.getVariantsByCodecs_(variants);
    variantsByCodecs = StreamUtils.filterVariantsByDensity_(variantsByCodecs);

    const bestCodecs = StreamUtils.chooseCodecsByDecodingAttributes_(
        variantsByCodecs, preferredDecodingAttributes);

    // Filter out any variants that don't match, forcing AbrManager to choose
    // from a single video codec and a single audio codec possible.
    manifest.variants = manifest.variants.filter((variant) => {
      const codecs = StreamUtils.getVariantCodecs_(variant);
      if (codecs == bestCodecs) {
        return true;
      }

      shaka.log.debug('Dropping Variant (better codec available)', variant);
      return false;
    });
  }

  /**
  * Get variants by codecs.
  *
  * @param {!Array<shaka.extern.Variant>} variants
  * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  * @private
  */
  static getVariantsByCodecs_(variants) {
    const variantsByCodecs = new shaka.util.MultiMap();
    for (const variant of variants) {
      const variantCodecs = shaka.util.StreamUtils.getVariantCodecs_(variant);
      variantsByCodecs.push(variantCodecs, variant);
    }

    return variantsByCodecs;
  }

  /**
  * Filters variants by density.
  * Get variants by codecs map with the max density where all codecs are
  * present.
  *
  * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  * @private
  */
  static filterVariantsByDensity_(variantsByCodecs) {
    let maxDensity = 0;
    const codecGroupsByDensity = new Map();
    const countCodecs = variantsByCodecs.size();

    variantsByCodecs.forEach((codecs, variants) => {
      for (const variant of variants) {
        const video = variant.video;
        if (!video || !video.width || !video.height) {
          continue;
        }

        const density = video.width * video.height * (video.frameRate || 1);
        if (!codecGroupsByDensity.has(density)) {
          codecGroupsByDensity.set(density, new shaka.util.MultiMap());
        }

        /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
        const group = codecGroupsByDensity.get(density);
        group.push(codecs, variant);

        // We want to look at the groups in which all codecs are present.
        // Take the max density from those groups where all codecs are present.
        // Later, we will compare bandwidth numbers only within this group.
        // Effectively, only the bandwidth differences in the highest-res and
        // highest-framerate content will matter in choosing a codec.
        if (group.size() === countCodecs) {
          maxDensity = Math.max(maxDensity, density);
        }
      }
    });

    return maxDensity ? codecGroupsByDensity.get(maxDensity) : variantsByCodecs;
  }

  /**
   * Choose the codecs by configured preferred audio and video codecs.
   *
   * @param {!Array<shaka.extern.Variant>} variants
   * @param {!Array.<string>} preferredVideoCodecs
   * @param {!Array.<string>} preferredAudioCodecs
   * @return {!Array<shaka.extern.Variant>}
   */
  static choosePreferredCodecs(variants, preferredVideoCodecs,
      preferredAudioCodecs) {
    let subset = variants;
    for (const videoCodec of preferredVideoCodecs) {
      const filtered = subset.filter((variant) => {
        return variant.video && variant.video.codecs.startsWith(videoCodec);
      });
      if (filtered.length) {
        subset = filtered;
        break;
      }
    }

    for (const audioCodec of preferredAudioCodecs) {
      const filtered = subset.filter((variant) => {
        return variant.audio && variant.audio.codecs.startsWith(audioCodec);
      });
      if (filtered.length) {
        subset = filtered;
        break;
      }
    }
    return subset;
  }

  /**
   * Choose the codecs by configured preferred decoding attributes.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @param {!Array.<string>} attributes
   * @return {string}
   * @private
   */
  static chooseCodecsByDecodingAttributes_(variantsByCodecs, attributes) {
    const StreamUtils = shaka.util.StreamUtils;

    for (const attribute of attributes) {
      if (attribute == StreamUtils.DecodingAttributes.SMOOTH ||
          attribute == StreamUtils.DecodingAttributes.POWER) {
        variantsByCodecs = StreamUtils.chooseCodecsByMediaCapabilitiesInfo_(
            variantsByCodecs, attribute);
        // If we only have one smooth or powerEfficient codecs, choose it as the
        // best codecs.
        if (variantsByCodecs.size() == 1) {
          return variantsByCodecs.keys()[0];
        }
      } else if (attribute == StreamUtils.DecodingAttributes.BANDWIDTH) {
        return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
      }
    }
    // If there's no configured decoding preferences, or we have multiple codecs
    // that meets the configured decoding preferences, choose the one with
    // the lowest bandwidth.
    return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
  }

  /**
   * Choose the best codecs by configured preferred MediaCapabilitiesInfo
   * attributes.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @param {string} attribute
   * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
   * @private
   */
  static chooseCodecsByMediaCapabilitiesInfo_(variantsByCodecs, attribute) {
    let highestScore = 0;
    const bestVariantsByCodecs = new shaka.util.MultiMap();
    variantsByCodecs.forEach((codecs, variants) => {
      let sum = 0;
      let num = 0;

      for (const variant of variants) {
        if (variant.decodingInfos.length) {
          sum += variant.decodingInfos[0][attribute] ? 1 : 0;
          num++;
        }
      }

      const averageScore = sum / num;
      shaka.log.debug('codecs', codecs, 'avg', attribute, averageScore);

      if (averageScore > highestScore) {
        bestVariantsByCodecs.clear();
        bestVariantsByCodecs.push(codecs, variants);
        highestScore = averageScore;
      } else if (averageScore == highestScore) {
        bestVariantsByCodecs.push(codecs, variants);
      }
    });
    return bestVariantsByCodecs;
  }

  /**
   * Find the lowest-bandwidth (best) codecs.
   * Compute the average bandwidth for each group of variants.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @return {string}
   * @private
   */
  static findCodecsByLowestBandwidth_(variantsByCodecs) {
    let bestCodecs = '';
    let lowestAverageBandwidth = Infinity;

    variantsByCodecs.forEach((codecs, variants) => {
      let sum = 0;
      let num = 0;
      for (const variant of variants) {
        sum += variant.bandwidth || 0;
        ++num;
      }

      const averageBandwidth = sum / num;
      shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth);

      if (averageBandwidth < lowestAverageBandwidth) {
        bestCodecs = codecs;
        lowestAverageBandwidth = averageBandwidth;
      }
    });

    goog.asserts.assert(bestCodecs !== '', 'Should have chosen codecs!');
    goog.asserts.assert(!isNaN(lowestAverageBandwidth),
        'Bandwidth should be a number!');

    return bestCodecs;
  }

  /**
   * Get a string representing all codecs used in a variant.
   *
   * @param {!shaka.extern.Variant} variant
   * @return {string}
   * @private
   */
  static getVariantCodecs_(variant) {
    // Only consider the base of the codec string.  For example, these should
    // both be considered the same codec: avc1.42c01e, avc1.4d401f
    let baseVideoCodec = '';
    if (variant.video) {
      baseVideoCodec =
        shaka.util.MimeUtils.getNormalizedCodec(variant.video.codecs);
    }

    let baseAudioCodec = '';
    if (variant.audio) {
      baseAudioCodec =
        shaka.util.MimeUtils.getNormalizedCodec(variant.audio.codecs);
    }

    return baseVideoCodec + '-' + baseAudioCodec;
  }

  /**
   * Filter the variants in |manifest| to only include the variants that meet
   * the given restrictions.
   *
   * @param {!shaka.extern.Manifest} manifest
   * @param {shaka.extern.Restrictions} restrictions
   * @param {{width: number, height:number}} maxHwResolution
   */
  static filterByRestrictions(manifest, restrictions, maxHwResolution) {
    manifest.variants = manifest.variants.filter((variant) => {
      return shaka.util.StreamUtils.meetsRestrictions(
          variant, restrictions, maxHwResolution);
    });
  }

  /**
   * @param {shaka.extern.Variant} variant
   * @param {shaka.extern.Restrictions} restrictions
   *   Configured restrictions from the user.
   * @param {{width: number, height: number}} maxHwRes
   *   The maximum resolution the hardware can handle.
   *   This is applied separately from user restrictions because the setting
   *   should not be easily replaced by the user's configuration.
   * @return {boolean}
   */
  static meetsRestrictions(variant, restrictions, maxHwRes) {
    /** @type {function(number, number, number):boolean} */
    const inRange = (x, min, max) => {
      return x >= min && x <= max;
    };

    const video = variant.video;

    if (variant.disabledUntilTime != 0) {
      if (variant.disabledUntilTime > Date.now() / 1000) {
        return false;
      }
      variant.disabledUntilTime = 0;
    }

    // |video.width| and |video.height| can be undefined, which breaks
    // the math, so make sure they are there first.
    if (video && video.width && video.height) {
      if (!inRange(video.width,
          restrictions.minWidth,
          Math.min(restrictions.maxWidth, maxHwRes.width))) {
        return false;
      }

      if (!inRange(video.height,
          restrictions.minHeight,
          Math.min(restrictions.maxHeight, maxHwRes.height))) {
        return false;
      }

      if (!inRange(video.width * video.height,
          restrictions.minPixels,
          restrictions.maxPixels)) {
        return false;
      }
    }

    // |variant.frameRate| can be undefined, which breaks
    // the math, so make sure they are there first.
    if (variant && variant.video && variant.video.frameRate) {
      if (!inRange(variant.video.frameRate,
          restrictions.minFrameRate,
          restrictions.maxFrameRate)) {
        return false;
      }
    }

    if (!inRange(variant.bandwidth,
        restrictions.minBandwidth,
        restrictions.maxBandwidth)) {
      return false;
    }

    return true;
  }


  /**
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {shaka.extern.Restrictions} restrictions
   * @param {{width: number, height: number}} maxHwRes
   * @return {boolean} Whether the tracks changed.
   */
  static applyRestrictions(variants, restrictions, maxHwRes) {
    let tracksChanged = false;

    for (const variant of variants) {
      const originalAllowed = variant.allowedByApplication;
      variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions(
          variant, restrictions, maxHwRes);

      if (originalAllowed != variant.allowedByApplication) {
        tracksChanged = true;
      }
    }

    return tracksChanged;
  }


  /**
   * Alters the given Manifest to filter out any unplayable streams.
   *
   * @param {shaka.media.DrmEngine} drmEngine
   * @param {?shaka.extern.Variant} currentVariant
   * @param {shaka.extern.Manifest} manifest
   */
  static async filterManifest(
      drmEngine, currentVariant, manifest) {
    await shaka.util.StreamUtils.filterManifestByMediaCapabilities(manifest,
        manifest.offlineSessionIds.length > 0);
    shaka.util.StreamUtils.filterManifestByCurrentVariant(
        currentVariant, manifest);
    shaka.util.StreamUtils.filterTextStreams_(manifest);
    await shaka.util.StreamUtils.filterImageStreams_(manifest);
  }


  /**
   * Alters the given Manifest to filter out any streams unsupported by the
   * platform via MediaCapabilities.decodingInfo() API.
   *
   * @param {shaka.extern.Manifest} manifest
   * @param {boolean} usePersistentLicenses
   */
  static async filterManifestByMediaCapabilities(
      manifest, usePersistentLicenses) {
    goog.asserts.assert(navigator.mediaCapabilities,
        'MediaCapabilities should be valid.');

    await shaka.util.StreamUtils.getDecodingInfosForVariants(
        manifest.variants, usePersistentLicenses, /* srcEquals= */ false);
    manifest.variants = manifest.variants.filter((variant) => {
      // See: https://github.com/shaka-project/shaka-player/issues/3860
      const video = variant.video;
      const ContentType = shaka.util.ManifestParserUtils.ContentType;
      if (video) {
        let videoCodecs = shaka.util.StreamUtils.patchVp9(video.codecs);
        // For multiplexed streams. Here we must check the audio of the
        // stream to see if it is compatible.
        if (video.codecs.includes(',')) {
          const allCodecs = video.codecs.split(',');
          videoCodecs = shaka.util.ManifestParserUtils.guessCodecs(
              ContentType.VIDEO, allCodecs);
          const audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
              ContentType.AUDIO, allCodecs);
          const audioFullType = shaka.util.MimeUtils.getFullOrConvertedType(
              video.mimeType, audioCodecs, ContentType.AUDIO);
          if (!MediaSource.isTypeSupported(audioFullType)) {
            return false;
          }
        }
        const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
            video.mimeType, videoCodecs, ContentType.VIDEO);
        if (!MediaSource.isTypeSupported(fullType)) {
          return false;
        }
      }
      const audio = variant.audio;
      if (audio) {
        const codecs =
            shaka.util.StreamUtils.getCorrectAudioCodecs_(audio.codecs);
        const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
            audio.mimeType, codecs, ContentType.AUDIO);
        if (!MediaSource.isTypeSupported(fullType)) {
          return false;
        }
      }

      // See: https://github.com/shaka-project/shaka-player/issues/3380
      if (shaka.util.Platform.isXboxOne() && video &&
          ((video.width && video.width > 1920) ||
          (video.height && video.height > 1080)) &&
          video.codecs.includes('avc1.')) {
        shaka.log.debug('Dropping variant - not compatible with platform',
            shaka.util.StreamUtils.getVariantSummaryString_(variant));
        return false;
      }

      const supported = variant.decodingInfos.some((decodingInfo) => {
        return decodingInfo.supported;
      });
      // Filter out all unsupported variants.
      if (!supported) {
        shaka.log.debug('Dropping variant - not compatible with platform',
            shaka.util.StreamUtils.getVariantSummaryString_(variant));
      }
      return supported;
    });
  }


  /**
   * Get the decodingInfo results of the variants via MediaCapabilities.
   * This should be called after the DrmEngine is created and configured, and
   * before DrmEngine sets the mediaKeys.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {boolean} usePersistentLicenses
   * @param {boolean} srcEquals
   * @exportDoc
   */
  static async getDecodingInfosForVariants(variants, usePersistentLicenses,
      srcEquals) {<commit_msg>fix: Filter unsupported H.264 streams in Xbox (#4493)<commit_after>/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.util.StreamUtils');

goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.MultiMap');
goog.require('shaka.util.Platform');
goog.requireType('shaka.media.DrmEngine');


/**
 * @summary A set of utility functions for dealing with Streams and Manifests.
 */
shaka.util.StreamUtils = class {
  /**
   * In case of multiple usable codecs, choose one based on lowest average
   * bandwidth and filter out the rest.
   * Also filters out variants that have too many audio channels.
   * @param {!shaka.extern.Manifest} manifest
   * @param {!Array.<string>} preferredVideoCodecs
   * @param {!Array.<string>} preferredAudioCodecs
   * @param {number} preferredAudioChannelCount
   * @param {!Array.<string>} preferredDecodingAttributes
   */
  static chooseCodecsAndFilterManifest(manifest, preferredVideoCodecs,
      preferredAudioCodecs, preferredAudioChannelCount,
      preferredDecodingAttributes) {
    const StreamUtils = shaka.util.StreamUtils;

    let variants = manifest.variants;
    // To start, choose the codecs based on configured preferences if available.
    if (preferredVideoCodecs.length || preferredAudioCodecs.length) {
      variants = StreamUtils.choosePreferredCodecs(variants,
          preferredVideoCodecs, preferredAudioCodecs);
    }

    // Consider a subset of variants based on audio channel
    // preferences.
    // For some content (#1013), surround-sound variants will use a different
    // codec than stereo variants, so it is important to choose codecs **after**
    // considering the audio channel config.
    variants = StreamUtils.filterVariantsByAudioChannelCount(
        variants, preferredAudioChannelCount);

    // Now organize variants into buckets by codecs.
    /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
    let variantsByCodecs = StreamUtils.getVariantsByCodecs_(variants);
    variantsByCodecs = StreamUtils.filterVariantsByDensity_(variantsByCodecs);

    const bestCodecs = StreamUtils.chooseCodecsByDecodingAttributes_(
        variantsByCodecs, preferredDecodingAttributes);

    // Filter out any variants that don't match, forcing AbrManager to choose
    // from a single video codec and a single audio codec possible.
    manifest.variants = manifest.variants.filter((variant) => {
      const codecs = StreamUtils.getVariantCodecs_(variant);
      if (codecs == bestCodecs) {
        return true;
      }

      shaka.log.debug('Dropping Variant (better codec available)', variant);
      return false;
    });
  }

  /**
  * Get variants by codecs.
  *
  * @param {!Array<shaka.extern.Variant>} variants
  * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  * @private
  */
  static getVariantsByCodecs_(variants) {
    const variantsByCodecs = new shaka.util.MultiMap();
    for (const variant of variants) {
      const variantCodecs = shaka.util.StreamUtils.getVariantCodecs_(variant);
      variantsByCodecs.push(variantCodecs, variant);
    }

    return variantsByCodecs;
  }

  /**
  * Filters variants by density.
  * Get variants by codecs map with the max density where all codecs are
  * present.
  *
  * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
  * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
  * @private
  */
  static filterVariantsByDensity_(variantsByCodecs) {
    let maxDensity = 0;
    const codecGroupsByDensity = new Map();
    const countCodecs = variantsByCodecs.size();

    variantsByCodecs.forEach((codecs, variants) => {
      for (const variant of variants) {
        const video = variant.video;
        if (!video || !video.width || !video.height) {
          continue;
        }

        const density = video.width * video.height * (video.frameRate || 1);
        if (!codecGroupsByDensity.has(density)) {
          codecGroupsByDensity.set(density, new shaka.util.MultiMap());
        }

        /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
        const group = codecGroupsByDensity.get(density);
        group.push(codecs, variant);

        // We want to look at the groups in which all codecs are present.
        // Take the max density from those groups where all codecs are present.
        // Later, we will compare bandwidth numbers only within this group.
        // Effectively, only the bandwidth differences in the highest-res and
        // highest-framerate content will matter in choosing a codec.
        if (group.size() === countCodecs) {
          maxDensity = Math.max(maxDensity, density);
        }
      }
    });

    return maxDensity ? codecGroupsByDensity.get(maxDensity) : variantsByCodecs;
  }

  /**
   * Choose the codecs by configured preferred audio and video codecs.
   *
   * @param {!Array<shaka.extern.Variant>} variants
   * @param {!Array.<string>} preferredVideoCodecs
   * @param {!Array.<string>} preferredAudioCodecs
   * @return {!Array<shaka.extern.Variant>}
   */
  static choosePreferredCodecs(variants, preferredVideoCodecs,
      preferredAudioCodecs) {
    let subset = variants;
    for (const videoCodec of preferredVideoCodecs) {
      const filtered = subset.filter((variant) => {
        return variant.video && variant.video.codecs.startsWith(videoCodec);
      });
      if (filtered.length) {
        subset = filtered;
        break;
      }
    }

    for (const audioCodec of preferredAudioCodecs) {
      const filtered = subset.filter((variant) => {
        return variant.audio && variant.audio.codecs.startsWith(audioCodec);
      });
      if (filtered.length) {
        subset = filtered;
        break;
      }
    }
    return subset;
  }

  /**
   * Choose the codecs by configured preferred decoding attributes.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @param {!Array.<string>} attributes
   * @return {string}
   * @private
   */
  static chooseCodecsByDecodingAttributes_(variantsByCodecs, attributes) {
    const StreamUtils = shaka.util.StreamUtils;

    for (const attribute of attributes) {
      if (attribute == StreamUtils.DecodingAttributes.SMOOTH ||
          attribute == StreamUtils.DecodingAttributes.POWER) {
        variantsByCodecs = StreamUtils.chooseCodecsByMediaCapabilitiesInfo_(
            variantsByCodecs, attribute);
        // If we only have one smooth or powerEfficient codecs, choose it as the
        // best codecs.
        if (variantsByCodecs.size() == 1) {
          return variantsByCodecs.keys()[0];
        }
      } else if (attribute == StreamUtils.DecodingAttributes.BANDWIDTH) {
        return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
      }
    }
    // If there's no configured decoding preferences, or we have multiple codecs
    // that meets the configured decoding preferences, choose the one with
    // the lowest bandwidth.
    return StreamUtils.findCodecsByLowestBandwidth_(variantsByCodecs);
  }

  /**
   * Choose the best codecs by configured preferred MediaCapabilitiesInfo
   * attributes.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @param {string} attribute
   * @return {!shaka.util.MultiMap.<shaka.extern.Variant>}
   * @private
   */
  static chooseCodecsByMediaCapabilitiesInfo_(variantsByCodecs, attribute) {
    let highestScore = 0;
    const bestVariantsByCodecs = new shaka.util.MultiMap();
    variantsByCodecs.forEach((codecs, variants) => {
      let sum = 0;
      let num = 0;

      for (const variant of variants) {
        if (variant.decodingInfos.length) {
          sum += variant.decodingInfos[0][attribute] ? 1 : 0;
          num++;
        }
      }

      const averageScore = sum / num;
      shaka.log.debug('codecs', codecs, 'avg', attribute, averageScore);

      if (averageScore > highestScore) {
        bestVariantsByCodecs.clear();
        bestVariantsByCodecs.push(codecs, variants);
        highestScore = averageScore;
      } else if (averageScore == highestScore) {
        bestVariantsByCodecs.push(codecs, variants);
      }
    });
    return bestVariantsByCodecs;
  }

  /**
   * Find the lowest-bandwidth (best) codecs.
   * Compute the average bandwidth for each group of variants.
   *
   * @param {!shaka.util.MultiMap.<shaka.extern.Variant>} variantsByCodecs
   * @return {string}
   * @private
   */
  static findCodecsByLowestBandwidth_(variantsByCodecs) {
    let bestCodecs = '';
    let lowestAverageBandwidth = Infinity;

    variantsByCodecs.forEach((codecs, variants) => {
      let sum = 0;
      let num = 0;
      for (const variant of variants) {
        sum += variant.bandwidth || 0;
        ++num;
      }

      const averageBandwidth = sum / num;
      shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth);

      if (averageBandwidth < lowestAverageBandwidth) {
        bestCodecs = codecs;
        lowestAverageBandwidth = averageBandwidth;
      }
    });

    goog.asserts.assert(bestCodecs !== '', 'Should have chosen codecs!');
    goog.asserts.assert(!isNaN(lowestAverageBandwidth),
        'Bandwidth should be a number!');

    return bestCodecs;
  }

  /**
   * Get a string representing all codecs used in a variant.
   *
   * @param {!shaka.extern.Variant} variant
   * @return {string}
   * @private
   */
  static getVariantCodecs_(variant) {
    // Only consider the base of the codec string.  For example, these should
    // both be considered the same codec: avc1.42c01e, avc1.4d401f
    let baseVideoCodec = '';
    if (variant.video) {
      baseVideoCodec =
        shaka.util.MimeUtils.getNormalizedCodec(variant.video.codecs);
    }

    let baseAudioCodec = '';
    if (variant.audio) {
      baseAudioCodec =
        shaka.util.MimeUtils.getNormalizedCodec(variant.audio.codecs);
    }

    return baseVideoCodec + '-' + baseAudioCodec;
  }

  /**
   * Filter the variants in |manifest| to only include the variants that meet
   * the given restrictions.
   *
   * @param {!shaka.extern.Manifest} manifest
   * @param {shaka.extern.Restrictions} restrictions
   * @param {{width: number, height:number}} maxHwResolution
   */
  static filterByRestrictions(manifest, restrictions, maxHwResolution) {
    manifest.variants = manifest.variants.filter((variant) => {
      return shaka.util.StreamUtils.meetsRestrictions(
          variant, restrictions, maxHwResolution);
    });
  }

  /**
   * @param {shaka.extern.Variant} variant
   * @param {shaka.extern.Restrictions} restrictions
   *   Configured restrictions from the user.
   * @param {{width: number, height: number}} maxHwRes
   *   The maximum resolution the hardware can handle.
   *   This is applied separately from user restrictions because the setting
   *   should not be easily replaced by the user's configuration.
   * @return {boolean}
   */
  static meetsRestrictions(variant, restrictions, maxHwRes) {
    /** @type {function(number, number, number):boolean} */
    const inRange = (x, min, max) => {
      return x >= min && x <= max;
    };

    const video = variant.video;

    if (variant.disabledUntilTime != 0) {
      if (variant.disabledUntilTime > Date.now() / 1000) {
        return false;
      }
      variant.disabledUntilTime = 0;
    }

    // |video.width| and |video.height| can be undefined, which breaks
    // the math, so make sure they are there first.
    if (video && video.width && video.height) {
      if (!inRange(video.width,
          restrictions.minWidth,
          Math.min(restrictions.maxWidth, maxHwRes.width))) {
        return false;
      }

      if (!inRange(video.height,
          restrictions.minHeight,
          Math.min(restrictions.maxHeight, maxHwRes.height))) {
        return false;
      }

      if (!inRange(video.width * video.height,
          restrictions.minPixels,
          restrictions.maxPixels)) {
        return false;
      }
    }

    // |variant.frameRate| can be undefined, which breaks
    // the math, so make sure they are there first.
    if (variant && variant.video && variant.video.frameRate) {
      if (!inRange(variant.video.frameRate,
          restrictions.minFrameRate,
          restrictions.maxFrameRate)) {
        return false;
      }
    }

    if (!inRange(variant.bandwidth,
        restrictions.minBandwidth,
        restrictions.maxBandwidth)) {
      return false;
    }

    return true;
  }


  /**
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {shaka.extern.Restrictions} restrictions
   * @param {{width: number, height: number}} maxHwRes
   * @return {boolean} Whether the tracks changed.
   */
  static applyRestrictions(variants, restrictions, maxHwRes) {
    let tracksChanged = false;

    for (const variant of variants) {
      const originalAllowed = variant.allowedByApplication;
      variant.allowedByApplication = shaka.util.StreamUtils.meetsRestrictions(
          variant, restrictions, maxHwRes);

      if (originalAllowed != variant.allowedByApplication) {
        tracksChanged = true;
      }
    }

    return tracksChanged;
  }


  /**
   * Alters the given Manifest to filter out any unplayable streams.
   *
   * @param {shaka.media.DrmEngine} drmEngine
   * @param {?shaka.extern.Variant} currentVariant
   * @param {shaka.extern.Manifest} manifest
   */
  static async filterManifest(
      drmEngine, currentVariant, manifest) {
    await shaka.util.StreamUtils.filterManifestByMediaCapabilities(manifest,
        manifest.offlineSessionIds.length > 0);
    shaka.util.StreamUtils.filterManifestByCurrentVariant(
        currentVariant, manifest);
    shaka.util.StreamUtils.filterTextStreams_(manifest);
    await shaka.util.StreamUtils.filterImageStreams_(manifest);
  }


  /**
   * Alters the given Manifest to filter out any streams unsupported by the
   * platform via MediaCapabilities.decodingInfo() API.
   *
   * @param {shaka.extern.Manifest} manifest
   * @param {boolean} usePersistentLicenses
   */
  static async filterManifestByMediaCapabilities(
      manifest, usePersistentLicenses) {
    goog.asserts.assert(navigator.mediaCapabilities,
        'MediaCapabilities should be valid.');

    await shaka.util.StreamUtils.getDecodingInfosForVariants(
        manifest.variants, usePersistentLicenses, /* srcEquals= */ false);
    manifest.variants = manifest.variants.filter((variant) => {
      // See: https://github.com/shaka-project/shaka-player/issues/3860
      const video = variant.video;
      const ContentType = shaka.util.ManifestParserUtils.ContentType;
      if (video) {
        let videoCodecs = shaka.util.StreamUtils.patchVp9(video.codecs);
        // For multiplexed streams. Here we must check the audio of the
        // stream to see if it is compatible.
        if (video.codecs.includes(',')) {
          const allCodecs = video.codecs.split(',');
          videoCodecs = shaka.util.ManifestParserUtils.guessCodecs(
              ContentType.VIDEO, allCodecs);
          const audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
              ContentType.AUDIO, allCodecs);
          const audioFullType = shaka.util.MimeUtils.getFullOrConvertedType(
              video.mimeType, audioCodecs, ContentType.AUDIO);
          if (!MediaSource.isTypeSupported(audioFullType)) {
            return false;
          }
        }
        const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
            video.mimeType, videoCodecs, ContentType.VIDEO);
        if (!MediaSource.isTypeSupported(fullType)) {
          return false;
        }
      }
      const audio = variant.audio;
      if (audio) {
        const codecs =
            shaka.util.StreamUtils.getCorrectAudioCodecs_(audio.codecs);
        const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
            audio.mimeType, codecs, ContentType.AUDIO);
        if (!MediaSource.isTypeSupported(fullType)) {
          return false;
        }
      }

      // See: https://github.com/shaka-project/shaka-player/issues/3380
      if (shaka.util.Platform.isXboxOne() && video &&
          ((video.width && video.width > 1920) ||
          (video.height && video.height > 1080)) &&
          (video.codecs.includes('avc1.') ||
          video.codecs.includes('avc3.'))) {
        shaka.log.debug('Dropping variant - not compatible with platform',
            shaka.util.StreamUtils.getVariantSummaryString_(variant));
        return false;
      }

      const supported = variant.decodingInfos.some((decodingInfo) => {
        return decodingInfo.supported;
      });
      // Filter out all unsupported variants.
      if (!supported) {
        shaka.log.debug('Dropping variant - not compatible with platform',
            shaka.util.StreamUtils.getVariantSummaryString_(variant));
      }
      return supported;
    });
  }


  /**
   * Get the decodingInfo results of the variants via MediaCapabilities.
   * This should be called after the DrmEngine is created and configured, and
   * before DrmEngine sets the mediaKeys.
   *
   * @param {!Array.<shaka.extern.Variant>} variants
   * @param {boolean} usePersistentLicenses
   * @param {boolean} srcEquals
   * @exportDoc
   */
  static async getDecodingInfosForVariants(variants, usePersistentLicenses,
      srcEquals) {
{'fraction_non_alphanumeric': 0.10718162600697023, 'fraction_numerical': 0.0035422499000171397, 'mean_word_length': 2.9342548887390425, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 75, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6925881', 'n_tokens_mistral': 10490, 'n_tokens_neox': 10122, 'n_words': 2392}