static isAudioStreamBetterMatch_(outputStream, best, candidate) {
    const LanguageUtils = shaka.util.LanguageUtils;
    const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;

    // An exact match is better than a non-exact match.
    const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
        outputStream, best);
    const candidateIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_(
        outputStream, candidate);
    if (bestIsExact && !candidateIsExact) {
      return false;
    }
    if (!bestIsExact && candidateIsExact) {
      return true;
    }

    // The most important thing is language.  In some cases, we will accept a
    // different language across periods when we must.
    const bestRelatedness = LanguageUtils.relatedness(
        outputStream.language, best.language);
    const candidateRelatedness = LanguageUtils.relatedness(
        outputStream.language, candidate.language);

    if (candidateRelatedness > bestRelatedness) {
      return true;
    }
    if (candidateRelatedness < bestRelatedness) {
      return false;
    }

    // If language-based differences haven't decided this, look at labels.
    // If available options differ, look does any matches with output stream.
    if (best.label !== candidate.label) {
      if (outputStream.label === best.label) {
        return false;
      }
      if (outputStream.label === candidate.label) {
        return true;
      }
    }

    // If label-based differences haven't decided this, look at roles.  If
    // the candidate has more roles in common with the output, upgrade to the
    // candidate.
    if (outputStream.roles.length) {
      const bestRoleMatches =
          best.roles.filter((role) => outputStream.roles.includes(role));
      const candidateRoleMatches =
          candidate.roles.filter((role) => outputStream.roles.includes(role));
      if (candidateRoleMatches.length > bestRoleMatches.length) {
        return true;
      } else if (candidateRoleMatches.length < bestRoleMatches.length) {
        return false;
      } else if (candidate.roles.length !== best.roles.length) {
        // Both streams have the same role overlap with the outputStream
        // If this is the case, choose the stream with the fewer roles overall.
        // Streams that match best together tend to be streams with the same
        // roles, e g stream1 with roles [r1, r2] is likely a better match
        // for stream2 with roles [r1, r2] vs stream3 with roles
        // [r1, r2, r3, r4].
        // If we match stream1 with stream3 due to the same role overlap,
        // stream2 is likely to be left unmatched and error out later.
        // See https://github.com/shaka-project/shaka-player/issues/2542 for
        // more details.
        return candidate.roles.length < best.roles.length;
      }
    } else if (!candidate.roles.length && best.roles.length) {
      // If outputStream has no roles, and only one of the streams has no roles,
      // choose the one with no roles.
      return true;
    } else if (candidate.roles.length && !best.roles.length) {
      return false;
    }

    // If the language doesn't match, but the candidate is the "primary"
    // language, then that should be preferred as a fallback.
    if (!best.primary && candidate.primary) {
      return true;
    }
    if (best.primary && !candidate.primary) {
      return false;
    }

    // If language-based and role-based features are equivalent, take the audio
    // with the closes channel count to the output.
    const channelsBetterOrWorse =
        shaka.util.PeriodCombiner.compareClosestPreferLower(
            outputStream.channelsCount,
            best.channelsCount,
            candidate.channelsCount);
    if (channelsBetterOrWorse == BETTER) {
      return true;
    } else if (channelsBetterOrWorse == WORSE) {
      return false;
    }

    // If channels are equal, take the closest sample rate to the output.
    const sampleRateBetterOrWorse =
        shaka.util.PeriodCombiner.compareClosestPreferLower(
            outputStream.audioSamplingRate,
            best.audioSamplingRate,
            candidate.audioSamplingRate);
    if (sampleRateBetterOrWorse == BETTER) {
      return true;
    } else if (sampleRateBetterOrWorse == WORSE) {
      return false;
    }

    if (outputStream.bandwidth) {
      // Take the audio with the closest bandwidth to the output.
      const bandwidthBetterOrWorse =
          shaka.util.PeriodCombiner.compareClosestPreferMinimalAbsDiff_(
              outputStream.bandwidth,
              best.bandwidth,
              candidate.bandwidth);
      if (bandwidthBetterOrWorse == BETTER) {
        return true;
      } else if (bandwidthBetterOrWorse == WORSE) {
        return false;
      }
    }

    // If the result of each comparison was inconclusive, default to false.
    return false;
  }
@param {T} outputStream An audio output stream
@param {T} best The best match so far for this period
@param {T} candidate A candidate stream which might be better
@return {boolean} True if the candidate is a better match

@template T
Accepts either a StreamDB or Stream type.

@private