From a4fbc2c98db8d0ff08a3b1a111d7c684665f4355 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Jan 2021 18:00:24 +0000 Subject: [PATCH] Clean up parallel adaptation code. - The AdaptiveTrackSelection doesn't need to use the experimental terminolgy because the code is always triggered if there are multiple adaptive selections. - It's also confusing to pass the state on the outside after the object creation, so moving everything into a simple control flow again where the adaptation checkpoints are passed in via the constructor. - Instead of triple arrays, we can use more readable named structures. - The calculation of the checkpoints can be cleaned up to be more readable by moving things to helper methods. - The reserved bandwidth from all fixed track selections is really just a special case of multiple parallel adaptataions. So this logic doesn't need to be separate. - The whole logic also didn't have test coverage so far. Added tests for the actual adaptation using these checkpoints and the builder calculating the checkpoints. Overall this should be a no-op change. PiperOrigin-RevId: 350162834 --- .../AdaptiveTrackSelection.java | 393 ++++++++---------- .../AdaptiveTrackSelectionTest.java | 222 +++++++++- 2 files changed, 400 insertions(+), 215 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index e6ad3e869d8..e8328a2c426 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.trackselection; -import static java.lang.Math.max; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; @@ -27,11 +26,14 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -135,48 +137,23 @@ public Factory( BandwidthMeter bandwidthMeter, MediaPeriodId mediaPeriodId, Timeline timeline) { + ImmutableList> adaptationCheckpoints = + getAdaptationCheckpoints(definitions); 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 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) - .experimentalSetBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]); + @Nullable Definition definition = definitions[i]; + if (definition == null || definition.tracks.length == 0) { + continue; } + selections[i] = + definition.tracks.length == 1 + ? new FixedTrackSelection( + definition.group, definition.tracks[0], definition.reason, definition.data) + : createAdaptiveTrackSelection( + definition.group, + bandwidthMeter, + definition.tracks, + adaptationCheckpoints.get(i)); } return selections; } @@ -187,23 +164,25 @@ public Factory( * @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. + * @param adaptationCheckpoints The {@link AdaptationCheckpoint checkpoints} that can be used to + * calculate available bandwidth for this selection. * @return An {@link AdaptiveTrackSelection} for the specified tracks. */ protected AdaptiveTrackSelection createAdaptiveTrackSelection( TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks, - int totalFixedTrackBandwidth) { + ImmutableList adaptationCheckpoints) { return new AdaptiveTrackSelection( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), + bandwidthMeter, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, + bandwidthFraction, bufferedFractionToLiveEdgeForQualityIncrease, + adaptationCheckpoints, clock); } } @@ -216,11 +195,13 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection( private static final long MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 1000; - private final BandwidthProvider bandwidthProvider; + private final BandwidthMeter bandwidthMeter; private final long minDurationForQualityIncreaseUs; private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; + private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final ImmutableList adaptationCheckpoints; private final Clock clock; private float playbackSpeed; @@ -235,18 +216,17 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection( * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ - public AdaptiveTrackSelection(TrackGroup group, int[] tracks, - BandwidthMeter bandwidthMeter) { + 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, + /* adaptationCheckpoints= */ ImmutableList.of(), Clock.DEFAULT); } @@ -255,8 +235,6 @@ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, * @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 @@ -274,62 +252,36 @@ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, * 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 adaptationCheckpoints The {@link AdaptationCheckpoint checkpoints} that can be used to + * calculate available bandwidth for this selection. + * @param clock The {@link Clock}. */ - public AdaptiveTrackSelection( + protected AdaptiveTrackSelection( TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, - long reservedBandwidth, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease, - Clock clock) { - this( - group, - tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), - minDurationForQualityIncreaseMs, - maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, - bufferedFractionToLiveEdgeForQualityIncrease, - clock); - } - - private AdaptiveTrackSelection( - TrackGroup group, - int[] tracks, - BandwidthProvider bandwidthProvider, - long minDurationForQualityIncreaseMs, - long maxDurationForQualityDecreaseMs, - long minDurationToRetainAfterDiscardMs, - float bufferedFractionToLiveEdgeForQualityIncrease, + List adaptationCheckpoints, Clock clock) { super(group, tracks); - this.bandwidthProvider = bandwidthProvider; + this.bandwidthMeter = bandwidthMeter; this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; + this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + this.adaptationCheckpoints = ImmutableList.copyOf(adaptationCheckpoints); 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 experimentalSetBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) { - ((DefaultBandwidthProvider) bandwidthProvider) - .experimentalSetBandwidthAllocationCheckpoints(allocationCheckpoints); - } - @CallSuper @Override public void enable() { @@ -502,7 +454,7 @@ protected long getMinDurationToRetainAfterDiscardUs() { * Long#MIN_VALUE} to ignore track exclusion. */ private int determineIdealSelectedIndex(long nowMs) { - long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth(); + long effectiveBitrate = getAllocatedBandwidth(); int lowestBitrateAllowedIndex = 0; for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { @@ -525,162 +477,181 @@ private long minDurationForQualityIncreaseUs(long availableDurationUs) { : 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 */ DefaultBandwidthProvider( - BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { - this.bandwidthMeter = bandwidthMeter; - this.bandwidthFraction = bandwidthFraction; - this.reservedBandwidth = reservedBandwidth; + private long getAllocatedBandwidth() { + long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); + if (adaptationCheckpoints.isEmpty()) { + return totalBandwidth; + } + int nextIndex = 1; + while (nextIndex < adaptationCheckpoints.size() - 1 + && adaptationCheckpoints.get(nextIndex).totalBandwidth < totalBandwidth) { + nextIndex++; } + AdaptationCheckpoint previous = adaptationCheckpoints.get(nextIndex - 1); + AdaptationCheckpoint next = adaptationCheckpoints.get(nextIndex); + float fractionBetweenCheckpoints = + (float) (totalBandwidth - previous.totalBandwidth) + / (next.totalBandwidth - previous.totalBandwidth); + return previous.allocatedBandwidth + + (long) + (fractionBetweenCheckpoints * (next.allocatedBandwidth - previous.allocatedBandwidth)); + } - @Override - public long getAllocatedBandwidth() { - long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); - long allocatableBandwidth = max(0L, totalBandwidth - reservedBandwidth); - if (allocationCheckpoints == null) { - return allocatableBandwidth; + /** + * Returns adaptation checkpoints for allocating bandwidth for adaptive track selections. + * + * @param definitions Array of track selection {@link Definition definitions}. Elements may be + * null. + * @return List of {@link AdaptationCheckpoint checkpoints} for each adaptive {@link Definition} + * with more than one selected track. + */ + private static ImmutableList> getAdaptationCheckpoints( + @NullableType Definition[] definitions) { + List> checkPointBuilders = + new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + if (definitions[i] != null && definitions[i].tracks.length > 1) { + ImmutableList.Builder builder = ImmutableList.builder(); + // Add initial all-zero checkpoint. + builder.add(new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0)); + checkPointBuilders.add(builder); + } else { + checkPointBuilders.add(null); } - int nextIndex = 1; - while (nextIndex < allocationCheckpoints.length - 1 - && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) { - nextIndex++; + } + // Add minimum bitrate selection checkpoint. + long[][] trackBitrates = getSortedTrackBitrates(definitions); + int[] currentTrackIndices = new int[trackBitrates.length]; + long[] currentTrackBitrates = new long[trackBitrates.length]; + for (int i = 0; i < trackBitrates.length; i++) { + currentTrackBitrates[i] = trackBitrates[i].length == 0 ? 0 : trackBitrates[i][0]; + } + addCheckpoint(checkPointBuilders, currentTrackBitrates); + // Iterate through all adaptive checkpoints. + ImmutableList switchOrder = getSwitchOrder(trackBitrates); + for (int i = 0; i < switchOrder.size(); i++) { + int switchIndex = switchOrder.get(i); + int newTrackIndex = ++currentTrackIndices[switchIndex]; + currentTrackBitrates[switchIndex] = trackBitrates[switchIndex][newTrackIndex]; + addCheckpoint(checkPointBuilders, currentTrackBitrates); + } + // Add final checkpoint to extrapolate additional bandwidth for adaptive selections. + for (int i = 0; i < definitions.length; i++) { + if (checkPointBuilders.get(i) != null) { + currentTrackBitrates[i] *= 2; } - 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])); } + addCheckpoint(checkPointBuilders, currentTrackBitrates); + ImmutableList.Builder> output = ImmutableList.builder(); + for (int i = 0; i < checkPointBuilders.size(); i++) { + @Nullable ImmutableList.Builder builder = checkPointBuilders.get(i); + output.add(builder == null ? ImmutableList.of() : builder.build()); + } + return output.build(); + } - /* package */ void experimentalSetBandwidthAllocationCheckpoints( - long[][] allocationCheckpoints) { - Assertions.checkArgument(allocationCheckpoints.length >= 2); - this.allocationCheckpoints = allocationCheckpoints; + /** Returns sorted track bitrates for all selected tracks. */ + private static long[][] getSortedTrackBitrates(@NullableType Definition[] definitions) { + long[][] trackBitrates = new long[definitions.length][]; + for (int i = 0; i < definitions.length; i++) { + @Nullable Definition definition = definitions[i]; + if (definition == null) { + trackBitrates[i] = new long[0]; + continue; + } + trackBitrates[i] = new long[definition.tracks.length]; + for (int j = 0; j < definition.tracks.length; j++) { + trackBitrates[i][j] = definition.group.getFormat(definition.tracks[j]).bitrate; + } + Arrays.sort(trackBitrates[i]); } + return trackBitrates; } /** - * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track - * selections. + * Returns order of track indices in which the respective track should be switched up. * - * @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. + * @param trackBitrates Sorted tracks bitrates for each selection. + * @return List of track indices indicating in which order tracks should be switched up. */ - private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) { + private static ImmutableList getSwitchOrder(long[][] trackBitrates) { // Algorithm: - // 1. Use log bitrates to treat all resolution update steps equally. + // 1. Use log bitrates to treat all bitrate 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; - } + Multimap switchPoints = MultimapBuilder.treeKeys().arrayListValues().build(); + for (int i = 0; i < trackBitrates.length; i++) { + if (trackBitrates[i].length <= 1) { + continue; } - 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]); + double[] logBitrates = new double[trackBitrates[i].length]; + for (int j = 0; j < trackBitrates[i].length; j++) { + logBitrates[j] = trackBitrates[i][j] == Format.NO_VALUE ? 0 : Math.log(trackBitrates[i][j]); + } + double totalBitrateDiff = logBitrates[logBitrates.length - 1] - logBitrates[0]; + for (int j = 0; j < logBitrates.length - 1; j++) { + double switchBitrate = 0.5 * (logBitrates[j] + logBitrates[j + 1]); + double switchPoint = + totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[0]) / totalBitrateDiff; + switchPoints.put(switchPoint, i); } } - return logValues; + return ImmutableList.copyOf(switchPoints.values()); } /** - * Returns idealized switch points for each switch between consecutive track selection bitrates. + * Add a checkpoint to the builders. * - * @param logBitrates Log bitrates with [selectionCount][formatCount]. - * @return Linearly distributed switch points in the range of [0.0-1.0]. + * @param checkPointBuilders Builders for adaptation checkpoints. May have null elements. + * @param checkpointBitrates The bitrates of each track at this checkpoint. */ - 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) { + private static void addCheckpoint( + List> checkPointBuilders, + long[] checkpointBitrates) { + // Total bitrate includes all fixed tracks. + long totalBitrate = 0; + for (int i = 0; i < checkpointBitrates.length; i++) { + totalBitrate += checkpointBitrates[i]; + } + for (int i = 0; i < checkPointBuilders.size(); i++) { + @Nullable ImmutableList.Builder builder = checkPointBuilders.get(i); + if (builder == null) { 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; - } + builder.add( + new AdaptationCheckpoint( + /* totalBandwidth= */ totalBitrate, /* allocatedBandwidth= */ checkpointBitrates[i])); } - 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; + /** Checkpoint to determine allocated bandwidth. */ + protected static final class AdaptationCheckpoint { + + /** Total bandwidth in bits per second at which this checkpoint applies. */ + public final long totalBandwidth; + /** Allocated bandwidth at this checkpoint in bits per second. */ + public final long allocatedBandwidth; + + public AdaptationCheckpoint(long totalBandwidth, long allocatedBandwidth) { + this.totalBandwidth = totalBandwidth; + this.allocatedBandwidth = allocatedBandwidth; } - 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]; + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AdaptationCheckpoint)) { + return false; + } + AdaptationCheckpoint that = (AdaptationCheckpoint) o; + return totalBandwidth == that.totalBandwidth && allocatedBandwidth == that.allocatedBandwidth; } - for (long[][] points : checkpoints) { - points[checkpointIndex][0] = totalBitrate; + + @Override + public int hashCode() { + return 31 * (int) totalBandwidth + (int) allocatedBandwidth; } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index a7a8e5a4c1c..4de6297a944 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -22,10 +22,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeMediaChunk; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection.AdaptationCheckpoint; +import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; @@ -312,7 +317,7 @@ public void updateSelectedTrack_usesFormatOfLastChunkInTheQueueForSelection() { trackGroup, mockBandwidthMeter, /* tracks= */ new int[] {0, 1}, - /* totalFixedTrackBandwidth= */ 0); + /* adaptationCheckpoints= */ ImmutableList.of()); // Make initial selection. when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); @@ -380,6 +385,199 @@ public void updateSelectedTrack_withQueueOfUnknownFormats_doesntThrow() { assertThat(adaptiveTrackSelection.getSelectedFormat()).isAnyOf(format1, format2); } + @Test + public void updateSelectedTrack_withAdaptationCheckpoints_usesOnlyAllocatedBandwidth() { + Format format0 = videoFormat(/* bitrate= */ 100, /* width= */ 160, /* height= */ 120); + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 1500, /* width= */ 1024, /* height= */ 768); + TrackGroup trackGroup = new TrackGroup(format0, format1, format2, format3); + // Choose checkpoints relative to formats so that one is in the first range, one somewhere in + // the middle, and one needs to extrapolate beyond the last checkpoint. + List checkpoints = + ImmutableList.of( + new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0), + new AdaptationCheckpoint(/* totalBandwidth= */ 1500, /* allocatedBandwidth= */ 750), + new AdaptationCheckpoint(/* totalBandwidth= */ 3000, /* allocatedBandwidth= */ 750), + new AdaptationCheckpoint(/* totalBandwidth= */ 4000, /* allocatedBandwidth= */ 1250), + new AdaptationCheckpoint(/* totalBandwidth= */ 5000, /* allocatedBandwidth= */ 1300)); + AdaptiveTrackSelection adaptiveTrackSelection = + prepareTrackSelection( + adaptiveTrackSelectionWithAdaptationCheckpoints(trackGroup, checkpoints)); + + // Ensure format0 is selected initially so that we can assert the upswitches. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format0); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(999L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format0); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(2499L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(3500L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(8999L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(9000L); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 999_999_999_999L, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ ImmutableList.of(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); + } + + @Test + public void + builderCreateTrackSelections_withSingleAdaptiveGroup_usesCorrectAdaptationCheckpoints() { + Format formatFixed1 = new Format.Builder().setAverageBitrate(500).build(); + Format formatFixed2 = new Format.Builder().setAverageBitrate(1000).build(); + Format formatAdaptive1 = new Format.Builder().setAverageBitrate(2000).build(); + Format formatAdaptive2 = new Format.Builder().setAverageBitrate(3000).build(); + Format formatAdaptive3 = new Format.Builder().setAverageBitrate(4000).build(); + Format formatAdaptive4 = new Format.Builder().setAverageBitrate(5000).build(); + TrackGroup trackGroupMultipleFixed = new TrackGroup(formatFixed1, formatFixed2); + TrackGroup trackGroupAdaptive = + new TrackGroup(formatAdaptive1, formatAdaptive2, formatAdaptive3, formatAdaptive4); + Definition definitionFixed1 = new Definition(trackGroupMultipleFixed, /* tracks...= */ 0); + Definition definitionFixed2 = new Definition(trackGroupMultipleFixed, /* tracks...= */ 1); + Definition definitionAdaptive = new Definition(trackGroupAdaptive, /* tracks...= */ 1, 2, 3); + List> checkPoints = new ArrayList<>(); + AdaptiveTrackSelection.Factory factory = + new AdaptiveTrackSelection.Factory() { + @Override + protected AdaptiveTrackSelection createAdaptiveTrackSelection( + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + ImmutableList adaptationCheckpoints) { + checkPoints.add(adaptationCheckpoints); + return super.createAdaptiveTrackSelection( + group, bandwidthMeter, tracks, adaptationCheckpoints); + } + }; + + Timeline timeline = new FakeTimeline(); + factory.createTrackSelections( + new Definition[] {null, definitionFixed1, null, definitionFixed2, definitionAdaptive}, + mockBandwidthMeter, + new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 0)), + timeline); + + assertThat(checkPoints).hasSize(1); + assertThat(checkPoints.get(0)) + .containsExactly( + new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0), + new AdaptationCheckpoint(/* totalBandwidth= */ 4500, /* allocatedBandwidth= */ 3000), + new AdaptationCheckpoint(/* totalBandwidth= */ 5500, /* allocatedBandwidth= */ 4000), + new AdaptationCheckpoint(/* totalBandwidth= */ 6500, /* allocatedBandwidth= */ 5000), + new AdaptationCheckpoint(/* totalBandwidth= */ 11500, /* allocatedBandwidth= */ 10000)) + .inOrder(); + } + + @Test + public void + builderCreateTrackSelections_withMultipleAdaptiveGroups_usesCorrectAdaptationCheckpoints() { + Format group1Format1 = new Format.Builder().setAverageBitrate(500).build(); + Format group1Format2 = new Format.Builder().setAverageBitrate(1000).build(); + Format group2Format1 = new Format.Builder().setAverageBitrate(250).build(); + Format group2Format2 = new Format.Builder().setAverageBitrate(500).build(); + Format group2Format3 = new Format.Builder().setAverageBitrate(1250).build(); + Format group2UnusedFormat = new Format.Builder().setAverageBitrate(2000).build(); + Format fixedFormat = new Format.Builder().setAverageBitrate(5000).build(); + TrackGroup trackGroup1 = new TrackGroup(group1Format1, group1Format2); + TrackGroup trackGroup2 = + new TrackGroup(group2Format1, group2Format2, group2Format3, group2UnusedFormat); + TrackGroup fixedGroup = new TrackGroup(fixedFormat); + Definition definition1 = new Definition(trackGroup1, /* tracks...= */ 0, 1); + Definition definition2 = new Definition(trackGroup2, /* tracks...= */ 0, 1, 2); + Definition fixedDefinition = new Definition(fixedGroup, /* tracks...= */ 0); + List> checkPoints = new ArrayList<>(); + AdaptiveTrackSelection.Factory factory = + new AdaptiveTrackSelection.Factory() { + @Override + protected AdaptiveTrackSelection createAdaptiveTrackSelection( + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + ImmutableList adaptationCheckpoints) { + checkPoints.add(adaptationCheckpoints); + return super.createAdaptiveTrackSelection( + group, bandwidthMeter, tracks, adaptationCheckpoints); + } + }; + + Timeline timeline = new FakeTimeline(); + factory.createTrackSelections( + new Definition[] {null, definition1, fixedDefinition, definition2, null}, + mockBandwidthMeter, + new MediaSource.MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ 0)), + timeline); + + assertThat(checkPoints).hasSize(2); + assertThat(checkPoints.get(0)) + .containsExactly( + new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0), + new AdaptationCheckpoint(/* totalBandwidth= */ 5750, /* allocatedBandwidth= */ 500), + new AdaptationCheckpoint(/* totalBandwidth= */ 6000, /* allocatedBandwidth= */ 500), + new AdaptationCheckpoint(/* totalBandwidth= */ 6500, /* allocatedBandwidth= */ 1000), + new AdaptationCheckpoint(/* totalBandwidth= */ 7250, /* allocatedBandwidth= */ 1000), + new AdaptationCheckpoint(/* totalBandwidth= */ 9500, /* allocatedBandwidth= */ 2000)) + .inOrder(); + assertThat(checkPoints.get(1)) + .containsExactly( + new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0), + new AdaptationCheckpoint(/* totalBandwidth= */ 5750, /* allocatedBandwidth= */ 250), + new AdaptationCheckpoint(/* totalBandwidth= */ 6000, /* allocatedBandwidth= */ 500), + new AdaptationCheckpoint(/* totalBandwidth= */ 6500, /* allocatedBandwidth= */ 500), + new AdaptationCheckpoint(/* totalBandwidth= */ 7250, /* allocatedBandwidth= */ 1250), + new AdaptationCheckpoint(/* totalBandwidth= */ 9500, /* allocatedBandwidth= */ 2500)) + .inOrder(); + } + private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup) { return adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( trackGroup, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS); @@ -392,12 +590,12 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIn trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - /* reservedBandwidth= */ 0, minDurationForQualityIncreaseMs, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, /* bandwidthFraction= */ 1.0f, AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + /* adaptationCheckpoints= */ ImmutableList.of(), fakeClock)); } @@ -408,12 +606,12 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDe trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, maxDurationForQualityDecreaseMs, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, /* bandwidthFraction= */ 1.0f, AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + /* adaptationCheckpoints= */ ImmutableList.of(), fakeClock)); } @@ -424,12 +622,28 @@ private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferRee trackGroup, selectedAllTracksInGroup(trackGroup), mockBandwidthMeter, - /* reservedBandwidth= */ 0, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, durationToRetainAfterDiscardMs, /* bandwidthFraction= */ 1.0f, AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + /* adaptationCheckpoints= */ ImmutableList.of(), + fakeClock)); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithAdaptationCheckpoints( + TrackGroup trackGroup, List adaptationCheckpoints) { + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + adaptationCheckpoints, fakeClock)); }