summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java761
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java217
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java494
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java2827
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java117
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java541
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java143
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java269
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java77
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java336
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java100
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java157
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java105
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java19
14 files changed, 6163 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
new file mode 100644
index 0000000000..33f8606e9b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -0,0 +1,761 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.SimpleExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one
+ * of highest quality given the current network conditions and the state of the buffer.
+ */
+public class AdaptiveTrackSelection extends BaseTrackSelection {
+
+ /** Factory for {@link AdaptiveTrackSelection} instances. */
+ public static class Factory implements TrackSelection.Factory {
+
+ @Nullable private final BandwidthMeter bandwidthMeter;
+ private final int minDurationForQualityIncreaseMs;
+ private final int maxDurationForQualityDecreaseMs;
+ private final int minDurationToRetainAfterDiscardMs;
+ private final float bandwidthFraction;
+ private final float bufferedFractionToLiveEdgeForQualityIncrease;
+ private final long minTimeBetweenBufferReevaluationMs;
+ private final Clock clock;
+
+ /** Creates an adaptive track selection factory with default parameters. */
+ public Factory() {
+ this(
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
+ DEFAULT_BANDWIDTH_FRACTION,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed
+ * to the player in {@link SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public Factory(BandwidthMeter bandwidthMeter) {
+ this(
+ bandwidthMeter,
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
+ DEFAULT_BANDWIDTH_FRACTION,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * Creates an adaptive track selection factory.
+ *
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+ * selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+ * selected track to switch to one of lower quality.
+ * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
+ * quality, the selection may indicate that media already buffered at the lower quality can
+ * be discarded to speed up the switch. This is the minimum duration of media that must be
+ * retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ */
+ public Factory(
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction) {
+ this(
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bandwidthFraction,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should
+ * be directly passed to the player in {@link SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public Factory(
+ BandwidthMeter bandwidthMeter,
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction) {
+ this(
+ bandwidthMeter,
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bandwidthFraction,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * Creates an adaptive track selection factory.
+ *
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+ * selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+ * selected track to switch to one of lower quality.
+ * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
+ * quality, the selection may indicate that media already buffered at the lower quality can
+ * be discarded to speed up the switch. This is the minimum duration of media that must be
+ * retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
+ * duration from current playback position to the live edge that has to be buffered before
+ * the selected track can be switched to one of higher quality. This parameter is only
+ * applied when the playback position is closer to the live edge than {@code
+ * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
+ * quality from happening.
+ * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
+ * buffer and discard some chunks of lower quality to improve the playback quality if
+ * network conditions have changed. This is the minimum duration between 2 consecutive
+ * buffer reevaluation calls.
+ * @param clock A {@link Clock}.
+ */
+ @SuppressWarnings("deprecation")
+ public Factory(
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
+ this(
+ /* bandwidthMeter= */ null,
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bandwidthFraction,
+ bufferedFractionToLiveEdgeForQualityIncrease,
+ minTimeBetweenBufferReevaluationMs,
+ clock);
+ }
+
+ /**
+ * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom
+ * bandwidth meter should be directly passed to the player in {@link
+ * SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ public Factory(
+ @Nullable BandwidthMeter bandwidthMeter,
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
+ this.bandwidthMeter = bandwidthMeter;
+ this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
+ this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
+ this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
+ this.bandwidthFraction = bandwidthFraction;
+ this.bufferedFractionToLiveEdgeForQualityIncrease =
+ bufferedFractionToLiveEdgeForQualityIncrease;
+ this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
+ this.clock = clock;
+ }
+
+ @Override
+ public final @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ if (this.bandwidthMeter != null) {
+ bandwidthMeter = this.bandwidthMeter;
+ }
+ TrackSelection[] selections = new TrackSelection[definitions.length];
+ int totalFixedBandwidth = 0;
+ for (int i = 0; i < definitions.length; i++) {
+ Definition definition = definitions[i];
+ if (definition != null && definition.tracks.length == 1) {
+ // Make fixed selections first to know their total bandwidth.
+ selections[i] =
+ new FixedTrackSelection(
+ definition.group, definition.tracks[0], definition.reason, definition.data);
+ int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
+ if (trackBitrate != Format.NO_VALUE) {
+ totalFixedBandwidth += trackBitrate;
+ }
+ }
+ }
+ List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>();
+ for (int i = 0; i < definitions.length; i++) {
+ Definition definition = definitions[i];
+ if (definition != null && definition.tracks.length > 1) {
+ AdaptiveTrackSelection adaptiveSelection =
+ createAdaptiveTrackSelection(
+ definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth);
+ adaptiveSelections.add(adaptiveSelection);
+ selections[i] = adaptiveSelection;
+ }
+ }
+ if (adaptiveSelections.size() > 1) {
+ long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][];
+ for (int i = 0; i < adaptiveSelections.size(); i++) {
+ AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i);
+ adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()];
+ for (int j = 0; j < adaptiveSelection.length(); j++) {
+ adaptiveTrackBitrates[i][j] =
+ adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate;
+ }
+ }
+ long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates);
+ for (int i = 0; i < adaptiveSelections.size(); i++) {
+ adaptiveSelections
+ .get(i)
+ .experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]);
+ }
+ }
+ return selections;
+ }
+
+ /**
+ * Creates a single adaptive selection for the given group, bandwidth meter and tracks.
+ *
+ * @param group The {@link TrackGroup}.
+ * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
+ * @param tracks The indices of the selected tracks in the track group.
+ * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits
+ * per second.
+ * @return An {@link AdaptiveTrackSelection} for the specified tracks.
+ */
+ protected AdaptiveTrackSelection createAdaptiveTrackSelection(
+ TrackGroup group,
+ BandwidthMeter bandwidthMeter,
+ int[] tracks,
+ int totalFixedTrackBandwidth) {
+ return new AdaptiveTrackSelection(
+ group,
+ tracks,
+ new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth),
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bufferedFractionToLiveEdgeForQualityIncrease,
+ minTimeBetweenBufferReevaluationMs,
+ clock);
+ }
+ }
+
+ public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
+ public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
+ public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
+ public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f;
+ public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
+ public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;
+
+ private final BandwidthProvider bandwidthProvider;
+ private final long minDurationForQualityIncreaseUs;
+ private final long maxDurationForQualityDecreaseUs;
+ private final long minDurationToRetainAfterDiscardUs;
+ private final float bufferedFractionToLiveEdgeForQualityIncrease;
+ private final long minTimeBetweenBufferReevaluationMs;
+ private final Clock clock;
+
+ private float playbackSpeed;
+ private int selectedIndex;
+ private int reason;
+ private long lastBufferEvaluationMs;
+
+ /**
+ * @param group The {@link TrackGroup}.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * empty. May be in any order.
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ */
+ public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
+ BandwidthMeter bandwidthMeter) {
+ this(
+ group,
+ tracks,
+ bandwidthMeter,
+ /* reservedBandwidth= */ 0,
+ DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+ DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+ DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
+ DEFAULT_BANDWIDTH_FRACTION,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * empty. May be in any order.
+ * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+ * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for
+ * use, in bits per second.
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+ * selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+ * selected track to switch to one of lower quality.
+ * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
+ * quality, the selection may indicate that media already buffered at the lower quality can be
+ * discarded to speed up the switch. This is the minimum duration of media that must be
+ * retained at the lower quality.
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
+ * duration from current playback position to the live edge that has to be buffered before the
+ * selected track can be switched to one of higher quality. This parameter is only applied
+ * when the playback position is closer to the live edge than {@code
+ * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
+ * quality from happening.
+ * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
+ * buffer and discard some chunks of lower quality to improve the playback quality if network
+ * condition has changed. This is the minimum duration between 2 consecutive buffer
+ * reevaluation calls.
+ */
+ public AdaptiveTrackSelection(
+ TrackGroup group,
+ int[] tracks,
+ BandwidthMeter bandwidthMeter,
+ long reservedBandwidth,
+ long minDurationForQualityIncreaseMs,
+ long maxDurationForQualityDecreaseMs,
+ long minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
+ this(
+ group,
+ tracks,
+ new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth),
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bufferedFractionToLiveEdgeForQualityIncrease,
+ minTimeBetweenBufferReevaluationMs,
+ clock);
+ }
+
+ private AdaptiveTrackSelection(
+ TrackGroup group,
+ int[] tracks,
+ BandwidthProvider bandwidthProvider,
+ long minDurationForQualityIncreaseMs,
+ long maxDurationForQualityDecreaseMs,
+ long minDurationToRetainAfterDiscardMs,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
+ super(group, tracks);
+ this.bandwidthProvider = bandwidthProvider;
+ this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+ this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+ this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+ this.bufferedFractionToLiveEdgeForQualityIncrease =
+ bufferedFractionToLiveEdgeForQualityIncrease;
+ this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
+ this.clock = clock;
+ playbackSpeed = 1f;
+ reason = C.SELECTION_REASON_UNKNOWN;
+ lastBufferEvaluationMs = C.TIME_UNSET;
+ }
+
+ /**
+ * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth.
+ *
+ * @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0]
+ * being the total bandwidth and [1] being the allocated bandwidth.
+ */
+ public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) {
+ ((DefaultBandwidthProvider) bandwidthProvider)
+ .experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints);
+ }
+
+ @Override
+ public void enable() {
+ lastBufferEvaluationMs = C.TIME_UNSET;
+ }
+
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ this.playbackSpeed = playbackSpeed;
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ long nowMs = clock.elapsedRealtime();
+
+ // Make initial selection
+ if (reason == C.SELECTION_REASON_UNKNOWN) {
+ reason = C.SELECTION_REASON_INITIAL;
+ selectedIndex = determineIdealSelectedIndex(nowMs);
+ return;
+ }
+
+ // Stash the current selection, then make a new one.
+ int currentSelectedIndex = selectedIndex;
+ selectedIndex = determineIdealSelectedIndex(nowMs);
+ if (selectedIndex == currentSelectedIndex) {
+ return;
+ }
+
+ if (!isBlacklisted(currentSelectedIndex, nowMs)) {
+ // Revert back to the current selection if conditions are not suitable for switching.
+ Format currentFormat = getFormat(currentSelectedIndex);
+ Format selectedFormat = getFormat(selectedIndex);
+ if (selectedFormat.bitrate > currentFormat.bitrate
+ && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) {
+ // The selected track is a higher quality, but we have insufficient buffer to safely switch
+ // up. Defer switching up for now.
+ selectedIndex = currentSelectedIndex;
+ } else if (selectedFormat.bitrate < currentFormat.bitrate
+ && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+ // The selected track is a lower quality, but we have sufficient buffer to defer switching
+ // down for now.
+ selectedIndex = currentSelectedIndex;
+ }
+ }
+ // If we adapted, update the trigger.
+ if (selectedIndex != currentSelectedIndex) {
+ reason = C.SELECTION_REASON_ADAPTIVE;
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ long nowMs = clock.elapsedRealtime();
+ if (!shouldEvaluateQueueSize(nowMs)) {
+ return queue.size();
+ }
+
+ lastBufferEvaluationMs = nowMs;
+ if (queue.isEmpty()) {
+ return 0;
+ }
+
+ int queueSize = queue.size();
+ MediaChunk lastChunk = queue.get(queueSize - 1);
+ long playoutBufferedDurationBeforeLastChunkUs =
+ Util.getPlayoutDurationForMediaDuration(
+ lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
+ long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs();
+ if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
+ return queueSize;
+ }
+ int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
+ Format idealFormat = getFormat(idealSelectedIndex);
+ // If the chunks contain video, discard from the first SD chunk beyond
+ // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
+ // track.
+ for (int i = 0; i < queueSize; i++) {
+ MediaChunk chunk = queue.get(i);
+ Format format = chunk.trackFormat;
+ long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
+ long playoutDurationBeforeThisChunkUs =
+ Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed);
+ if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
+ && format.bitrate < idealFormat.bitrate
+ && format.height != Format.NO_VALUE && format.height < 720
+ && format.width != Format.NO_VALUE && format.width < 1280
+ && format.height < idealFormat.height) {
+ return i;
+ }
+ }
+ return queueSize;
+ }
+
+ /**
+ * Called when updating the selected track to determine whether a candidate track can be selected.
+ *
+ * @param format The {@link Format} of the candidate track.
+ * @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate}
+ * if a more accurate estimate of the current track bitrate is available.
+ * @param playbackSpeed The current playback speed.
+ * @param effectiveBitrate The bitrate available to this selection.
+ * @return Whether this {@link Format} can be selected.
+ */
+ @SuppressWarnings("unused")
+ protected boolean canSelectFormat(
+ Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) {
+ return Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate;
+ }
+
+ /**
+ * Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be
+ * performed.
+ *
+ * @param nowMs The current value of {@link Clock#elapsedRealtime()}.
+ * @return Whether an evaluation should be performed.
+ */
+ protected boolean shouldEvaluateQueueSize(long nowMs) {
+ return lastBufferEvaluationMs == C.TIME_UNSET
+ || nowMs - lastBufferEvaluationMs >= minTimeBetweenBufferReevaluationMs;
+ }
+
+ /**
+ * Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer
+ * to retain after discarding chunks.
+ *
+ * @return The minimum duration of buffer to retain after discarding chunks, in microseconds.
+ */
+ protected long getMinDurationToRetainAfterDiscardUs() {
+ return minDurationToRetainAfterDiscardUs;
+ }
+
+ /**
+ * Computes the ideal selected index ignoring buffer health.
+ *
+ * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
+ * Long#MIN_VALUE} to ignore blacklisting.
+ */
+ private int determineIdealSelectedIndex(long nowMs) {
+ long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth();
+ int lowestBitrateNonBlacklistedIndex = 0;
+ for (int i = 0; i < length; i++) {
+ if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
+ Format format = getFormat(i);
+ if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) {
+ return i;
+ } else {
+ lowestBitrateNonBlacklistedIndex = i;
+ }
+ }
+ }
+ return lowestBitrateNonBlacklistedIndex;
+ }
+
+ private long minDurationForQualityIncreaseUs(long availableDurationUs) {
+ boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET
+ && availableDurationUs <= minDurationForQualityIncreaseUs;
+ return isAvailableDurationTooShort
+ ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease)
+ : minDurationForQualityIncreaseUs;
+ }
+
+ /** Provides the allocated bandwidth. */
+ private interface BandwidthProvider {
+
+ /** Returns the allocated bitrate. */
+ long getAllocatedBandwidth();
+ }
+
+ private static final class DefaultBandwidthProvider implements BandwidthProvider {
+
+ private final BandwidthMeter bandwidthMeter;
+ private final float bandwidthFraction;
+ private final long reservedBandwidth;
+
+ @Nullable private long[][] allocationCheckpoints;
+
+ /* package */
+ // the constructor does not initialize fields: allocationCheckpoints
+ @SuppressWarnings("nullness:initialization.fields.uninitialized")
+ DefaultBandwidthProvider(
+ BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) {
+ this.bandwidthMeter = bandwidthMeter;
+ this.bandwidthFraction = bandwidthFraction;
+ this.reservedBandwidth = reservedBandwidth;
+ }
+
+ // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0]
+ @SuppressWarnings("nullness:unboxing.of.nullable")
+ @Override
+ public long getAllocatedBandwidth() {
+ long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
+ long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth);
+ if (allocationCheckpoints == null) {
+ return allocatableBandwidth;
+ }
+ int nextIndex = 1;
+ while (nextIndex < allocationCheckpoints.length - 1
+ && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {
+ nextIndex++;
+ }
+ long[] previous = allocationCheckpoints[nextIndex - 1];
+ long[] next = allocationCheckpoints[nextIndex];
+ float fractionBetweenCheckpoints =
+ (float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]);
+ return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1]));
+ }
+
+ /* package */ void experimental_setBandwidthAllocationCheckpoints(
+ long[][] allocationCheckpoints) {
+ Assertions.checkArgument(allocationCheckpoints.length >= 2);
+ this.allocationCheckpoints = allocationCheckpoints;
+ }
+ }
+
+ /**
+ * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track
+ * selections.
+ *
+ * @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate.
+ * @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total
+ * bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint.
+ */
+ private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) {
+ // Algorithm:
+ // 1. Use log bitrates to treat all resolution update steps equally.
+ // 2. Distribute switch points for each selection equally in the same [0.0-1.0] range.
+ // 3. Switch up one format at a time in the order of the switch points.
+ double[][] logBitrates = getLogArrayValues(trackBitrates);
+ double[][] switchPoints = getSwitchPoints(logBitrates);
+
+ // There will be (count(switch point) + 3) checkpoints:
+ // [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points,
+ // [end] = extra point to set slope for additional bitrate.
+ int checkpointCount = countArrayElements(switchPoints) + 3;
+ long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2];
+ int[] currentSelection = new int[logBitrates.length];
+ setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection);
+ for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) {
+ int nextUpdateIndex = 0;
+ double nextUpdateSwitchPoint = Double.MAX_VALUE;
+ for (int i = 0; i < logBitrates.length; i++) {
+ if (currentSelection[i] + 1 == logBitrates[i].length) {
+ continue;
+ }
+ double switchPoint = switchPoints[i][currentSelection[i]];
+ if (switchPoint < nextUpdateSwitchPoint) {
+ nextUpdateSwitchPoint = switchPoint;
+ nextUpdateIndex = i;
+ }
+ }
+ currentSelection[nextUpdateIndex]++;
+ setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection);
+ }
+ for (long[][] points : checkpoints) {
+ points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0];
+ points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1];
+ }
+ return checkpoints;
+ }
+
+ /** Converts all input values to Math.log(value). */
+ private static double[][] getLogArrayValues(long[][] values) {
+ double[][] logValues = new double[values.length][];
+ for (int i = 0; i < values.length; i++) {
+ logValues[i] = new double[values[i].length];
+ for (int j = 0; j < values[i].length; j++) {
+ logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]);
+ }
+ }
+ return logValues;
+ }
+
+ /**
+ * Returns idealized switch points for each switch between consecutive track selection bitrates.
+ *
+ * @param logBitrates Log bitrates with [selectionCount][formatCount].
+ * @return Linearly distributed switch points in the range of [0.0-1.0].
+ */
+ private static double[][] getSwitchPoints(double[][] logBitrates) {
+ double[][] switchPoints = new double[logBitrates.length][];
+ for (int i = 0; i < logBitrates.length; i++) {
+ switchPoints[i] = new double[logBitrates[i].length - 1];
+ if (switchPoints[i].length == 0) {
+ continue;
+ }
+ double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
+ for (int j = 0; j < logBitrates[i].length - 1; j++) {
+ double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);
+ switchPoints[i][j] =
+ totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
+ }
+ }
+ return switchPoints;
+ }
+
+ /** Returns total number of elements in a 2D array. */
+ private static int countArrayElements(double[][] array) {
+ int count = 0;
+ for (double[] subArray : array) {
+ count += subArray.length;
+ }
+ return count;
+ }
+
+ /**
+ * Sets checkpoint bitrates.
+ *
+ * @param checkpoints Output checkpoints with [selectionIndex][checkpointIndex][2] where [0]=Total
+ * bitrate and [1]=Allocated bitrate.
+ * @param checkpointIndex The checkpoint index.
+ * @param trackBitrates The track bitrates with [selectionIndex][trackIndex].
+ * @param selectedTracks The indices of selected tracks for each selection for this checkpoint.
+ */
+ private static void setCheckpointValues(
+ long[][][] checkpoints, int checkpointIndex, long[][] trackBitrates, int[] selectedTracks) {
+ long totalBitrate = 0;
+ for (int i = 0; i < checkpoints.length; i++) {
+ checkpoints[i][checkpointIndex][1] = trackBitrates[i][selectedTracks[i]];
+ totalBitrate += checkpoints[i][checkpointIndex][1];
+ }
+ for (long[][] points : checkpoints) {
+ points[checkpointIndex][0] = totalBitrate;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
new file mode 100644
index 0000000000..d7e94cb561
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -0,0 +1,217 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An abstract base class suitable for most {@link TrackSelection} implementations.
+ */
+public abstract class BaseTrackSelection implements TrackSelection {
+
+ /**
+ * The selected {@link TrackGroup}.
+ */
+ protected final TrackGroup group;
+ /**
+ * The number of selected tracks within the {@link TrackGroup}. Always greater than zero.
+ */
+ protected final int length;
+ /**
+ * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth.
+ */
+ protected final int[] tracks;
+
+ /**
+ * The {@link Format}s of the selected tracks, in order of decreasing bandwidth.
+ */
+ private final Format[] formats;
+ /**
+ * Selected track blacklist timestamps, in order of decreasing bandwidth.
+ */
+ private final long[] blacklistUntilTimes;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ */
+ public BaseTrackSelection(TrackGroup group, int... tracks) {
+ Assertions.checkState(tracks.length > 0);
+ this.group = Assertions.checkNotNull(group);
+ this.length = tracks.length;
+ // Set the formats, sorted in order of decreasing bandwidth.
+ formats = new Format[length];
+ for (int i = 0; i < tracks.length; i++) {
+ formats[i] = group.getFormat(tracks[i]);
+ }
+ Arrays.sort(formats, new DecreasingBandwidthComparator());
+ // Set the format indices in the same order.
+ this.tracks = new int[length];
+ for (int i = 0; i < length; i++) {
+ this.tracks[i] = group.indexOf(formats[i]);
+ }
+ blacklistUntilTimes = new long[length];
+ }
+
+ @Override
+ public void enable() {
+ // Do nothing.
+ }
+
+ @Override
+ public void disable() {
+ // Do nothing.
+ }
+
+ @Override
+ public final TrackGroup getTrackGroup() {
+ return group;
+ }
+
+ @Override
+ public final int length() {
+ return tracks.length;
+ }
+
+ @Override
+ public final Format getFormat(int index) {
+ return formats[index];
+ }
+
+ @Override
+ public final int getIndexInTrackGroup(int index) {
+ return tracks[index];
+ }
+
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public final int indexOf(Format format) {
+ for (int i = 0; i < length; i++) {
+ if (formats[i] == format) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final int indexOf(int indexInTrackGroup) {
+ for (int i = 0; i < length; i++) {
+ if (tracks[i] == indexInTrackGroup) {
+ return i;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public final Format getSelectedFormat() {
+ return formats[getSelectedIndex()];
+ }
+
+ @Override
+ public final int getSelectedIndexInTrackGroup() {
+ return tracks[getSelectedIndex()];
+ }
+
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ // Do nothing.
+ }
+
+ @Override
+ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+ return queue.size();
+ }
+
+ @Override
+ public final boolean blacklist(int index, long blacklistDurationMs) {
+ long nowMs = SystemClock.elapsedRealtime();
+ boolean canBlacklist = isBlacklisted(index, nowMs);
+ for (int i = 0; i < length && !canBlacklist; i++) {
+ canBlacklist = i != index && !isBlacklisted(i, nowMs);
+ }
+ if (!canBlacklist) {
+ return false;
+ }
+ blacklistUntilTimes[index] =
+ Math.max(
+ blacklistUntilTimes[index],
+ Util.addWithOverflowDefault(nowMs, blacklistDurationMs, Long.MAX_VALUE));
+ return true;
+ }
+
+ /**
+ * Returns whether the track at the specified index in the selection is blacklisted.
+ *
+ * @param index The index of the track in the selection.
+ * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}.
+ */
+ protected final boolean isBlacklisted(int index, long nowMs) {
+ return blacklistUntilTimes[index] > nowMs;
+ }
+
+ // Object overrides.
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks);
+ }
+ return hashCode;
+ }
+
+ // Track groups are compared by identity not value, as distinct groups may have the same value.
+ @Override
+ @SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ BaseTrackSelection other = (BaseTrackSelection) obj;
+ return group == other.group && Arrays.equals(tracks, other.tracks);
+ }
+
+ /**
+ * Sorts {@link Format} objects in order of decreasing bandwidth.
+ */
+ private static final class DecreasingBandwidthComparator implements Comparator<Format> {
+
+ @Override
+ public int compare(Format a, Format b) {
+ return b.bitrate - a.bitrate;
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
new file mode 100644
index 0000000000..735889bfaa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.DefaultLoadControl;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.LoadControl;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultAllocator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Builder for a {@link TrackSelection.Factory} and {@link LoadControl} that implement buffer size
+ * based track adaptation.
+ */
+public final class BufferSizeAdaptationBuilder {
+
+ /** Dynamic filter for formats, which is applied when selecting a new track. */
+ public interface DynamicFormatFilter {
+
+ /** Filter which allows all formats. */
+ DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true;
+
+ /**
+ * Called when updating the selected track to determine whether a candidate track is allowed. If
+ * no format is allowed or eligible, the lowest quality format will be used.
+ *
+ * @param format The {@link Format} of the candidate track.
+ * @param trackBitrate The estimated bitrate of the track. May differ from {@link
+ * Format#bitrate} if a more accurate estimate of the current track bitrate is available.
+ * @param isInitialSelection Whether this is for the initial track selection.
+ */
+ boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection);
+ }
+
+ /**
+ * The default minimum duration of media that the player will attempt to ensure is buffered at all
+ * times, in milliseconds.
+ */
+ public static final int DEFAULT_MIN_BUFFER_MS = 15000;
+
+ /**
+ * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+ */
+ public static final int DEFAULT_MAX_BUFFER_MS = 50000;
+
+ /**
+ * The default duration of media that must be buffered for playback to start or resume following a
+ * user action such as a seek, in milliseconds.
+ */
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS =
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
+
+ /**
+ * The default duration of media that must be buffered for playback to resume after a rebuffer, in
+ * milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action.
+ */
+ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS =
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
+
+ /**
+ * The default offset the current duration of buffered media must deviate from the ideal duration
+ * of buffered media for the currently selected format, before the selected format is changed.
+ */
+ public static final int DEFAULT_HYSTERESIS_BUFFER_MS = 5000;
+
+ /**
+ * During start-up phase, the default fraction of the available bandwidth that the selection
+ * should consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ */
+ public static final float DEFAULT_START_UP_BANDWIDTH_FRACTION =
+ AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION;
+
+ /**
+ * During start-up phase, the default minimum duration of buffered media required for the selected
+ * track to switch to one of higher quality based on measured bandwidth.
+ */
+ public static final int DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS =
+ AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS;
+
+ @Nullable private DefaultAllocator allocator;
+ private Clock clock;
+ private int minBufferMs;
+ private int maxBufferMs;
+ private int bufferForPlaybackMs;
+ private int bufferForPlaybackAfterRebufferMs;
+ private int hysteresisBufferMs;
+ private float startUpBandwidthFraction;
+ private int startUpMinBufferForQualityIncreaseMs;
+ private DynamicFormatFilter dynamicFormatFilter;
+ private boolean buildCalled;
+
+ /** Creates builder with default values. */
+ public BufferSizeAdaptationBuilder() {
+ clock = Clock.DEFAULT;
+ minBufferMs = DEFAULT_MIN_BUFFER_MS;
+ maxBufferMs = DEFAULT_MAX_BUFFER_MS;
+ bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS;
+ bufferForPlaybackAfterRebufferMs = DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
+ hysteresisBufferMs = DEFAULT_HYSTERESIS_BUFFER_MS;
+ startUpBandwidthFraction = DEFAULT_START_UP_BANDWIDTH_FRACTION;
+ startUpMinBufferForQualityIncreaseMs = DEFAULT_START_UP_MIN_BUFFER_FOR_QUALITY_INCREASE_MS;
+ dynamicFormatFilter = DynamicFormatFilter.NO_FILTER;
+ }
+
+ /**
+ * Set the clock to use. Should only be set for testing purposes.
+ *
+ * @param clock The {@link Clock}.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setClock(Clock clock) {
+ Assertions.checkState(!buildCalled);
+ this.clock = clock;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DefaultAllocator} used by the loader.
+ *
+ * @param allocator The {@link DefaultAllocator}.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setAllocator(DefaultAllocator allocator) {
+ Assertions.checkState(!buildCalled);
+ this.allocator = allocator;
+ return this;
+ }
+
+ /**
+ * Sets the buffer duration parameters.
+ *
+ * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+ * buffered at all times, in milliseconds.
+ * @param maxBufferMs The maximum duration of media that the player will attempt to buffer, in
+ * milliseconds.
+ * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+ * resume following a user action such as a seek, in milliseconds.
+ * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+ * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+ * buffer depletion rather than a user action.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setBufferDurationsMs(
+ int minBufferMs,
+ int maxBufferMs,
+ int bufferForPlaybackMs,
+ int bufferForPlaybackAfterRebufferMs) {
+ Assertions.checkState(!buildCalled);
+ this.minBufferMs = minBufferMs;
+ this.maxBufferMs = maxBufferMs;
+ this.bufferForPlaybackMs = bufferForPlaybackMs;
+ this.bufferForPlaybackAfterRebufferMs = bufferForPlaybackAfterRebufferMs;
+ return this;
+ }
+
+ /**
+ * Sets the hysteresis buffer used to prevent repeated format switching.
+ *
+ * @param hysteresisBufferMs The offset the current duration of buffered media must deviate from
+ * the ideal duration of buffered media for the currently selected format, before the selected
+ * format is changed. This value must be smaller than {@code maxBufferMs - minBufferMs}.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setHysteresisBufferMs(int hysteresisBufferMs) {
+ Assertions.checkState(!buildCalled);
+ this.hysteresisBufferMs = hysteresisBufferMs;
+ return this;
+ }
+
+ /**
+ * Sets track selection parameters used during the start-up phase before the selection can be made
+ * purely on based on buffer size. During the start-up phase the selection is based on the current
+ * bandwidth estimate.
+ *
+ * @param bandwidthFraction The fraction of the available bandwidth that the selection should
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ * @param minBufferForQualityIncreaseMs The minimum duration of buffered media required for the
+ * selected track to switch to one of higher quality.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setStartUpTrackSelectionParameters(
+ float bandwidthFraction, int minBufferForQualityIncreaseMs) {
+ Assertions.checkState(!buildCalled);
+ this.startUpBandwidthFraction = bandwidthFraction;
+ this.startUpMinBufferForQualityIncreaseMs = minBufferForQualityIncreaseMs;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DynamicFormatFilter} to use when updating the selected track.
+ *
+ * @param dynamicFormatFilter The {@link DynamicFormatFilter}.
+ * @return This builder, for convenience.
+ * @throws IllegalStateException If {@link #buildPlayerComponents()} has already been called.
+ */
+ public BufferSizeAdaptationBuilder setDynamicFormatFilter(
+ DynamicFormatFilter dynamicFormatFilter) {
+ Assertions.checkState(!buildCalled);
+ this.dynamicFormatFilter = dynamicFormatFilter;
+ return this;
+ }
+
+ /**
+ * Builds player components for buffer size based track adaptation.
+ *
+ * @return A pair of a {@link TrackSelection.Factory} and a {@link LoadControl}, which should be
+ * used to construct the player.
+ */
+ public Pair<TrackSelection.Factory, LoadControl> buildPlayerComponents() {
+ Assertions.checkArgument(hysteresisBufferMs < maxBufferMs - minBufferMs);
+ Assertions.checkState(!buildCalled);
+ buildCalled = true;
+
+ DefaultLoadControl.Builder loadControlBuilder =
+ new DefaultLoadControl.Builder()
+ .setTargetBufferBytes(/* targetBufferBytes = */ Integer.MAX_VALUE)
+ .setBufferDurationsMs(
+ /* minBufferMs= */ maxBufferMs,
+ maxBufferMs,
+ bufferForPlaybackMs,
+ bufferForPlaybackAfterRebufferMs);
+ if (allocator != null) {
+ loadControlBuilder.setAllocator(allocator);
+ }
+
+ TrackSelection.Factory trackSelectionFactory =
+ new TrackSelection.Factory() {
+ @Override
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition ->
+ new BufferSizeAdaptiveTrackSelection(
+ definition.group,
+ definition.tracks,
+ bandwidthMeter,
+ minBufferMs,
+ maxBufferMs,
+ hysteresisBufferMs,
+ startUpBandwidthFraction,
+ startUpMinBufferForQualityIncreaseMs,
+ dynamicFormatFilter,
+ clock));
+ }
+ };
+
+ return Pair.create(trackSelectionFactory, loadControlBuilder.createDefaultLoadControl());
+ }
+
+ private static final class BufferSizeAdaptiveTrackSelection extends BaseTrackSelection {
+
+ private static final int BITRATE_BLACKLISTED = Format.NO_VALUE;
+
+ private final BandwidthMeter bandwidthMeter;
+ private final Clock clock;
+ private final DynamicFormatFilter dynamicFormatFilter;
+ private final int[] formatBitrates;
+ private final long minBufferUs;
+ private final long maxBufferUs;
+ private final long hysteresisBufferUs;
+ private final float startUpBandwidthFraction;
+ private final long startUpMinBufferForQualityIncreaseUs;
+ private final int minBitrate;
+ private final int maxBitrate;
+ private final double bitrateToBufferFunctionSlope;
+ private final double bitrateToBufferFunctionIntercept;
+
+ private boolean isInSteadyState;
+ private int selectedIndex;
+ private int selectionReason;
+ private float playbackSpeed;
+
+ private BufferSizeAdaptiveTrackSelection(
+ TrackGroup trackGroup,
+ int[] tracks,
+ BandwidthMeter bandwidthMeter,
+ int minBufferMs,
+ int maxBufferMs,
+ int hysteresisBufferMs,
+ float startUpBandwidthFraction,
+ int startUpMinBufferForQualityIncreaseMs,
+ DynamicFormatFilter dynamicFormatFilter,
+ Clock clock) {
+ super(trackGroup, tracks);
+ this.bandwidthMeter = bandwidthMeter;
+ this.minBufferUs = C.msToUs(minBufferMs);
+ this.maxBufferUs = C.msToUs(maxBufferMs);
+ this.hysteresisBufferUs = C.msToUs(hysteresisBufferMs);
+ this.startUpBandwidthFraction = startUpBandwidthFraction;
+ this.startUpMinBufferForQualityIncreaseUs = C.msToUs(startUpMinBufferForQualityIncreaseMs);
+ this.dynamicFormatFilter = dynamicFormatFilter;
+ this.clock = clock;
+
+ formatBitrates = new int[length];
+ maxBitrate = getFormat(/* index= */ 0).bitrate;
+ minBitrate = getFormat(/* index= */ length - 1).bitrate;
+ selectionReason = C.SELECTION_REASON_UNKNOWN;
+ playbackSpeed = 1.0f;
+
+ // We use a log-linear function to map from bitrate to buffer size:
+ // buffer = slope * ln(bitrate) + intercept,
+ // with buffer(minBitrate) = minBuffer and buffer(maxBitrate) = maxBuffer - hysteresisBuffer.
+ bitrateToBufferFunctionSlope =
+ (maxBufferUs - hysteresisBufferUs - minBufferUs)
+ / Math.log((double) maxBitrate / minBitrate);
+ bitrateToBufferFunctionIntercept =
+ minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate);
+ }
+
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ this.playbackSpeed = playbackSpeed;
+ }
+
+ @Override
+ public void onDiscontinuity() {
+ isInSteadyState = false;
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return selectionReason;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime());
+
+ // Make initial selection
+ if (selectionReason == C.SELECTION_REASON_UNKNOWN) {
+ selectionReason = C.SELECTION_REASON_INITIAL;
+ selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true);
+ return;
+ }
+
+ long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs);
+ int oldSelectedIndex = selectedIndex;
+ if (isInSteadyState) {
+ selectIndexSteadyState(bufferUs);
+ } else {
+ selectIndexStartUpPhase(bufferUs);
+ }
+ if (selectedIndex != oldSelectedIndex) {
+ selectionReason = C.SELECTION_REASON_ADAPTIVE;
+ }
+ }
+
+ // Steady state.
+
+ private void selectIndexSteadyState(long bufferUs) {
+ if (isOutsideHysteresis(bufferUs)) {
+ selectedIndex = selectIdealIndexUsingBufferSize(bufferUs);
+ }
+ }
+
+ private boolean isOutsideHysteresis(long bufferUs) {
+ if (formatBitrates[selectedIndex] == BITRATE_BLACKLISTED) {
+ return true;
+ }
+ long targetBufferForCurrentBitrateUs =
+ getTargetBufferForBitrateUs(formatBitrates[selectedIndex]);
+ long bufferDiffUs = bufferUs - targetBufferForCurrentBitrateUs;
+ return Math.abs(bufferDiffUs) > hysteresisBufferUs;
+ }
+
+ private int selectIdealIndexUsingBufferSize(long bufferUs) {
+ int lowestBitrateNonBlacklistedIndex = 0;
+ for (int i = 0; i < formatBitrates.length; i++) {
+ if (formatBitrates[i] != BITRATE_BLACKLISTED) {
+ if (getTargetBufferForBitrateUs(formatBitrates[i]) <= bufferUs
+ && dynamicFormatFilter.isFormatAllowed(
+ getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) {
+ return i;
+ }
+ lowestBitrateNonBlacklistedIndex = i;
+ }
+ }
+ return lowestBitrateNonBlacklistedIndex;
+ }
+
+ // Startup.
+
+ private void selectIndexStartUpPhase(long bufferUs) {
+ int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false);
+ int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs);
+ if (steadyStateSelectedIndex <= selectedIndex) {
+ // Switch to steady state if we have enough buffer to maintain current selection.
+ selectedIndex = steadyStateSelectedIndex;
+ isInSteadyState = true;
+ } else {
+ if (bufferUs < startUpMinBufferForQualityIncreaseUs
+ && startUpSelectedIndex < selectedIndex
+ && formatBitrates[selectedIndex] != BITRATE_BLACKLISTED) {
+ // Switching up from a non-blacklisted track is only allowed if we have enough buffer.
+ return;
+ }
+ selectedIndex = startUpSelectedIndex;
+ }
+ }
+
+ private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) {
+ long effectiveBitrate =
+ (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction);
+ int lowestBitrateNonBlacklistedIndex = 0;
+ for (int i = 0; i < formatBitrates.length; i++) {
+ if (formatBitrates[i] != BITRATE_BLACKLISTED) {
+ if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate
+ && dynamicFormatFilter.isFormatAllowed(
+ getFormat(i), formatBitrates[i], isInitialSelection)) {
+ return i;
+ }
+ lowestBitrateNonBlacklistedIndex = i;
+ }
+ }
+ return lowestBitrateNonBlacklistedIndex;
+ }
+
+ // Utility methods.
+
+ private void updateFormatBitrates(long nowMs) {
+ for (int i = 0; i < length; i++) {
+ if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
+ formatBitrates[i] = getFormat(i).bitrate;
+ } else {
+ formatBitrates[i] = BITRATE_BLACKLISTED;
+ }
+ }
+ }
+
+ private long getTargetBufferForBitrateUs(int bitrate) {
+ if (bitrate <= minBitrate) {
+ return minBufferUs;
+ }
+ if (bitrate >= maxBitrate) {
+ return maxBufferUs - hysteresisBufferUs;
+ }
+ return (int)
+ (bitrateToBufferFunctionSlope * Math.log(bitrate) + bitrateToBufferFunctionIntercept);
+ }
+
+ private static long getCurrentPeriodBufferedDurationUs(
+ long playbackPositionUs, long bufferedDurationUs) {
+ return playbackPositionUs >= 0 ? bufferedDurationUs : playbackPositionUs + bufferedDurationUs;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
new file mode 100644
index 0000000000..549e5991b9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -0,0 +1,2827 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.Capabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.FormatSupport;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import org.checkerframework.checker.initialization.qual.UnderInitialization;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A default {@link TrackSelector} suitable for most use cases. Track selections are made according
+ * to configurable {@link Parameters}, which can be set by calling {@link
+ * #setParameters(Parameters)}.
+ *
+ * <h3>Modifying parameters</h3>
+ *
+ * To modify only some aspects of the parameters currently used by a selector, it's possible to
+ * obtain a {@link ParametersBuilder} initialized with the current {@link Parameters}. The desired
+ * modifications can be made on the builder, and the resulting {@link Parameters} can then be built
+ * and set on the selector. For example the following code modifies the parameters to restrict video
+ * track selections to SD, and to select a German audio track if there is one:
+ *
+ * <pre>{@code
+ * // Build on the current parameters.
+ * Parameters currentParameters = trackSelector.getParameters();
+ * // Build the resulting parameters.
+ * Parameters newParameters = currentParameters
+ * .buildUpon()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu")
+ * .build();
+ * // Set the new parameters.
+ * trackSelector.setParameters(newParameters);
+ * }</pre>
+ *
+ * Convenience methods and chaining allow this to be written more concisely as:
+ *
+ * <pre>{@code
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
+ * }</pre>
+ *
+ * Selection {@link Parameters} support many different options, some of which are described below.
+ *
+ * <h3>Selecting specific tracks</h3>
+ *
+ * Track selection overrides can be used to select specific tracks. To specify an override for a
+ * renderer, it's first necessary to obtain the tracks that have been mapped to it:
+ *
+ * <pre>{@code
+ * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
+ * : mappedTrackInfo.getTrackGroups(rendererIndex);
+ * }</pre>
+ *
+ * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so
+ * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the
+ * player can be used to determine when the current tracks (and therefore the mapping) changes. If
+ * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query
+ * the properties of the available tracks to determine the {@code groupIndex} and the {@code
+ * trackIndices} within the group it that should be selected. The override can then be specified
+ * using {@link ParametersBuilder#setSelectionOverride}:
+ *
+ * <pre>{@code
+ * SelectionOverride selectionOverride = new SelectionOverride(groupIndex, trackIndices);
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setSelectionOverride(rendererIndex, rendererTrackGroups, selectionOverride));
+ * }</pre>
+ *
+ * <h3>Constraint based track selection</h3>
+ *
+ * Whilst track selection overrides make it possible to select specific tracks, the recommended way
+ * of controlling which tracks are selected is by specifying constraints. For example consider the
+ * case of wanting to restrict video track selections to SD, and preferring German audio tracks.
+ * Track selection overrides could be used to select specific tracks meeting these criteria, however
+ * a simpler and more flexible approach is to specify these constraints directly:
+ *
+ * <pre>{@code
+ * trackSelector.setParameters(
+ * trackSelector
+ * .buildUponParameters()
+ * .setMaxVideoSizeSd()
+ * .setPreferredAudioLanguage("deu"));
+ * }</pre>
+ *
+ * There are several benefits to using constraint based track selection instead of specific track
+ * overrides:
+ *
+ * <ul>
+ * <li>You can specify constraints before knowing what tracks the media provides. This can
+ * simplify track selection code (e.g. you don't have to listen for changes in the available
+ * tracks before configuring the selector).
+ * <li>Constraints can be applied consistently across all periods in a complex piece of media,
+ * even if those periods contain different tracks. In contrast, a specific track override is
+ * only applied to periods whose tracks match those for which the override was set.
+ * </ul>
+ *
+ * <h3>Disabling renderers</h3>
+ *
+ * Renderers can be disabled using {@link ParametersBuilder#setRendererDisabled}. Disabling a
+ * renderer differs from setting a {@code null} override because the renderer is disabled
+ * unconditionally, whereas a {@code null} override is applied only when the track groups available
+ * to the renderer match the {@link TrackGroupArray} for which it was specified.
+ *
+ * <h3>Tunneling</h3>
+ *
+ * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks
+ * support it. Tunneled playback is enabled by passing an audio session ID to {@link
+ * ParametersBuilder#setTunnelingAudioSessionId(int)}.
+ */
+public class DefaultTrackSelector extends MappingTrackSelector {
+
+ /**
+ * 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 ParametersBuilder extends TrackSelectionParameters.Builder {
+
+ // Video
+ private int maxVideoWidth;
+ private int maxVideoHeight;
+ private int maxVideoFrameRate;
+ private int maxVideoBitrate;
+ private boolean exceedVideoConstraintsIfNecessary;
+ private boolean allowVideoMixedMimeTypeAdaptiveness;
+ private boolean allowVideoNonSeamlessAdaptiveness;
+ private int viewportWidth;
+ private int viewportHeight;
+ private boolean viewportOrientationMayChange;
+ // Audio
+ private int maxAudioChannelCount;
+ private int maxAudioBitrate;
+ private boolean exceedAudioConstraintsIfNecessary;
+ private boolean allowAudioMixedMimeTypeAdaptiveness;
+ private boolean allowAudioMixedSampleRateAdaptiveness;
+ private boolean allowAudioMixedChannelCountAdaptiveness;
+ // General
+ private boolean forceLowestBitrate;
+ private boolean forceHighestSupportedBitrate;
+ private boolean exceedRendererCapabilitiesIfNecessary;
+ private int tunnelingAudioSessionId;
+
+ 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
+ * #ParametersBuilder(Context)} instead.
+ */
+ @Deprecated
+ @SuppressWarnings({"deprecation"})
+ public ParametersBuilder() {
+ super();
+ setInitialValuesWithoutContext();
+ selectionOverrides = new SparseArray<>();
+ rendererDisabledFlags = new SparseBooleanArray();
+ }
+
+ /**
+ * Creates a builder with default initial values.
+ *
+ * @param context Any context.
+ */
+
+ public ParametersBuilder(Context context) {
+ super(context);
+ setInitialValuesWithoutContext();
+ selectionOverrides = new SparseArray<>();
+ rendererDisabledFlags = new SparseBooleanArray();
+ setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true);
+ }
+
+ /**
+ * @param initialValues The {@link Parameters} from which the initial values of the builder are
+ * obtained.
+ */
+ private ParametersBuilder(Parameters initialValues) {
+ super(initialValues);
+ // Video
+ maxVideoWidth = initialValues.maxVideoWidth;
+ maxVideoHeight = initialValues.maxVideoHeight;
+ maxVideoFrameRate = initialValues.maxVideoFrameRate;
+ maxVideoBitrate = initialValues.maxVideoBitrate;
+ exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary;
+ allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness;
+ allowVideoNonSeamlessAdaptiveness = initialValues.allowVideoNonSeamlessAdaptiveness;
+ viewportWidth = initialValues.viewportWidth;
+ viewportHeight = initialValues.viewportHeight;
+ viewportOrientationMayChange = initialValues.viewportOrientationMayChange;
+ // Audio
+ maxAudioChannelCount = initialValues.maxAudioChannelCount;
+ maxAudioBitrate = initialValues.maxAudioBitrate;
+ exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary;
+ allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness;
+ allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness;
+ allowAudioMixedChannelCountAdaptiveness =
+ initialValues.allowAudioMixedChannelCountAdaptiveness;
+ // General
+ forceLowestBitrate = initialValues.forceLowestBitrate;
+ forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate;
+ exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary;
+ tunnelingAudioSessionId = initialValues.tunnelingAudioSessionId;
+ // Overrides
+ selectionOverrides = cloneSelectionOverrides(initialValues.selectionOverrides);
+ rendererDisabledFlags = initialValues.rendererDisabledFlags.clone();
+ }
+
+ // Video
+
+ /**
+ * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoSizeSd() {
+ return setMaxVideoSize(1279, 719);
+ }
+
+ /**
+ * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder clearVideoSizeConstraints() {
+ return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Sets the maximum allowed video width and height.
+ *
+ * @param maxVideoWidth Maximum allowed video width in pixels.
+ * @param maxVideoHeight Maximum allowed video height in pixels.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
+ this.maxVideoWidth = maxVideoWidth;
+ this.maxVideoHeight = maxVideoHeight;
+ return this;
+ }
+
+ /**
+ * Sets the maximum allowed video frame rate.
+ *
+ * @param maxVideoFrameRate Maximum allowed video frame rate in hertz.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoFrameRate(int maxVideoFrameRate) {
+ this.maxVideoFrameRate = maxVideoFrameRate;
+ return this;
+ }
+
+ /**
+ * Sets the maximum allowed video bitrate.
+ *
+ * @param maxVideoBitrate Maximum allowed video bitrate in bits per second.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) {
+ this.maxVideoBitrate = maxVideoBitrate;
+ return this;
+ }
+
+ /**
+ * Sets whether to exceed the {@link #setMaxVideoSize(int, int)} and {@link
+ * #setMaxAudioBitrate(int)} 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) {
+ 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 ParametersBuilder 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 ParametersBuilder setAllowVideoNonSeamlessAdaptiveness(
+ boolean allowVideoNonSeamlessAdaptiveness) {
+ this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;
+ return this;
+ }
+
+ /**
+ * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size
+ * obtained from {@link Util#getCurrentDisplayModeSize(Context)}.
+ *
+ * @param context Any context.
+ * @param viewportOrientationMayChange Whether the viewport orientation may change during
+ * playback.
+ * @return This builder.
+ */
+ public ParametersBuilder setViewportSizeToPhysicalDisplaySize(
+ Context context, boolean viewportOrientationMayChange) {
+ // Assume the viewport is fullscreen.
+ Point viewportSize = Util.getCurrentDisplayModeSize(context);
+ return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
+ }
+
+ /**
+ * Equivalent to {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE,
+ * true)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder clearViewportSizeConstraints() {
+ return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+ }
+
+ /**
+ * Sets the viewport size to constrain adaptive video selections so that only tracks suitable
+ * for the viewport are selected.
+ *
+ * @param viewportWidth Viewport width in pixels.
+ * @param viewportHeight Viewport height in pixels.
+ * @param viewportOrientationMayChange Whether the viewport orientation may change during
+ * playback.
+ * @return This builder.
+ */
+ public ParametersBuilder setViewportSize(
+ int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ this.viewportOrientationMayChange = viewportOrientationMayChange;
+ return this;
+ }
+
+ // Audio
+
+ @Override
+ public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
+ super.setPreferredAudioLanguage(preferredAudioLanguage);
+ return this;
+ }
+
+ /**
+ * Sets the maximum allowed audio channel count.
+ *
+ * @param maxAudioChannelCount Maximum allowed audio channel count.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) {
+ this.maxAudioChannelCount = maxAudioChannelCount;
+ return this;
+ }
+
+ /**
+ * Sets the maximum allowed audio bitrate.
+ *
+ * @param maxAudioBitrate Maximum allowed audio bitrate in bits per second.
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxAudioBitrate(int maxAudioBitrate) {
+ this.maxAudioBitrate = 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) {
+ 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 ParametersBuilder 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 ParametersBuilder 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 ParametersBuilder setAllowAudioMixedChannelCountAdaptiveness(
+ boolean allowAudioMixedChannelCountAdaptiveness) {
+ this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness;
+ return this;
+ }
+
+ // Text
+
+ @Override
+ public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
+ Context context) {
+ super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
+ return this;
+ }
+
+ @Override
+ public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {
+ super.setPreferredTextLanguage(preferredTextLanguage);
+ return this;
+ }
+
+ @Override
+ public ParametersBuilder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
+ super.setPreferredTextRoleFlags(preferredTextRoleFlags);
+ return this;
+ }
+
+ @Override
+ public ParametersBuilder setSelectUndeterminedTextLanguage(
+ boolean selectUndeterminedTextLanguage) {
+ super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
+ return this;
+ }
+
+ @Override
+ public ParametersBuilder setDisabledTextTrackSelectionFlags(
+ @C.SelectionFlags int disabledTextTrackSelectionFlags) {
+ super.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags);
+ return this;
+ }
+
+ // General
+
+ /**
+ * Sets whether to force selection of the single lowest bitrate audio and video tracks that
+ * comply with all other constraints.
+ *
+ * @param forceLowestBitrate Whether to force selection of the single lowest bitrate audio and
+ * video tracks.
+ * @return This builder.
+ */
+ public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) {
+ this.forceLowestBitrate = forceLowestBitrate;
+ return this;
+ }
+
+ /**
+ * Sets whether to force selection of the highest bitrate audio and video tracks that comply
+ * with all other constraints.
+ *
+ * @param forceHighestSupportedBitrate Whether to force selection of the highest bitrate audio
+ * and video tracks.
+ * @return This builder.
+ */
+ public ParametersBuilder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
+ this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
+ return this;
+ }
+
+ /**
+ * @deprecated Use {@link #setAllowVideoMixedMimeTypeAdaptiveness(boolean)} and {@link
+ * #setAllowAudioMixedMimeTypeAdaptiveness(boolean)}.
+ */
+ @Deprecated
+ public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {
+ setAllowAudioMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness);
+ setAllowVideoMixedMimeTypeAdaptiveness(allowMixedMimeAdaptiveness);
+ return this;
+ }
+
+ /** @deprecated Use {@link #setAllowVideoNonSeamlessAdaptiveness(boolean)} */
+ @Deprecated
+ public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {
+ return setAllowVideoNonSeamlessAdaptiveness(allowNonSeamlessAdaptiveness);
+ }
+
+ /**
+ * 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) {
+ this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
+ return this;
+ }
+
+ /**
+ * Sets the audio session id to use when tunneling.
+ *
+ * <p>Enables or disables tunneling. To enable tunneling, pass an audio session id to use when
+ * in tunneling mode. Session ids can be generated using {@link
+ * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link
+ * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
+ * supported by the audio and video renderers for the selected tracks.
+ *
+ * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link
+ * C#AUDIO_SESSION_ID_UNSET} to disable tunneling.
+ * @return This builder.
+ */
+ public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) {
+ this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+ 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 final ParametersBuilder 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.
+ */
+ public final ParametersBuilder 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.
+ */
+ public final ParametersBuilder 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.
+ */
+ public final ParametersBuilder 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.
+ */
+ public final ParametersBuilder clearSelectionOverrides() {
+ if (selectionOverrides.size() == 0) {
+ // Nothing to clear.
+ return this;
+ }
+ selectionOverrides.clear();
+ return this;
+ }
+
+ /**
+ * Builds a {@link Parameters} instance with the selected values.
+ */
+ public Parameters build() {
+ return new Parameters(
+ // Video
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoFrameRate,
+ maxVideoBitrate,
+ exceedVideoConstraintsIfNecessary,
+ allowVideoMixedMimeTypeAdaptiveness,
+ allowVideoNonSeamlessAdaptiveness,
+ viewportWidth,
+ viewportHeight,
+ viewportOrientationMayChange,
+ // Audio
+ preferredAudioLanguage,
+ maxAudioChannelCount,
+ maxAudioBitrate,
+ exceedAudioConstraintsIfNecessary,
+ allowAudioMixedMimeTypeAdaptiveness,
+ allowAudioMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness,
+ // Text
+ preferredTextLanguage,
+ preferredTextRoleFlags,
+ selectUndeterminedTextLanguage,
+ disabledTextTrackSelectionFlags,
+ // General
+ forceLowestBitrate,
+ forceHighestSupportedBitrate,
+ exceedRendererCapabilitiesIfNecessary,
+ tunnelingAudioSessionId,
+ selectionOverrides,
+ rendererDisabledFlags);
+ }
+
+ private void setInitialValuesWithoutContext(@UnderInitialization ParametersBuilder this) {
+ // Video
+ maxVideoWidth = Integer.MAX_VALUE;
+ maxVideoHeight = Integer.MAX_VALUE;
+ maxVideoFrameRate = Integer.MAX_VALUE;
+ maxVideoBitrate = Integer.MAX_VALUE;
+ exceedVideoConstraintsIfNecessary = true;
+ allowVideoMixedMimeTypeAdaptiveness = false;
+ allowVideoNonSeamlessAdaptiveness = true;
+ viewportWidth = Integer.MAX_VALUE;
+ viewportHeight = Integer.MAX_VALUE;
+ viewportOrientationMayChange = true;
+ // Audio
+ maxAudioChannelCount = Integer.MAX_VALUE;
+ maxAudioBitrate = Integer.MAX_VALUE;
+ exceedAudioConstraintsIfNecessary = true;
+ allowAudioMixedMimeTypeAdaptiveness = false;
+ allowAudioMixedSampleRateAdaptiveness = false;
+ allowAudioMixedChannelCountAdaptiveness = false;
+ // General
+ forceLowestBitrate = false;
+ forceHighestSupportedBitrate = false;
+ exceedRendererCapabilitiesIfNecessary = true;
+ tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;
+ }
+
+ 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;
+ }
+ }
+
+ /**
+ * Extends {@link TrackSelectionParameters} by adding fields that are specific to {@link
+ * DefaultTrackSelector}.
+ */
+ public static final class Parameters extends TrackSelectionParameters {
+
+ /**
+ * 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>{@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean)
+ * Viewport constraints} configured for the primary display.
+ * <li>{@link
+ * ParametersBuilder#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 ParametersBuilder().build();
+
+ /**
+ * @deprecated This instance does not have {@link Context} constraints configured. Use {@link
+ * #getDefaults(Context)} instead.
+ */
+ @Deprecated public static final Parameters DEFAULT_WITHOUT_VIEWPORT = DEFAULT_WITHOUT_CONTEXT;
+
+ /**
+ * @deprecated This instance does not have {@link Context} constraints configured. 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 ParametersBuilder(context).build();
+ }
+
+ // Video
+ /**
+ * Maximum allowed video width in pixels. The default value is {@link Integer#MAX_VALUE} (i.e.
+ * no constraint).
+ *
+ * <p>To constrain adaptive video track selections to be suitable for a given viewport (the
+ * region of the display within which video will be played), use ({@link #viewportWidth}, {@link
+ * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
+ */
+ public final int maxVideoWidth;
+ /**
+ * Maximum allowed video height in pixels. The default value is {@link Integer#MAX_VALUE} (i.e.
+ * no constraint).
+ *
+ * <p>To constrain adaptive video track selections to be suitable for a given viewport (the
+ * region of the display within which video will be played), use ({@link #viewportWidth}, {@link
+ * #viewportHeight} and {@link #viewportOrientationMayChange}) instead.
+ */
+ public final int maxVideoHeight;
+ /**
+ * Maximum allowed video frame rate in hertz. The default value is {@link Integer#MAX_VALUE}
+ * (i.e. no constraint).
+ */
+ public final int maxVideoFrameRate;
+ /**
+ * Maximum allowed video bitrate in bits per second. The default value is {@link
+ * Integer#MAX_VALUE} (i.e. no constraint).
+ */
+ public final int maxVideoBitrate;
+ /**
+ * 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;
+ /**
+ * Viewport width in pixels. Constrains video track selections for adaptive content so that only
+ * tracks suitable for the viewport are selected. The default value is the physical width of the
+ * primary display, in pixels.
+ */
+ public final int viewportWidth;
+ /**
+ * Viewport height in pixels. Constrains video track selections for adaptive content so that
+ * only tracks suitable for the viewport are selected. The default value is the physical height
+ * of the primary display, in pixels.
+ */
+ public final int viewportHeight;
+ /**
+ * Whether the viewport orientation may change during playback. Constrains video track
+ * selections for adaptive content so that only tracks suitable for the viewport are selected.
+ * The default value is {@code true}.
+ */
+ public final boolean viewportOrientationMayChange;
+ // Audio
+ /**
+ * Maximum allowed audio channel count. The default value is {@link Integer#MAX_VALUE} (i.e. no
+ * constraint).
+ */
+ public final int maxAudioChannelCount;
+ /**
+ * Maximum allowed audio bitrate in bits per second. The default value is {@link
+ * Integer#MAX_VALUE} (i.e. no constraint).
+ */
+ public final int maxAudioBitrate;
+ /**
+ * 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;
+
+ // General
+ /**
+ * Whether to force selection of the single lowest bitrate audio and video tracks that comply
+ * with all other constraints. The default value is {@code false}.
+ */
+ public final boolean forceLowestBitrate;
+ /**
+ * Whether to force selection of the highest bitrate audio and video tracks that comply with all
+ * other constraints. The default value is {@code false}.
+ */
+ public final boolean forceHighestSupportedBitrate;
+ /**
+ * @deprecated Use {@link #allowVideoMixedMimeTypeAdaptiveness} and {@link
+ * #allowAudioMixedMimeTypeAdaptiveness}.
+ */
+ @Deprecated public final boolean allowMixedMimeAdaptiveness;
+ /** @deprecated Use {@link #allowVideoNonSeamlessAdaptiveness}. */
+ @Deprecated public final boolean allowNonSeamlessAdaptiveness;
+ /**
+ * 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;
+ /**
+ * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
+ * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is
+ * disabled).
+ */
+ public final int tunnelingAudioSessionId;
+
+ // Overrides
+ private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ selectionOverrides;
+ private final SparseBooleanArray rendererDisabledFlags;
+
+ /* package */ Parameters(
+ // Video
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ boolean exceedVideoConstraintsIfNecessary,
+ boolean allowVideoMixedMimeTypeAdaptiveness,
+ boolean allowVideoNonSeamlessAdaptiveness,
+ int viewportWidth,
+ int viewportHeight,
+ boolean viewportOrientationMayChange,
+ // Audio
+ @Nullable String preferredAudioLanguage,
+ int maxAudioChannelCount,
+ int maxAudioBitrate,
+ boolean exceedAudioConstraintsIfNecessary,
+ boolean allowAudioMixedMimeTypeAdaptiveness,
+ boolean allowAudioMixedSampleRateAdaptiveness,
+ boolean allowAudioMixedChannelCountAdaptiveness,
+ // Text
+ @Nullable String preferredTextLanguage,
+ @C.RoleFlags int preferredTextRoleFlags,
+ boolean selectUndeterminedTextLanguage,
+ @C.SelectionFlags int disabledTextTrackSelectionFlags,
+ // General
+ boolean forceLowestBitrate,
+ boolean forceHighestSupportedBitrate,
+ boolean exceedRendererCapabilitiesIfNecessary,
+ int tunnelingAudioSessionId,
+ // Overrides
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides,
+ SparseBooleanArray rendererDisabledFlags) {
+ super(
+ preferredAudioLanguage,
+ preferredTextLanguage,
+ preferredTextRoleFlags,
+ selectUndeterminedTextLanguage,
+ disabledTextTrackSelectionFlags);
+ // Video
+ this.maxVideoWidth = maxVideoWidth;
+ this.maxVideoHeight = maxVideoHeight;
+ this.maxVideoFrameRate = maxVideoFrameRate;
+ this.maxVideoBitrate = maxVideoBitrate;
+ this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
+ this.allowVideoMixedMimeTypeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;
+ this.allowVideoNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ this.viewportOrientationMayChange = viewportOrientationMayChange;
+ // Audio
+ this.maxAudioChannelCount = maxAudioChannelCount;
+ this.maxAudioBitrate = maxAudioBitrate;
+ this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary;
+ this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness;
+ this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness;
+ this.allowAudioMixedChannelCountAdaptiveness = allowAudioMixedChannelCountAdaptiveness;
+ // General
+ this.forceLowestBitrate = forceLowestBitrate;
+ this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
+ this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
+ this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+ // Deprecated fields.
+ this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;
+ this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;
+ // Overrides
+ this.selectionOverrides = selectionOverrides;
+ this.rendererDisabledFlags = rendererDisabledFlags;
+ }
+
+ /* package */
+ Parameters(Parcel in) {
+ super(in);
+ // Video
+ this.maxVideoWidth = in.readInt();
+ this.maxVideoHeight = in.readInt();
+ this.maxVideoFrameRate = in.readInt();
+ this.maxVideoBitrate = in.readInt();
+ this.exceedVideoConstraintsIfNecessary = Util.readBoolean(in);
+ this.allowVideoMixedMimeTypeAdaptiveness = Util.readBoolean(in);
+ this.allowVideoNonSeamlessAdaptiveness = Util.readBoolean(in);
+ this.viewportWidth = in.readInt();
+ this.viewportHeight = in.readInt();
+ this.viewportOrientationMayChange = Util.readBoolean(in);
+ // Audio
+ this.maxAudioChannelCount = in.readInt();
+ this.maxAudioBitrate = in.readInt();
+ this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in);
+ this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in);
+ this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in);
+ this.allowAudioMixedChannelCountAdaptiveness = Util.readBoolean(in);
+ // General
+ this.forceLowestBitrate = Util.readBoolean(in);
+ this.forceHighestSupportedBitrate = Util.readBoolean(in);
+ this.exceedRendererCapabilitiesIfNecessary = Util.readBoolean(in);
+ this.tunnelingAudioSessionId = in.readInt();
+ // Overrides
+ this.selectionOverrides = readSelectionOverrides(in);
+ this.rendererDisabledFlags = Util.castNonNull(in.readSparseBooleanArray());
+ // Deprecated fields.
+ this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness;
+ this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness;
+ }
+
+ /**
+ * Returns whether the renderer is disabled.
+ *
+ * @param rendererIndex The renderer index.
+ * @return Whether the renderer is disabled.
+ */
+ public final 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.
+ */
+ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ 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.
+ */
+ @Nullable
+ public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
+ selectionOverrides.get(rendererIndex);
+ return overrides != null ? overrides.get(groups) : null;
+ }
+
+ /** Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */
+ @Override
+ public ParametersBuilder buildUpon() {
+ return new ParametersBuilder(this);
+ }
+
+ @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(obj)
+ // Video
+ && maxVideoWidth == other.maxVideoWidth
+ && maxVideoHeight == other.maxVideoHeight
+ && maxVideoFrameRate == other.maxVideoFrameRate
+ && maxVideoBitrate == other.maxVideoBitrate
+ && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary
+ && allowVideoMixedMimeTypeAdaptiveness == other.allowVideoMixedMimeTypeAdaptiveness
+ && allowVideoNonSeamlessAdaptiveness == other.allowVideoNonSeamlessAdaptiveness
+ && viewportOrientationMayChange == other.viewportOrientationMayChange
+ && viewportWidth == other.viewportWidth
+ && viewportHeight == other.viewportHeight
+ // Audio
+ && maxAudioChannelCount == other.maxAudioChannelCount
+ && maxAudioBitrate == other.maxAudioBitrate
+ && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary
+ && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness
+ && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness
+ && allowAudioMixedChannelCountAdaptiveness
+ == other.allowAudioMixedChannelCountAdaptiveness
+ // General
+ && forceLowestBitrate == other.forceLowestBitrate
+ && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate
+ && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
+ && tunnelingAudioSessionId == other.tunnelingAudioSessionId
+ // Overrides
+ && areRendererDisabledFlagsEqual(rendererDisabledFlags, other.rendererDisabledFlags)
+ && areSelectionOverridesEqual(selectionOverrides, other.selectionOverrides);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ // Video
+ result = 31 * result + maxVideoWidth;
+ result = 31 * result + maxVideoHeight;
+ result = 31 * result + maxVideoFrameRate;
+ result = 31 * result + maxVideoBitrate;
+ result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);
+ result = 31 * result + (allowVideoMixedMimeTypeAdaptiveness ? 1 : 0);
+ result = 31 * result + (allowVideoNonSeamlessAdaptiveness ? 1 : 0);
+ result = 31 * result + (viewportOrientationMayChange ? 1 : 0);
+ result = 31 * result + viewportWidth;
+ result = 31 * result + viewportHeight;
+ // Audio
+ result = 31 * result + maxAudioChannelCount;
+ result = 31 * result + maxAudioBitrate;
+ result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0);
+ result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0);
+ result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0);
+ result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0);
+ // General
+ result = 31 * result + (forceLowestBitrate ? 1 : 0);
+ result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0);
+ result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
+ result = 31 * result + tunnelingAudioSessionId;
+ // Overrides (omitted from hashCode).
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ // Video
+ dest.writeInt(maxVideoWidth);
+ dest.writeInt(maxVideoHeight);
+ dest.writeInt(maxVideoFrameRate);
+ dest.writeInt(maxVideoBitrate);
+ Util.writeBoolean(dest, exceedVideoConstraintsIfNecessary);
+ Util.writeBoolean(dest, allowVideoMixedMimeTypeAdaptiveness);
+ Util.writeBoolean(dest, allowVideoNonSeamlessAdaptiveness);
+ dest.writeInt(viewportWidth);
+ dest.writeInt(viewportHeight);
+ Util.writeBoolean(dest, viewportOrientationMayChange);
+ // Audio
+ dest.writeInt(maxAudioChannelCount);
+ dest.writeInt(maxAudioBitrate);
+ Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary);
+ Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness);
+ Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness);
+ Util.writeBoolean(dest, allowAudioMixedChannelCountAdaptiveness);
+ // General
+ Util.writeBoolean(dest, forceLowestBitrate);
+ Util.writeBoolean(dest, forceHighestSupportedBitrate);
+ Util.writeBoolean(dest, exceedRendererCapabilitiesIfNecessary);
+ dest.writeInt(tunnelingAudioSessionId);
+ // Overrides
+ writeSelectionOverridesToParcel(dest, selectionOverrides);
+ dest.writeSparseBooleanArray(rendererDisabledFlags);
+ }
+
+ public static final Parcelable.Creator<Parameters> CREATOR =
+ new Parcelable.Creator<Parameters>() {
+
+ @Override
+ public Parameters createFromParcel(Parcel in) {
+ return new Parameters(in);
+ }
+
+ @Override
+ public Parameters[] newArray(int size) {
+ return new Parameters[size];
+ }
+ };
+
+ // Static utility methods.
+
+ private static SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
+ readSelectionOverrides(Parcel in) {
+ int renderersWithOverridesCount = in.readInt();
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides =
+ new SparseArray<>(renderersWithOverridesCount);
+ for (int i = 0; i < renderersWithOverridesCount; i++) {
+ int rendererIndex = in.readInt();
+ int overrideCount = in.readInt();
+ Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
+ new HashMap<>(overrideCount);
+ for (int j = 0; j < overrideCount; j++) {
+ TrackGroupArray trackGroups =
+ Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader()));
+ @Nullable
+ SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader());
+ overrides.put(trackGroups, override);
+ }
+ selectionOverrides.put(rendererIndex, overrides);
+ }
+ return selectionOverrides;
+ }
+
+ private static void writeSelectionOverridesToParcel(
+ Parcel dest,
+ SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>> selectionOverrides) {
+ int renderersWithOverridesCount = selectionOverrides.size();
+ dest.writeInt(renderersWithOverridesCount);
+ for (int i = 0; i < renderersWithOverridesCount; i++) {
+ int rendererIndex = selectionOverrides.keyAt(i);
+ Map<TrackGroupArray, @NullableType SelectionOverride> overrides =
+ selectionOverrides.valueAt(i);
+ int overrideCount = overrides.size();
+ dest.writeInt(rendererIndex);
+ dest.writeInt(overrideCount);
+ for (Map.Entry<TrackGroupArray, @NullableType SelectionOverride> override :
+ overrides.entrySet()) {
+ dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0);
+ dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0);
+ }
+ }
+ }
+
+ 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 Parcelable {
+
+ public final int groupIndex;
+ public final int[] tracks;
+ public final int length;
+ public final int reason;
+ public final int data;
+
+ /**
+ * @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, C.SELECTION_REASON_MANUAL, /* data= */ 0);
+ }
+
+ /**
+ * @param groupIndex The overriding track group index.
+ * @param tracks The overriding track indices within the track group.
+ * @param reason The reason for the override. One of the {@link C} SELECTION_REASON_ constants.
+ * @param data Optional data associated with this override.
+ */
+ public SelectionOverride(int groupIndex, int[] tracks, int reason, int data) {
+ this.groupIndex = groupIndex;
+ this.tracks = Arrays.copyOf(tracks, tracks.length);
+ this.length = tracks.length;
+ this.reason = reason;
+ this.data = data;
+ Arrays.sort(this.tracks);
+ }
+
+ /* package */ SelectionOverride(Parcel in) {
+ groupIndex = in.readInt();
+ length = in.readByte();
+ tracks = new int[length];
+ in.readIntArray(tracks);
+ reason = in.readInt();
+ data = in.readInt();
+ }
+
+ /** 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);
+ hash = 31 * hash + reason;
+ return 31 * hash + data;
+ }
+
+ @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)
+ && reason == other.reason
+ && data == other.data;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(groupIndex);
+ dest.writeInt(tracks.length);
+ dest.writeIntArray(tracks);
+ dest.writeInt(reason);
+ dest.writeInt(data);
+ }
+
+ public static final Parcelable.Creator<SelectionOverride> CREATOR =
+ new Parcelable.Creator<SelectionOverride>() {
+
+ @Override
+ public SelectionOverride createFromParcel(Parcel in) {
+ return new SelectionOverride(in);
+ }
+
+ @Override
+ public SelectionOverride[] newArray(int size) {
+ return new SelectionOverride[size];
+ }
+ };
+ }
+
+ /**
+ * 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;
+ private static final int[] NO_TRACKS = new int[0];
+ private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
+
+ private final TrackSelection.Factory trackSelectionFactory;
+ private final AtomicReference<Parameters> parametersReference;
+
+ private boolean allowMultipleAdaptiveSelections;
+
+ /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultTrackSelector() {
+ this(new AdaptiveTrackSelection.Factory());
+ }
+
+ /**
+ * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be
+ * passed directly to the player in {@link
+ * com.google.android.exoplayer2.SimpleExoPlayer.Builder}.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public DefaultTrackSelector(BandwidthMeter bandwidthMeter) {
+ this(new AdaptiveTrackSelection.Factory(bandwidthMeter));
+ }
+
+ /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */
+ @Deprecated
+ public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) {
+ this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory);
+ }
+
+ /** @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 TrackSelection}s.
+ */
+ public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) {
+ this(Parameters.getDefaults(context), trackSelectionFactory);
+ }
+
+ /**
+ * @param parameters Initial {@link Parameters}.
+ * @param trackSelectionFactory A factory for {@link TrackSelection}s.
+ */
+ public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) {
+ this.trackSelectionFactory = trackSelectionFactory;
+ parametersReference = new AtomicReference<>(parameters);
+ }
+
+ /**
+ * Atomically sets the provided parameters for track selection.
+ *
+ * @param parameters The parameters for track selection.
+ */
+ public void setParameters(Parameters parameters) {
+ Assertions.checkNotNull(parameters);
+ if (!parametersReference.getAndSet(parameters).equals(parameters)) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Atomically sets the provided parameters for track selection.
+ *
+ * @param parametersBuilder A builder from which to obtain the parameters for track selection.
+ */
+ public void setParameters(ParametersBuilder parametersBuilder) {
+ setParameters(parametersBuilder.build());
+ }
+
+ /**
+ * Gets the current selection parameters.
+ *
+ * @return The current selection parameters.
+ */
+ public Parameters getParameters() {
+ return parametersReference.get();
+ }
+
+ /** Returns a new {@link ParametersBuilder} initialized with the current selection parameters. */
+ public ParametersBuilder buildUponParameters() {
+ return getParameters().buildUpon();
+ }
+
+ /** @deprecated Use {@link ParametersBuilder#setRendererDisabled(int, boolean)}. */
+ @Deprecated
+ public final void setRendererDisabled(int rendererIndex, boolean disabled) {
+ setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled));
+ }
+
+ /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. */
+ @Deprecated
+ public final boolean getRendererDisabled(int rendererIndex) {
+ return getParameters().getRendererDisabled(rendererIndex);
+ }
+
+ /**
+ * @deprecated Use {@link ParametersBuilder#setSelectionOverride(int, TrackGroupArray,
+ * SelectionOverride)}.
+ */
+ @Deprecated
+ public final void setSelectionOverride(
+ int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) {
+ setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override));
+ }
+
+ /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. */
+ @Deprecated
+ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ return getParameters().hasSelectionOverride(rendererIndex, groups);
+ }
+
+ /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */
+ @Deprecated
+ @Nullable
+ public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ return getParameters().getSelectionOverride(rendererIndex, groups);
+ }
+
+ /** @deprecated Use {@link ParametersBuilder#clearSelectionOverride(int, TrackGroupArray)}. */
+ @Deprecated
+ public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+ setParameters(buildUponParameters().clearSelectionOverride(rendererIndex, groups));
+ }
+
+ /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides(int)}. */
+ @Deprecated
+ public final void clearSelectionOverrides(int rendererIndex) {
+ setParameters(buildUponParameters().clearSelectionOverrides(rendererIndex));
+ }
+
+ /** @deprecated Use {@link ParametersBuilder#clearSelectionOverrides()}. */
+ @Deprecated
+ public final void clearSelectionOverrides() {
+ setParameters(buildUponParameters().clearSelectionOverrides());
+ }
+
+ /** @deprecated Use {@link ParametersBuilder#setTunnelingAudioSessionId(int)}. */
+ @Deprecated
+ public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) {
+ setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId));
+ }
+
+ /**
+ * Allows the creation of multiple adaptive track selections.
+ *
+ * <p>This method is experimental, and will be renamed or removed in a future release.
+ */
+ public void experimental_allowMultipleAdaptiveSelections() {
+ this.allowMultipleAdaptiveSelections = true;
+ }
+
+ // MappingTrackSelector implementation.
+
+ @Override
+ protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>
+ selectTracks(
+ MappedTrackInfo mappedTrackInfo,
+ @Capabilities int[][][] rendererFormatSupports,
+ @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports)
+ throws ExoPlaybackException {
+ Parameters params = parametersReference.get();
+ int rendererCount = mappedTrackInfo.getRendererCount();
+ TrackSelection.@NullableType Definition[] definitions =
+ selectAllTracks(
+ mappedTrackInfo,
+ rendererFormatSupports,
+ rendererMixedMimeTypeAdaptationSupports,
+ params);
+
+ // Apply track disabling and overriding.
+ for (int i = 0; i < rendererCount; i++) {
+ if (params.getRendererDisabled(i)) {
+ definitions[i] = null;
+ continue;
+ }
+ TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(i);
+ if (params.hasSelectionOverride(i, rendererTrackGroups)) {
+ SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups);
+ definitions[i] =
+ override == null
+ ? null
+ : new TrackSelection.Definition(
+ rendererTrackGroups.get(override.groupIndex),
+ override.tracks,
+ override.reason,
+ override.data);
+ }
+ }
+
+ @NullableType
+ TrackSelection[] rendererTrackSelections =
+ trackSelectionFactory.createTrackSelections(definitions, getBandwidthMeter());
+
+ // 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++) {
+ boolean forceRendererDisabled = params.getRendererDisabled(i);
+ 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.
+ maybeConfigureRenderersForTunneling(
+ mappedTrackInfo,
+ rendererFormatSupports,
+ rendererConfigurations,
+ rendererTrackSelections,
+ params.tunnelingAudioSessionId);
+
+ return Pair.create(rendererConfigurations, rendererTrackSelections);
+ }
+
+ // Track selection prior to overrides and disabled flags being applied.
+
+ /**
+ * Called from {@link #selectTracks(MappedTrackInfo, int[][][], int[])} 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 TrackSelection.Definition}s for the renderers. A null entry indicates no
+ * selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ protected TrackSelection.@NullableType Definition[] selectAllTracks(
+ MappedTrackInfo mappedTrackInfo,
+ @Capabilities int[][][] rendererFormatSupports,
+ @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports,
+ Parameters params)
+ throws ExoPlaybackException {
+ int rendererCount = mappedTrackInfo.getRendererCount();
+ TrackSelection.@NullableType Definition[] definitions =
+ new TrackSelection.Definition[rendererCount];
+
+ boolean seenVideoRendererWithMappedTracks = false;
+ boolean selectedVideoTracks = false;
+ for (int i = 0; i < rendererCount; i++) {
+ if (C.TRACK_TYPE_VIDEO == mappedTrackInfo.getRendererType(i)) {
+ if (!selectedVideoTracks) {
+ definitions[i] =
+ selectVideoTrack(
+ mappedTrackInfo.getTrackGroups(i),
+ rendererFormatSupports[i],
+ rendererMixedMimeTypeAdaptationSupports[i],
+ params,
+ /* enableAdaptiveTrackSelection= */ true);
+ selectedVideoTracks = definitions[i] != null;
+ }
+ seenVideoRendererWithMappedTracks |= mappedTrackInfo.getTrackGroups(i).length > 0;
+ }
+ }
+
+ AudioTrackScore selectedAudioTrackScore = null;
+ String selectedAudioLanguage = null;
+ int selectedAudioRendererIndex = C.INDEX_UNSET;
+ for (int i = 0; i < rendererCount; i++) {
+ if (C.TRACK_TYPE_AUDIO == mappedTrackInfo.getRendererType(i)) {
+ boolean enableAdaptiveTrackSelection =
+ allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks;
+ Pair<TrackSelection.Definition, AudioTrackScore> audioSelection =
+ selectAudioTrack(
+ mappedTrackInfo.getTrackGroups(i),
+ rendererFormatSupports[i],
+ rendererMixedMimeTypeAdaptationSupports[i],
+ params,
+ enableAdaptiveTrackSelection);
+ if (audioSelection != null
+ && (selectedAudioTrackScore == null
+ || audioSelection.second.compareTo(selectedAudioTrackScore) > 0)) {
+ if (selectedAudioRendererIndex != C.INDEX_UNSET) {
+ // We've already made a selection for another audio renderer, but it had a lower
+ // score. Clear the selection for that renderer.
+ definitions[selectedAudioRendererIndex] = null;
+ }
+ TrackSelection.Definition definition = audioSelection.first;
+ definitions[i] = definition;
+ // We assume that audio tracks in the same group have matching language.
+ selectedAudioLanguage = definition.group.getFormat(definition.tracks[0]).language;
+ selectedAudioTrackScore = audioSelection.second;
+ selectedAudioRendererIndex = i;
+ }
+ }
+ }
+
+ TextTrackScore selectedTextTrackScore = null;
+ int selectedTextRendererIndex = C.INDEX_UNSET;
+ for (int i = 0; i < rendererCount; i++) {
+ int trackType = mappedTrackInfo.getRendererType(i);
+ switch (trackType) {
+ case C.TRACK_TYPE_VIDEO:
+ case C.TRACK_TYPE_AUDIO:
+ // Already done. Do nothing.
+ break;
+ case C.TRACK_TYPE_TEXT:
+ Pair<TrackSelection.Definition, TextTrackScore> textSelection =
+ selectTextTrack(
+ mappedTrackInfo.getTrackGroups(i),
+ rendererFormatSupports[i],
+ params,
+ selectedAudioLanguage);
+ if (textSelection != null
+ && (selectedTextTrackScore == null
+ || textSelection.second.compareTo(selectedTextTrackScore) > 0)) {
+ if (selectedTextRendererIndex != C.INDEX_UNSET) {
+ // We've already made a selection for another text renderer, but it had a lower score.
+ // Clear the selection for that renderer.
+ definitions[selectedTextRendererIndex] = null;
+ }
+ definitions[i] = textSelection.first;
+ selectedTextTrackScore = textSelection.second;
+ selectedTextRendererIndex = i;
+ }
+ break;
+ default:
+ definitions[i] =
+ selectOtherTrack(
+ trackType, mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params);
+ break;
+ }
+ }
+
+ return definitions;
+ }
+
+ // Video track selection implementation.
+
+ /**
+ * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
+ * {@link TrackSelection} for a video renderer.
+ *
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer,
+ * track group and track (in that order).
+ * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type
+ * adaptation for the renderer.
+ * @param params The selector's current constraint parameters.
+ * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.
+ * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was
+ * made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ @Nullable
+ protected TrackSelection.Definition selectVideoTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupports,
+ @AdaptiveSupport int mixedMimeTypeAdaptationSupports,
+ Parameters params,
+ boolean enableAdaptiveTrackSelection)
+ throws ExoPlaybackException {
+ TrackSelection.Definition definition = null;
+ if (!params.forceHighestSupportedBitrate
+ && !params.forceLowestBitrate
+ && enableAdaptiveTrackSelection) {
+ definition =
+ selectAdaptiveVideoTrack(groups, formatSupports, mixedMimeTypeAdaptationSupports, params);
+ }
+ if (definition == null) {
+ definition = selectFixedVideoTrack(groups, formatSupports, params);
+ }
+ return definition;
+ }
+
+ @Nullable
+ private static TrackSelection.Definition selectAdaptiveVideoTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupport,
+ @AdaptiveSupport int mixedMimeTypeAdaptationSupports,
+ Parameters params) {
+ int requiredAdaptiveSupport =
+ params.allowVideoNonSeamlessAdaptiveness
+ ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
+ : RendererCapabilities.ADAPTIVE_SEAMLESS;
+ boolean allowMixedMimeTypes =
+ params.allowVideoMixedMimeTypeAdaptiveness
+ && (mixedMimeTypeAdaptationSupports & requiredAdaptiveSupport) != 0;
+ for (int i = 0; i < groups.length; i++) {
+ TrackGroup group = groups.get(i);
+ int[] adaptiveTracks =
+ getAdaptiveVideoTracksForGroup(
+ group,
+ formatSupport[i],
+ allowMixedMimeTypes,
+ requiredAdaptiveSupport,
+ params.maxVideoWidth,
+ params.maxVideoHeight,
+ params.maxVideoFrameRate,
+ params.maxVideoBitrate,
+ params.viewportWidth,
+ params.viewportHeight,
+ params.viewportOrientationMayChange);
+ if (adaptiveTracks.length > 0) {
+ return new TrackSelection.Definition(group, adaptiveTracks);
+ }
+ }
+ return null;
+ }
+
+ private static int[] getAdaptiveVideoTracksForGroup(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ boolean allowMixedMimeTypes,
+ int requiredAdaptiveSupport,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ int viewportWidth,
+ int viewportHeight,
+ boolean viewportOrientationMayChange) {
+ if (group.length < 2) {
+ return NO_TRACKS;
+ }
+
+ List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
+ viewportHeight, viewportOrientationMayChange);
+ if (selectedTrackIndices.size() < 2) {
+ return NO_TRACKS;
+ }
+
+ String selectedMimeType = null;
+ if (!allowMixedMimeTypes) {
+ // Select the mime type for which we have the most adaptive tracks.
+ HashSet<@NullableType String> seenMimeTypes = new HashSet<>();
+ int selectedMimeTypeTrackCount = 0;
+ for (int i = 0; i < selectedTrackIndices.size(); i++) {
+ int trackIndex = selectedTrackIndices.get(i);
+ String sampleMimeType = group.getFormat(trackIndex).sampleMimeType;
+ if (seenMimeTypes.add(sampleMimeType)) {
+ int countForMimeType =
+ getAdaptiveVideoTrackCountForMimeType(
+ group,
+ formatSupport,
+ requiredAdaptiveSupport,
+ sampleMimeType,
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoFrameRate,
+ maxVideoBitrate,
+ selectedTrackIndices);
+ if (countForMimeType > selectedMimeTypeTrackCount) {
+ selectedMimeType = sampleMimeType;
+ selectedMimeTypeTrackCount = countForMimeType;
+ }
+ }
+ }
+ }
+
+ // Filter by the selected mime type.
+ filterAdaptiveVideoTrackCountForMimeType(
+ group,
+ formatSupport,
+ requiredAdaptiveSupport,
+ selectedMimeType,
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoFrameRate,
+ maxVideoBitrate,
+ selectedTrackIndices);
+
+ return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
+ }
+
+ private static int getAdaptiveVideoTrackCountForMimeType(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ int requiredAdaptiveSupport,
+ @Nullable String mimeType,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ List<Integer> selectedTrackIndices) {
+ int adaptiveTrackCount = 0;
+ for (int i = 0; i < selectedTrackIndices.size(); i++) {
+ int trackIndex = selectedTrackIndices.get(i);
+ if (isSupportedAdaptiveVideoTrack(
+ group.getFormat(trackIndex),
+ mimeType,
+ formatSupport[trackIndex],
+ requiredAdaptiveSupport,
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoFrameRate,
+ maxVideoBitrate)) {
+ adaptiveTrackCount++;
+ }
+ }
+ return adaptiveTrackCount;
+ }
+
+ private static void filterAdaptiveVideoTrackCountForMimeType(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ int requiredAdaptiveSupport,
+ @Nullable String mimeType,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate,
+ List<Integer> selectedTrackIndices) {
+ for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
+ int trackIndex = selectedTrackIndices.get(i);
+ if (!isSupportedAdaptiveVideoTrack(
+ group.getFormat(trackIndex),
+ mimeType,
+ formatSupport[trackIndex],
+ requiredAdaptiveSupport,
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoFrameRate,
+ maxVideoBitrate)) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ private static boolean isSupportedAdaptiveVideoTrack(
+ Format format,
+ @Nullable String mimeType,
+ @Capabilities int formatSupport,
+ int requiredAdaptiveSupport,
+ int maxVideoWidth,
+ int maxVideoHeight,
+ int maxVideoFrameRate,
+ int maxVideoBitrate) {
+ return isSupported(formatSupport, false)
+ && ((formatSupport & requiredAdaptiveSupport) != 0)
+ && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
+ && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)
+ && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight)
+ && (format.frameRate == Format.NO_VALUE || format.frameRate <= maxVideoFrameRate)
+ && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate);
+ }
+
+ @Nullable
+ private static TrackSelection.Definition selectFixedVideoTrack(
+ TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ int selectedBitrate = Format.NO_VALUE;
+ int selectedPixelCount = Format.NO_VALUE;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup,
+ params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);
+ @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ boolean isWithinConstraints =
+ selectedTrackIndices.contains(trackIndex)
+ && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth)
+ && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight)
+ && (format.frameRate == Format.NO_VALUE
+ || format.frameRate <= params.maxVideoFrameRate)
+ && (format.bitrate == Format.NO_VALUE
+ || format.bitrate <= params.maxVideoBitrate);
+ if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) {
+ // Track should not be selected.
+ continue;
+ }
+ int trackScore = isWithinConstraints ? 2 : 1;
+ boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false);
+ if (isWithinCapabilities) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ boolean selectTrack = trackScore > selectedTrackScore;
+ if (trackScore == selectedTrackScore) {
+ int bitrateComparison = compareFormatValues(format.bitrate, selectedBitrate);
+ if (params.forceLowestBitrate && bitrateComparison != 0) {
+ // Use bitrate as a tie breaker, preferring the lower bitrate.
+ selectTrack = bitrateComparison < 0;
+ } else {
+ // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If
+ // we're within constraints prefer a higher pixel count (or bitrate), else prefer a
+ // lower count (or bitrate). If still tied then prefer the first track (i.e. the one
+ // that's already selected).
+ int formatPixelCount = format.getPixelCount();
+ int comparisonResult = formatPixelCount != selectedPixelCount
+ ? compareFormatValues(formatPixelCount, selectedPixelCount)
+ : compareFormatValues(format.bitrate, selectedBitrate);
+ selectTrack = isWithinCapabilities && isWithinConstraints
+ ? comparisonResult > 0 : comparisonResult < 0;
+ }
+ }
+ if (selectTrack) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ selectedBitrate = format.bitrate;
+ selectedPixelCount = format.getPixelCount();
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ }
+
+ // Audio track selection implementation.
+
+ /**
+ * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
+ * {@link TrackSelection} for an audio renderer.
+ *
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer,
+ * track group and track (in that order).
+ * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type
+ * adaptation for the renderer.
+ * @param params The selector's current constraint parameters.
+ * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed.
+ * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or
+ * null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ @SuppressWarnings("unused")
+ @Nullable
+ protected Pair<TrackSelection.Definition, AudioTrackScore> selectAudioTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupports,
+ @AdaptiveSupport int mixedMimeTypeAdaptationSupports,
+ Parameters params,
+ boolean enableAdaptiveTrackSelection)
+ throws ExoPlaybackException {
+ int selectedTrackIndex = C.INDEX_UNSET;
+ int selectedGroupIndex = C.INDEX_UNSET;
+ AudioTrackScore selectedTrackScore = null;
+ for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ TrackGroup trackGroup = groups.get(groupIndex);
+ @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ if (isSupported(trackFormatSupport[trackIndex],
+ params.exceedRendererCapabilitiesIfNecessary)) {
+ Format format = trackGroup.getFormat(trackIndex);
+ AudioTrackScore trackScore =
+ new AudioTrackScore(format, params, trackFormatSupport[trackIndex]);
+ if (!trackScore.isWithinConstraints && !params.exceedAudioConstraintsIfNecessary) {
+ // Track should not be selected.
+ continue;
+ }
+ if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) {
+ selectedGroupIndex = groupIndex;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+
+ if (selectedGroupIndex == C.INDEX_UNSET) {
+ return null;
+ }
+
+ TrackGroup selectedGroup = groups.get(selectedGroupIndex);
+
+ TrackSelection.Definition definition = null;
+ if (!params.forceHighestSupportedBitrate
+ && !params.forceLowestBitrate
+ && enableAdaptiveTrackSelection) {
+ // If the group of the track with the highest score allows it, try to enable adaptation.
+ int[] adaptiveTracks =
+ getAdaptiveAudioTracks(
+ selectedGroup,
+ formatSupports[selectedGroupIndex],
+ params.maxAudioBitrate,
+ params.allowAudioMixedMimeTypeAdaptiveness,
+ params.allowAudioMixedSampleRateAdaptiveness,
+ params.allowAudioMixedChannelCountAdaptiveness);
+ if (adaptiveTracks.length > 0) {
+ definition = new TrackSelection.Definition(selectedGroup, adaptiveTracks);
+ }
+ }
+ if (definition == null) {
+ // We didn't make an adaptive selection, so make a fixed one instead.
+ definition = new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ }
+
+ return Pair.create(definition, Assertions.checkNotNull(selectedTrackScore));
+ }
+
+ private static int[] getAdaptiveAudioTracks(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ int maxAudioBitrate,
+ boolean allowMixedMimeTypeAdaptiveness,
+ boolean allowMixedSampleRateAdaptiveness,
+ boolean allowAudioMixedChannelCountAdaptiveness) {
+ int selectedConfigurationTrackCount = 0;
+ AudioConfigurationTuple selectedConfiguration = null;
+ HashSet<AudioConfigurationTuple> seenConfigurationTuples = new HashSet<>();
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ AudioConfigurationTuple configuration =
+ new AudioConfigurationTuple(
+ format.channelCount, format.sampleRate, format.sampleMimeType);
+ if (seenConfigurationTuples.add(configuration)) {
+ int configurationCount =
+ getAdaptiveAudioTrackCount(
+ group,
+ formatSupport,
+ configuration,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness);
+ if (configurationCount > selectedConfigurationTrackCount) {
+ selectedConfiguration = configuration;
+ selectedConfigurationTrackCount = configurationCount;
+ }
+ }
+ }
+
+ if (selectedConfigurationTrackCount > 1) {
+ Assertions.checkNotNull(selectedConfiguration);
+ int[] adaptiveIndices = new int[selectedConfigurationTrackCount];
+ int index = 0;
+ for (int i = 0; i < group.length; i++) {
+ Format format = group.getFormat(i);
+ if (isSupportedAdaptiveAudioTrack(
+ format,
+ formatSupport[i],
+ selectedConfiguration,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness)) {
+ adaptiveIndices[index++] = i;
+ }
+ }
+ return adaptiveIndices;
+ }
+ return NO_TRACKS;
+ }
+
+ private static int getAdaptiveAudioTrackCount(
+ TrackGroup group,
+ @Capabilities int[] formatSupport,
+ AudioConfigurationTuple configuration,
+ int maxAudioBitrate,
+ boolean allowMixedMimeTypeAdaptiveness,
+ boolean allowMixedSampleRateAdaptiveness,
+ boolean allowAudioMixedChannelCountAdaptiveness) {
+ int count = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (isSupportedAdaptiveAudioTrack(
+ group.getFormat(i),
+ formatSupport[i],
+ configuration,
+ maxAudioBitrate,
+ allowMixedMimeTypeAdaptiveness,
+ allowMixedSampleRateAdaptiveness,
+ allowAudioMixedChannelCountAdaptiveness)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private static boolean isSupportedAdaptiveAudioTrack(
+ Format format,
+ @Capabilities int formatSupport,
+ AudioConfigurationTuple configuration,
+ int maxAudioBitrate,
+ boolean allowMixedMimeTypeAdaptiveness,
+ boolean allowMixedSampleRateAdaptiveness,
+ boolean allowAudioMixedChannelCountAdaptiveness) {
+ return isSupported(formatSupport, false)
+ && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxAudioBitrate)
+ && (allowAudioMixedChannelCountAdaptiveness
+ || (format.channelCount != Format.NO_VALUE
+ && format.channelCount == configuration.channelCount))
+ && (allowMixedMimeTypeAdaptiveness
+ || (format.sampleMimeType != null
+ && TextUtils.equals(format.sampleMimeType, configuration.mimeType)))
+ && (allowMixedSampleRateAdaptiveness
+ || (format.sampleRate != Format.NO_VALUE
+ && format.sampleRate == configuration.sampleRate));
+ }
+
+ // Text track selection implementation.
+
+ /**
+ * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
+ * {@link TrackSelection} for a text renderer.
+ *
+ * @param groups The {@link TrackGroupArray} mapped to the renderer.
+ * @param formatSupport 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 text track declares no language or no text track was selected.
+ * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null
+ * if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ @Nullable
+ protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
+ TrackGroupArray groups,
+ @Capabilities int[][] formatSupport,
+ Parameters params,
+ @Nullable String selectedAudioLanguage)
+ throws ExoPlaybackException {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = C.INDEX_UNSET;
+ TextTrackScore 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);
+ TextTrackScore trackScore =
+ new TextTrackScore(
+ format, params, trackFormatSupport[trackIndex], selectedAudioLanguage);
+ if (trackScore.isWithinConstraints
+ && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : Pair.create(
+ new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
+ Assertions.checkNotNull(selectedTrackScore));
+ }
+
+ // General track selection methods.
+
+ /**
+ * Called by {@link #selectAllTracks(MappedTrackInfo, int[][][], int[], Parameters)} to create a
+ * {@link TrackSelection} 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 renderer, track
+ * group and track (in that order).
+ * @param params The selector's current constraint parameters.
+ * @return The {@link TrackSelection} for the renderer, or null if no selection was made.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ @Nullable
+ protected TrackSelection.Definition selectOtherTrack(
+ int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params)
+ throws ExoPlaybackException {
+ TrackGroup selectedGroup = null;
+ int selectedTrackIndex = 0;
+ int selectedTrackScore = 0;
+ 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);
+ boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+ int trackScore = isDefault ? 2 : 1;
+ if (isSupported(trackFormatSupport[trackIndex], false)) {
+ trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+ }
+ if (trackScore > selectedTrackScore) {
+ selectedGroup = trackGroup;
+ selectedTrackIndex = trackIndex;
+ selectedTrackScore = trackScore;
+ }
+ }
+ }
+ }
+ return selectedGroup == null
+ ? null
+ : new TrackSelection.Definition(selectedGroup, selectedTrackIndex);
+ }
+
+ // Utility methods.
+
+ /**
+ * Determines whether tunneling should 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.
+ * @param tunnelingAudioSessionId The audio session id to use when tunneling, or {@link
+ * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
+ */
+ private static void maybeConfigureRenderersForTunneling(
+ MappedTrackInfo mappedTrackInfo,
+ @Capabilities int[][][] renderererFormatSupports,
+ @NullableType RendererConfiguration[] rendererConfigurations,
+ @NullableType TrackSelection[] trackSelections,
+ int tunnelingAudioSessionId) {
+ if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+ return;
+ }
+ // 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);
+ TrackSelection 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(tunnelingAudioSessionId);
+ rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration;
+ rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration;
+ }
+ }
+
+ /**
+ * Returns whether a renderer supports tunneling for a {@link TrackSelection}.
+ *
+ * @param formatSupports 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 TrackSelection}.
+ */
+ private static boolean rendererSupportsTunneling(
+ @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) {
+ if (selection == null) {
+ return false;
+ }
+ int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
+ for (int i = 0; i < selection.length(); i++) {
+ @Capabilities
+ int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)];
+ if (RendererCapabilities.getTunnelingSupport(trackFormatSupport)
+ != RendererCapabilities.TUNNELING_SUPPORTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compares two format values for order. A known value is considered greater than {@link
+ * Format#NO_VALUE}.
+ *
+ * @param first The first value.
+ * @param second The second value.
+ * @return A negative integer if the first value is less than the second. Zero if they are equal.
+ * A positive integer if the first value is greater than the second.
+ */
+ private static int compareFormatValues(int first, int second) {
+ return first == Format.NO_VALUE
+ ? (second == Format.NO_VALUE ? 0 : -1)
+ : (second == Format.NO_VALUE ? 1 : (first - second));
+ }
+
+ /**
+ * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link
+ * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the
+ * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ *
+ * @param formatSupport {@link Capabilities}.
+ * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link
+ * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if
+ * {@code allowExceedsCapabilities} is set and the format support is {@link
+ * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}.
+ */
+ protected static boolean isSupported(
+ @Capabilities int formatSupport, boolean allowExceedsCapabilities) {
+ @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport);
+ return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities
+ && maskedSupport == RendererCapabilities.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 List<Integer> getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,
+ int viewportHeight, boolean orientationMayChange) {
+ // Initially include all indices.
+ ArrayList<Integer> selectedTrackIndices = new ArrayList<>(group.length);
+ for (int i = 0; i < group.length; i++) {
+ selectedTrackIndices.add(i);
+ }
+
+ if (viewportWidth == Integer.MAX_VALUE || viewportHeight == Integer.MAX_VALUE) {
+ // Viewport dimensions not set. Return the full set of indices.
+ return selectedTrackIndices;
+ }
+
+ 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.
+ // We'll discard formats of higher resolution.
+ 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;
+ }
+ }
+ }
+
+ // Filter out formats that exceed maxVideoPixelsToRetain. These formats have an unnecessarily
+ // high resolution given the size at which the video will be displayed within the viewport. Also
+ // filter out formats with unknown dimensions, since we have some whose dimensions are known.
+ if (maxVideoPixelsToRetain != Integer.MAX_VALUE) {
+ for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) {
+ Format format = group.getFormat(selectedTrackIndices.get(i));
+ int pixelCount = format.getPixelCount();
+ if (pixelCount == Format.NO_VALUE || pixelCount > maxVideoPixelsToRetain) {
+ selectedTrackIndices.remove(i);
+ }
+ }
+ }
+
+ return selectedTrackIndices;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Compares two integers in a safe way avoiding potential overflow.
+ *
+ * @param first The first value.
+ * @param second The second value.
+ * @return A negative integer if the first value is less than the second. Zero if they are equal.
+ * A positive integer if the first value is greater than the second.
+ */
+ private static int compareInts(int first, int second) {
+ return first > second ? 1 : (second > first ? -1 : 0);
+ }
+
+ /** Represents how well an audio track matches the selection {@link Parameters}. */
+ protected static final class AudioTrackScore implements Comparable<AudioTrackScore> {
+
+ /**
+ * Whether the provided format is within the parameter constraints. If {@code false}, the format
+ * should not be selected.
+ */
+ public final boolean isWithinConstraints;
+
+ @Nullable private final String language;
+ private final Parameters parameters;
+ private final boolean isWithinRendererCapabilities;
+ private final int preferredLanguageScore;
+ private final int localeLanguageMatchIndex;
+ private final int localeLanguageScore;
+ private final boolean isDefaultSelectionFlag;
+ private final int channelCount;
+ private final int sampleRate;
+ private final int bitrate;
+
+ public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) {
+ this.parameters = parameters;
+ this.language = normalizeUndeterminedLanguageToNull(format.language);
+ isWithinRendererCapabilities = isSupported(formatSupport, false);
+ preferredLanguageScore =
+ getFormatLanguageScore(
+ format,
+ parameters.preferredAudioLanguage,
+ /* allowUndeterminedFormatLanguage= */ false);
+ 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);
+ String[] localeLanguages = Util.getSystemLanguageCodes();
+ int bestMatchIndex = Integer.MAX_VALUE;
+ int bestMatchScore = 0;
+ for (int i = 0; i < localeLanguages.length; i++) {
+ int score =
+ getFormatLanguageScore(
+ format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false);
+ if (score > 0) {
+ bestMatchIndex = i;
+ bestMatchScore = score;
+ break;
+ }
+ }
+ localeLanguageMatchIndex = bestMatchIndex;
+ localeLanguageScore = bestMatchScore;
+ }
+
+ /**
+ * Compares this score with another.
+ *
+ * @param other The other score to compare to.
+ * @return A positive integer if this score is better than the other. Zero if they are equal. A
+ * negative integer if this score is worse than the other.
+ */
+ @Override
+ public int compareTo(AudioTrackScore other) {
+ if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {
+ return this.isWithinRendererCapabilities ? 1 : -1;
+ }
+ if (this.preferredLanguageScore != other.preferredLanguageScore) {
+ return compareInts(this.preferredLanguageScore, other.preferredLanguageScore);
+ }
+ if (this.isWithinConstraints != other.isWithinConstraints) {
+ return this.isWithinConstraints ? 1 : -1;
+ }
+ if (parameters.forceLowestBitrate) {
+ int bitrateComparison = compareFormatValues(bitrate, other.bitrate);
+ if (bitrateComparison != 0) {
+ return bitrateComparison > 0 ? -1 : 1;
+ }
+ }
+ if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) {
+ return this.isDefaultSelectionFlag ? 1 : -1;
+ }
+ if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) {
+ return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex);
+ }
+ if (this.localeLanguageScore != other.localeLanguageScore) {
+ return compareInts(this.localeLanguageScore, other.localeLanguageScore);
+ }
+ // 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.
+ int resultSign = isWithinConstraints && isWithinRendererCapabilities ? 1 : -1;
+ if (this.channelCount != other.channelCount) {
+ return resultSign * compareInts(this.channelCount, other.channelCount);
+ }
+ if (this.sampleRate != other.sampleRate) {
+ return resultSign * compareInts(this.sampleRate, other.sampleRate);
+ }
+ if (Util.areEqual(this.language, other.language)) {
+ // Only compare bit rates of tracks with the same or unknown language.
+ return resultSign * compareInts(this.bitrate, other.bitrate);
+ }
+ return 0;
+ }
+ }
+
+ private static final class AudioConfigurationTuple {
+
+ public final int channelCount;
+ public final int sampleRate;
+ @Nullable public final String mimeType;
+
+ public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) {
+ this.channelCount = channelCount;
+ this.sampleRate = sampleRate;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AudioConfigurationTuple other = (AudioConfigurationTuple) obj;
+ return channelCount == other.channelCount && sampleRate == other.sampleRate
+ && TextUtils.equals(mimeType, other.mimeType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = channelCount;
+ result = 31 * result + sampleRate;
+ result = 31 * result + (mimeType != null ? mimeType.hashCode() : 0);
+ return result;
+ }
+
+ }
+
+ /** Represents how well a text track matches the selection {@link Parameters}. */
+ protected static final class TextTrackScore implements Comparable<TextTrackScore> {
+
+ /**
+ * Whether the provided format is within the parameter constraints. If {@code false}, the format
+ * should not be selected.
+ */
+ public final boolean isWithinConstraints;
+
+ private final boolean isWithinRendererCapabilities;
+ private final boolean isDefault;
+ private final boolean hasPreferredIsForcedFlag;
+ private final int preferredLanguageScore;
+ private final int preferredRoleFlagsScore;
+ private final int selectedAudioLanguageScore;
+ private final boolean hasCaptionRoleFlags;
+
+ public TextTrackScore(
+ Format format,
+ Parameters parameters,
+ @Capabilities int trackFormatSupport,
+ @Nullable String selectedAudioLanguage) {
+ isWithinRendererCapabilities =
+ isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);
+ int maskedSelectionFlags =
+ format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags;
+ isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+ boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
+ preferredLanguageScore =
+ getFormatLanguageScore(
+ format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage);
+ preferredRoleFlagsScore =
+ Integer.bitCount(format.roleFlags & parameters.preferredTextRoleFlags);
+ hasCaptionRoleFlags =
+ (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0;
+ // 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.
+ hasPreferredIsForcedFlag =
+ (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced);
+ boolean selectedAudioLanguageUndetermined =
+ normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null;
+ selectedAudioLanguageScore =
+ getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined);
+ isWithinConstraints =
+ preferredLanguageScore > 0
+ || (parameters.preferredTextLanguage == null && preferredRoleFlagsScore > 0)
+ || isDefault
+ || (isForced && selectedAudioLanguageScore > 0);
+ }
+
+ /**
+ * Compares this score with another.
+ *
+ * @param other The other score to compare to.
+ * @return A positive integer if this score is better than the other. Zero if they are equal. A
+ * negative integer if this score is worse than the other.
+ */
+ @Override
+ public int compareTo(TextTrackScore other) {
+ if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {
+ return this.isWithinRendererCapabilities ? 1 : -1;
+ }
+ if (this.preferredLanguageScore != other.preferredLanguageScore) {
+ return compareInts(this.preferredLanguageScore, other.preferredLanguageScore);
+ }
+ if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) {
+ return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore);
+ }
+ if (this.isDefault != other.isDefault) {
+ return this.isDefault ? 1 : -1;
+ }
+ if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) {
+ return this.hasPreferredIsForcedFlag ? 1 : -1;
+ }
+ if (this.selectedAudioLanguageScore != other.selectedAudioLanguageScore) {
+ return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore);
+ }
+ if (preferredRoleFlagsScore == 0 && this.hasCaptionRoleFlags != other.hasCaptionRoleFlags) {
+ return this.hasCaptionRoleFlags ? -1 : 1;
+ }
+ return 0;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
new file mode 100644
index 0000000000..824abaccfa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
@@ -0,0 +1,117 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A {@link TrackSelection} consisting of a single track.
+ */
+public final class FixedTrackSelection extends BaseTrackSelection {
+
+ /**
+ * @deprecated Don't use as adaptive track selection factory as it will throw when multiple tracks
+ * are selected. If you would like to disable adaptive selection in {@link
+ * DefaultTrackSelector}, enable the {@link
+ * DefaultTrackSelector.Parameters#forceHighestSupportedBitrate} flag instead.
+ */
+ @Deprecated
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final int reason;
+ @Nullable private final Object data;
+
+ public Factory() {
+ this.reason = C.SELECTION_REASON_UNKNOWN;
+ this.data = null;
+ }
+
+ /**
+ * @param reason A reason for the track selection.
+ * @param data Optional data associated with the track selection.
+ */
+ public Factory(int reason, @Nullable Object data) {
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition ->
+ new FixedTrackSelection(definition.group, definition.tracks[0], reason, data));
+ }
+ }
+
+ private final int reason;
+ @Nullable private final Object data;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ */
+ public FixedTrackSelection(TrackGroup group, int track) {
+ this(group, track, C.SELECTION_REASON_UNKNOWN, null);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param track The index of the selected track within the {@link TrackGroup}.
+ * @param reason A reason for the track selection.
+ * @param data Optional data associated with the track selection.
+ */
+ public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) {
+ super(group, track);
+ this.reason = reason;
+ this.data = data;
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ // Do nothing.
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return reason;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return data;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
new file mode 100644
index 0000000000..8ba581020b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -0,0 +1,541 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.util.Pair;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.Capabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities.FormatSupport;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
+ * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each
+ * renderer.
+ */
+public abstract class MappingTrackSelector extends TrackSelector {
+
+ /**
+ * Provides mapped track information for each renderer.
+ */
+ public static final class MappedTrackInfo {
+
+ /**
+ * Levels of renderer support. Higher numerical values indicate higher levels of support. One of
+ * {@link #RENDERER_SUPPORT_NO_TRACKS}, {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS}, {@link
+ * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS} or {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RENDERER_SUPPORT_NO_TRACKS,
+ RENDERER_SUPPORT_UNSUPPORTED_TRACKS,
+ RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS,
+ RENDERER_SUPPORT_PLAYABLE_TRACKS
+ })
+ @interface RendererSupport {}
+ /** The renderer does not have any associated tracks. */
+ public static final int RENDERER_SUPPORT_NO_TRACKS = 0;
+ /**
+ * The renderer has tracks mapped to it, but all are unsupported. In other words, {@link
+ * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} or {@link
+ * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all tracks mapped to the renderer.
+ */
+ public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;
+ /**
+ * The renderer has tracks mapped to it and at least one is of a supported type, but all such
+ * tracks exceed the renderer's capabilities. In other words, {@link #getTrackSupport(int, int,
+ * int)} returns {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} for at least one
+ * track mapped to the renderer, but does not return {@link
+ * RendererCapabilities#FORMAT_HANDLED} for any tracks mapped to the renderer.
+ */
+ public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;
+ /**
+ * The renderer has tracks mapped to it, and at least one such track is playable. In other
+ * words, {@link #getTrackSupport(int, int, int)} returns {@link
+ * RendererCapabilities#FORMAT_HANDLED} for at least one track mapped to the renderer.
+ */
+ public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;
+
+ /** @deprecated Use {@link #getRendererCount()}. */
+ @Deprecated public final int length;
+
+ private final int rendererCount;
+ private final int[] rendererTrackTypes;
+ private final TrackGroupArray[] rendererTrackGroups;
+ @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports;
+ @Capabilities private final int[][][] rendererFormatSupports;
+ private final TrackGroupArray unmappedTrackGroups;
+
+ /**
+ * @param rendererTrackTypes The track type handled by each renderer.
+ * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer.
+ * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type
+ * adaptation for the renderer.
+ * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by
+ * renderer, track group and track (in that order).
+ * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer.
+ */
+ @SuppressWarnings("deprecation")
+ /* package */ MappedTrackInfo(
+ int[] rendererTrackTypes,
+ TrackGroupArray[] rendererTrackGroups,
+ @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports,
+ @Capabilities int[][][] rendererFormatSupports,
+ TrackGroupArray unmappedTrackGroups) {
+ this.rendererTrackTypes = rendererTrackTypes;
+ this.rendererTrackGroups = rendererTrackGroups;
+ this.rendererFormatSupports = rendererFormatSupports;
+ this.rendererMixedMimeTypeAdaptiveSupports = rendererMixedMimeTypeAdaptiveSupports;
+ this.unmappedTrackGroups = unmappedTrackGroups;
+ this.rendererCount = rendererTrackTypes.length;
+ this.length = rendererCount;
+ }
+
+ /** Returns the number of renderers. */
+ public int getRendererCount() {
+ return rendererCount;
+ }
+
+ /**
+ * Returns the track type that the renderer at a given index handles.
+ *
+ * @see Renderer#getTrackType()
+ * @param rendererIndex The renderer index.
+ * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+ */
+ public int getRendererType(int rendererIndex) {
+ return rendererTrackTypes[rendererIndex];
+ }
+
+ /**
+ * Returns the {@link TrackGroup}s mapped to the renderer at the specified index.
+ *
+ * @param rendererIndex The renderer index.
+ * @return The corresponding {@link TrackGroup}s.
+ */
+ public TrackGroupArray getTrackGroups(int rendererIndex) {
+ return rendererTrackGroups[rendererIndex];
+ }
+
+ /**
+ * Returns the extent to which a renderer can play the tracks that are mapped to it.
+ *
+ * @param rendererIndex The renderer index.
+ * @return The {@link RendererSupport}.
+ */
+ @RendererSupport
+ public int getRendererSupport(int rendererIndex) {
+ @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex];
+ for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) {
+ for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) {
+ int trackRendererSupport;
+ switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) {
+ case RendererCapabilities.FORMAT_HANDLED:
+ return RENDERER_SUPPORT_PLAYABLE_TRACKS;
+ case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
+ trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;
+ break;
+ case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE:
+ case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE:
+ case RendererCapabilities.FORMAT_UNSUPPORTED_DRM:
+ trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /** @deprecated Use {@link #getTypeSupport(int)}. */
+ @Deprecated
+ @RendererSupport
+ public int getTrackTypeRendererSupport(int trackType) {
+ return getTypeSupport(trackType);
+ }
+
+ /**
+ * Returns the extent to which tracks of a specified type are supported. This is the best level
+ * of support obtained from {@link #getRendererSupport(int)} for all renderers that handle the
+ * specified type. If no such renderers exist then {@link #RENDERER_SUPPORT_NO_TRACKS} is
+ * returned.
+ *
+ * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.
+ * @return The {@link RendererSupport}.
+ */
+ @RendererSupport
+ public int getTypeSupport(int trackType) {
+ @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+ for (int i = 0; i < rendererCount; i++) {
+ if (rendererTrackTypes[i] == trackType) {
+ bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));
+ }
+ }
+ return bestRendererSupport;
+ }
+
+ /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */
+ @Deprecated
+ @FormatSupport
+ public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {
+ return getTrackSupport(rendererIndex, groupIndex, trackIndex);
+ }
+
+ /**
+ * Returns the extent to which an individual track is supported by the renderer.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group to which the track belongs.
+ * @param trackIndex The index of the track within the track group.
+ * @return The {@link FormatSupport}.
+ */
+ @FormatSupport
+ public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) {
+ return RendererCapabilities.getFormatSupport(
+ rendererFormatSupports[rendererIndex][groupIndex][trackIndex]);
+ }
+
+ /**
+ * Returns the extent to which a renderer supports adaptation between supported tracks in a
+ * specified {@link TrackGroup}.
+ *
+ * <p>Tracks for which {@link #getTrackSupport(int, int, int)} returns {@link
+ * RendererCapabilities#FORMAT_HANDLED} are always considered. Tracks for which {@link
+ * #getTrackSupport(int, int, int)} returns {@link
+ * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are also considered if {@code
+ * includeCapabilitiesExceededTracks} is set to {@code true}. Tracks for which {@link
+ * #getTrackSupport(int, int, int)} returns {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM},
+ * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or {@link
+ * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group.
+ * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the
+ * renderer are included when determining support.
+ * @return The {@link AdaptiveSupport}.
+ */
+ @AdaptiveSupport
+ public int getAdaptiveSupport(
+ int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) {
+ int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length;
+ // Iterate over the tracks in the group, recording the indices of those to consider.
+ int[] trackIndices = new int[trackCount];
+ int trackIndexCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i);
+ if (fixedSupport == RendererCapabilities.FORMAT_HANDLED
+ || (includeCapabilitiesExceededTracks
+ && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {
+ trackIndices[trackIndexCount++] = i;
+ }
+ }
+ trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);
+ return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);
+ }
+
+ /**
+ * Returns the extent to which a renderer supports adaptation between specified tracks within a
+ * {@link TrackGroup}.
+ *
+ * @param rendererIndex The renderer index.
+ * @param groupIndex The index of the track group.
+ * @return The {@link AdaptiveSupport}.
+ */
+ @AdaptiveSupport
+ public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {
+ int handledTrackCount = 0;
+ @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;
+ boolean multipleMimeTypes = false;
+ String firstSampleMimeType = null;
+ for (int i = 0; i < trackIndices.length; i++) {
+ int trackIndex = trackIndices[i];
+ String sampleMimeType =
+ rendererTrackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex).sampleMimeType;
+ if (handledTrackCount++ == 0) {
+ firstSampleMimeType = sampleMimeType;
+ } else {
+ multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);
+ }
+ adaptiveSupport =
+ Math.min(
+ adaptiveSupport,
+ RendererCapabilities.getAdaptiveSupport(
+ rendererFormatSupports[rendererIndex][groupIndex][i]));
+ }
+ return multipleMimeTypes
+ ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex])
+ : adaptiveSupport;
+ }
+
+ /** @deprecated Use {@link #getUnmappedTrackGroups()}. */
+ @Deprecated
+ public TrackGroupArray getUnassociatedTrackGroups() {
+ return getUnmappedTrackGroups();
+ }
+
+ /** Returns {@link TrackGroup}s not mapped to any renderer. */
+ public TrackGroupArray getUnmappedTrackGroups() {
+ return unmappedTrackGroups;
+ }
+
+ }
+
+ @Nullable private MappedTrackInfo currentMappedTrackInfo;
+
+ /**
+ * Returns the mapping information for the currently active track selection, or null if no
+ * selection is currently active.
+ */
+ public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
+ return currentMappedTrackInfo;
+ }
+
+ // TrackSelector implementation.
+
+ @Override
+ public final void onSelectionActivated(Object info) {
+ currentMappedTrackInfo = (MappedTrackInfo) info;
+ }
+
+ @Override
+ public final TrackSelectorResult selectTracks(
+ RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray trackGroups,
+ MediaPeriodId periodId,
+ Timeline timeline)
+ throws ExoPlaybackException {
+ // Structures into which data will be written during the selection. The extra item at the end
+ // of each array is to store data associated with track groups that cannot be associated with
+ // any renderer.
+ int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1];
+ TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][];
+ @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][];
+ for (int i = 0; i < rendererTrackGroups.length; i++) {
+ rendererTrackGroups[i] = new TrackGroup[trackGroups.length];
+ rendererFormatSupports[i] = new int[trackGroups.length][];
+ }
+
+ // Determine the extent to which each renderer supports mixed mimeType adaptation.
+ @AdaptiveSupport
+ int[] rendererMixedMimeTypeAdaptationSupports =
+ getMixedMimeTypeAdaptationSupports(rendererCapabilities);
+
+ // Associate each track group to a preferred renderer, and evaluate the support that the
+ // renderer provides for each track in the group.
+ for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
+ TrackGroup group = trackGroups.get(groupIndex);
+ // Associate the group to a preferred renderer.
+ boolean preferUnassociatedRenderer =
+ MimeTypes.getTrackType(group.getFormat(0).sampleMimeType) == C.TRACK_TYPE_METADATA;
+ int rendererIndex =
+ findRenderer(
+ rendererCapabilities, group, rendererTrackGroupCounts, preferUnassociatedRenderer);
+ // Evaluate the support that the renderer provides for each track in the group.
+ @Capabilities
+ int[] rendererFormatSupport =
+ rendererIndex == rendererCapabilities.length
+ ? new int[group.length]
+ : getFormatSupport(rendererCapabilities[rendererIndex], group);
+ // Stash the results.
+ int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex];
+ rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group;
+ rendererFormatSupports[rendererIndex][rendererTrackGroupCount] = rendererFormatSupport;
+ rendererTrackGroupCounts[rendererIndex]++;
+ }
+
+ // Create a track group array for each renderer, and trim each rendererFormatSupports entry.
+ TrackGroupArray[] rendererTrackGroupArrays = new TrackGroupArray[rendererCapabilities.length];
+ int[] rendererTrackTypes = new int[rendererCapabilities.length];
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ int rendererTrackGroupCount = rendererTrackGroupCounts[i];
+ rendererTrackGroupArrays[i] =
+ new TrackGroupArray(
+ Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount));
+ rendererFormatSupports[i] =
+ Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount);
+ rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
+ }
+
+ // Create a track group array for track groups not mapped to a renderer.
+ int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
+ TrackGroupArray unmappedTrackGroupArray =
+ new TrackGroupArray(
+ Util.nullSafeArrayCopy(
+ rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount));
+
+ // Package up the track information and selections.
+ MappedTrackInfo mappedTrackInfo =
+ new MappedTrackInfo(
+ rendererTrackTypes,
+ rendererTrackGroupArrays,
+ rendererMixedMimeTypeAdaptationSupports,
+ rendererFormatSupports,
+ unmappedTrackGroupArray);
+
+ Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result =
+ selectTracks(
+ mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports);
+ return new TrackSelectorResult(result.first, result.second, mappedTrackInfo);
+ }
+
+ /**
+ * Given mapped track information, returns a track selection and configuration for each renderer.
+ *
+ * @param mappedTrackInfo Mapped track information.
+ * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by
+ * renderer, track group and track (in that order).
+ * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type
+ * adaptation for the renderer.
+ * @return A pair consisting of the track selections and configurations for each renderer. A null
+ * configuration indicates the renderer should be disabled, in which case the track selection
+ * will also be null. A track selection may also be null for a non-disabled renderer if {@link
+ * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}.
+ * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+ */
+ protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]>
+ selectTracks(
+ MappedTrackInfo mappedTrackInfo,
+ @Capabilities int[][][] rendererFormatSupports,
+ @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport)
+ throws ExoPlaybackException;
+
+ /**
+ * Finds the renderer to which the provided {@link TrackGroup} should be mapped.
+ *
+ * <p>A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in
+ * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, {@link
+ * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link
+ * RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and {@link
+ * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}.
+ *
+ * <p>In the case that two or more renderers report the same level of support, the assignment
+ * depends on {@code preferUnassociatedRenderer}.
+ *
+ * <ul>
+ * <li>If {@code preferUnassociatedRenderer} is false, the renderer with the lowest index is
+ * chosen regardless of how many other track groups are already mapped to this renderer.
+ * <li>If {@code preferUnassociatedRenderer} is true, the renderer with the lowest index and no
+ * other mapped track group is chosen, or the renderer with the lowest index if all
+ * available renderers have already mapped track groups.
+ * </ul>
+ *
+ * <p>If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the
+ * tracks in the group, then {@code renderers.length} is returned to indicate that the group was
+ * not mapped to any renderer.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+ * @param group The track group to map to a renderer.
+ * @param rendererTrackGroupCounts The number of already mapped track groups for each renderer.
+ * @param preferUnassociatedRenderer Whether renderers unassociated to any track group should be
+ * preferred.
+ * @return The index of the renderer to which the track group was mapped, or {@code
+ * renderers.length} if it was not mapped to any renderer.
+ * @throws ExoPlaybackException If an error occurs finding a renderer.
+ */
+ private static int findRenderer(
+ RendererCapabilities[] rendererCapabilities,
+ TrackGroup group,
+ int[] rendererTrackGroupCounts,
+ boolean preferUnassociatedRenderer)
+ throws ExoPlaybackException {
+ int bestRendererIndex = rendererCapabilities.length;
+ @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;
+ boolean bestRendererIsUnassociated = true;
+ for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) {
+ RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex];
+ @FormatSupport int formatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;
+ for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
+ @FormatSupport
+ int trackFormatSupportLevel =
+ RendererCapabilities.getFormatSupport(
+ rendererCapability.supportsFormat(group.getFormat(trackIndex)));
+ formatSupportLevel = Math.max(formatSupportLevel, trackFormatSupportLevel);
+ }
+ boolean rendererIsUnassociated = rendererTrackGroupCounts[rendererIndex] == 0;
+ if (formatSupportLevel > bestFormatSupportLevel
+ || (formatSupportLevel == bestFormatSupportLevel
+ && preferUnassociatedRenderer
+ && !bestRendererIsUnassociated
+ && rendererIsUnassociated)) {
+ bestRendererIndex = rendererIndex;
+ bestFormatSupportLevel = formatSupportLevel;
+ bestRendererIsUnassociated = rendererIsUnassociated;
+ }
+ }
+ return bestRendererIndex;
+ }
+
+ /**
+ * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link
+ * TrackGroup}, returning the results in an array.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderer.
+ * @param group The track group to evaluate.
+ * @return An array containing {@link Capabilities} for each track in the group.
+ * @throws ExoPlaybackException If an error occurs determining the format support.
+ */
+ @Capabilities
+ private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group)
+ throws ExoPlaybackException {
+ @Capabilities int[] formatSupport = new int[group.length];
+ for (int i = 0; i < group.length; i++) {
+ formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i));
+ }
+ return formatSupport;
+ }
+
+ /**
+ * Calls {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer,
+ * returning the results in an array.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+ * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the
+ * renderer.
+ * @throws ExoPlaybackException If an error occurs determining the adaptation support.
+ */
+ @AdaptiveSupport
+ private static int[] getMixedMimeTypeAdaptationSupports(
+ RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {
+ @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length];
+ for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) {
+ mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation();
+ }
+ return mixedMimeTypeAdaptationSupport;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
new file mode 100644
index 0000000000..75b7fc21f1
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
@@ -0,0 +1,143 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import java.util.List;
+import java.util.Random;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A {@link TrackSelection} whose selected track is updated randomly.
+ */
+public final class RandomTrackSelection extends BaseTrackSelection {
+
+ /**
+ * Factory for {@link RandomTrackSelection} instances.
+ */
+ public static final class Factory implements TrackSelection.Factory {
+
+ private final Random random;
+
+ public Factory() {
+ random = new Random();
+ }
+
+ /**
+ * @param seed A seed for the {@link Random} instance used by the factory.
+ */
+ public Factory(int seed) {
+ random = new Random(seed);
+ }
+
+ @Override
+ public @NullableType TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
+ return TrackSelectionUtil.createTrackSelectionsForDefinitions(
+ definitions,
+ definition -> new RandomTrackSelection(definition.group, definition.tracks, random));
+ }
+ }
+
+ private final Random random;
+
+ private int selectedIndex;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ */
+ public RandomTrackSelection(TrackGroup group, int... tracks) {
+ super(group, tracks);
+ random = new Random();
+ selectedIndex = random.nextInt(length);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ * @param seed A seed for the {@link Random} instance used to update the selected track.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) {
+ this(group, tracks, new Random(seed));
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ * @param random A source of random numbers.
+ */
+ public RandomTrackSelection(TrackGroup group, int[] tracks, Random random) {
+ super(group, tracks);
+ this.random = random;
+ selectedIndex = random.nextInt(length);
+ }
+
+ @Override
+ public void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators) {
+ // Count the number of non-blacklisted formats.
+ long nowMs = SystemClock.elapsedRealtime();
+ int nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs)) {
+ nonBlacklistedFormatCount++;
+ }
+ }
+
+ selectedIndex = random.nextInt(nonBlacklistedFormatCount);
+ if (nonBlacklistedFormatCount != length) {
+ // Adjust the format index to account for blacklisted formats.
+ nonBlacklistedFormatCount = 0;
+ for (int i = 0; i < length; i++) {
+ if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) {
+ selectedIndex = i;
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+
+ @Override
+ public int getSelectionReason() {
+ return C.SELECTION_REASON_ADAPTIVE;
+ }
+
+ @Override
+ @Nullable
+ public Object getSelectionData() {
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
new file mode 100644
index 0000000000..d2f32222fa
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -0,0 +1,269 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import java.util.List;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A track selection consisting of a static subset of selected tracks belonging to a {@link
+ * TrackGroup}, and a possibly varying individual selected track from the subset.
+ *
+ * <p>Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual
+ * selected track may change dynamically as a result of calling {@link #updateSelectedTrack(long,
+ * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)}. This only
+ * happens between calls to {@link #enable()} and {@link #disable()}.
+ */
+public interface TrackSelection {
+
+ /** Contains of a subset of selected tracks belonging to a {@link TrackGroup}. */
+ final class Definition {
+ /** The {@link TrackGroup} which tracks belong to. */
+ public final TrackGroup group;
+ /** The indices of the selected tracks in {@link #group}. */
+ public final int[] tracks;
+ /** The track selection reason. One of the {@link C} SELECTION_REASON_ constants. */
+ public final int reason;
+ /** Optional data associated with this selection of tracks. */
+ @Nullable public final Object data;
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * null or empty. May be in any order.
+ */
+ public Definition(TrackGroup group, int... tracks) {
+ this(group, tracks, C.SELECTION_REASON_UNKNOWN, /* data= */ null);
+ }
+
+ /**
+ * @param group The {@link TrackGroup}. Must not be null.
+ * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+ * @param reason The track selection reason. One of the {@link C} SELECTION_REASON_ constants.
+ * @param data Optional data associated with this selection of tracks.
+ */
+ public Definition(TrackGroup group, int[] tracks, int reason, @Nullable Object data) {
+ this.group = group;
+ this.tracks = tracks;
+ this.reason = reason;
+ this.data = data;
+ }
+ }
+
+ /**
+ * Factory for {@link TrackSelection} instances.
+ */
+ interface Factory {
+
+ /**
+ * Creates track selections for the provided {@link Definition Definitions}.
+ *
+ * <p>Implementations that create at most one adaptive track selection may use {@link
+ * TrackSelectionUtil#createTrackSelectionsForDefinitions}.
+ *
+ * @param definitions A {@link Definition} array. May include null values.
+ * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
+ * @return The created selections. Must have the same length as {@code definitions} and may
+ * include null values.
+ */
+ @NullableType
+ TrackSelection[] createTrackSelections(
+ @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter);
+ }
+
+ /**
+ * Enables the track selection. Dynamic changes via {@link #updateSelectedTrack(long, long, long,
+ * List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will only happen after
+ * this call.
+ *
+ * <p>This method may not be called when the track selection is already enabled.
+ */
+ void enable();
+
+ /**
+ * Disables this track selection. No further dynamic changes via {@link #updateSelectedTrack(long,
+ * long, long, List, MediaChunkIterator[])} or {@link #evaluateQueueSize(long, List)} will happen
+ * after this call.
+ *
+ * <p>This method may only be called when the track selection is already enabled.
+ */
+ void disable();
+
+ /**
+ * Returns the {@link TrackGroup} to which the selected tracks belong.
+ */
+ TrackGroup getTrackGroup();
+
+ // Static subset of selected tracks.
+
+ /**
+ * Returns the number of tracks in the selection.
+ */
+ int length();
+
+ /**
+ * Returns the format of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The format of the selected track.
+ */
+ Format getFormat(int index);
+
+ /**
+ * Returns the index in the track group of the track at a given index in the selection.
+ *
+ * @param index The index in the selection.
+ * @return The index of the selected track.
+ */
+ int getIndexInTrackGroup(int index);
+
+ /**
+ * Returns the index in the selection of the track with the specified format. The format is
+ * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) ==
+ * index} even if multiple selected tracks have formats that contain the same values.
+ *
+ * @param format The format.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * format is not part of the selection.
+ */
+ int indexOf(Format format);
+
+ /**
+ * Returns the index in the selection of the track with the specified index in the track group.
+ *
+ * @param indexInTrackGroup The index in the track group.
+ * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified
+ * index is not part of the selection.
+ */
+ int indexOf(int indexInTrackGroup);
+
+ // Individual selected track.
+
+ /**
+ * Returns the {@link Format} of the individual selected track.
+ */
+ Format getSelectedFormat();
+
+ /**
+ * Returns the index in the track group of the individual selected track.
+ */
+ int getSelectedIndexInTrackGroup();
+
+ /**
+ * Returns the index of the selected track.
+ */
+ int getSelectedIndex();
+
+ /**
+ * Returns the reason for the current track selection.
+ */
+ int getSelectionReason();
+
+ /** Returns optional data associated with the current track selection. */
+ @Nullable Object getSelectionData();
+
+ // Adaptation.
+
+ /**
+ * Called to notify the selection of the current playback speed. The playback speed may affect
+ * adaptive track selection.
+ *
+ * @param speed The playback speed.
+ */
+ void onPlaybackSpeed(float speed);
+
+ /**
+ * Called to notify the selection of a position discontinuity.
+ *
+ * <p>This happens when the playback position jumps, e.g., as a result of a seek being performed.
+ */
+ default void onDiscontinuity() {}
+
+ /**
+ * Updates the selected track for sources that load media in discrete {@link MediaChunk}s.
+ *
+ * <p>This method may only be called when the selection is enabled.
+ *
+ * @param playbackPositionUs The current playback position in microseconds. If playback of the
+ * period to which this track selection belongs has not yet started, the value will be the
+ * starting position in the period minus the duration of any media in previous periods still
+ * to be played.
+ * @param bufferedDurationUs The duration of media currently buffered from the current playback
+ * position, in microseconds. Note that the next load position can be calculated as {@code
+ * (playbackPositionUs + bufferedDurationUs)}.
+ * @param availableDurationUs The duration of media available for buffering from the current
+ * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered to the
+ * end of the current period. Note that if not set to {@link C#TIME_UNSET}, the position up to
+ * which media is available for buffering can be calculated as {@code (playbackPositionUs +
+ * availableDurationUs)}.
+ * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified.
+ * @param mediaChunkIterators An array of {@link MediaChunkIterator}s providing information about
+ * the sequence of upcoming media chunks for each track in the selection. All iterators start
+ * from the media chunk which will be loaded next if the respective track is selected. Note
+ * that this information may not be available for all tracks, and so some iterators may be
+ * empty.
+ */
+ void updateSelectedTrack(
+ long playbackPositionUs,
+ long bufferedDurationUs,
+ long availableDurationUs,
+ List<? extends MediaChunk> queue,
+ MediaChunkIterator[] mediaChunkIterators);
+
+ /**
+ * May be called periodically by sources that load media in discrete {@link MediaChunk}s and
+ * support discarding of buffered chunks in order to re-buffer using a different selected track.
+ * Returns the number of chunks that should be retained in the queue.
+ * <p>
+ * To avoid excessive re-buffering, implementations should normally return the size of the queue.
+ * An example of a case where a smaller value may be returned is if network conditions have
+ * improved dramatically, allowing chunks to be discarded and re-buffered in a track of
+ * significantly higher quality. Discarding chunks may allow faster switching to a higher quality
+ * track in this case. This method may only be called when the selection is enabled.
+ *
+ * @param playbackPositionUs The current playback position in microseconds. If playback of the
+ * period to which this track selection belongs has not yet started, the value will be the
+ * starting position in the period minus the duration of any media in previous periods still
+ * to be played.
+ * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified.
+ * @return The number of chunks to retain in the queue.
+ */
+ int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue);
+
+ /**
+ * Attempts to blacklist the track at the specified index in the selection, making it ineligible
+ * for selection by calls to {@link #updateSelectedTrack(long, long, long, List,
+ * MediaChunkIterator[])} for the specified period of time. Blacklisting will fail if all other
+ * tracks are currently blacklisted. If blacklisting the currently selected track, note that it
+ * will remain selected until the next call to {@link #updateSelectedTrack(long, long, long, List,
+ * MediaChunkIterator[])}.
+ *
+ * <p>This method may only be called when the selection is enabled.
+ *
+ * @param index The index of the track in the selection.
+ * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in
+ * milliseconds.
+ * @return Whether blacklisting was successful.
+ */
+ boolean blacklist(int index, long blacklistDurationMs);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
new file mode 100644
index 0000000000..7953ef354c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import java.util.Arrays;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** An array of {@link TrackSelection}s. */
+public final class TrackSelectionArray {
+
+ /** The length of this array. */
+ public final int length;
+
+ private final @NullableType TrackSelection[] trackSelections;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /** @param trackSelections The selections. Must not be null, but may contain null elements. */
+ public TrackSelectionArray(@NullableType TrackSelection... trackSelections) {
+ this.trackSelections = trackSelections;
+ this.length = trackSelections.length;
+ }
+
+ /**
+ * Returns the selection at a given index.
+ *
+ * @param index The index of the selection.
+ * @return The selection.
+ */
+ @Nullable
+ public TrackSelection get(int index) {
+ return trackSelections[index];
+ }
+
+ /** Returns the selections in a newly allocated array. */
+ public @NullableType TrackSelection[] getAll() {
+ return trackSelections.clone();
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = 17;
+ result = 31 * result + Arrays.hashCode(trackSelections);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackSelectionArray other = (TrackSelectionArray) obj;
+ return Arrays.equals(trackSelections, other.trackSelections);
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java
new file mode 100644
index 0000000000..b6086fa594
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.view.accessibility.CaptioningManager;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.Locale;
+
+/** Constraint parameters for track selection. */
+public class TrackSelectionParameters implements Parcelable {
+
+ /**
+ * A builder for {@link TrackSelectionParameters}. See the {@link TrackSelectionParameters}
+ * documentation for explanations of the parameters that can be configured using this builder.
+ */
+ public static class Builder {
+
+ @Nullable /* package */ String preferredAudioLanguage;
+ @Nullable /* package */ String preferredTextLanguage;
+ @C.RoleFlags /* package */ int preferredTextRoleFlags;
+ /* package */ boolean selectUndeterminedTextLanguage;
+ @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags;
+
+ /**
+ * Creates a builder with default initial values.
+ *
+ * @param context Any context.
+ */
+ @SuppressWarnings({"deprecation", "initialization:method.invocation.invalid"})
+ public Builder(Context context) {
+ this();
+ setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context);
+ }
+
+ /**
+ * @deprecated {@link Context} constraints will not be set when using this constructor. Use
+ * {@link #Builder(Context)} instead.
+ */
+ @Deprecated
+ public Builder() {
+ preferredAudioLanguage = null;
+ preferredTextLanguage = null;
+ preferredTextRoleFlags = 0;
+ selectUndeterminedTextLanguage = false;
+ disabledTextTrackSelectionFlags = 0;
+ }
+
+ /**
+ * @param initialValues The {@link TrackSelectionParameters} from which the initial values of
+ * the builder are obtained.
+ */
+ /* package */ Builder(TrackSelectionParameters initialValues) {
+ preferredAudioLanguage = initialValues.preferredAudioLanguage;
+ preferredTextLanguage = initialValues.preferredTextLanguage;
+ preferredTextRoleFlags = initialValues.preferredTextRoleFlags;
+ selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage;
+ disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags;
+ }
+
+ /**
+ * Sets the preferred language for audio and forced text tracks.
+ *
+ * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag, or
+ * {@code null} to select the default track, or the first track if there's no default.
+ * @return This builder.
+ */
+ public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
+ this.preferredAudioLanguage = preferredAudioLanguage;
+ return this;
+ }
+
+ /**
+ * Sets the preferred language and role flags for text tracks based on the accessibility
+ * settings of {@link CaptioningManager}.
+ *
+ * <p>Does nothing for API levels &lt; 19 or when the {@link CaptioningManager} is disabled.
+ *
+ * @param context A {@link Context}.
+ * @return This builder.
+ */
+ public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
+ Context context) {
+ if (Util.SDK_INT >= 19) {
+ setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(context);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the preferred language for text tracks.
+ *
+ * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag, or
+ * {@code null} to select the default track if there is one, or no track otherwise.
+ * @return This builder.
+ */
+ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {
+ this.preferredTextLanguage = preferredTextLanguage;
+ return this;
+ }
+
+ /**
+ * Sets the preferred {@link C.RoleFlags} for text tracks.
+ *
+ * @param preferredTextRoleFlags Preferred text role flags.
+ * @return This builder.
+ */
+ public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
+ this.preferredTextRoleFlags = preferredTextRoleFlags;
+ return this;
+ }
+
+ /**
+ * Sets whether a text track with undetermined language should be selected if no track with
+ * {@link #setPreferredTextLanguage(String)} is available, or if the preferred language is
+ * unset.
+ *
+ * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should
+ * be selected if no preferred language track is available.
+ * @return This builder.
+ */
+ public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) {
+ this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
+ return this;
+ }
+
+ /**
+ * Sets a bitmask of selection flags that are disabled for text track selections.
+ *
+ * @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are
+ * disabled for text track selections.
+ * @return This builder.
+ */
+ public Builder setDisabledTextTrackSelectionFlags(
+ @C.SelectionFlags int disabledTextTrackSelectionFlags) {
+ this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;
+ return this;
+ }
+
+ /** Builds a {@link TrackSelectionParameters} instance with the selected values. */
+ public TrackSelectionParameters build() {
+ return new TrackSelectionParameters(
+ // Audio
+ preferredAudioLanguage,
+ // Text
+ preferredTextLanguage,
+ preferredTextRoleFlags,
+ selectUndeterminedTextLanguage,
+ disabledTextTrackSelectionFlags);
+ }
+
+ @TargetApi(19)
+ private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(
+ Context context) {
+ if (Util.SDK_INT < 23 && Looper.myLooper() == null) {
+ // Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when
+ // CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904].
+ return;
+ }
+ CaptioningManager captioningManager =
+ (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+ if (captioningManager == null || !captioningManager.isEnabled()) {
+ return;
+ }
+ preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
+ Locale preferredLocale = captioningManager.getLocale();
+ if (preferredLocale != null) {
+ preferredTextLanguage = Util.getLocaleLanguageTag(preferredLocale);
+ }
+ }
+ }
+
+ /**
+ * 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>{@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context)
+ * Preferred text language and role flags} configured to the accessibility settings of
+ * {@link CaptioningManager}.
+ * </ul>
+ */
+ @SuppressWarnings("deprecation")
+ public static final TrackSelectionParameters 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 TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;
+
+ /** Returns an instance configured with default values. */
+ public static TrackSelectionParameters getDefaults(Context context) {
+ return new Builder(context).build();
+ }
+
+ /**
+ * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag.
+ * {@code null} selects the default track, or the first track if there's no default. The default
+ * value is {@code null}.
+ */
+ @Nullable public final String preferredAudioLanguage;
+ /**
+ * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects
+ * the default track if there is one, or no track otherwise. The default value is {@code null}, or
+ * the language of the accessibility {@link CaptioningManager} if enabled.
+ */
+ @Nullable public final String preferredTextLanguage;
+ /**
+ * The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there
+ * is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE}
+ * | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager}
+ * is enabled.
+ */
+ @C.RoleFlags public final int preferredTextRoleFlags;
+ /**
+ * Whether a text track with undetermined language should be selected if no track with {@link
+ * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The
+ * default value is {@code false}.
+ */
+ public final boolean selectUndeterminedTextLanguage;
+ /**
+ * Bitmask of selection flags that are disabled for text track selections. See {@link
+ * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).
+ */
+ @C.SelectionFlags public final int disabledTextTrackSelectionFlags;
+
+ /* package */ TrackSelectionParameters(
+ @Nullable String preferredAudioLanguage,
+ @Nullable String preferredTextLanguage,
+ @C.RoleFlags int preferredTextRoleFlags,
+ boolean selectUndeterminedTextLanguage,
+ @C.SelectionFlags int disabledTextTrackSelectionFlags) {
+ // Audio
+ this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage);
+ // Text
+ this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage);
+ this.preferredTextRoleFlags = preferredTextRoleFlags;
+ this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
+ this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;
+ }
+
+ /* package */ TrackSelectionParameters(Parcel in) {
+ this.preferredAudioLanguage = in.readString();
+ this.preferredTextLanguage = in.readString();
+ this.preferredTextRoleFlags = in.readInt();
+ this.selectUndeterminedTextLanguage = Util.readBoolean(in);
+ this.disabledTextTrackSelectionFlags = in.readInt();
+ }
+
+ /** Creates a new {@link Builder}, copying the initial values from this instance. */
+ public Builder buildUpon() {
+ return new Builder(this);
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TrackSelectionParameters other = (TrackSelectionParameters) obj;
+ return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage)
+ && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage)
+ && preferredTextRoleFlags == other.preferredTextRoleFlags
+ && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
+ && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode());
+ result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode());
+ result = 31 * result + preferredTextRoleFlags;
+ result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
+ result = 31 * result + disabledTextTrackSelectionFlags;
+ return result;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(preferredAudioLanguage);
+ dest.writeString(preferredTextLanguage);
+ dest.writeInt(preferredTextRoleFlags);
+ Util.writeBoolean(dest, selectUndeterminedTextLanguage);
+ dest.writeInt(disabledTextTrackSelectionFlags);
+ }
+
+ public static final Creator<TrackSelectionParameters> CREATOR =
+ new Creator<TrackSelectionParameters>() {
+
+ @Override
+ public TrackSelectionParameters createFromParcel(Parcel in) {
+ return new TrackSelectionParameters(in);
+ }
+
+ @Override
+ public TrackSelectionParameters[] newArray(int size) {
+ return new TrackSelectionParameters[size];
+ }
+ };
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
new file mode 100644
index 0000000000..b2fcf5c13c
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection.Definition;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/** Track selection related utility methods. */
+public final class TrackSelectionUtil {
+
+ private TrackSelectionUtil() {}
+
+ /** Functional interface to create a single adaptive track selection. */
+ public interface AdaptiveTrackSelectionFactory {
+
+ /**
+ * Creates an adaptive track selection for the provided track selection definition.
+ *
+ * @param trackSelectionDefinition A {@link Definition} for the track selection.
+ * @return The created track selection.
+ */
+ TrackSelection createAdaptiveTrackSelection(Definition trackSelectionDefinition);
+ }
+
+ /**
+ * Creates track selections for an array of track selection definitions, with at most one
+ * multi-track adaptive selection.
+ *
+ * @param definitions The list of track selection {@link Definition definitions}. May include null
+ * values.
+ * @param adaptiveTrackSelectionFactory A factory for the multi-track adaptive track selection.
+ * @return The array of created track selection. For null entries in {@code definitions} returns
+ * null values.
+ */
+ public static @NullableType TrackSelection[] createTrackSelectionsForDefinitions(
+ @NullableType Definition[] definitions,
+ AdaptiveTrackSelectionFactory adaptiveTrackSelectionFactory) {
+ TrackSelection[] selections = new TrackSelection[definitions.length];
+ boolean createdAdaptiveTrackSelection = false;
+ for (int i = 0; i < definitions.length; i++) {
+ Definition definition = definitions[i];
+ if (definition == null) {
+ continue;
+ }
+ if (definition.tracks.length > 1 && !createdAdaptiveTrackSelection) {
+ createdAdaptiveTrackSelection = true;
+ selections[i] = adaptiveTrackSelectionFactory.createAdaptiveTrackSelection(definition);
+ } else {
+ selections[i] =
+ new FixedTrackSelection(
+ definition.group, definition.tracks[0], definition.reason, definition.data);
+ }
+ }
+ return selections;
+ }
+
+ /**
+ * Updates {@link DefaultTrackSelector.Parameters} with an override.
+ *
+ * @param parameters The current {@link DefaultTrackSelector.Parameters} to build upon.
+ * @param rendererIndex The renderer index to update.
+ * @param trackGroupArray The {@link TrackGroupArray} of the renderer.
+ * @param isDisabled Whether the renderer should be set disabled.
+ * @param override An optional override for the renderer. If null, no override will be set and an
+ * existing override for this renderer will be cleared.
+ * @return The updated {@link DefaultTrackSelector.Parameters}.
+ */
+ public static DefaultTrackSelector.Parameters updateParametersWithOverride(
+ DefaultTrackSelector.Parameters parameters,
+ int rendererIndex,
+ TrackGroupArray trackGroupArray,
+ boolean isDisabled,
+ @Nullable SelectionOverride override) {
+ DefaultTrackSelector.ParametersBuilder builder =
+ parameters
+ .buildUpon()
+ .clearSelectionOverrides(rendererIndex)
+ .setRendererDisabled(rendererIndex, isDisabled);
+ if (override != null) {
+ builder.setSelectionOverride(rendererIndex, trackGroupArray, override);
+ }
+ return builder.build();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java
new file mode 100644
index 0000000000..878031824d
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java
@@ -0,0 +1,157 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Renderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of
+ * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be
+ * suitable for most use cases.
+ *
+ * <h3>Interactions with the player</h3>
+ *
+ * The following interactions occur between the player and its track selector during playback.
+ *
+ * <ul>
+ * <li>When the player is created it will initialize the track selector by calling {@link
+ * #init(InvalidationListener, BandwidthMeter)}.
+ * <li>When the player needs to make a track selection it will call {@link
+ * #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}. This
+ * typically occurs at the start of playback, when the player starts to buffer a new period of
+ * the media being played, and when the track selector invalidates its previous selections.
+ * <li>The player may perform a track selection well in advance of the selected tracks becoming
+ * active, where active is defined to mean that the renderers are actually consuming media
+ * corresponding to the selection that was made. For example when playing media containing
+ * multiple periods, the track selection for a period is made when the player starts to buffer
+ * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the
+ * selection will occur approximately 30 seconds in advance of it becoming active. In fact the
+ * selection may never become active, for example if the user seeks to some other period of
+ * the media during the 30 second gap. The player indicates to the track selector when a
+ * selection it has previously made becomes active by calling {@link
+ * #onSelectionActivated(Object)}.
+ * <li>If the track selector wishes to indicate to the player that selections it has previously
+ * made are invalid, it can do so by calling {@link
+ * InvalidationListener#onTrackSelectionsInvalidated()} on the {@link InvalidationListener}
+ * that was passed to {@link #init(InvalidationListener, BandwidthMeter)}. A track selector
+ * may wish to do this if its configuration has changed, for example if it now wishes to
+ * prefer audio tracks in a particular language. This will trigger the player to make new
+ * track selections. Note that the player will have to re-buffer in the case that the new
+ * track selection for the currently playing period differs from the one that was invalidated.
+ * </ul>
+ *
+ * <h3>Renderer configuration</h3>
+ *
+ * The {@link TrackSelectorResult} returned by {@link #selectTracks(RendererCapabilities[],
+ * TrackGroupArray, MediaPeriodId, Timeline)} contains not only {@link TrackSelection}s for each
+ * renderer, but also {@link RendererConfiguration}s defining configuration parameters that the
+ * renderers should apply when consuming the corresponding media. Whilst it may seem counter-
+ * intuitive for a track selector to also specify renderer configuration information, in practice
+ * the two are tightly bound together. It may only be possible to play a certain combination tracks
+ * if the renderers are configured in a particular way. Equally, it may only be possible to
+ * configure renderers in a particular way if certain tracks are selected. Hence it makes sense to
+ * determine the track selection and corresponding renderer configurations in a single step.
+ *
+ * <h3>Threading model</h3>
+ *
+ * All calls made by the player into the track selector are on the player's internal playback
+ * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()}
+ * from any thread.
+ */
+public abstract class TrackSelector {
+
+ /**
+ * Notified when selections previously made by a {@link TrackSelector} are no longer valid.
+ */
+ public interface InvalidationListener {
+
+ /**
+ * Called by a {@link TrackSelector} to indicate that selections it has previously made are no
+ * longer valid. May be called from any thread.
+ */
+ void onTrackSelectionsInvalidated();
+
+ }
+
+ @Nullable private InvalidationListener listener;
+ @Nullable private BandwidthMeter bandwidthMeter;
+
+ /**
+ * Called by the player to initialize the selector.
+ *
+ * @param listener An invalidation listener that the selector can call to indicate that selections
+ * it has previously made are no longer valid.
+ * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks.
+ */
+ public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) {
+ this.listener = listener;
+ this.bandwidthMeter = bandwidthMeter;
+ }
+
+ /**
+ * Called by the player to perform a track selection.
+ *
+ * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
+ * are to be selected.
+ * @param trackGroups The available track groups.
+ * @param periodId The {@link MediaPeriodId} of the period for which tracks are to be selected.
+ * @param timeline The {@link Timeline} holding the period for which tracks are to be selected.
+ * @return A {@link TrackSelectorResult} describing the track selections.
+ * @throws ExoPlaybackException If an error occurs selecting tracks.
+ */
+ public abstract TrackSelectorResult selectTracks(
+ RendererCapabilities[] rendererCapabilities,
+ TrackGroupArray trackGroups,
+ MediaPeriodId periodId,
+ Timeline timeline)
+ throws ExoPlaybackException;
+
+ /**
+ * Called by the player when a {@link TrackSelectorResult} previously generated by {@link
+ * #selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)} is activated.
+ *
+ * @param info The value of {@link TrackSelectorResult#info} in the activated selection.
+ */
+ public abstract void onSelectionActivated(Object info);
+
+ /**
+ * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously
+ * generated track selections.
+ */
+ protected final void invalidate() {
+ if (listener != null) {
+ listener.onTrackSelectionsInvalidated();
+ }
+ }
+
+ /**
+ * Returns a bandwidth meter which can be used by track selections to select tracks. Must only be
+ * called after {@link #init(InvalidationListener, BandwidthMeter)} has been called.
+ */
+ protected final BandwidthMeter getBandwidthMeter() {
+ return Assertions.checkNotNull(bandwidthMeter);
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
new file mode 100644
index 0000000000..9c005497cc
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererConfiguration;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * The result of a {@link TrackSelector} operation.
+ */
+public final class TrackSelectorResult {
+
+ /** The number of selections in the result. Greater than or equal to zero. */
+ public final int length;
+ /**
+ * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding
+ * renderer should be disabled.
+ */
+ public final @NullableType RendererConfiguration[] rendererConfigurations;
+ /**
+ * A {@link TrackSelectionArray} containing the track selection for each renderer.
+ */
+ public final TrackSelectionArray selections;
+ /**
+ * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
+ * should the selections be activated.
+ */
+ public final Object info;
+
+ /**
+ * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry
+ * indicates the corresponding renderer should be disabled.
+ * @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
+ * @param info An opaque object that will be returned to {@link
+ * TrackSelector#onSelectionActivated(Object)} should the selection be activated.
+ */
+ public TrackSelectorResult(
+ @NullableType RendererConfiguration[] rendererConfigurations,
+ @NullableType TrackSelection[] selections,
+ Object info) {
+ this.rendererConfigurations = rendererConfigurations;
+ this.selections = new TrackSelectionArray(selections);
+ this.info = info;
+ length = rendererConfigurations.length;
+ }
+
+ /** Returns whether the renderer at the specified index is enabled. */
+ public boolean isRendererEnabled(int index) {
+ return rendererConfigurations[index] != null;
+ }
+
+ /**
+ * Returns whether this result is equivalent to {@code other} for all renderers.
+ *
+ * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+ * will be returned.
+ * @return Whether this result is equivalent to {@code other} for all renderers.
+ */
+ public boolean isEquivalent(@Nullable TrackSelectorResult other) {
+ if (other == null || other.selections.length != selections.length) {
+ return false;
+ }
+ for (int i = 0; i < selections.length; i++) {
+ if (!isEquivalent(other, i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether this result is equivalent to {@code other} for the renderer at the given index.
+ * The results are equivalent if they have equal track selections and configurations for the
+ * renderer.
+ *
+ * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+ * will be returned.
+ * @param index The renderer index to check for equivalence.
+ * @return Whether this result is equivalent to {@code other} for the renderer at the specified
+ * index.
+ */
+ public boolean isEquivalent(@Nullable TrackSelectorResult other, int index) {
+ if (other == null) {
+ return false;
+ }
+ return Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index])
+ && Util.areEqual(selections.get(index), other.selections.get(index));
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java
new file mode 100644
index 0000000000..4a04290d0f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;