diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection')
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 < 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; |