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
This commit is contained in:
tonihei 2021-01-05 18:00:24 +00:00 committed by Ian Baker
parent 0901fe6e38
commit a4fbc2c98d
2 changed files with 419 additions and 234 deletions

View file

@ -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.TrackGroup;
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 class AdaptiveTrackSelection extends BaseTrackSelection {
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
Timeline timeline) {
ImmutableList<ImmutableList<AdaptationCheckpoint>> 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<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)
.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 class AdaptiveTrackSelection extends BaseTrackSelection {
* @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<AdaptationCheckpoint> adaptationCheckpoints) {
return new AdaptiveTrackSelection(
group,
tracks,
new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth),
bandwidthMeter,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
adaptationCheckpoints,
clock);
}
}
@ -216,11 +195,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
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<AdaptationCheckpoint> adaptationCheckpoints;
private final Clock clock;
private float playbackSpeed;
@ -235,18 +216,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
* 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 class AdaptiveTrackSelection extends BaseTrackSelection {
* @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 class AdaptiveTrackSelection extends BaseTrackSelection {
* 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<AdaptationCheckpoint> 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 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
* 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 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
: minDurationForQualityIncreaseUs;
}
/** Provides the allocated bandwidth. */
private interface BandwidthProvider {
/** Returns the allocated bitrate. */
long getAllocatedBandwidth();
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));
}
private static final class DefaultBandwidthProvider implements BandwidthProvider {
/**
* 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<ImmutableList<AdaptationCheckpoint>> getAdaptationCheckpoints(
@NullableType Definition[] definitions) {
List<ImmutableList.@NullableType Builder<AdaptationCheckpoint>> checkPointBuilders =
new ArrayList<>();
for (int i = 0; i < definitions.length; i++) {
if (definitions[i] != null && definitions[i].tracks.length > 1) {
ImmutableList.Builder<AdaptationCheckpoint> builder = ImmutableList.builder();
// Add initial all-zero checkpoint.
builder.add(new AdaptationCheckpoint(/* totalBandwidth= */ 0, /* allocatedBandwidth= */ 0));
checkPointBuilders.add(builder);
} else {
checkPointBuilders.add(null);
}
}
// 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<Integer> 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;
}
}
addCheckpoint(checkPointBuilders, currentTrackBitrates);
ImmutableList.Builder<ImmutableList<AdaptationCheckpoint>> output = ImmutableList.builder();
for (int i = 0; i < checkPointBuilders.size(); i++) {
@Nullable ImmutableList.Builder<AdaptationCheckpoint> builder = checkPointBuilders.get(i);
output.add(builder == null ? ImmutableList.of() : builder.build());
}
return output.build();
}
private final BandwidthMeter bandwidthMeter;
private final float bandwidthFraction;
private final long reservedBandwidth;
/** 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;
}
@Nullable private long[][] allocationCheckpoints;
/**
* Returns order of track indices in which the respective track should be switched up.
*
* @param trackBitrates Sorted tracks bitrates for each selection.
* @return List of track indices indicating in which order tracks should be switched up.
*/
private static ImmutableList<Integer> getSwitchOrder(long[][] trackBitrates) {
// Algorithm:
// 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.
Multimap<Double, Integer> switchPoints = MultimapBuilder.treeKeys().arrayListValues().build();
for (int i = 0; i < trackBitrates.length; i++) {
if (trackBitrates[i].length <= 1) {
continue;
}
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 ImmutableList.copyOf(switchPoints.values());
}
/* package */ DefaultBandwidthProvider(
BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) {
this.bandwidthMeter = bandwidthMeter;
this.bandwidthFraction = bandwidthFraction;
this.reservedBandwidth = reservedBandwidth;
/**
* Add a checkpoint to the builders.
*
* @param checkPointBuilders Builders for adaptation checkpoints. May have null elements.
* @param checkpointBitrates The bitrates of each track at this checkpoint.
*/
private static void addCheckpoint(
List<ImmutableList.@NullableType Builder<AdaptationCheckpoint>> 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<AdaptationCheckpoint> builder = checkPointBuilders.get(i);
if (builder == null) {
continue;
}
builder.add(
new AdaptationCheckpoint(
/* totalBandwidth= */ totalBitrate, /* allocatedBandwidth= */ checkpointBitrates[i]));
}
}
/** 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;
}
@Override
public long getAllocatedBandwidth() {
long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
long allocatableBandwidth = max(0L, totalBandwidth - reservedBandwidth);
if (allocationCheckpoints == null) {
return allocatableBandwidth;
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
int nextIndex = 1;
while (nextIndex < allocationCheckpoints.length - 1
&& allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {
nextIndex++;
if (!(o instanceof AdaptationCheckpoint)) {
return false;
}
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]));
AdaptationCheckpoint that = (AdaptationCheckpoint) o;
return totalBandwidth == that.totalBandwidth && allocatedBandwidth == that.allocatedBandwidth;
}
/* package */ void experimentalSetBandwidthAllocationCheckpoints(
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;
@Override
public int hashCode() {
return 31 * (int) totalBandwidth + (int) allocatedBandwidth;
}
}
}

View file

@ -22,10 +22,15 @@ import static org.mockito.MockitoAnnotations.initMocks;
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 final class AdaptiveTrackSelectionTest {
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 final class AdaptiveTrackSelectionTest {
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<AdaptationCheckpoint> 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<List<AdaptationCheckpoint>> checkPoints = new ArrayList<>();
AdaptiveTrackSelection.Factory factory =
new AdaptiveTrackSelection.Factory() {
@Override
protected AdaptiveTrackSelection createAdaptiveTrackSelection(
TrackGroup group,
BandwidthMeter bandwidthMeter,
int[] tracks,
ImmutableList<AdaptationCheckpoint> 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<List<AdaptationCheckpoint>> checkPoints = new ArrayList<>();
AdaptiveTrackSelection.Factory factory =
new AdaptiveTrackSelection.Factory() {
@Override
protected AdaptiveTrackSelection createAdaptiveTrackSelection(
TrackGroup group,
BandwidthMeter bandwidthMeter,
int[] tracks,
ImmutableList<AdaptationCheckpoint> 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 @@ public final class AdaptiveTrackSelectionTest {
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 @@ public final class AdaptiveTrackSelectionTest {
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 @@ public final class AdaptiveTrackSelectionTest {
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<AdaptationCheckpoint> 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));
}