DefaultTrackSelector.java

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.trackselection;

import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.util.Collections.max;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.Spatializer;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.Bundleable;
import androidx.media3.common.C;
import androidx.media3.common.C.FormatSupport;
import androidx.media3.common.C.RoleFlags;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.RendererCapabilities.AdaptiveSupport;
import androidx.media3.exoplayer.RendererCapabilities.Capabilities;
import androidx.media3.exoplayer.RendererConfiguration;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.TrackGroupArray;
import com.google.common.base.Predicate;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/**
 * A default {@link TrackSelector} suitable for most use cases.
 *
 * <h2>Modifying parameters</h2>
 *
 * Track selection parameters should be modified by obtaining a {@link
 * TrackSelectionParameters.Builder} initialized with the current {@link TrackSelectionParameters}
 * from the player. The desired modifications can be made on the builder, and the resulting {@link
 * TrackSelectionParameters} can then be built and set on the player:
 *
 * <pre>{@code
 * player.setTrackSelectionParameters(
 *     player.getTrackSelectionParameters()
 *         .buildUpon()
 *         .setMaxVideoSizeSd()
 *         .setPreferredAudioLanguage("de")
 *         .build());
 * }</pre>
 *
 * Some specialized parameters are only available in the extended {@link Parameters} class, which
 * can be retrieved and modified in a similar way by calling methods directly on this class:
 *
 * <pre>{@code
 * defaultTrackSelector.setParameters(
 *     defaultTrackSelector.getParameters()
 *         .buildUpon()
 *         .setTunnelingEnabled(true)
 *         .build());
 * }</pre>
 */
@UnstableApi
public class DefaultTrackSelector extends MappingTrackSelector {

  private static final String TAG = "DefaultTrackSelector";
  private static final String AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE =
      "Audio channel count constraints cannot be applied without reference to Context. Build the"
          + " track selector instance with one of the non-deprecated constructors that take a"
          + " Context argument.";

  /**
   * @deprecated Use {@link Parameters.Builder} instead.
   */
  @Deprecated
  public static final class ParametersBuilder extends TrackSelectionParameters.Builder {

    private final Parameters.Builder delegate;

    /**
     * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
     *     #ParametersBuilder(Context)} instead.
     */
    @Deprecated
    @SuppressWarnings({"deprecation"})
    public ParametersBuilder() {
      delegate = new Parameters.Builder();
    }

    /**
     * Creates a builder with default initial values.
     *
     * @param context Any context.
     */
    public ParametersBuilder(Context context) {
      delegate = new Parameters.Builder(context);
    }

    @Override
    protected ParametersBuilder set(TrackSelectionParameters parameters) {
      delegate.set(parameters);
      return this;
    }

    // Video

    @Override
    public DefaultTrackSelector.ParametersBuilder setMaxVideoSizeSd() {
      delegate.setMaxVideoSizeSd();
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder clearVideoSizeConstraints() {
      delegate.clearVideoSizeConstraints();
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMaxVideoSize(
        int maxVideoWidth, int maxVideoHeight) {
      delegate.setMaxVideoSize(maxVideoWidth, maxVideoHeight);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) {
      delegate.setMaxVideoFrameRate(maxVideoFrameRate);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) {
      delegate.setMaxVideoBitrate(maxVideoBitrate);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMinVideoSize(
        int minVideoWidth, int minVideoHeight) {
      delegate.setMinVideoSize(minVideoWidth, minVideoHeight);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMinVideoFrameRate(int minVideoFrameRate) {
      delegate.setMinVideoFrameRate(minVideoFrameRate);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setMinVideoBitrate(int minVideoBitrate) {
      delegate.setMinVideoBitrate(minVideoBitrate);
      return this;
    }

    /**
     * Sets whether to exceed the {@link #setMaxVideoBitrate}, {@link #setMaxVideoSize(int, int)}
     * and {@link #setMaxVideoFrameRate} constraints when no selection can be made otherwise.
     *
     * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
     *     selection can be made otherwise.
     * @return This builder.
     */
    public ParametersBuilder setExceedVideoConstraintsIfNecessary(
        boolean exceedVideoConstraintsIfNecessary) {
      delegate.setExceedVideoConstraintsIfNecessary(exceedVideoConstraintsIfNecessary);
      return this;
    }

    /**
     * Sets whether to allow adaptive video selections containing mixed MIME types.
     *
     * <p>Adaptations between different MIME types may not be completely seamless, in which case
     * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for
     * mixed MIME type selections to be made.
     *
     * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections
     *     containing mixed MIME types.
     * @return This builder.
     */
    public ParametersBuilder setAllowVideoMixedMimeTypeAdaptiveness(
        boolean allowVideoMixedMimeTypeAdaptiveness) {
      delegate.setAllowVideoMixedMimeTypeAdaptiveness(allowVideoMixedMimeTypeAdaptiveness);
      return this;
    }

    /**
     * Sets whether to allow adaptive video selections where adaptation may not be completely
     * seamless.
     *
     * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where
     *     adaptation may not be completely seamless.
     * @return This builder.
     */
    public ParametersBuilder setAllowVideoNonSeamlessAdaptiveness(
        boolean allowVideoNonSeamlessAdaptiveness) {
      delegate.setAllowVideoNonSeamlessAdaptiveness(allowVideoNonSeamlessAdaptiveness);
      return this;
    }

    /**
     * Sets whether to allow adaptive video selections with mixed levels of {@link
     * RendererCapabilities.DecoderSupport} and {@link
     * RendererCapabilities.HardwareAccelerationSupport}.
     *
     * @param allowVideoMixedDecoderSupportAdaptiveness Whether to allow adaptive video selections
     *     with mixed levels of decoder and hardware acceleration support.
     * @return This builder.
     */
    public ParametersBuilder setAllowVideoMixedDecoderSupportAdaptiveness(
        boolean allowVideoMixedDecoderSupportAdaptiveness) {
      delegate.setAllowVideoMixedDecoderSupportAdaptiveness(
          allowVideoMixedDecoderSupportAdaptiveness);
      return this;
    }

    @Override
    public ParametersBuilder setViewportSizeToPhysicalDisplaySize(
        Context context, boolean viewportOrientationMayChange) {
      delegate.setViewportSizeToPhysicalDisplaySize(context, viewportOrientationMayChange);
      return this;
    }

    @Override
    public ParametersBuilder clearViewportSizeConstraints() {
      delegate.clearViewportSizeConstraints();
      return this;
    }

    @Override
    public ParametersBuilder setViewportSize(
        int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
      delegate.setViewportSize(viewportWidth, viewportHeight, viewportOrientationMayChange);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredVideoMimeType(@Nullable String mimeType) {
      delegate.setPreferredVideoMimeType(mimeType);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredVideoMimeTypes(String... mimeTypes) {
      delegate.setPreferredVideoMimeTypes(mimeTypes);
      return this;
    }

    @Override
    public DefaultTrackSelector.ParametersBuilder setPreferredVideoRoleFlags(
        @RoleFlags int preferredVideoRoleFlags) {
      delegate.setPreferredVideoRoleFlags(preferredVideoRoleFlags);
      return this;
    }

    // Audio

    @Override
    public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
      delegate.setPreferredAudioLanguage(preferredAudioLanguage);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredAudioLanguages(String... preferredAudioLanguages) {
      delegate.setPreferredAudioLanguages(preferredAudioLanguages);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) {
      delegate.setPreferredAudioRoleFlags(preferredAudioRoleFlags);
      return this;
    }

    @Override
    public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) {
      delegate.setMaxAudioChannelCount(maxAudioChannelCount);
      return this;
    }

    @Override
    public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) {
      delegate.setMaxAudioBitrate(maxAudioBitrate);
      return this;
    }

    /**
     * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link
     * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise.
     *
     * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no
     *     selection can be made otherwise.
     * @return This builder.
     */
    public ParametersBuilder setExceedAudioConstraintsIfNecessary(
        boolean exceedAudioConstraintsIfNecessary) {
      delegate.setExceedAudioConstraintsIfNecessary(exceedAudioConstraintsIfNecessary);
      return this;
    }

    /**
     * Sets whether to allow adaptive audio selections containing mixed MIME types.
     *
     * <p>Adaptations between different MIME types may not be completely seamless.
     *
     * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections
     *     containing mixed MIME types.
     * @return This builder.
     */
    public ParametersBuilder setAllowAudioMixedMimeTypeAdaptiveness(
        boolean allowAudioMixedMimeTypeAdaptiveness) {
      delegate.setAllowAudioMixedMimeTypeAdaptiveness(allowAudioMixedMimeTypeAdaptiveness);
      return this;
    }

    /**
     * Sets whether to allow adaptive audio selections containing mixed sample rates.
     *
     * <p>Adaptations between different sample rates may not be completely seamless.
     *
     * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections
     *     containing mixed sample rates.
     * @return This builder.
     */
    public ParametersBuilder setAllowAudioMixedSampleRateAdaptiveness(
        boolean allowAudioMixedSampleRateAdaptiveness) {
      delegate.setAllowAudioMixedSampleRateAdaptiveness(allowAudioMixedSampleRateAdaptiveness);
      return this;
    }

    /**
     * Sets whether to allow adaptive audio selections containing mixed channel counts.
     *
     * <p>Adaptations between different channel counts may not be completely seamless.
     *
     * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections
     *     containing mixed channel counts.
     * @return This builder.
     */
    public ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness(
        boolean allowAudioMixedChannelCountAdaptiveness) {
      delegate.setAllowAudioMixedChannelCountAdaptiveness(allowAudioMixedChannelCountAdaptiveness);
      return this;
    }

    /**
     * Sets whether to allow adaptive audio selections with mixed levels of {@link
     * RendererCapabilities.DecoderSupport} and {@link
     * RendererCapabilities.HardwareAccelerationSupport}.
     *
     * @param allowAudioMixedDecoderSupportAdaptiveness Whether to allow adaptive audio selections
     *     with mixed levels of decoder and hardware acceleration support.
     * @return This builder.
     */
    public ParametersBuilder setAllowAudioMixedDecoderSupportAdaptiveness(
        boolean allowAudioMixedDecoderSupportAdaptiveness) {
      delegate.setAllowAudioMixedDecoderSupportAdaptiveness(
          allowAudioMixedDecoderSupportAdaptiveness);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredAudioMimeType(@Nullable String mimeType) {
      delegate.setPreferredAudioMimeType(mimeType);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredAudioMimeTypes(String... mimeTypes) {
      delegate.setPreferredAudioMimeTypes(mimeTypes);
      return this;
    }

    // Text

    @Override
    public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
        Context context) {
      delegate.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {
      delegate.setPreferredTextLanguage(preferredTextLanguage);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredTextLanguages(String... preferredTextLanguages) {
      delegate.setPreferredTextLanguages(preferredTextLanguages);
      return this;
    }

    @Override
    public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
      delegate.setPreferredTextRoleFlags(preferredTextRoleFlags);
      return this;
    }

    @Override
    public ParametersBuilder setIgnoredTextSelectionFlags(
        @C.SelectionFlags int ignoredTextSelectionFlags) {
      delegate.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags);
      return this;
    }

    @Override
    public ParametersBuilder setSelectUndeterminedTextLanguage(
        boolean selectUndeterminedTextLanguage) {
      delegate.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
      return this;
    }

    /**
     * @deprecated Use {@link #setIgnoredTextSelectionFlags}.
     */
    @Deprecated
    public ParametersBuilder setDisabledTextTrackSelectionFlags(
        @C.SelectionFlags int disabledTextTrackSelectionFlags) {
      delegate.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags);
      return this;
    }

    // General

    @Override
    public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) {
      delegate.setForceLowestBitrate(forceLowestBitrate);
      return this;
    }

    @Override
    public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
      delegate.setForceHighestSupportedBitrate(forceHighestSupportedBitrate);
      return this;
    }

    @Override
    public ParametersBuilder addOverride(TrackSelectionOverride override) {
      delegate.addOverride(override);
      return this;
    }

    @Override
    public ParametersBuilder clearOverride(TrackGroup trackGroup) {
      delegate.clearOverride(trackGroup);
      return this;
    }

    @Override
    public ParametersBuilder setOverrideForType(TrackSelectionOverride override) {
      delegate.setOverrideForType(override);
      return this;
    }

    @Override
    public ParametersBuilder clearOverridesOfType(@C.TrackType int trackType) {
      delegate.clearOverridesOfType(trackType);
      return this;
    }

    @Override
    public ParametersBuilder clearOverrides() {
      delegate.clearOverrides();
      return this;
    }

    /**
     * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
     */
    @Override
    @Deprecated
    @SuppressWarnings("deprecation")
    public ParametersBuilder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
      delegate.setDisabledTrackTypes(disabledTrackTypes);
      return this;
    }

    @Override
    public ParametersBuilder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
      delegate.setTrackTypeDisabled(trackType, disabled);
      return this;
    }

    /**
     * Sets whether to exceed renderer capabilities when no selection can be made otherwise.
     *
     * <p>This parameter applies when all of the tracks available for a renderer exceed the
     * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality
     * track will still be selected. Playback may succeed if the renderer has under-reported its
     * true capabilities. If {@code false} then no track will be selected.
     *
     * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
     *     selection can be made otherwise.
     * @return This builder.
     */
    public ParametersBuilder setExceedRendererCapabilitiesIfNecessary(
        boolean exceedRendererCapabilitiesIfNecessary) {
      delegate.setExceedRendererCapabilitiesIfNecessary(exceedRendererCapabilitiesIfNecessary);
      return this;
    }

    /**
     * Sets whether to enable tunneling if possible. Tunneling will only be enabled if it's
     * supported by the audio and video renderers for the selected tracks.
     *
     * <p>Tunneling is known to have many device specific issues and limitations. Manual testing is
     * strongly recommended to check that the media plays correctly when this option is enabled. See
     * [#9661](https://github.com/google/ExoPlayer/issues/9661),
     * [#9133](https://github.com/google/ExoPlayer/issues/9133),
     * [#9317](https://github.com/google/ExoPlayer/issues/9317),
     * [#9502](https://github.com/google/ExoPlayer/issues/9502).
     *
     * @param tunnelingEnabled Whether to enable tunneling if possible.
     * @return This builder.
     */
    public ParametersBuilder setTunnelingEnabled(boolean tunnelingEnabled) {
      delegate.setTunnelingEnabled(tunnelingEnabled);
      return this;
    }

    /**
     * Sets whether multiple adaptive selections with more than one track are allowed.
     *
     * @param allowMultipleAdaptiveSelections Whether multiple adaptive selections are allowed.
     * @return This builder.
     */
    public ParametersBuilder setAllowMultipleAdaptiveSelections(
        boolean allowMultipleAdaptiveSelections) {
      delegate.setAllowMultipleAdaptiveSelections(allowMultipleAdaptiveSelections);
      return this;
    }

    // Overrides

    /**
     * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents
     * the selector from selecting any tracks for it.
     *
     * @param rendererIndex The renderer index.
     * @param disabled Whether the renderer is disabled.
     * @return This builder.
     */
    public ParametersBuilder setRendererDisabled(int rendererIndex, boolean disabled) {
      delegate.setRendererDisabled(rendererIndex, disabled);
      return this;
    }

    /**
     * Overrides the track selection for the renderer at the specified index.
     *
     * <p>When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the
     * override is applied. When the {@link TrackGroupArray} does not match, the override has no
     * effect. The override replaces any previous override for the specified {@link TrackGroupArray}
     * for the specified {@link Renderer}.
     *
     * <p>Passing a {@code null} override will cause the renderer to be disabled when the {@link
     * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} does
     * not match a {@code null} override has no effect. Hence a {@code null} override differs from
     * disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the renderer
     * is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as {@link
     * #setRendererDisabled(int, boolean)} disables the renderer unconditionally.
     *
     * <p>To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link
     * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}.
     *
     * @param rendererIndex The renderer index.
     * @param groups The {@link TrackGroupArray} for which the override should be applied.
     * @param override The override.
     * @return This builder.
     * @deprecated Use {@link TrackSelectionParameters.Builder#addOverride(TrackSelectionOverride)}.
     */
    @Deprecated
    public ParametersBuilder setSelectionOverride(
        int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) {
      delegate.setSelectionOverride(rendererIndex, groups, override);
      return this;
    }

    /**
     * Clears a track selection override for the specified renderer and {@link TrackGroupArray}.
     *
     * @param rendererIndex The renderer index.
     * @param groups The {@link TrackGroupArray} for which the override should be cleared.
     * @return This builder.
     * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverride(TrackGroup)}.
     */
    @Deprecated
    public ParametersBuilder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {
      delegate.clearSelectionOverride(rendererIndex, groups);
      return this;
    }

    /**
     * Clears all track selection overrides for the specified renderer.
     *
     * @param rendererIndex The renderer index.
     * @return This builder.
     * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverridesOfType(int)}.
     */
    @Deprecated
    public ParametersBuilder clearSelectionOverrides(int rendererIndex) {
      delegate.clearSelectionOverrides(rendererIndex);
      return this;
    }

    /**
     * Clears all track selection overrides for all renderers.
     *
     * @return This builder.
     * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverrides()}.
     */
    @Deprecated
    public ParametersBuilder clearSelectionOverrides() {
      delegate.clearSelectionOverrides();
      return this;
    }

    /** Builds a {@link Parameters} instance with the selected values. */
    @Override
    public Parameters build() {
      return delegate.build();
    }
  }

  /**
   * Extends {@link Parameters} by adding fields that are specific to {@link DefaultTrackSelector}.
   */
  public static final class Parameters extends TrackSelectionParameters implements Bundleable {

    /**
     * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations
     * of the parameters that can be configured using this builder.
     */
    public static final class Builder extends TrackSelectionParameters.Builder {

      // Video
      private boolean exceedVideoConstraintsIfNecessary;
      private boolean allowVideoMixedMimeTypeAdaptiveness;
      private boolean allowVideoNonSeamlessAdaptiveness;
      private boolean allowVideoMixedDecoderSupportAdaptiveness;
      // Audio
      private boolean exceedAudioConstraintsIfNecessary;
      private boolean allowAudioMixedMimeTypeAdaptiveness;
      private boolean allowAudioMixedSampleRateAdaptiveness;
      private boolean allowAudioMixedChannelCountAdaptiveness;
      private boolean allowAudioMixedDecoderSupportAdaptiveness;
      private boolean constrainAudioChannelCountToDeviceCapabilities;
      // General
      private boolean exceedRendererCapabilitiesIfNecessary;
      private boolean tunnelingEnabled;
      private boolean allowMultipleAdaptiveSelections;
      // Overrides
      private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
          selectionOverrides;
      private final SparseBooleanArray rendererDisabledFlags;

      /**
       * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
       *     #Builder(Context)} instead.
       */
      @Deprecated
      @SuppressWarnings({"deprecation"})
      public Builder() {
        super();
        selectionOverrides = new SparseArray<>();
        rendererDisabledFlags = new SparseBooleanArray();
        init();
      }

      /**
       * Creates a builder with default initial values.
       *
       * @param context Any context.
       */
      public Builder(Context context) {
        super(context);
        selectionOverrides = new SparseArray<>();
        rendererDisabledFlags = new SparseBooleanArray();
        init();
      }

      /**
       * @param initialValues The {@link Parameters} from which the initial values of the builder
       *     are obtained.
       */
      private Builder(Parameters initialValues) {
        super(initialValues);
        // Video
        exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary;
        allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness;
        allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness;
        allowVideoMixedDecoderSupportAdaptiveness =
            initialValues.allowVideoMixedDecoderSupportAdaptiveness;
        // Audio
        exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary;
        allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness;
        allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness;
        allowAudioMixedChannelCountAdaptiveness =
            initialValues.allowAudioMixedChannelCountAdaptiveness;
        allowAudioMixedDecoderSupportAdaptiveness =
            initialValues.allowAudioMixedDecoderSupportAdaptiveness;
        constrainAudioChannelCountToDeviceCapabilities =
            initialValues.constrainAudioChannelCountToDeviceCapabilities;
        // General
        exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary;
        tunnelingEnabled = initialValues.tunnelingEnabled;
        allowMultipleAdaptiveSelections = initialValues.allowMultipleAdaptiveSelections;
        // Overrides
        selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides);
        rendererDisabledFlags = initialValues.rendererDisabledFlags.clone();
      }

      @SuppressWarnings("method.invocation") // Only setter are invoked.
      private Builder(Bundle bundle) {
        super(bundle);
        init();
        Parameters defaultValue = Parameters.DEFAULT_WITHOUT_CONTEXT;
        // Video
        setExceedVideoConstraintsIfNecessary(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY),
                defaultValue.exceedVideoConstraintsIfNecessary));
        setAllowVideoMixedMimeTypeAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS),
                defaultValue.allowVideoMixedMimeTypeAdaptiveness));
        setAllowVideoNonSeamlessAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS),
                defaultValue.allowVideoNonSeamlessAdaptiveness));
        setAllowVideoMixedDecoderSupportAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(
                    Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
                defaultValue.allowVideoMixedDecoderSupportAdaptiveness));
        // Audio
        setExceedAudioConstraintsIfNecessary(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY),
                defaultValue.exceedAudioConstraintsIfNecessary));
        setAllowAudioMixedMimeTypeAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS),
                defaultValue.allowAudioMixedMimeTypeAdaptiveness));
        setAllowAudioMixedSampleRateAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS),
                defaultValue.allowAudioMixedSampleRateAdaptiveness));
        setAllowAudioMixedChannelCountAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(
                    Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS),
                defaultValue.allowAudioMixedChannelCountAdaptiveness));
        setAllowAudioMixedDecoderSupportAdaptiveness(
            bundle.getBoolean(
                Parameters.keyForField(
                    Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
                defaultValue.allowAudioMixedDecoderSupportAdaptiveness));
        setConstrainAudioChannelCountToDeviceCapabilities(
            bundle.getBoolean(
                Parameters.keyForField(
                    Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES),
                defaultValue.constrainAudioChannelCountToDeviceCapabilities));
        // General
        setExceedRendererCapabilitiesIfNecessary(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY),
                defaultValue.exceedRendererCapabilitiesIfNecessary));
        setTunnelingEnabled(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_TUNNELING_ENABLED),
                defaultValue.tunnelingEnabled));
        setAllowMultipleAdaptiveSelections(
            bundle.getBoolean(
                Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS),
                defaultValue.allowMultipleAdaptiveSelections));
        // Overrides
        selectionOverrides = new SparseArray<>();
        setSelectionOverridesFromBundle(bundle);
        rendererDisabledFlags =
            makeSparseBooleanArrayFromTrueKeys(
                bundle.getIntArray(
                    Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES)));
      }

      @Override
      protected Builder set(TrackSelectionParameters parameters) {
        super.set(parameters);
        return this;
      }

      // Video

      @Override
      public Builder setMaxVideoSizeSd() {
        super.setMaxVideoSizeSd();
        return this;
      }

      @Override
      public Builder clearVideoSizeConstraints() {
        super.clearVideoSizeConstraints();
        return this;
      }

      @Override
      public Builder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
        super.setMaxVideoSize(maxVideoWidth, maxVideoHeight);
        return this;
      }

      @Override
      public Builder setMaxVideoFrameRate(int maxVideoFrameRate) {
        super.setMaxVideoFrameRate(maxVideoFrameRate);
        return this;
      }

      @Override
      public Builder setMaxVideoBitrate(int maxVideoBitrate) {
        super.setMaxVideoBitrate(maxVideoBitrate);
        return this;
      }

      @Override
      public Builder setMinVideoSize(int minVideoWidth, int minVideoHeight) {
        super.setMinVideoSize(minVideoWidth, minVideoHeight);
        return this;
      }

      @Override
      public Builder setMinVideoFrameRate(int minVideoFrameRate) {
        super.setMinVideoFrameRate(minVideoFrameRate);
        return this;
      }

      @Override
      public Builder setMinVideoBitrate(int minVideoBitrate) {
        super.setMinVideoBitrate(minVideoBitrate);
        return this;
      }

      /**
       * Sets whether to exceed the {@link #setMaxVideoBitrate}, {@link #setMaxVideoSize(int, int)}
       * and {@link #setMaxVideoFrameRate} constraints when no selection can be made otherwise.
       *
       * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
       *     selection can be made otherwise.
       * @return This builder.
       */
      public Builder setExceedVideoConstraintsIfNecessary(
          boolean exceedVideoConstraintsIfNecessary) {
        this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
        return this;
      }

      /**
       * Sets whether to allow adaptive video selections containing mixed MIME types.
       *
       * <p>Adaptations between different MIME types may not be completely seamless, in which case
       * {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for
       * mixed MIME type selections to be made.
       *
       * @param allowVideoMixedMimeTypeAdaptiveness Whether to allow adaptive video selections
       *     containing mixed MIME types.
       * @return This builder.
       */
      public Builder setAllowVideoMixedMimeTypeAdaptiveness(
          boolean allowVideoMixedMimeTypeAdaptiveness) {
        this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;
        return this;
      }

      /**
       * Sets whether to allow adaptive video selections where adaptation may not be completely
       * seamless.
       *
       * @param allowVideoNonSeamlessAdaptiveness Whether to allow adaptive video selections where
       *     adaptation may not be completely seamless.
       * @return This builder.
       */
      public Builder setAllowVideoNonSeamlessAdaptiveness(
          boolean allowVideoNonSeamlessAdaptiveness) {
        this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;
        return this;
      }

      /**
       * Sets whether to allow adaptive video selections with mixed levels of {@link
       * RendererCapabilities.DecoderSupport} and {@link
       * RendererCapabilities.HardwareAccelerationSupport}.
       *
       * @param allowVideoMixedDecoderSupportAdaptiveness Whether to allow adaptive video selections
       *     with mixed levels of decoder and hardware acceleration support.
       * @return This builder.
       */
      public Builder setAllowVideoMixedDecoderSupportAdaptiveness(
          boolean allowVideoMixedDecoderSupportAdaptiveness) {
        this.allowVideoMixedDecoderSupportAdaptiveness = allowVideoMixedDecoderSupportAdaptiveness;
        return this;
      }

      @Override
      public Builder setViewportSizeToPhysicalDisplaySize(
          Context context, boolean viewportOrientationMayChange) {
        super.setViewportSizeToPhysicalDisplaySize(context, viewportOrientationMayChange);
        return this;
      }

      @Override
      public Builder clearViewportSizeConstraints() {
        super.clearViewportSizeConstraints();
        return this;
      }

      @Override
      public Builder setViewportSize(
          int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
        super.setViewportSize(viewportWidth, viewportHeight, viewportOrientationMayChange);
        return this;
      }

      @Override
      public Builder setPreferredVideoMimeType(@Nullable String mimeType) {
        super.setPreferredVideoMimeType(mimeType);
        return this;
      }

      @Override
      public Builder setPreferredVideoMimeTypes(String... mimeTypes) {
        super.setPreferredVideoMimeTypes(mimeTypes);
        return this;
      }

      @Override
      public Builder setPreferredVideoRoleFlags(@RoleFlags int preferredVideoRoleFlags) {
        super.setPreferredVideoRoleFlags(preferredVideoRoleFlags);
        return this;
      }

      // Audio

      @Override
      public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
        super.setPreferredAudioLanguage(preferredAudioLanguage);
        return this;
      }

      @Override
      public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) {
        super.setPreferredAudioLanguages(preferredAudioLanguages);
        return this;
      }

      @Override
      public Builder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) {
        super.setPreferredAudioRoleFlags(preferredAudioRoleFlags);
        return this;
      }

      @Override
      public Builder setMaxAudioChannelCount(int maxAudioChannelCount) {
        super.setMaxAudioChannelCount(maxAudioChannelCount);
        return this;
      }

      @Override
      public Builder setMaxAudioBitrate(int maxAudioBitrate) {
        super.setMaxAudioBitrate(maxAudioBitrate);
        return this;
      }

      /**
       * Sets whether to exceed the {@link #setMaxAudioChannelCount(int)} and {@link
       * #setMaxAudioBitrate(int)} constraints when no selection can be made otherwise.
       *
       * @param exceedAudioConstraintsIfNecessary Whether to exceed audio constraints when no
       *     selection can be made otherwise.
       * @return This builder.
       */
      public Builder setExceedAudioConstraintsIfNecessary(
          boolean exceedAudioConstraintsIfNecessary) {
        this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary;
        return this;
      }

      /**
       * Sets whether to allow adaptive audio selections containing mixed MIME types.
       *
       * <p>Adaptations between different MIME types may not be completely seamless.
       *
       * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections
       *     containing mixed MIME types.
       * @return This builder.
       */
      public Builder setAllowAudioMixedMimeTypeAdaptiveness(
          boolean allowAudioMixedMimeTypeAdaptiveness) {
        this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness;
        return this;
      }

      /**
       * Sets whether to allow adaptive audio selections containing mixed sample rates.
       *
       * <p>Adaptations between different sample rates may not be completely seamless.
       *
       * @param allowAudioMixedSampleRateAdaptiveness Whether to allow adaptive audio selections
       *     containing mixed sample rates.
       * @return This builder.
       */
      public Builder setAllowAudioMixedSampleRateAdaptiveness(
          boolean allowAudioMixedSampleRateAdaptiveness) {
        this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness;
        return this;
      }

      /**
       * Sets whether to allow adaptive audio selections containing mixed channel counts.
       *
       * <p>Adaptations between different channel counts may not be completely seamless.
       *
       * @param allowAudioMixedChannelCountAdaptiveness Whether to allow adaptive audio selections
       *     containing mixed channel counts.
       * @return This builder.
       */
      public Builder setAllowAudioMixedChannelCountAdaptiveness(
          boolean allowAudioMixedChannelCountAdaptiveness) {
        this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness;
        return this;
      }

      /**
       * Sets whether to allow adaptive audio selections with mixed levels of {@link
       * RendererCapabilities.DecoderSupport} and {@link
       * RendererCapabilities.HardwareAccelerationSupport}.
       *
       * @param allowAudioMixedDecoderSupportAdaptiveness Whether to allow adaptive audio selections
       *     with mixed levels of decoder and hardware acceleration support.
       * @return This builder.
       */
      public Builder setAllowAudioMixedDecoderSupportAdaptiveness(
          boolean allowAudioMixedDecoderSupportAdaptiveness) {
        this.allowAudioMixedDecoderSupportAdaptiveness = allowAudioMixedDecoderSupportAdaptiveness;
        return this;
      }

      @Override
      public Builder setPreferredAudioMimeType(@Nullable String mimeType) {
        super.setPreferredAudioMimeType(mimeType);
        return this;
      }

      @Override
      public Builder setPreferredAudioMimeTypes(String... mimeTypes) {
        super.setPreferredAudioMimeTypes(mimeTypes);
        return this;
      }

      /**
       * Whether to only select audio tracks with channel counts that don't exceed the device's
       * output capabilities. The default value is {@code true}.
       *
       * <p>When enabled, the track selector will prefer stereo/mono audio tracks over multichannel
       * if the audio cannot be spatialized or the device is outputting stereo audio. For example,
       * on a mobile device that outputs non-spatialized audio to its speakers. Dolby surround sound
       * formats are excluded from these constraints because some Dolby decoders are known to
       * spatialize multichannel audio on Android OS versions that don't support the {@link
       * Spatializer} API.
       *
       * <p>For devices with Android 12L+ that support {@linkplain Spatializer audio
       * spatialization}, when this is enabled the track selector will trigger a new track selection
       * everytime a change in {@linkplain Spatializer.OnSpatializerStateChangedListener
       * spatialization properties} is detected.
       *
       * <p>The constraints do not apply on devices with <a
       * href="https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier">{@code
       * television} UI mode</a>.
       *
       * <p>The constraints do not apply when the track selector is created without a reference to a
       * {@link Context} via the deprecated {@link
       * DefaultTrackSelector#DefaultTrackSelector(TrackSelectionParameters,
       * ExoTrackSelection.Factory)} constructor.
       */
      public Builder setConstrainAudioChannelCountToDeviceCapabilities(boolean enabled) {
        constrainAudioChannelCountToDeviceCapabilities = enabled;
        return this;
      }

      // Text

      @Override
      public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
          Context context) {
        super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
        return this;
      }

      @Override
      public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {
        super.setPreferredTextLanguage(preferredTextLanguage);
        return this;
      }

      @Override
      public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
        super.setPreferredTextLanguages(preferredTextLanguages);
        return this;
      }

      @Override
      public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
        super.setPreferredTextRoleFlags(preferredTextRoleFlags);
        return this;
      }

      @Override
      public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
        super.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags);
        return this;
      }

      @Override
      public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) {
        super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
        return this;
      }

      /**
       * @deprecated Use {@link #setIgnoredTextSelectionFlags}.
       */
      @Deprecated
      public Builder setDisabledTextTrackSelectionFlags(
          @C.SelectionFlags int disabledTextTrackSelectionFlags) {
        return setIgnoredTextSelectionFlags(disabledTextTrackSelectionFlags);
      }

      // General

      @Override
      public Builder setForceLowestBitrate(boolean forceLowestBitrate) {
        super.setForceLowestBitrate(forceLowestBitrate);
        return this;
      }

      @Override
      public Builder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
        super.setForceHighestSupportedBitrate(forceHighestSupportedBitrate);
        return this;
      }

      @Override
      public Builder addOverride(TrackSelectionOverride override) {
        super.addOverride(override);
        return this;
      }

      @Override
      public Builder clearOverride(TrackGroup trackGroup) {
        super.clearOverride(trackGroup);
        return this;
      }

      @Override
      public Builder setOverrideForType(TrackSelectionOverride override) {
        super.setOverrideForType(override);
        return this;
      }

      @Override
      public Builder clearOverridesOfType(@C.TrackType int trackType) {
        super.clearOverridesOfType(trackType);
        return this;
      }

      @Override
      public Builder clearOverrides() {
        super.clearOverrides();
        return this;
      }

      /**
       * @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
       */
      @Override
      @Deprecated
      @SuppressWarnings("deprecation")
      public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
        super.setDisabledTrackTypes(disabledTrackTypes);
        return this;
      }

      @Override
      public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
        super.setTrackTypeDisabled(trackType, disabled);
        return this;
      }

      /**
       * Sets whether to exceed renderer capabilities when no selection can be made otherwise.
       *
       * <p>This parameter applies when all of the tracks available for a renderer exceed the
       * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality
       * track will still be selected. Playback may succeed if the renderer has under-reported its
       * true capabilities. If {@code false} then no track will be selected.
       *
       * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when
       *     no selection can be made otherwise.
       * @return This builder.
       */
      public Builder setExceedRendererCapabilitiesIfNecessary(
          boolean exceedRendererCapabilitiesIfNecessary) {
        this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
        return this;
      }

      /**
       * Sets whether to enable tunneling if possible. Tunneling will only be enabled if it's
       * supported by the audio and video renderers for the selected tracks.
       *
       * <p>Tunneling is known to have many device specific issues and limitations. Manual testing
       * is strongly recommended to check that the media plays correctly when this option is
       * enabled. See [#9661](https://github.com/google/ExoPlayer/issues/9661),
       * [#9133](https://github.com/google/ExoPlayer/issues/9133),
       * [#9317](https://github.com/google/ExoPlayer/issues/9317),
       * [#9502](https://github.com/google/ExoPlayer/issues/9502).
       *
       * @param tunnelingEnabled Whether to enable tunneling if possible.
       * @return This builder.
       */
      public Builder setTunnelingEnabled(boolean tunnelingEnabled) {
        this.tunnelingEnabled = tunnelingEnabled;
        return this;
      }

      /**
       * Sets whether multiple adaptive selections with more than one track are allowed.
       *
       * @param allowMultipleAdaptiveSelections Whether multiple adaptive selections are allowed.
       * @return This builder.
       */
      public Builder setAllowMultipleAdaptiveSelections(boolean allowMultipleAdaptiveSelections) {
        this.allowMultipleAdaptiveSelections = allowMultipleAdaptiveSelections;
        return this;
      }

      // Overrides

      /**
       * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents
       * the selector from selecting any tracks for it.
       *
       * @param rendererIndex The renderer index.
       * @param disabled Whether the renderer is disabled.
       * @return This builder.
       */
      public Builder setRendererDisabled(int rendererIndex, boolean disabled) {
        if (rendererDisabledFlags.get(rendererIndex) == disabled) {
          // The disabled flag is unchanged.
          return this;
        }
        // Only true values are placed in the array to make it easier to check for equality.
        if (disabled) {
          rendererDisabledFlags.put(rendererIndex, true);
        } else {
          rendererDisabledFlags.delete(rendererIndex);
        }
        return this;
      }

      /**
       * Overrides the track selection for the renderer at the specified index.
       *
       * <p>When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the
       * override is applied. When the {@link TrackGroupArray} does not match, the override has no
       * effect. The override replaces any previous override for the specified {@link
       * TrackGroupArray} for the specified {@link Renderer}.
       *
       * <p>Passing a {@code null} override will cause the renderer to be disabled when the {@link
       * TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray}
       * does not match a {@code null} override has no effect. Hence a {@code null} override differs
       * from disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the
       * renderer is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as
       * {@link #setRendererDisabled(int, boolean)} disables the renderer unconditionally.
       *
       * <p>To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link
       * #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}.
       *
       * @param rendererIndex The renderer index.
       * @param groups The {@link TrackGroupArray} for which the override should be applied.
       * @param override The override.
       * @return This builder.
       * @deprecated Use {@link
       *     TrackSelectionParameters.Builder#addOverride(TrackSelectionOverride)}.
       */
      @Deprecated
      public Builder setSelectionOverride(
          int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) {
        Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
            selectionOverrides.get(rendererIndex);
        if (overrides == null) {
          overrides = new HashMap<>();
          selectionOverrides.put(rendererIndex, overrides);
        }
        if (overrides.containsKey(groups) && Util.areEqual(overrides.get(groups), override)) {
          // The override is unchanged.
          return this;
        }
        overrides.put(groups, override);
        return this;
      }

      /**
       * Clears a track selection override for the specified renderer and {@link TrackGroupArray}.
       *
       * @param rendererIndex The renderer index.
       * @param groups The {@link TrackGroupArray} for which the override should be cleared.
       * @return This builder.
       * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverride(TrackGroup)}.
       */
      @Deprecated
      public Builder clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {
        Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
            selectionOverrides.get(rendererIndex);
        if (overrides == null || !overrides.containsKey(groups)) {
          // Nothing to clear.
          return this;
        }
        overrides.remove(groups);
        if (overrides.isEmpty()) {
          selectionOverrides.remove(rendererIndex);
        }
        return this;
      }

      /**
       * Clears all track selection overrides for the specified renderer.
       *
       * @param rendererIndex The renderer index.
       * @return This builder.
       * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverridesOfType(int)}.
       */
      @Deprecated
      public Builder clearSelectionOverrides(int rendererIndex) {
        Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
            selectionOverrides.get(rendererIndex);
        if (overrides == null || overrides.isEmpty()) {
          // Nothing to clear.
          return this;
        }
        selectionOverrides.remove(rendererIndex);
        return this;
      }

      /**
       * Clears all track selection overrides for all renderers.
       *
       * @return This builder.
       * @deprecated Use {@link TrackSelectionParameters.Builder#clearOverrides()}.
       */
      @Deprecated
      public Builder clearSelectionOverrides() {
        if (selectionOverrides.size() == 0) {
          // Nothing to clear.
          return this;
        }
        selectionOverrides.clear();
        return this;
      }

      /** Builds a {@link Parameters} instance with the selected values. */
      @Override
      public Parameters build() {
        return new Parameters(this);
      }

      private void init(Builder this) {
        // Video
        exceedVideoConstraintsIfNecessary = true;
        allowVideoMixedMimeTypeAdaptiveness = false;
        allowVideoNonSeamlessAdaptiveness = true;
        allowVideoMixedDecoderSupportAdaptiveness = false;
        // Audio
        exceedAudioConstraintsIfNecessary = true;
        allowAudioMixedMimeTypeAdaptiveness = false;
        allowAudioMixedSampleRateAdaptiveness = false;
        allowAudioMixedChannelCountAdaptiveness = false;
        allowAudioMixedDecoderSupportAdaptiveness = false;
        constrainAudioChannelCountToDeviceCapabilities = true;
        // General
        exceedRendererCapabilitiesIfNecessary = true;
        tunnelingEnabled = false;
        allowMultipleAdaptiveSelections = true;
      }

      private static SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
          cloneSelectionOverrides(
              SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
                  selectionOverrides) {
        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> clone =
            new SparseArray<>();
        for (int i = 0; i < selectionOverrides.size(); i++) {
          clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i)));
        }
        return clone;
      }

      private void setSelectionOverridesFromBundle(Bundle bundle) {
        @Nullable
        int[] rendererIndices =
            bundle.getIntArray(
                Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES));
        @Nullable
        ArrayList<Bundle> trackGroupArrayBundles =
            bundle.getParcelableArrayList(
                Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS));
        List<TrackGroupArray> trackGroupArrays =
            trackGroupArrayBundles == null
                ? ImmutableList.of()
                : BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles);
        @Nullable
        SparseArray<Bundle> selectionOverrideBundles =
            bundle.getSparseParcelableArray(
                Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES));
        SparseArray<SelectionOverride> selectionOverrides =
            selectionOverrideBundles == null
                ? new SparseArray<>()
                : BundleableUtil.fromBundleSparseArray(
                    SelectionOverride.CREATOR, selectionOverrideBundles);

        if (rendererIndices == null || rendererIndices.length != trackGroupArrays.size()) {
          return; // Incorrect format, ignore all overrides.
        }
        for (int i = 0; i < rendererIndices.length; i++) {
          int rendererIndex = rendererIndices[i];
          TrackGroupArray groups = trackGroupArrays.get(i);
          @Nullable SelectionOverride selectionOverride = selectionOverrides.get(i);
          setSelectionOverride(rendererIndex, groups, selectionOverride);
        }
      }

      private SparseBooleanArray makeSparseBooleanArrayFromTrueKeys(@Nullable int[] trueKeys) {
        if (trueKeys == null) {
          return new SparseBooleanArray();
        }
        SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(trueKeys.length);
        for (int trueKey : trueKeys) {
          sparseBooleanArray.append(trueKey, true);
        }
        return sparseBooleanArray;
      }
    }

    /**
     * An instance with default values, except those obtained from the {@link Context}.
     *
     * <p>If possible, use {@link #getDefaults(Context)} instead.
     *
     * <p>This instance will not have the following settings:
     *
     * <ul>
     *   <li>{@linkplain Builder#setViewportSizeToPhysicalDisplaySize(Context, boolean) Viewport
     *       constraints} configured for the primary display.
     *   <li>{@linkplain
     *       Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
     *       Preferred text language and role flags} configured to the accessibility settings of
     *       {@link android.view.accessibility.CaptioningManager}.
     * </ul>
     */
    @SuppressWarnings("deprecation")
    public static final Parameters DEFAULT_WITHOUT_CONTEXT = new Builder().build();
    /**
     * @deprecated This instance is not configured using {@link Context} constraints. Use {@link
     *     #getDefaults(Context)} instead.
     */
    @Deprecated public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;

    /** Returns an instance configured with default values. */
    public static Parameters getDefaults(Context context) {
      return new Parameters.Builder(context).build();
    }

    // Video

    /**
     * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link
     * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is
     * {@code true}.
     */
    public final boolean exceedVideoConstraintsIfNecessary;
    /**
     * Whether to allow adaptive video selections containing mixed MIME types. Adaptations between
     * different MIME types may not be completely seamless, in which case {@link
     * #allowVideoNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type
     * selections to be made. The default value is {@code false}.
     */
    public final boolean allowVideoMixedMimeTypeAdaptiveness;
    /**
     * Whether to allow adaptive video selections where adaptation may not be completely seamless.
     * The default value is {@code true}.
     */
    public final boolean allowVideoNonSeamlessAdaptiveness;
    /**
     * Whether to allow adaptive video selections with mixed levels of {@link
     * RendererCapabilities.DecoderSupport} and {@link
     * RendererCapabilities.HardwareAccelerationSupport}.
     */
    public final boolean allowVideoMixedDecoderSupportAdaptiveness;

    // Audio

    /**
     * Whether to exceed the {@link #maxAudioChannelCount} and {@link #maxAudioBitrate} constraints
     * when no selection can be made otherwise. The default value is {@code true}.
     */
    public final boolean exceedAudioConstraintsIfNecessary;
    /**
     * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between
     * different MIME types may not be completely seamless. The default value is {@code false}.
     */
    public final boolean allowAudioMixedMimeTypeAdaptiveness;
    /**
     * Whether to allow adaptive audio selections containing mixed sample rates. Adaptations between
     * different sample rates may not be completely seamless. The default value is {@code false}.
     */
    public final boolean allowAudioMixedSampleRateAdaptiveness;
    /**
     * Whether to allow adaptive audio selections containing mixed channel counts. Adaptations
     * between different channel counts may not be completely seamless. The default value is {@code
     * false}.
     */
    public final boolean allowAudioMixedChannelCountAdaptiveness;
    /**
     * Whether to allow adaptive audio selections with mixed levels of {@link
     * RendererCapabilities.DecoderSupport} and {@link
     * RendererCapabilities.HardwareAccelerationSupport}.
     */
    public final boolean allowAudioMixedDecoderSupportAdaptiveness;
    /**
     * Whether to constrain audio track selection so that the selected track's channel count does
     * not exceed the device's output capabilities. The default value is {@code true}.
     */
    public final boolean constrainAudioChannelCountToDeviceCapabilities;

    // General

    /**
     * Whether to exceed renderer capabilities when no selection can be made otherwise.
     *
     * <p>This parameter applies when all of the tracks available for a renderer exceed the
     * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality
     * track will still be selected. Playback may succeed if the renderer has under-reported its
     * true capabilities. If {@code false} then no track will be selected. The default value is
     * {@code true}.
     */
    public final boolean exceedRendererCapabilitiesIfNecessary;
    /** Whether to enable tunneling if possible. */
    public final boolean tunnelingEnabled;
    /**
     * Whether multiple adaptive selections with more than one track are allowed. The default value
     * is {@code true}.
     *
     * <p>Note that tracks are only eligible for adaptation if they define a bitrate, the renderers
     * support the tracks and allow adaptation between them, and they are not excluded based on
     * other track selection parameters.
     */
    public final boolean allowMultipleAdaptiveSelections;

    // Overrides
    private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
        selectionOverrides;
    private final SparseBooleanArray rendererDisabledFlags;

    private Parameters(Builder builder) {
      super(builder);
      // Video
      exceedVideoConstraintsIfNecessary = builder.exceedVideoConstraintsIfNecessary;
      allowVideoMixedMimeTypeAdaptiveness = builder.allowVideoMixedMimeTypeAdaptiveness;
      allowVideoNonSeamlessAdaptiveness = builder.allowVideoNonSeamlessAdaptiveness;
      allowVideoMixedDecoderSupportAdaptiveness = builder.allowVideoMixedDecoderSupportAdaptiveness;
      // Audio
      exceedAudioConstraintsIfNecessary = builder.exceedAudioConstraintsIfNecessary;
      allowAudioMixedMimeTypeAdaptiveness = builder.allowAudioMixedMimeTypeAdaptiveness;
      allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness;
      allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness;
      allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness;
      constrainAudioChannelCountToDeviceCapabilities =
          builder.constrainAudioChannelCountToDeviceCapabilities;
      // General
      exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary;
      tunnelingEnabled = builder.tunnelingEnabled;
      allowMultipleAdaptiveSelections = builder.allowMultipleAdaptiveSelections;
      // Overrides
      selectionOverrides = builder.selectionOverrides;
      rendererDisabledFlags = builder.rendererDisabledFlags;
    }

    /**
     * Returns whether the renderer is disabled.
     *
     * @param rendererIndex The renderer index.
     * @return Whether the renderer is disabled.
     */
    public boolean getRendererDisabled(int rendererIndex) {
      return rendererDisabledFlags.get(rendererIndex);
    }

    /**
     * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}.
     *
     * @param rendererIndex The renderer index.
     * @param groups The {@link TrackGroupArray}.
     * @return Whether there is an override.
     * @deprecated Only works to retrieve the overrides set with the deprecated {@link
     *     Builder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use {@link
     *     TrackSelectionParameters#overrides} instead.
     */
    @Deprecated
    public boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {
      @Nullable
      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
          selectionOverrides.get(rendererIndex);
      return overrides != null && overrides.containsKey(groups);
    }

    /**
     * Returns the override for the specified renderer and {@link TrackGroupArray}.
     *
     * @param rendererIndex The renderer index.
     * @param groups The {@link TrackGroupArray}.
     * @return The override, or null if no override exists.
     * @deprecated Only works to retrieve the overrides set with the deprecated {@link
     *     Builder#setSelectionOverride(int, TrackGroupArray, SelectionOverride)}. Use {@link
     *     TrackSelectionParameters#overrides} instead.
     */
    @Deprecated
    @Nullable
    public SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
      @Nullable
      Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
          selectionOverrides.get(rendererIndex);
      return overrides != null ? overrides.get(groups) : null;
    }

    /** Creates a new {@link Parameters.Builder}, copying the initial values from this instance. */
    @Override
    public Parameters.Builder buildUpon() {
      return new Parameters.Builder(this);
    }

    @SuppressWarnings(
        "EqualsGetClass") // Class extends TrackSelectionParameters for backwards compatibility.
    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null || getClass() != obj.getClass()) {
        return false;
      }
      Parameters other = (Parameters) obj;
      return super.equals(other)
          // Video
          && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary
          && allowVideoMixedMimeTypeAdaptiveness == other.allowVideoMixedMimeTypeAdaptiveness
          && allowVideoNonSeamlessAdaptiveness == other.allowVideoNonSeamlessAdaptiveness
          && allowVideoMixedDecoderSupportAdaptiveness
              == other.allowVideoMixedDecoderSupportAdaptiveness
          // Audio
          && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary
          && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness
          && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness
          && allowAudioMixedChannelCountAdaptiveness
              == other.allowAudioMixedChannelCountAdaptiveness
          && allowAudioMixedDecoderSupportAdaptiveness
              == other.allowAudioMixedDecoderSupportAdaptiveness
          && constrainAudioChannelCountToDeviceCapabilities
              == other.constrainAudioChannelCountToDeviceCapabilities
          // General
          && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
          && tunnelingEnabled == other.tunnelingEnabled
          && allowMultipleAdaptiveSelections == other.allowMultipleAdaptiveSelections
          // Overrides
          && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags)
          && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides);
    }

    @Override
    public int hashCode() {
      int result = 1;
      result = 31 * result + super.hashCode();
      // Video
      result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);
      result = 31 * result + (allowVideoMixedMimeTypeAdaptiveness ? 1 : 0);
      result = 31 * result + (allowVideoNonSeamlessAdaptiveness ? 1 : 0);
      result = 31 * result + (allowVideoMixedDecoderSupportAdaptiveness ? 1 : 0);
      // Audio
      result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0);
      result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0);
      result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0);
      result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0);
      result = 31 * result + (allowAudioMixedDecoderSupportAdaptiveness ? 1 : 0);
      result = 31 * result + (constrainAudioChannelCountToDeviceCapabilities ? 1 : 0);
      // General
      result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
      result = 31 * result + (tunnelingEnabled ? 1 : 0);
      result = 31 * result + (allowMultipleAdaptiveSelections ? 1 : 0);
      // Overrides (omitted from hashCode).
      return result;
    }

    // Bundleable implementation.

    private static final int FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE;
    private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 1;
    private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 2;
    private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY = FIELD_CUSTOM_ID_BASE + 3;
    private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 4;
    private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 5;
    private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 6;
    private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY =
        FIELD_CUSTOM_ID_BASE + 7;
    private static final int FIELD_TUNNELING_ENABLED = FIELD_CUSTOM_ID_BASE + 8;
    private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = FIELD_CUSTOM_ID_BASE + 9;
    private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = FIELD_CUSTOM_ID_BASE + 10;
    private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS =
        FIELD_CUSTOM_ID_BASE + 11;
    private static final int FIELD_SELECTION_OVERRIDES = FIELD_CUSTOM_ID_BASE + 12;
    private static final int FIELD_RENDERER_DISABLED_INDICES = FIELD_CUSTOM_ID_BASE + 13;
    private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 14;
    private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS =
        FIELD_CUSTOM_ID_BASE + 15;
    private static final int FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES =
        FIELD_CUSTOM_ID_BASE + 16;

    @Override
    public Bundle toBundle() {
      Bundle bundle = super.toBundle();

      // Video
      bundle.putBoolean(
          keyForField(FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY),
          exceedVideoConstraintsIfNecessary);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS),
          allowVideoMixedMimeTypeAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS),
          allowVideoNonSeamlessAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
          allowVideoMixedDecoderSupportAdaptiveness);
      // Audio
      bundle.putBoolean(
          keyForField(FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY),
          exceedAudioConstraintsIfNecessary);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS),
          allowAudioMixedMimeTypeAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS),
          allowAudioMixedSampleRateAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS),
          allowAudioMixedChannelCountAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
          allowAudioMixedDecoderSupportAdaptiveness);
      bundle.putBoolean(
          keyForField(FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES),
          constrainAudioChannelCountToDeviceCapabilities);
      // General
      bundle.putBoolean(
          keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY),
          exceedRendererCapabilitiesIfNecessary);
      bundle.putBoolean(keyForField(FIELD_TUNNELING_ENABLED), tunnelingEnabled);
      bundle.putBoolean(
          keyForField(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), allowMultipleAdaptiveSelections);

      putSelectionOverridesToBundle(bundle, selectionOverrides);
      // Only true values are put into rendererDisabledFlags.
      bundle.putIntArray(
          keyForField(FIELD_RENDERER_DISABLED_INDICES),
          getKeysFromSparseBooleanArray(rendererDisabledFlags));

      return bundle;
    }

    /** Object that can restore {@code Parameters} from a {@link Bundle}. */
    public static final Creator<Parameters> CREATOR =
        bundle -> new Parameters.Builder(bundle).build();

    /**
     * Bundles selection overrides in 3 arrays of equal length. Each triplet of matching indices is:
     * the selection override (stored in a sparse array as they can be null), the trackGroupArray of
     * that override, the rendererIndex of that override.
     */
    private static void putSelectionOverridesToBundle(
        Bundle bundle,
        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {
      ArrayList<Integer> rendererIndices = new ArrayList<>();
      ArrayList<TrackGroupArray> trackGroupArrays = new ArrayList<>();
      SparseArray<SelectionOverride> selections = new SparseArray<>();

      for (int i = 0; i < selectionOverrides.size(); i++) {
        int rendererIndex = selectionOverrides.keyAt(i);
        for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> override :
            selectionOverrides.valueAt(i).entrySet()) {
          @Nullable SelectionOverride selection = override.getValue();
          if (selection != null) {
            selections.put(trackGroupArrays.size(), selection);
          }
          trackGroupArrays.add(override.getKey());
          rendererIndices.add(rendererIndex);
        }
        bundle.putIntArray(
            keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDICES), Ints.toArray(rendererIndices));
        bundle.putParcelableArrayList(
            keyForField(FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS),
            BundleableUtil.toBundleArrayList(trackGroupArrays));
        bundle.putSparseParcelableArray(
            keyForField(FIELD_SELECTION_OVERRIDES), BundleableUtil.toBundleSparseArray(selections));
      }
    }

    private static int[] getKeysFromSparseBooleanArray(SparseBooleanArray sparseBooleanArray) {
      int[] keys = new int[sparseBooleanArray.size()];
      for (int i = 0; i < sparseBooleanArray.size(); i++) {
        keys[i] = sparseBooleanArray.keyAt(i);
      }
      return keys;
    }

    private static boolean areRendererDisabledFlagsEqual(
        SparseBooleanArray first, SparseBooleanArray second) {
      int firstSize = first.size();
      if (second.size() != firstSize) {
        return false;
      }
      // Only true values are put into rendererDisabledFlags, so we don't need to compare values.
      for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) {
        if (second.indexOfKey(first.keyAt(indexInFirst)) < 0) {
          return false;
        }
      }
      return true;
    }

    private static boolean areSelectionOverridesEqual(
        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> first,
        SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> second) {
      int firstSize = first.size();
      if (second.size() != firstSize) {
        return false;
      }
      for (int indexInFirst = 0; indexInFirst < firstSize; indexInFirst++) {
        int indexInSecond = second.indexOfKey(first.keyAt(indexInFirst));
        if (indexInSecond < 0
            || !areSelectionOverridesEqual(
                first.valueAt(indexInFirst), second.valueAt(indexInSecond))) {
          return false;
        }
      }
      return true;
    }

    private static boolean areSelectionOverridesEqual(
        Map<TrackGroupArray, @NullableType SelectionOverride> first,
        Map<TrackGroupArray, @NullableType SelectionOverride> second) {
      int firstSize = first.size();
      if (second.size() != firstSize) {
        return false;
      }
      for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> firstEntry :
          first.entrySet()) {
        TrackGroupArray key = firstEntry.getKey();
        if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) {
          return false;
        }
      }
      return true;
    }
  }

  /** A track selection override. */
  public static final class SelectionOverride implements Bundleable {

    public final int groupIndex;
    public final int[] tracks;
    public final int length;
    public final @TrackSelection.Type int type;

    /**
     * Constructs a {@code SelectionOverride} to override tracks of a group.
     *
     * @param groupIndex The overriding track group index.
     * @param tracks The overriding track indices within the track group.
     */
    public SelectionOverride(int groupIndex, int... tracks) {
      this(groupIndex, tracks, TrackSelection.TYPE_UNSET);
    }

    /**
     * Constructs a {@code SelectionOverride} of the given type to override tracks of a group.
     *
     * @param groupIndex The overriding track group index.
     * @param tracks The overriding track indices within the track group.
     * @param type The type that will be returned from {@link TrackSelection#getType()}.
     */
    @UnstableApi
    public SelectionOverride(int groupIndex, int[] tracks, @TrackSelection.Type int type) {
      this.groupIndex = groupIndex;
      this.tracks = Arrays.copyOf(tracks, tracks.length);
      this.length = tracks.length;
      this.type = type;
      Arrays.sort(this.tracks);
    }

    /** Returns whether this override contains the specified track index. */
    public boolean containsTrack(int track) {
      for (int overrideTrack : tracks) {
        if (overrideTrack == track) {
          return true;
        }
      }
      return false;
    }

    @Override
    public int hashCode() {
      int hash = 31 * groupIndex + Arrays.hashCode(tracks);
      return 31 * hash + type;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null || getClass() != obj.getClass()) {
        return false;
      }
      SelectionOverride other = (SelectionOverride) obj;
      return groupIndex == other.groupIndex
          && Arrays.equals(tracks, other.tracks)
          && type == other.type;
    }

    // Bundleable implementation.

    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef({
      FIELD_GROUP_INDEX,
      FIELD_TRACKS,
      FIELD_TRACK_TYPE,
    })
    private @interface FieldNumber {}

    private static final int FIELD_GROUP_INDEX = 0;
    private static final int FIELD_TRACKS = 1;
    private static final int FIELD_TRACK_TYPE = 2;

    @UnstableApi
    @Override
    public Bundle toBundle() {
      Bundle bundle = new Bundle();
      bundle.putInt(keyForField(FIELD_GROUP_INDEX), groupIndex);
      bundle.putIntArray(keyForField(FIELD_TRACKS), tracks);
      bundle.putInt(keyForField(FIELD_TRACK_TYPE), type);
      return bundle;
    }

    /** Object that can restore {@code SelectionOverride} from a {@link Bundle}. */
    @UnstableApi
    public static final Creator<SelectionOverride> CREATOR =
        bundle -> {
          int groupIndex = bundle.getInt(keyForField(FIELD_GROUP_INDEX), -1);
          @Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS));
          int trackType = bundle.getInt(keyForField(FIELD_TRACK_TYPE), -1);
          Assertions.checkArgument(groupIndex >= 0 && trackType >= 0);
          Assertions.checkNotNull(tracks);
          return new SelectionOverride(groupIndex, tracks, trackType);
        };

    private static String keyForField(@FieldNumber int field) {
      return Integer.toString(field, Character.MAX_RADIX);
    }
  }

  /**
   * The extent to which tracks are eligible for selection. One of {@link
   * #SELECTION_ELIGIBILITY_NO}, {@link #SELECTION_ELIGIBILITY_FIXED} or {@link
   * #SELECTION_ELIGIBILITY_ADAPTIVE}.
   */
  @Documented
  @Retention(RetentionPolicy.SOURCE)
  @Target(TYPE_USE)
  @IntDef({SELECTION_ELIGIBILITY_NO, SELECTION_ELIGIBILITY_FIXED, SELECTION_ELIGIBILITY_ADAPTIVE})
  protected @interface SelectionEligibility {}

  /** Track is not eligible for selection. */
  protected static final int SELECTION_ELIGIBILITY_NO = 0;
  /** Track is eligible for a fixed selection with one track. */
  protected static final int SELECTION_ELIGIBILITY_FIXED = 1;
  /**
   * Track is eligible for both a fixed selection and as part of an adaptive selection with multiple
   * tracks.
   */
  protected static final int SELECTION_ELIGIBILITY_ADAPTIVE = 2;

  /**
   * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the
   * corresponding viewport dimension, then the video is considered as filling the viewport (in that
   * dimension).
   */
  private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f;

  /** Ordering of two format values. A known value is considered greater than Format#NO_VALUE. */
  private static final Ordering<Integer> FORMAT_VALUE_ORDERING =
      Ordering.from(
          (first, second) ->
              first == Format.NO_VALUE
                  ? (second == Format.NO_VALUE ? 0 : -1)
                  : (second == Format.NO_VALUE ? 1 : (first - second)));
  /** Ordering where all elements are equal. */
  private static final Ordering<Integer> NO_ORDER = Ordering.from((first, second) -> 0);

  private final Object lock;
  @Nullable public final Context context;
  private final ExoTrackSelection.Factory trackSelectionFactory;
  private final boolean deviceIsTV;

  @GuardedBy("lock")
  private Parameters parameters;

  @GuardedBy("lock")
  @Nullable
  private SpatializerWrapperV32 spatializer;

  @GuardedBy("lock")
  private AudioAttributes audioAttributes;

  /**
   * @deprecated Use {@link #DefaultTrackSelector(Context)} instead.
   */
  @Deprecated
  public DefaultTrackSelector() {
    this(Parameters.DEFAULT_WITHOUT_CONTEXT, new AdaptiveTrackSelection.Factory());
  }

  /**
   * @param context Any {@link Context}.
   */
  public DefaultTrackSelector(Context context) {
    this(context, new AdaptiveTrackSelection.Factory());
  }

  /**
   * @param context Any {@link Context}.
   * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s.
   */
  public DefaultTrackSelector(Context context, ExoTrackSelection.Factory trackSelectionFactory) {
    this(context, Parameters.getDefaults(context), trackSelectionFactory);
  }

  /**
   * @param context Any {@link Context}.
   * @param parameters Initial {@link TrackSelectionParameters}.
   */
  public DefaultTrackSelector(Context context, TrackSelectionParameters parameters) {
    this(context, parameters, new AdaptiveTrackSelection.Factory());
  }

  /**
   * @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelectionParameters,
   *     ExoTrackSelection.Factory)}
   */
  @Deprecated
  public DefaultTrackSelector(
      TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) {
    this(parameters, trackSelectionFactory, /* context= */ null);
  }

  /**
   * @param context Any {@link Context}.
   * @param parameters Initial {@link TrackSelectionParameters}.
   * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s.
   */
  public DefaultTrackSelector(
      Context context,
      TrackSelectionParameters parameters,
      ExoTrackSelection.Factory trackSelectionFactory) {
    this(parameters, trackSelectionFactory, context);
  }

  /**
   * Exists for backwards compatibility so that the deprecated constructor {@link
   * #DefaultTrackSelector(TrackSelectionParameters, ExoTrackSelection.Factory)} can initialize
   * {@code context} with {@code null} while we don't have a public constructor with a {@code
   * Nullable context}.
   *
   * @param context Any {@link Context}.
   * @param parameters Initial {@link TrackSelectionParameters}.
   * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s.
   */
  private DefaultTrackSelector(
      TrackSelectionParameters parameters,
      ExoTrackSelection.Factory trackSelectionFactory,
      @Nullable Context context) {
    this.lock = new Object();
    this.context = context != null ? context.getApplicationContext() : null;
    this.trackSelectionFactory = trackSelectionFactory;
    if (parameters instanceof Parameters) {
      this.parameters = (Parameters) parameters;
    } else {
      Parameters defaultParameters =
          context == null ? Parameters.DEFAULT_WITHOUT_CONTEXT : Parameters.getDefaults(context);
      this.parameters = defaultParameters.buildUpon().set(parameters).build();
    }
    this.audioAttributes = AudioAttributes.DEFAULT;
    this.deviceIsTV = context != null && Util.isTv(context);
    if (!deviceIsTV && context != null && Util.SDK_INT >= 32) {
      spatializer = SpatializerWrapperV32.tryCreateInstance(context);
    }
    if (this.parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) {
      Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE);
    }
  }

  @Override
  public void release() {
    synchronized (lock) {
      if (Util.SDK_INT >= 32 && spatializer != null) {
        spatializer.release();
      }
    }
    super.release();
  }

  @Override
  public Parameters getParameters() {
    synchronized (lock) {
      return parameters;
    }
  }

  @Override
  public boolean isSetParametersSupported() {
    return true;
  }

  @Override
  public void setParameters(TrackSelectionParameters parameters) {
    if (parameters instanceof Parameters) {
      setParametersInternal((Parameters) parameters);
    }
    // Only add the fields of `TrackSelectionParameters` to `parameters`.
    Parameters mergedParameters = new Parameters.Builder(getParameters()).set(parameters).build();
    setParametersInternal(mergedParameters);
  }

  @Override
  public void setAudioAttributes(AudioAttributes audioAttributes) {
    boolean audioAttributesChanged;
    synchronized (lock) {
      audioAttributesChanged = !this.audioAttributes.equals(audioAttributes);
      this.audioAttributes = audioAttributes;
    }
    if (audioAttributesChanged) {
      maybeInvalidateForAudioChannelCountConstraints();
    }
  }

  /**
   * @deprecated Use {@link #setParameters(Parameters.Builder)} instead.
   */
  @Deprecated
  @SuppressWarnings("deprecation") // Allow setting the deprecated builder
  public void setParameters(ParametersBuilder parametersBuilder) {
    setParametersInternal(parametersBuilder.build());
  }

  /**
   * Atomically sets the provided parameters for track selection.
   *
   * @param parametersBuilder A builder from which to obtain the parameters for track selection.
   */
  public void setParameters(Parameters.Builder parametersBuilder) {
    setParametersInternal(parametersBuilder.build());
  }

  /** Returns a new {@link Parameters.Builder} initialized with the current selection parameters. */
  public Parameters.Builder buildUponParameters() {
    return getParameters().buildUpon();
  }

  /**
   * Atomically sets the provided {@link Parameters} for track selection.
   *
   * @param parameters The parameters for track selection.
   */
  private void setParametersInternal(Parameters parameters) {
    Assertions.checkNotNull(parameters);
    boolean parametersChanged;
    synchronized (lock) {
      parametersChanged = !this.parameters.equals(parameters);
      this.parameters = parameters;
    }

    if (parametersChanged) {
      if (parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) {
        Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE);
      }
      invalidate();
    }
  }

  // MappingTrackSelector implementation.

  @Override
  protected final Pair<@NullableType RendererConfiguration[], @NullableType ExoTrackSelection[]>
      selectTracks(
          MappedTrackInfo mappedTrackInfo,
          @Capabilities int[][][] rendererFormatSupports,
          @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
          MediaPeriodId mediaPeriodId,
          Timeline timeline)
          throws ExoPlaybackException {
    Parameters parameters;
    synchronized (lock) {
      parameters = this.parameters;
      if (parameters.constrainAudioChannelCountToDeviceCapabilities
          && Util.SDK_INT >= 32
          && spatializer != null) {
        // Initialize the spatializer now so we can get a reference to the playback looper with
        // Looper.myLooper().
        spatializer.ensureInitialized(this, checkStateNotNull(Looper.myLooper()));
      }
    }
    int rendererCount = mappedTrackInfo.getRendererCount();
    ExoTrackSelection.@NullableType Definition[] definitions =
        selectAllTracks(
            mappedTrackInfo,
            rendererFormatSupports,
            rendererMixedMimeTypeAdaptationSupports,
            parameters);

    applyTrackSelectionOverrides(mappedTrackInfo, parameters, definitions);
    applyLegacyRendererOverrides(mappedTrackInfo, parameters, definitions);

    // Disable renderers if needed.
    for (int i = 0; i < rendererCount; i++) {
      @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i);
      if (parameters.getRendererDisabled(i)
          || parameters.disabledTrackTypes.contains(rendererType)) {
        definitions[i] = null;
      }
    }

    @NullableType
    ExoTrackSelection[] rendererTrackSelections =
        trackSelectionFactory.createTrackSelections(
            definitions, getBandwidthMeter(), mediaPeriodId, timeline);

    // Initialize the renderer configurations to the default configuration for all renderers with
    // selections, and null otherwise.
    @NullableType
    RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount];
    for (int i = 0; i < rendererCount; i++) {
      @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i);
      boolean forceRendererDisabled =
          parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType);
      boolean rendererEnabled =
          !forceRendererDisabled
              && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE
                  || rendererTrackSelections[i] != null);
      rendererConfigurations[i] = rendererEnabled ? RendererConfiguration.DEFAULT : null;
    }

    // Configure audio and video renderers to use tunneling if appropriate.
    if (parameters.tunnelingEnabled) {
      maybeConfigureRenderersForTunneling(
          mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections);
    }

    return Pair.create(rendererConfigurations, rendererTrackSelections);
  }

  // Track selection prior to overrides and disabled flags being applied.

  /**
   * Called from {@link #selectTracks(MappedTrackInfo, int[][][], int[], MediaPeriodId, Timeline)}
   * to make a track selection for each renderer, prior to overrides and disabled flags being
   * applied.
   *
   * <p>The implementation should not account for overrides and disabled flags. Track selections
   * generated by this method will be overridden to account for these properties.
   *
   * @param mappedTrackInfo Mapped track information.
   * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by
   *     renderer, track group and track (in that order).
   * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type
   *     adaptation for the renderer.
   * @return The {@link ExoTrackSelection.Definition}s for the renderers. A null entry indicates no
   *     selection was made.
   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
   */
  protected ExoTrackSelection.@NullableType Definition[] selectAllTracks(
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] rendererFormatSupports,
      @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
      Parameters params)
      throws ExoPlaybackException {
    int rendererCount = mappedTrackInfo.getRendererCount();
    ExoTrackSelection.@NullableType Definition[] definitions =
        new ExoTrackSelection.Definition[rendererCount];

    @Nullable
    Pair<ExoTrackSelection.Definition, Integer> selectedVideo =
        selectVideoTrack(
            mappedTrackInfo,
            rendererFormatSupports,
            rendererMixedMimeTypeAdaptationSupports,
            params);
    if (selectedVideo != null) {
      definitions[selectedVideo.second] = selectedVideo.first;
    }

    @Nullable
    Pair<ExoTrackSelection.Definition, Integer> selectedAudio =
        selectAudioTrack(
            mappedTrackInfo,
            rendererFormatSupports,
            rendererMixedMimeTypeAdaptationSupports,
            params);
    if (selectedAudio != null) {
      definitions[selectedAudio.second] = selectedAudio.first;
    }

    @Nullable
    String selectedAudioLanguage =
        selectedAudio == null
            ? null
            : selectedAudio.first.group.getFormat(selectedAudio.first.tracks[0]).language;
    @Nullable
    Pair<ExoTrackSelection.Definition, Integer> selectedText =
        selectTextTrack(mappedTrackInfo, rendererFormatSupports, params, selectedAudioLanguage);
    if (selectedText != null) {
      definitions[selectedText.second] = selectedText.first;
    }

    for (int i = 0; i < rendererCount; i++) {
      int trackType = mappedTrackInfo.getRendererType(i);
      if (trackType != C.TRACK_TYPE_VIDEO
          && trackType != C.TRACK_TYPE_AUDIO
          && trackType != C.TRACK_TYPE_TEXT) {
        definitions[i] =
            selectOtherTrack(
                trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params);
      }
    }

    return definitions;
  }

  // Video track selection implementation.

  /**
   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
   * {@link ExoTrackSelection.Definition} for a video track selection.
   *
   * @param mappedTrackInfo Mapped track information.
   * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by
   *     renderer, track group and track (in that order).
   * @param mixedMimeTypeSupports The {@link AdaptiveSupport} for mixed MIME type adaptation for the
   *     renderer.
   * @param params The selector's current constraint parameters.
   * @return A pair of the selected {@link ExoTrackSelection.Definition} and the corresponding
   *     renderer index, or null if no selection was made.
   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
   */
  @Nullable
  protected Pair<ExoTrackSelection.Definition, Integer> selectVideoTrack(
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] rendererFormatSupports,
      @AdaptiveSupport int[] mixedMimeTypeSupports,
      Parameters params)
      throws ExoPlaybackException {
    return selectTracksForType(
        C.TRACK_TYPE_VIDEO,
        mappedTrackInfo,
        rendererFormatSupports,
        (int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
            VideoTrackInfo.createForTrackGroup(
                rendererIndex, group, params, support, mixedMimeTypeSupports[rendererIndex]),
        VideoTrackInfo::compareSelections);
  }

  // Audio track selection implementation.

  /**
   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
   * {@link ExoTrackSelection.Definition} for an audio track selection.
   *
   * @param mappedTrackInfo Mapped track information.
   * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by
   *     renderer, track group and track (in that order).
   * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type
   *     adaptation for the renderer.
   * @param params The selector's current constraint parameters.
   * @return A pair of the selected {@link ExoTrackSelection.Definition} and the corresponding
   *     renderer index, or null if no selection was made.
   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
   */
  @Nullable
  protected Pair<ExoTrackSelection.Definition, Integer> selectAudioTrack(
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] rendererFormatSupports,
      @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
      Parameters params)
      throws ExoPlaybackException {
    boolean hasVideoRendererWithMappedTracks = false;
    for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
      if (C.TRACK_TYPE_VIDEO == mappedTrackInfo.getRendererType(i)
          && mappedTrackInfo.getTrackGroups(i).length > 0) {
        hasVideoRendererWithMappedTracks = true;
        break;
      }
    }
    boolean hasVideoRendererWithMappedTracksFinal = hasVideoRendererWithMappedTracks;
    return selectTracksForType(
        C.TRACK_TYPE_AUDIO,
        mappedTrackInfo,
        rendererFormatSupports,
        (int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
            AudioTrackInfo.createForTrackGroup(
                rendererIndex,
                group,
                params,
                support,
                hasVideoRendererWithMappedTracksFinal,
                this::isAudioFormatWithinAudioChannelCountConstraints),
        AudioTrackInfo::compareSelections);
  }

  /**
   * Returns whether an audio format is within the audio channel count constraints.
   *
   * <p>This method returns {@code true} if one of the following holds:
   *
   * <ul>
   *   <li>Audio channel count constraints are not applicable (all formats are considered within
   *       constraints).
   *   <li>The device has a <a
   *       href="https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier">{@code
   *       television} UI mode</a>.
   *   <li>{@code format} has up to 2 channels.
   *   <li>The device does not support audio spatialization and the format is {@linkplain
   *       #isDolbyAudio(Format) a Dolby one}.
   *   <li>Audio spatialization is applicable and {@code format} can be spatialized.
   * </ul>
   */
  private boolean isAudioFormatWithinAudioChannelCountConstraints(Format format) {
    synchronized (lock) {
      return !parameters.constrainAudioChannelCountToDeviceCapabilities
          || deviceIsTV
          || format.channelCount <= 2
          || (isDolbyAudio(format)
              && (Util.SDK_INT < 32
                  || spatializer == null
                  || !spatializer.isSpatializationSupported()))
          || (Util.SDK_INT >= 32
              && spatializer != null
              && spatializer.isSpatializationSupported()
              && spatializer.isAvailable()
              && spatializer.isEnabled()
              && spatializer.canBeSpatialized(audioAttributes, format));
    }
  }

  // Text track selection implementation.

  /**
   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
   * {@link ExoTrackSelection.Definition} for a text track selection.
   *
   * @param mappedTrackInfo Mapped track information.
   * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by
   *     renderer, track group and track (in that order).
   * @param params The selector's current constraint parameters.
   * @param selectedAudioLanguage The language of the selected audio track. May be null if the
   *     selected audio track declares no language or no audio track was selected.
   * @return A pair of the selected {@link ExoTrackSelection.Definition} and the corresponding
   *     renderer index, or null if no selection was made.
   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
   */
  @Nullable
  protected Pair<ExoTrackSelection.Definition, Integer> selectTextTrack(
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] rendererFormatSupports,
      Parameters params,
      @Nullable String selectedAudioLanguage)
      throws ExoPlaybackException {
    return selectTracksForType(
        C.TRACK_TYPE_TEXT,
        mappedTrackInfo,
        rendererFormatSupports,
        (int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
            TextTrackInfo.createForTrackGroup(
                rendererIndex, group, params, support, selectedAudioLanguage),
        TextTrackInfo::compareSelections);
  }

  // Generic track selection methods.

  /**
   * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
   * {@link ExoTrackSelection} for a renderer whose type is neither video, audio or text.
   *
   * @param trackType The type of the renderer.
   * @param groups The {@link TrackGroupArray} mapped to the renderer.
   * @param formatSupport The {@link Capabilities} for each mapped track, indexed by track group and
   *     track (in that order).
   * @param params The selector's current constraint parameters.
   * @return The {@link ExoTrackSelection} for the renderer, or null if no selection was made.
   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
   */
  @Nullable
  protected ExoTrackSelection.Definition selectOtherTrack(
      int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params)
      throws ExoPlaybackException {
    @Nullable TrackGroup selectedGroup = null;
    int selectedTrackIndex = 0;
    @Nullable OtherTrackScore selectedTrackScore = null;
    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
      TrackGroup trackGroup = groups.get(groupIndex);
      @Capabilities int[] trackFormatSupport = formatSupport[groupIndex];
      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
        if (isSupported(
            trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) {
          Format format = trackGroup.getFormat(trackIndex);
          OtherTrackScore trackScore = new OtherTrackScore(format, trackFormatSupport[trackIndex]);
          if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) {
            selectedGroup = trackGroup;
            selectedTrackIndex = trackIndex;
            selectedTrackScore = trackScore;
          }
        }
      }
    }
    return selectedGroup == null
        ? null
        : new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex);
  }

  @Nullable
  private <T extends TrackInfo<T>> Pair<ExoTrackSelection.Definition, Integer> selectTracksForType(
      @C.TrackType int trackType,
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] formatSupport,
      TrackInfo.Factory<T> trackInfoFactory,
      Comparator<List<T>> selectionComparator) {
    ArrayList<List<T>> possibleSelections = new ArrayList<>();
    int rendererCount = mappedTrackInfo.getRendererCount();
    for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
      if (trackType == mappedTrackInfo.getRendererType(rendererIndex)) {
        TrackGroupArray groups = mappedTrackInfo.getTrackGroups(rendererIndex);
        for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
          TrackGroup trackGroup = groups.get(groupIndex);
          @Capabilities int[] groupSupport = formatSupport[rendererIndex][groupIndex];
          List<T> trackInfos = trackInfoFactory.create(rendererIndex, trackGroup, groupSupport);
          boolean[] usedTrackInSelection = new boolean[trackGroup.length];
          for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
            T trackInfo = trackInfos.get(trackIndex);
            @SelectionEligibility int eligibility = trackInfo.getSelectionEligibility();
            if (usedTrackInSelection[trackIndex] || eligibility == SELECTION_ELIGIBILITY_NO) {
              continue;
            }
            List<T> selection;
            if (eligibility == SELECTION_ELIGIBILITY_FIXED) {
              selection = ImmutableList.of(trackInfo);
            } else {
              selection = new ArrayList<>();
              selection.add(trackInfo);
              for (int i = trackIndex + 1; i < trackGroup.length; i++) {
                T otherTrackInfo = trackInfos.get(i);
                if (otherTrackInfo.getSelectionEligibility() == SELECTION_ELIGIBILITY_ADAPTIVE) {
                  if (trackInfo.isCompatibleForAdaptationWith(otherTrackInfo)) {
                    selection.add(otherTrackInfo);
                    usedTrackInSelection[i] = true;
                  }
                }
              }
            }
            possibleSelections.add(selection);
          }
        }
      }
    }
    if (possibleSelections.isEmpty()) {
      return null;
    }
    List<T> bestSelection = max(possibleSelections, selectionComparator);
    int[] trackIndices = new int[bestSelection.size()];
    for (int i = 0; i < bestSelection.size(); i++) {
      trackIndices[i] = bestSelection.get(i).trackIndex;
    }
    T firstTrackInfo = bestSelection.get(0);
    return Pair.create(
        new ExoTrackSelection.Definition(firstTrackInfo.trackGroup, trackIndices),
        firstTrackInfo.rendererIndex);
  }

  private void maybeInvalidateForAudioChannelCountConstraints() {
    boolean shouldInvalidate;
    synchronized (lock) {
      shouldInvalidate =
          parameters.constrainAudioChannelCountToDeviceCapabilities
              && !deviceIsTV
              && Util.SDK_INT >= 32
              && spatializer != null
              && spatializer.isSpatializationSupported();
    }
    if (shouldInvalidate) {
      invalidate();
    }
  }

  // Utility methods.

  private static void applyTrackSelectionOverrides(
      MappedTrackInfo mappedTrackInfo,
      TrackSelectionParameters params,
      ExoTrackSelection.@NullableType Definition[] outDefinitions) {
    int rendererCount = mappedTrackInfo.getRendererCount();

    // Determine overrides to apply.
    HashMap<@C.TrackType Integer, TrackSelectionOverride> overridesByType = new HashMap<>();
    for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
      collectTrackSelectionOverrides(
          mappedTrackInfo.getTrackGroups(rendererIndex), params, overridesByType);
    }
    collectTrackSelectionOverrides(
        mappedTrackInfo.getUnmappedTrackGroups(), params, overridesByType);

    // Apply the overrides.
    for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
      @C.TrackType int trackType = mappedTrackInfo.getRendererType(rendererIndex);
      @Nullable TrackSelectionOverride overrideForType = overridesByType.get(trackType);
      if (overrideForType == null) {
        continue;
      }
      // If the override is non-empty and applies to this renderer, then apply it. Else we don't
      // want the renderer to be enabled at all, so clear any existing selection.
      @Nullable ExoTrackSelection.Definition selection;
      if (!overrideForType.trackIndices.isEmpty()
          && mappedTrackInfo.getTrackGroups(rendererIndex).indexOf(overrideForType.mediaTrackGroup)
              != -1) {
        selection =
            new ExoTrackSelection.Definition(
                overrideForType.mediaTrackGroup, Ints.toArray(overrideForType.trackIndices));
      } else {
        selection = null;
      }
      outDefinitions[rendererIndex] = selection;
    }
  }

  /**
   * Adds {@link TrackSelectionOverride TrackSelectionOverrides} in {@code params} to {@code
   * overridesByType} if they apply to tracks in {@code trackGroups}. If there's an existing
   * override for a track type, it is replaced only if the existing override is empty and the one
   * being considered is not.
   */
  private static void collectTrackSelectionOverrides(
      TrackGroupArray trackGroups,
      TrackSelectionParameters params,
      Map<@C.TrackType Integer, TrackSelectionOverride> overridesByType) {
    for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) {
      TrackGroup trackGroup = trackGroups.get(trackGroupIndex);
      @Nullable TrackSelectionOverride override = params.overrides.get(trackGroup);
      if (override == null) {
        continue;
      }
      @Nullable TrackSelectionOverride existingOverride = overridesByType.get(override.getType());
      // Only replace an existing override if it's empty and the one being considered is not.
      if (existingOverride == null
          || (existingOverride.trackIndices.isEmpty() && !override.trackIndices.isEmpty())) {
        overridesByType.put(override.getType(), override);
      }
    }
  }

  @SuppressWarnings("deprecation") // Calling legacy hasSelectionOverride and getSelectionOverride
  private static void applyLegacyRendererOverrides(
      MappedTrackInfo mappedTrackInfo,
      Parameters params,
      ExoTrackSelection.@NullableType Definition[] outDefinitions) {
    int rendererCount = mappedTrackInfo.getRendererCount();
    for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
      TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
      if (!params.hasSelectionOverride(rendererIndex, trackGroups)) {
        continue;
      }
      @Nullable
      SelectionOverride override = params.getSelectionOverride(rendererIndex, trackGroups);
      @Nullable ExoTrackSelection.Definition selection;
      if (override != null && override.tracks.length != 0) {
        selection =
            new ExoTrackSelection.Definition(
                trackGroups.get(override.groupIndex), override.tracks, override.type);
      } else {
        selection = null;
      }
      outDefinitions[rendererIndex] = selection;
    }
  }

  /**
   * Determines whether tunneling can be enabled, replacing {@link RendererConfiguration}s in {@code
   * rendererConfigurations} with configurations that enable tunneling on the appropriate renderers
   * if so.
   *
   * @param mappedTrackInfo Mapped track information.
   * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by
   *     renderer, track group and track (in that order).
   * @param rendererConfigurations The renderer configurations. Configurations may be replaced with
   *     ones that enable tunneling as a result of this call.
   * @param trackSelections The renderer track selections.
   */
  private static void maybeConfigureRenderersForTunneling(
      MappedTrackInfo mappedTrackInfo,
      @Capabilities int[][][] renderererFormatSupports,
      @NullableType RendererConfiguration[] rendererConfigurations,
      @NullableType ExoTrackSelection[] trackSelections) {
    // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and
    // one video renderer to support tunneling and have a selection.
    int tunnelingAudioRendererIndex = -1;
    int tunnelingVideoRendererIndex = -1;
    boolean enableTunneling = true;
    for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
      int rendererType = mappedTrackInfo.getRendererType(i);
      ExoTrackSelection trackSelection = trackSelections[i];
      if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO)
          && trackSelection != null) {
        if (rendererSupportsTunneling(
            renderererFormatSupports[i], mappedTrackInfo.getTrackGroups(i), trackSelection)) {
          if (rendererType == C.TRACK_TYPE_AUDIO) {
            if (tunnelingAudioRendererIndex != -1) {
              enableTunneling = false;
              break;
            } else {
              tunnelingAudioRendererIndex = i;
            }
          } else {
            if (tunnelingVideoRendererIndex != -1) {
              enableTunneling = false;
              break;
            } else {
              tunnelingVideoRendererIndex = i;
            }
          }
        }
      }
    }
    enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1;
    if (enableTunneling) {
      RendererConfiguration tunnelingRendererConfiguration =
          new RendererConfiguration(/* tunneling= */ true);
      rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration;
      rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration;
    }
  }

  /**
   * Returns whether a renderer supports tunneling for a {@link ExoTrackSelection}.
   *
   * @param formatSupport The {@link Capabilities} for each track, indexed by group index and track
   *     index (in that order).
   * @param trackGroups The {@link TrackGroupArray}s for the renderer.
   * @param selection The track selection.
   * @return Whether the renderer supports tunneling for the {@link ExoTrackSelection}.
   */
  private static boolean rendererSupportsTunneling(
      @Capabilities int[][] formatSupport,
      TrackGroupArray trackGroups,
      ExoTrackSelection selection) {
    if (selection == null) {
      return false;
    }
    int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
    for (int i = 0; i < selection.length(); i++) {
      @Capabilities
      int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)];
      if (RendererCapabilities.getTunnelingSupport(trackFormatSupport)
          != RendererCapabilities.TUNNELING_SUPPORTED) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link
   * C#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the format support is
   * {@link C#FORMAT_EXCEEDS_CAPABILITIES}.
   *
   * @param formatSupport {@link Capabilities}.
   * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link
   *     C#FORMAT_EXCEEDS_CAPABILITIES}.
   * @return True if {@link FormatSupport} is {@link C#FORMAT_HANDLED}, or if {@code
   *     allowExceedsCapabilities} is set and the format support is {@link
   *     C#FORMAT_EXCEEDS_CAPABILITIES}.
   */
  protected static boolean isSupported(
      @Capabilities int formatSupport, boolean allowExceedsCapabilities) {
    @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport);
    return maskedSupport == C.FORMAT_HANDLED
        || (allowExceedsCapabilities && maskedSupport == C.FORMAT_EXCEEDS_CAPABILITIES);
  }

  /**
   * Normalizes the input string to null if it does not define a language, or returns it otherwise.
   *
   * @param language The string.
   * @return The string, optionally normalized to null if it does not define a language.
   */
  @Nullable
  protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) {
    return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED)
        ? null
        : language;
  }

  /**
   * Returns a score for how well a language specified in a {@link Format} matches a given language.
   *
   * @param format The {@link Format}.
   * @param language The language, or null.
   * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format
   *     language tag are allowed.
   * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly,
   *     a score of 2 if the languages don't match but belong to the same main language, a score of
   *     1 if the format language is undetermined and such a match is allowed, and a score of 0 if
   *     the languages don't match at all.
   */
  protected static int getFormatLanguageScore(
      Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) {
    if (!TextUtils.isEmpty(language) && language.equals(format.language)) {
      // Full literal match of non-empty languages, including matches of an explicit "und" query.
      return 4;
    }
    language = normalizeUndeterminedLanguageToNull(language);
    String formatLanguage = normalizeUndeterminedLanguageToNull(format.language);
    if (formatLanguage == null || language == null) {
      // At least one of the languages is undetermined.
      return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0;
    }
    if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) {
      // Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk")
      return 3;
    }
    String formatMainLanguage = Util.splitAtFirst(formatLanguage, "-")[0];
    String queryMainLanguage = Util.splitAtFirst(language, "-")[0];
    if (formatMainLanguage.equals(queryMainLanguage)) {
      // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
      return 2;
    }
    return 0;
  }

  private static int getMaxVideoPixelsToRetainForViewport(
      TrackGroup group, int viewportWidth, int viewportHeight, boolean orientationMayChange) {
    if (viewportWidth == Integer.MAX_VALUE || viewportHeight == Integer.MAX_VALUE) {
      return Integer.MAX_VALUE;
    }
    int maxVideoPixelsToRetain = Integer.MAX_VALUE;
    for (int i = 0; i < group.length; i++) {
      Format format = group.getFormat(i);
      // Keep track of the number of pixels of the selected format whose resolution is the
      // smallest to exceed the maximum size at which it can be displayed within the viewport.
      if (format.width > 0 && format.height > 0) {
        Point maxVideoSizeInViewport =
            getMaxVideoSizeInViewport(
                orientationMayChange, viewportWidth, viewportHeight, format.width, format.height);
        int videoPixels = format.width * format.height;
        if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN)
            && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN)
            && videoPixels < maxVideoPixelsToRetain) {
          maxVideoPixelsToRetain = videoPixels;
        }
      }
    }
    return maxVideoPixelsToRetain;
  }

  /**
   * Given viewport dimensions and video dimensions, computes the maximum size of the video as it
   * will be rendered to fit inside of the viewport.
   */
  private static Point getMaxVideoSizeInViewport(
      boolean orientationMayChange,
      int viewportWidth,
      int viewportHeight,
      int videoWidth,
      int videoHeight) {
    if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) {
      // Rotation is allowed, and the video will be larger in the rotated viewport.
      int tempViewportWidth = viewportWidth;
      viewportWidth = viewportHeight;
      viewportHeight = tempViewportWidth;
    }

    if (videoWidth * viewportHeight >= videoHeight * viewportWidth) {
      // Horizontal letter-boxing along top and bottom.
      return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth));
    } else {
      // Vertical letter-boxing along edges.
      return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight);
    }
  }

  private static int getRoleFlagMatchScore(int trackRoleFlags, int preferredRoleFlags) {
    if (trackRoleFlags != 0 && trackRoleFlags == preferredRoleFlags) {
      // Prefer perfect match over partial matches.
      return Integer.MAX_VALUE;
    }
    return Integer.bitCount(trackRoleFlags & preferredRoleFlags);
  }

  /**
   * Returns preference score for primary, hardware-accelerated video codecs, with higher score
   * being preferred.
   */
  private static int getVideoCodecPreferenceScore(@Nullable String mimeType) {
    if (mimeType == null) {
      return 0;
    }
    switch (mimeType) {
      case MimeTypes.VIDEO_AV1:
        return 4;
      case MimeTypes.VIDEO_H265:
        return 3;
      case MimeTypes.VIDEO_VP9:
        return 2;
      case MimeTypes.VIDEO_H264:
        return 1;
      default:
        return 0;
    }
  }

  private static boolean isDolbyAudio(Format format) {
    if (format.sampleMimeType == null) {
      return false;
    }
    switch (format.sampleMimeType) {
      case MimeTypes.AUDIO_AC3:
      case MimeTypes.AUDIO_E_AC3:
      case MimeTypes.AUDIO_E_AC3_JOC:
      case MimeTypes.AUDIO_AC4:
        return true;
      default:
        return false;
    }
  }

  /** Base class for track selection information of a {@link Format}. */
  private abstract static class TrackInfo<T extends TrackInfo<T>> {
    /** Factory for {@link TrackInfo} implementations for a given {@link TrackGroup}. */
    public interface Factory<T extends TrackInfo<T>> {
      List<T> create(int rendererIndex, TrackGroup trackGroup, @Capabilities int[] formatSupports);
    }

    public final int rendererIndex;
    public final TrackGroup trackGroup;
    public final int trackIndex;
    public final Format format;

    public TrackInfo(int rendererIndex, TrackGroup trackGroup, int trackIndex) {
      this.rendererIndex = rendererIndex;
      this.trackGroup = trackGroup;
      this.trackIndex = trackIndex;
      this.format = trackGroup.getFormat(trackIndex);
    }

    /** Returns to what extent the track is {@link SelectionEligibility eligible for selection}. */
    public abstract @SelectionEligibility int getSelectionEligibility();

    /**
     * Returns whether this track is compatible for an adaptive selection with the specified other
     * track.
     */
    public abstract boolean isCompatibleForAdaptationWith(T otherTrack);
  }

  private static final class VideoTrackInfo extends TrackInfo<VideoTrackInfo> {

    public static ImmutableList<VideoTrackInfo> createForTrackGroup(
        int rendererIndex,
        TrackGroup trackGroup,
        Parameters params,
        @Capabilities int[] formatSupport,
        @AdaptiveSupport int mixedMimeTypeAdaptionSupport) {
      int maxPixelsToRetainForViewport =
          getMaxVideoPixelsToRetainForViewport(
              trackGroup,
              params.viewportWidth,
              params.viewportHeight,
              params.viewportOrientationMayChange);
      ImmutableList.Builder<VideoTrackInfo> listBuilder = ImmutableList.builder();
      for (int i = 0; i < trackGroup.length; i++) {
        int pixelCount = trackGroup.getFormat(i).getPixelCount();
        boolean isSuitableForViewport =
            maxPixelsToRetainForViewport == Integer.MAX_VALUE
                || (pixelCount != Format.NO_VALUE && pixelCount <= maxPixelsToRetainForViewport);
        listBuilder.add(
            new VideoTrackInfo(
                rendererIndex,
                trackGroup,
                /* trackIndex= */ i,
                params,
                formatSupport[i],
                mixedMimeTypeAdaptionSupport,
                isSuitableForViewport));
      }
      return listBuilder.build();
    }

    private final boolean isWithinMaxConstraints;
    private final Parameters parameters;
    private final boolean isWithinMinConstraints;
    private final boolean isWithinRendererCapabilities;
    private final int bitrate;
    private final int pixelCount;
    private final int preferredMimeTypeMatchIndex;
    private final int preferredRoleFlagsScore;
    private final boolean hasMainOrNoRoleFlag;
    private final boolean allowMixedMimeTypes;
    private final @SelectionEligibility int selectionEligibility;
    private final boolean usesPrimaryDecoder;
    private final boolean usesHardwareAcceleration;
    private final int codecPreferenceScore;

    public VideoTrackInfo(
        int rendererIndex,
        TrackGroup trackGroup,
        int trackIndex,
        Parameters parameters,
        @Capabilities int formatSupport,
        @AdaptiveSupport int mixedMimeTypeAdaptationSupport,
        boolean isSuitableForViewport) {
      super(rendererIndex, trackGroup, trackIndex);
      this.parameters = parameters;
      @SuppressLint("WrongConstant")
      int requiredAdaptiveSupport =
          parameters.allowVideoNonSeamlessAdaptiveness
              ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS
                  | RendererCapabilities.ADAPTIVE_SEAMLESS)
              : RendererCapabilities.ADAPTIVE_SEAMLESS;
      allowMixedMimeTypes =
          parameters.allowVideoMixedMimeTypeAdaptiveness
              && (mixedMimeTypeAdaptationSupport & requiredAdaptiveSupport) != 0;
      isWithinMaxConstraints =
          isSuitableForViewport
              && (format.width == Format.NO_VALUE || format.width <= parameters.maxVideoWidth)
              && (format.height == Format.NO_VALUE || format.height <= parameters.maxVideoHeight)
              && (format.frameRate == Format.NO_VALUE
                  || format.frameRate <= parameters.maxVideoFrameRate)
              && (format.bitrate == Format.NO_VALUE
                  || format.bitrate <= parameters.maxVideoBitrate);
      isWithinMinConstraints =
          isSuitableForViewport
              && (format.width == Format.NO_VALUE || format.width >= parameters.minVideoWidth)
              && (format.height == Format.NO_VALUE || format.height >= parameters.minVideoHeight)
              && (format.frameRate == Format.NO_VALUE
                  || format.frameRate >= parameters.minVideoFrameRate)
              && (format.bitrate == Format.NO_VALUE
                  || format.bitrate >= parameters.minVideoBitrate);
      isWithinRendererCapabilities =
          isSupported(formatSupport, /* allowExceedsCapabilities= */ false);
      bitrate = format.bitrate;
      pixelCount = format.getPixelCount();
      preferredRoleFlagsScore =
          getRoleFlagMatchScore(format.roleFlags, parameters.preferredVideoRoleFlags);
      hasMainOrNoRoleFlag = format.roleFlags == 0 || (format.roleFlags & C.ROLE_FLAG_MAIN) != 0;
      int bestMimeTypeMatchIndex = Integer.MAX_VALUE;
      for (int i = 0; i < parameters.preferredVideoMimeTypes.size(); i++) {
        if (format.sampleMimeType != null
            && format.sampleMimeType.equals(parameters.preferredVideoMimeTypes.get(i))) {
          bestMimeTypeMatchIndex = i;
          break;
        }
      }
      preferredMimeTypeMatchIndex = bestMimeTypeMatchIndex;
      usesPrimaryDecoder =
          RendererCapabilities.getDecoderSupport(formatSupport)
              == RendererCapabilities.DECODER_SUPPORT_PRIMARY;
      usesHardwareAcceleration =
          RendererCapabilities.getHardwareAccelerationSupport(formatSupport)
              == RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED;
      codecPreferenceScore = getVideoCodecPreferenceScore(format.sampleMimeType);
      selectionEligibility = evaluateSelectionEligibility(formatSupport, requiredAdaptiveSupport);
    }

    @Override
    public @SelectionEligibility int getSelectionEligibility() {
      return selectionEligibility;
    }

    @Override
    public boolean isCompatibleForAdaptationWith(VideoTrackInfo otherTrack) {
      return (allowMixedMimeTypes
              || Util.areEqual(format.sampleMimeType, otherTrack.format.sampleMimeType))
          && (parameters.allowVideoMixedDecoderSupportAdaptiveness
              || (this.usesPrimaryDecoder == otherTrack.usesPrimaryDecoder
                  && this.usesHardwareAcceleration == otherTrack.usesHardwareAcceleration));
    }

    private @SelectionEligibility int evaluateSelectionEligibility(
        @Capabilities int rendererSupport, @AdaptiveSupport int requiredAdaptiveSupport) {
      if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
        // Ignore trick-play tracks for now.
        return SELECTION_ELIGIBILITY_NO;
      }
      if (!isSupported(rendererSupport, parameters.exceedRendererCapabilitiesIfNecessary)) {
        return SELECTION_ELIGIBILITY_NO;
      }
      if (!isWithinMaxConstraints && !parameters.exceedVideoConstraintsIfNecessary) {
        return SELECTION_ELIGIBILITY_NO;
      }
      return isSupported(rendererSupport, /* allowExceedsCapabilities= */ false)
              && isWithinMinConstraints
              && isWithinMaxConstraints
              && format.bitrate != Format.NO_VALUE
              && !parameters.forceHighestSupportedBitrate
              && !parameters.forceLowestBitrate
              && ((rendererSupport & requiredAdaptiveSupport) != 0)
          ? SELECTION_ELIGIBILITY_ADAPTIVE
          : SELECTION_ELIGIBILITY_FIXED;
    }

    private static int compareNonQualityPreferences(VideoTrackInfo info1, VideoTrackInfo info2) {
      ComparisonChain chain =
          ComparisonChain.start()
              .compareFalseFirst(
                  info1.isWithinRendererCapabilities, info2.isWithinRendererCapabilities)
              // 1. Compare match with specific content preferences set by the parameters.
              .compare(info1.preferredRoleFlagsScore, info2.preferredRoleFlagsScore)
              // 2. Compare match with implicit content preferences set by the media.
              .compareFalseFirst(info1.hasMainOrNoRoleFlag, info2.hasMainOrNoRoleFlag)
              // 3. Compare match with technical preferences set by the parameters.
              .compareFalseFirst(info1.isWithinMaxConstraints, info2.isWithinMaxConstraints)
              .compareFalseFirst(info1.isWithinMinConstraints, info2.isWithinMinConstraints)
              .compare(
                  info1.preferredMimeTypeMatchIndex,
                  info2.preferredMimeTypeMatchIndex,
                  Ordering.natural().reverse())
              // 4. Compare match with renderer capability preferences.
              .compareFalseFirst(info1.usesPrimaryDecoder, info2.usesPrimaryDecoder)
              .compareFalseFirst(info1.usesHardwareAcceleration, info2.usesHardwareAcceleration);
      if (info1.usesPrimaryDecoder && info1.usesHardwareAcceleration) {
        chain = chain.compare(info1.codecPreferenceScore, info2.codecPreferenceScore);
      }
      return chain.result();
    }

    private static int compareQualityPreferences(VideoTrackInfo info1, VideoTrackInfo info2) {
      // The preferred ordering by video quality depends on the constraints:
      // - Not within renderer capabilities: Prefer lower quality because it's more likely to play.
      // - Within min and max constraints: Prefer higher quality.
      // - Within max constraints only: Prefer higher quality because it gets us closest to
      //   satisfying the violated min constraints.
      // - Within min constraints only: Prefer lower quality because it gets us closest to
      //   satisfying the violated max constraints.
      // - Outside min and max constraints: Arbitrarily prefer lower quality.
      Ordering<Integer> qualityOrdering =
          info1.isWithinMaxConstraints && info1.isWithinRendererCapabilities
              ? FORMAT_VALUE_ORDERING
              : FORMAT_VALUE_ORDERING.reverse();
      return ComparisonChain.start()
          .compare(
              info1.bitrate,
              info2.bitrate,
              info1.parameters.forceLowestBitrate ? FORMAT_VALUE_ORDERING.reverse() : NO_ORDER)
          .compare(info1.pixelCount, info2.pixelCount, qualityOrdering)
          .compare(info1.bitrate, info2.bitrate, qualityOrdering)
          .result();
    }

    public static int compareSelections(List<VideoTrackInfo> infos1, List<VideoTrackInfo> infos2) {
      return ComparisonChain.start()
          // Compare non-quality preferences of the best individual track with each other.
          .compare(
              max(infos1, VideoTrackInfo::compareNonQualityPreferences),
              max(infos2, VideoTrackInfo::compareNonQualityPreferences),
              VideoTrackInfo::compareNonQualityPreferences)
          // Prefer selections with more formats (all non-quality preferences being equal).
          .compare(infos1.size(), infos2.size())
          // Prefer selections with the best individual track quality.
          .compare(
              max(infos1, VideoTrackInfo::compareQualityPreferences),
              max(infos2, VideoTrackInfo::compareQualityPreferences),
              VideoTrackInfo::compareQualityPreferences)
          .result();
    }
  }

  private static final class AudioTrackInfo extends TrackInfo<AudioTrackInfo>
      implements Comparable<AudioTrackInfo> {

    public static ImmutableList<AudioTrackInfo> createForTrackGroup(
        int rendererIndex,
        TrackGroup trackGroup,
        Parameters params,
        @Capabilities int[] formatSupport,
        boolean hasMappedVideoTracks,
        Predicate<Format> withinAudioChannelCountConstraints) {
      ImmutableList.Builder<AudioTrackInfo> listBuilder = ImmutableList.builder();
      for (int i = 0; i < trackGroup.length; i++) {
        listBuilder.add(
            new AudioTrackInfo(
                rendererIndex,
                trackGroup,
                /* trackIndex= */ i,
                params,
                formatSupport[i],
                hasMappedVideoTracks,
                withinAudioChannelCountConstraints));
      }
      return listBuilder.build();
    }

    private final @SelectionEligibility int selectionEligibility;
    private final boolean isWithinConstraints;
    @Nullable private final String language;
    private final Parameters parameters;
    private final boolean isWithinRendererCapabilities;
    private final int preferredLanguageScore;
    private final int preferredLanguageIndex;
    private final int preferredRoleFlagsScore;
    private final boolean hasMainOrNoRoleFlag;
    private final int localeLanguageMatchIndex;
    private final int localeLanguageScore;
    private final boolean isDefaultSelectionFlag;
    private final int channelCount;
    private final int sampleRate;
    private final int bitrate;
    private final int preferredMimeTypeMatchIndex;
    private final boolean usesPrimaryDecoder;
    private final boolean usesHardwareAcceleration;

    public AudioTrackInfo(
        int rendererIndex,
        TrackGroup trackGroup,
        int trackIndex,
        Parameters parameters,
        @Capabilities int formatSupport,
        boolean hasMappedVideoTracks,
        Predicate<Format> withinAudioChannelCountConstraints) {
      super(rendererIndex, trackGroup, trackIndex);
      this.parameters = parameters;
      this.language = normalizeUndeterminedLanguageToNull(format.language);
      isWithinRendererCapabilities =
          isSupported(formatSupport, /* allowExceedsCapabilities= */ false);
      int bestLanguageScore = 0;
      int bestLanguageIndex = Integer.MAX_VALUE;
      for (int i = 0; i < parameters.preferredAudioLanguages.size(); i++) {
        int score =
            getFormatLanguageScore(
                format,
                parameters.preferredAudioLanguages.get(i),
                /* allowUndeterminedFormatLanguage= */ false);
        if (score > 0) {
          bestLanguageIndex = i;
          bestLanguageScore = score;
          break;
        }
      }
      preferredLanguageIndex = bestLanguageIndex;
      preferredLanguageScore = bestLanguageScore;
      preferredRoleFlagsScore =
          getRoleFlagMatchScore(format.roleFlags, parameters.preferredAudioRoleFlags);
      hasMainOrNoRoleFlag = format.roleFlags == 0 || (format.roleFlags & C.ROLE_FLAG_MAIN) != 0;
      isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
      channelCount = format.channelCount;
      sampleRate = format.sampleRate;
      bitrate = format.bitrate;
      isWithinConstraints =
          (format.bitrate == Format.NO_VALUE || format.bitrate <= parameters.maxAudioBitrate)
              && (format.channelCount == Format.NO_VALUE
                  || format.channelCount <= parameters.maxAudioChannelCount)
              && withinAudioChannelCountConstraints.apply(format);
      String[] localeLanguages = Util.getSystemLanguageCodes();
      int bestLocaleMatchIndex = Integer.MAX_VALUE;
      int bestLocaleMatchScore = 0;
      for (int i = 0; i < localeLanguages.length; i++) {
        int score =
            getFormatLanguageScore(
                format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false);
        if (score > 0) {
          bestLocaleMatchIndex = i;
          bestLocaleMatchScore = score;
          break;
        }
      }
      localeLanguageMatchIndex = bestLocaleMatchIndex;
      localeLanguageScore = bestLocaleMatchScore;
      int bestMimeTypeMatchIndex = Integer.MAX_VALUE;
      for (int i = 0; i < parameters.preferredAudioMimeTypes.size(); i++) {
        if (format.sampleMimeType != null
            && format.sampleMimeType.equals(parameters.preferredAudioMimeTypes.get(i))) {
          bestMimeTypeMatchIndex = i;
          break;
        }
      }
      preferredMimeTypeMatchIndex = bestMimeTypeMatchIndex;
      usesPrimaryDecoder =
          RendererCapabilities.getDecoderSupport(formatSupport)
              == RendererCapabilities.DECODER_SUPPORT_PRIMARY;
      usesHardwareAcceleration =
          RendererCapabilities.getHardwareAccelerationSupport(formatSupport)
              == RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED;
      selectionEligibility = evaluateSelectionEligibility(formatSupport, hasMappedVideoTracks);
    }

    @Override
    public @SelectionEligibility int getSelectionEligibility() {
      return selectionEligibility;
    }

    @Override
    public boolean isCompatibleForAdaptationWith(AudioTrackInfo otherTrack) {
      return (parameters.allowAudioMixedChannelCountAdaptiveness
              || (format.channelCount != Format.NO_VALUE
                  && format.channelCount == otherTrack.format.channelCount))
          && (parameters.allowAudioMixedMimeTypeAdaptiveness
              || (format.sampleMimeType != null
                  && TextUtils.equals(format.sampleMimeType, otherTrack.format.sampleMimeType)))
          && (parameters.allowAudioMixedSampleRateAdaptiveness
              || (format.sampleRate != Format.NO_VALUE
                  && format.sampleRate == otherTrack.format.sampleRate))
          && (parameters.allowAudioMixedDecoderSupportAdaptiveness
              || (this.usesPrimaryDecoder == otherTrack.usesPrimaryDecoder
                  && this.usesHardwareAcceleration == otherTrack.usesHardwareAcceleration));
    }

    @Override
    public int compareTo(AudioTrackInfo other) {
      // If the formats are within constraints and renderer capabilities then prefer higher values
      // of channel count, sample rate and bit rate in that order. Otherwise, prefer lower values.
      Ordering<Integer> qualityOrdering =
          isWithinConstraints && isWithinRendererCapabilities
              ? FORMAT_VALUE_ORDERING
              : FORMAT_VALUE_ORDERING.reverse();
      return ComparisonChain.start()
          .compareFalseFirst(this.isWithinRendererCapabilities, other.isWithinRendererCapabilities)
          // 1. Compare match with specific content preferences set by the parameters.
          .compare(
              this.preferredLanguageIndex,
              other.preferredLanguageIndex,
              Ordering.natural().reverse())
          .compare(this.preferredLanguageScore, other.preferredLanguageScore)
          .compare(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore)
          // 2. Compare match with implicit content preferences set by the media or the system.
          .compareFalseFirst(this.isDefaultSelectionFlag, other.isDefaultSelectionFlag)
          .compareFalseFirst(this.hasMainOrNoRoleFlag, other.hasMainOrNoRoleFlag)
          .compare(
              this.localeLanguageMatchIndex,
              other.localeLanguageMatchIndex,
              Ordering.natural().reverse())
          .compare(this.localeLanguageScore, other.localeLanguageScore)
          // 3. Compare match with technical preferences set by the parameters.
          .compareFalseFirst(this.isWithinConstraints, other.isWithinConstraints)
          .compare(
              this.preferredMimeTypeMatchIndex,
              other.preferredMimeTypeMatchIndex,
              Ordering.natural().reverse())
          .compare(
              this.bitrate,
              other.bitrate,
              parameters.forceLowestBitrate ? FORMAT_VALUE_ORDERING.reverse() : NO_ORDER)
          // 4. Compare match with renderer capability preferences.
          .compareFalseFirst(this.usesPrimaryDecoder, other.usesPrimaryDecoder)
          .compareFalseFirst(this.usesHardwareAcceleration, other.usesHardwareAcceleration)
          // 5. Compare technical quality.
          .compare(this.channelCount, other.channelCount, qualityOrdering)
          .compare(this.sampleRate, other.sampleRate, qualityOrdering)
          .compare(
              this.bitrate,
              other.bitrate,
              // Only compare bit rates of tracks with matching language information.
              Util.areEqual(this.language, other.language) ? qualityOrdering : NO_ORDER)
          .result();
    }

    private @SelectionEligibility int evaluateSelectionEligibility(
        @Capabilities int rendererSupport, boolean hasMappedVideoTracks) {
      if (!isSupported(rendererSupport, parameters.exceedRendererCapabilitiesIfNecessary)) {
        return SELECTION_ELIGIBILITY_NO;
      }
      if (!isWithinConstraints && !parameters.exceedAudioConstraintsIfNecessary) {
        return SELECTION_ELIGIBILITY_NO;
      }
      return isSupported(rendererSupport, /* allowExceedsCapabilities= */ false)
              && isWithinConstraints
              && format.bitrate != Format.NO_VALUE
              && !parameters.forceHighestSupportedBitrate
              && !parameters.forceLowestBitrate
              && (parameters.allowMultipleAdaptiveSelections || !hasMappedVideoTracks)
          ? SELECTION_ELIGIBILITY_ADAPTIVE
          : SELECTION_ELIGIBILITY_FIXED;
    }

    public static int compareSelections(List<AudioTrackInfo> infos1, List<AudioTrackInfo> infos2) {
      // Compare best tracks of each selection with each other.
      return max(infos1).compareTo(max(infos2));
    }
  }

  private static final class TextTrackInfo extends TrackInfo<TextTrackInfo>
      implements Comparable<TextTrackInfo> {

    public static ImmutableList<TextTrackInfo> createForTrackGroup(
        int rendererIndex,
        TrackGroup trackGroup,
        Parameters params,
        @Capabilities int[] formatSupport,
        @Nullable String selectedAudioLanguage) {
      ImmutableList.Builder<TextTrackInfo> listBuilder = ImmutableList.builder();
      for (int i = 0; i < trackGroup.length; i++) {
        listBuilder.add(
            new TextTrackInfo(
                rendererIndex,
                trackGroup,
                /* trackIndex= */ i,
                params,
                formatSupport[i],
                selectedAudioLanguage));
      }
      return listBuilder.build();
    }

    private final @SelectionEligibility int selectionEligibility;
    private final boolean isWithinRendererCapabilities;
    private final boolean isDefault;
    private final boolean isForced;
    private final int preferredLanguageIndex;
    private final int preferredLanguageScore;
    private final int preferredRoleFlagsScore;
    private final int selectedAudioLanguageScore;
    private final boolean hasCaptionRoleFlags;

    public TextTrackInfo(
        int rendererIndex,
        TrackGroup trackGroup,
        int trackIndex,
        Parameters parameters,
        @Capabilities int trackFormatSupport,
        @Nullable String selectedAudioLanguage) {
      super(rendererIndex, trackGroup, trackIndex);
      isWithinRendererCapabilities =
          isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);
      int maskedSelectionFlags = format.selectionFlags & ~parameters.ignoredTextSelectionFlags;
      isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
      isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
      int bestLanguageIndex = Integer.MAX_VALUE;
      int bestLanguageScore = 0;
      // Compare against empty (unset) language if no preference is given to allow the selection of
      // a text track with undetermined language.
      ImmutableList<String> preferredLanguages =
          parameters.preferredTextLanguages.isEmpty()
              ? ImmutableList.of("")
              : parameters.preferredTextLanguages;
      for (int i = 0; i < preferredLanguages.size(); i++) {
        int score =
            getFormatLanguageScore(
                format, preferredLanguages.get(i), parameters.selectUndeterminedTextLanguage);
        if (score > 0) {
          bestLanguageIndex = i;
          bestLanguageScore = score;
          break;
        }
      }
      preferredLanguageIndex = bestLanguageIndex;
      preferredLanguageScore = bestLanguageScore;
      preferredRoleFlagsScore =
          getRoleFlagMatchScore(format.roleFlags, parameters.preferredTextRoleFlags);
      hasCaptionRoleFlags =
          (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0;
      boolean selectedAudioLanguageUndetermined =
          normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null;
      selectedAudioLanguageScore =
          getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined);
      boolean isWithinConstraints =
          preferredLanguageScore > 0
              || (parameters.preferredTextLanguages.isEmpty() && preferredRoleFlagsScore > 0)
              || isDefault
              || (isForced && selectedAudioLanguageScore > 0);
      selectionEligibility =
          isSupported(trackFormatSupport, parameters.exceedRendererCapabilitiesIfNecessary)
                  && isWithinConstraints
              ? SELECTION_ELIGIBILITY_FIXED
              : SELECTION_ELIGIBILITY_NO;
    }

    @Override
    public @SelectionEligibility int getSelectionEligibility() {
      return selectionEligibility;
    }

    @Override
    public boolean isCompatibleForAdaptationWith(TextTrackInfo otherTrack) {
      return false;
    }

    @Override
    public int compareTo(TextTrackInfo other) {
      ComparisonChain chain =
          ComparisonChain.start()
              .compareFalseFirst(
                  this.isWithinRendererCapabilities, other.isWithinRendererCapabilities)
              // 1. Compare match with specific content preferences set by the parameters.
              .compare(
                  this.preferredLanguageIndex,
                  other.preferredLanguageIndex,
                  Ordering.natural().reverse())
              .compare(this.preferredLanguageScore, other.preferredLanguageScore)
              .compare(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore)
              // 2. Compare match with implicit content preferences set by the media.
              .compareFalseFirst(this.isDefault, other.isDefault)
              .compare(
                  this.isForced,
                  other.isForced,
                  // Prefer non-forced to forced if a preferred text language has been matched.
                  // Where both are provided the non-forced track will usually contain the forced
                  // subtitles as a subset. Otherwise, prefer a forced track.
                  preferredLanguageScore == 0 ? Ordering.natural() : Ordering.natural().reverse())
              .compare(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore);
      if (preferredRoleFlagsScore == 0) {
        chain = chain.compareTrueFirst(this.hasCaptionRoleFlags, other.hasCaptionRoleFlags);
      }
      return chain.result();
    }

    public static int compareSelections(List<TextTrackInfo> infos1, List<TextTrackInfo> infos2) {
      return infos1.get(0).compareTo(infos2.get(0));
    }
  }

  private static final class OtherTrackScore implements Comparable<OtherTrackScore> {

    private final boolean isDefault;
    private final boolean isWithinRendererCapabilities;

    public OtherTrackScore(Format format, @Capabilities int trackFormatSupport) {
      isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
      isWithinRendererCapabilities =
          isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);
    }

    @Override
    public int compareTo(OtherTrackScore other) {
      return ComparisonChain.start()
          .compareFalseFirst(this.isWithinRendererCapabilities, other.isWithinRendererCapabilities)
          .compareFalseFirst(this.isDefault, other.isDefault)
          .result();
    }
  }

  /**
   * Wraps the {@link Spatializer} in order to encapsulate its APIs within an inner class, to avoid
   * runtime linking on devices with {@code API < 32}.
   */
  @RequiresApi(32)
  private static class SpatializerWrapperV32 {

    private final Spatializer spatializer;
    private final boolean spatializationSupported;

    @Nullable private Handler handler;
    @Nullable private Spatializer.OnSpatializerStateChangedListener listener;

    @Nullable
    public static SpatializerWrapperV32 tryCreateInstance(Context context) {
      @Nullable
      AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
      return audioManager == null ? null : new SpatializerWrapperV32(audioManager.getSpatializer());
    }

    private SpatializerWrapperV32(Spatializer spatializer) {
      this.spatializer = spatializer;
      this.spatializationSupported =
          spatializer.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
    }

    public void ensureInitialized(DefaultTrackSelector defaultTrackSelector, Looper looper) {
      if (listener != null || handler != null) {
        return;
      }
      this.listener =
          new Spatializer.OnSpatializerStateChangedListener() {
            @Override
            public void onSpatializerEnabledChanged(Spatializer spatializer, boolean enabled) {
              defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints();
            }

            @Override
            public void onSpatializerAvailableChanged(Spatializer spatializer, boolean available) {
              defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints();
            }
          };
      this.handler = new Handler(looper);
      spatializer.addOnSpatializerStateChangedListener(handler::post, listener);
    }

    public boolean isSpatializationSupported() {
      return spatializationSupported;
    }

    public boolean isAvailable() {
      return spatializer.isAvailable();
    }

    public boolean isEnabled() {
      return spatializer.isEnabled();
    }

    public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) {
      // For E-AC3 JOC, the format is object based. When the channel count is 16, this maps to 12
      // linear channels and the rest are used for objects. See
      // https://github.com/google/ExoPlayer/pull/10322#discussion_r895265881
      int linearChannelCount =
          MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType) && format.channelCount == 16
              ? 12
              : format.channelCount;
      AudioFormat.Builder builder =
          new AudioFormat.Builder()
              .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
              .setChannelMask(Util.getAudioTrackChannelConfig(linearChannelCount));
      if (format.sampleRate != Format.NO_VALUE) {
        builder.setSampleRate(format.sampleRate);
      }
      return spatializer.canBeSpatialized(
          audioAttributes.getAudioAttributesV21().audioAttributes, builder.build());
    }

    public void release() {
      if (listener == null || handler == null) {
        return;
      }
      spatializer.removeOnSpatializerStateChangedListener(listener);
      castNonNull(handler).removeCallbacksAndMessages(/* token= */ null);
      handler = null;
      listener = null;
    }
  }
}