mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
0901fe6e38
commit
a4fbc2c98d
2 changed files with 419 additions and 234 deletions
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.trackselection;
|
package com.google.android.exoplayer2.trackselection;
|
||||||
|
|
||||||
import static java.lang.Math.max;
|
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.Nullable;
|
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.MediaChunk;
|
||||||
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
||||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
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.Clock;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
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.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
|
|
@ -135,48 +137,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
MediaPeriodId mediaPeriodId,
|
MediaPeriodId mediaPeriodId,
|
||||||
Timeline timeline) {
|
Timeline timeline) {
|
||||||
|
ImmutableList<ImmutableList<AdaptationCheckpoint>> adaptationCheckpoints =
|
||||||
|
getAdaptationCheckpoints(definitions);
|
||||||
TrackSelection[] selections = new TrackSelection[definitions.length];
|
TrackSelection[] selections = new TrackSelection[definitions.length];
|
||||||
int totalFixedBandwidth = 0;
|
|
||||||
for (int i = 0; i < definitions.length; i++) {
|
for (int i = 0; i < definitions.length; i++) {
|
||||||
Definition definition = definitions[i];
|
@Nullable Definition definition = definitions[i];
|
||||||
if (definition != null && definition.tracks.length == 1) {
|
if (definition == null || definition.tracks.length == 0) {
|
||||||
// Make fixed selections first to know their total bandwidth.
|
continue;
|
||||||
|
}
|
||||||
selections[i] =
|
selections[i] =
|
||||||
new FixedTrackSelection(
|
definition.tracks.length == 1
|
||||||
definition.group, definition.tracks[0], definition.reason, definition.data);
|
? new FixedTrackSelection(
|
||||||
int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
|
definition.group, definition.tracks[0], definition.reason, definition.data)
|
||||||
if (trackBitrate != Format.NO_VALUE) {
|
: createAdaptiveTrackSelection(
|
||||||
totalFixedBandwidth += trackBitrate;
|
definition.group,
|
||||||
}
|
bandwidthMeter,
|
||||||
}
|
definition.tracks,
|
||||||
}
|
adaptationCheckpoints.get(i));
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return selections;
|
return selections;
|
||||||
}
|
}
|
||||||
|
|
@ -187,23 +164,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
* @param group The {@link TrackGroup}.
|
* @param group The {@link TrackGroup}.
|
||||||
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
|
* @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 tracks The indices of the selected tracks in the track group.
|
||||||
* @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits
|
* @param adaptationCheckpoints The {@link AdaptationCheckpoint checkpoints} that can be used to
|
||||||
* per second.
|
* calculate available bandwidth for this selection.
|
||||||
* @return An {@link AdaptiveTrackSelection} for the specified tracks.
|
* @return An {@link AdaptiveTrackSelection} for the specified tracks.
|
||||||
*/
|
*/
|
||||||
protected AdaptiveTrackSelection createAdaptiveTrackSelection(
|
protected AdaptiveTrackSelection createAdaptiveTrackSelection(
|
||||||
TrackGroup group,
|
TrackGroup group,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
int[] tracks,
|
int[] tracks,
|
||||||
int totalFixedTrackBandwidth) {
|
ImmutableList<AdaptationCheckpoint> adaptationCheckpoints) {
|
||||||
return new AdaptiveTrackSelection(
|
return new AdaptiveTrackSelection(
|
||||||
group,
|
group,
|
||||||
tracks,
|
tracks,
|
||||||
new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth),
|
bandwidthMeter,
|
||||||
minDurationForQualityIncreaseMs,
|
minDurationForQualityIncreaseMs,
|
||||||
maxDurationForQualityDecreaseMs,
|
maxDurationForQualityDecreaseMs,
|
||||||
minDurationToRetainAfterDiscardMs,
|
minDurationToRetainAfterDiscardMs,
|
||||||
|
bandwidthFraction,
|
||||||
bufferedFractionToLiveEdgeForQualityIncrease,
|
bufferedFractionToLiveEdgeForQualityIncrease,
|
||||||
|
adaptationCheckpoints,
|
||||||
clock);
|
clock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,11 +195,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
|
|
||||||
private static final long MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 1000;
|
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 minDurationForQualityIncreaseUs;
|
||||||
private final long maxDurationForQualityDecreaseUs;
|
private final long maxDurationForQualityDecreaseUs;
|
||||||
private final long minDurationToRetainAfterDiscardUs;
|
private final long minDurationToRetainAfterDiscardUs;
|
||||||
|
private final float bandwidthFraction;
|
||||||
private final float bufferedFractionToLiveEdgeForQualityIncrease;
|
private final float bufferedFractionToLiveEdgeForQualityIncrease;
|
||||||
|
private final ImmutableList<AdaptationCheckpoint> adaptationCheckpoints;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
private float playbackSpeed;
|
private float playbackSpeed;
|
||||||
|
|
@ -235,18 +216,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
* empty. May be in any order.
|
* empty. May be in any order.
|
||||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
||||||
*/
|
*/
|
||||||
public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
|
public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) {
|
||||||
BandwidthMeter bandwidthMeter) {
|
|
||||||
this(
|
this(
|
||||||
group,
|
group,
|
||||||
tracks,
|
tracks,
|
||||||
bandwidthMeter,
|
bandwidthMeter,
|
||||||
/* reservedBandwidth= */ 0,
|
|
||||||
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||||
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||||
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||||
DEFAULT_BANDWIDTH_FRACTION,
|
DEFAULT_BANDWIDTH_FRACTION,
|
||||||
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
||||||
|
/* adaptationCheckpoints= */ ImmutableList.of(),
|
||||||
Clock.DEFAULT);
|
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
|
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
|
||||||
* empty. May be in any order.
|
* empty. May be in any order.
|
||||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
|
* @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
|
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
|
||||||
* selected track to switch to one of higher quality.
|
* selected track to switch to one of higher quality.
|
||||||
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
|
* @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
|
* when the playback position is closer to the live edge than {@code
|
||||||
* minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
|
* minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
|
||||||
* quality from happening.
|
* 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,
|
TrackGroup group,
|
||||||
int[] tracks,
|
int[] tracks,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
long reservedBandwidth,
|
|
||||||
long minDurationForQualityIncreaseMs,
|
long minDurationForQualityIncreaseMs,
|
||||||
long maxDurationForQualityDecreaseMs,
|
long maxDurationForQualityDecreaseMs,
|
||||||
long minDurationToRetainAfterDiscardMs,
|
long minDurationToRetainAfterDiscardMs,
|
||||||
float bandwidthFraction,
|
float bandwidthFraction,
|
||||||
float bufferedFractionToLiveEdgeForQualityIncrease,
|
float bufferedFractionToLiveEdgeForQualityIncrease,
|
||||||
Clock clock) {
|
List<AdaptationCheckpoint> adaptationCheckpoints,
|
||||||
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,
|
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
super(group, tracks);
|
super(group, tracks);
|
||||||
this.bandwidthProvider = bandwidthProvider;
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
|
||||||
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
|
||||||
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
|
||||||
|
this.bandwidthFraction = bandwidthFraction;
|
||||||
this.bufferedFractionToLiveEdgeForQualityIncrease =
|
this.bufferedFractionToLiveEdgeForQualityIncrease =
|
||||||
bufferedFractionToLiveEdgeForQualityIncrease;
|
bufferedFractionToLiveEdgeForQualityIncrease;
|
||||||
|
this.adaptationCheckpoints = ImmutableList.copyOf(adaptationCheckpoints);
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
playbackSpeed = 1f;
|
playbackSpeed = 1f;
|
||||||
reason = C.SELECTION_REASON_UNKNOWN;
|
reason = C.SELECTION_REASON_UNKNOWN;
|
||||||
lastBufferEvaluationMs = C.TIME_UNSET;
|
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
|
@CallSuper
|
||||||
@Override
|
@Override
|
||||||
public void enable() {
|
public void enable() {
|
||||||
|
|
@ -502,7 +454,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
* Long#MIN_VALUE} to ignore track exclusion.
|
* Long#MIN_VALUE} to ignore track exclusion.
|
||||||
*/
|
*/
|
||||||
private int determineIdealSelectedIndex(long nowMs) {
|
private int determineIdealSelectedIndex(long nowMs) {
|
||||||
long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth();
|
long effectiveBitrate = getAllocatedBandwidth();
|
||||||
int lowestBitrateAllowedIndex = 0;
|
int lowestBitrateAllowedIndex = 0;
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
|
if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
|
||||||
|
|
@ -525,162 +477,181 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
|
||||||
: minDurationForQualityIncreaseUs;
|
: minDurationForQualityIncreaseUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides the allocated bandwidth. */
|
private long getAllocatedBandwidth() {
|
||||||
private interface BandwidthProvider {
|
long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
|
||||||
|
if (adaptationCheckpoints.isEmpty()) {
|
||||||
/** Returns the allocated bitrate. */
|
return totalBandwidth;
|
||||||
long getAllocatedBandwidth();
|
}
|
||||||
|
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;
|
/** Returns sorted track bitrates for all selected tracks. */
|
||||||
private final float bandwidthFraction;
|
private static long[][] getSortedTrackBitrates(@NullableType Definition[] definitions) {
|
||||||
private final long reservedBandwidth;
|
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) {
|
* Add a checkpoint to the builders.
|
||||||
this.bandwidthMeter = bandwidthMeter;
|
*
|
||||||
this.bandwidthFraction = bandwidthFraction;
|
* @param checkPointBuilders Builders for adaptation checkpoints. May have null elements.
|
||||||
this.reservedBandwidth = reservedBandwidth;
|
* @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
|
@Override
|
||||||
public long getAllocatedBandwidth() {
|
public boolean equals(@Nullable Object o) {
|
||||||
long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
|
if (this == o) {
|
||||||
long allocatableBandwidth = max(0L, totalBandwidth - reservedBandwidth);
|
return true;
|
||||||
if (allocationCheckpoints == null) {
|
|
||||||
return allocatableBandwidth;
|
|
||||||
}
|
}
|
||||||
int nextIndex = 1;
|
if (!(o instanceof AdaptationCheckpoint)) {
|
||||||
while (nextIndex < allocationCheckpoints.length - 1
|
return false;
|
||||||
&& allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {
|
|
||||||
nextIndex++;
|
|
||||||
}
|
}
|
||||||
long[] previous = allocationCheckpoints[nextIndex - 1];
|
AdaptationCheckpoint that = (AdaptationCheckpoint) o;
|
||||||
long[] next = allocationCheckpoints[nextIndex];
|
return totalBandwidth == that.totalBandwidth && allocatedBandwidth == that.allocatedBandwidth;
|
||||||
float fractionBetweenCheckpoints =
|
|
||||||
(float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]);
|
|
||||||
return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void experimentalSetBandwidthAllocationCheckpoints(
|
@Override
|
||||||
long[][] allocationCheckpoints) {
|
public int hashCode() {
|
||||||
Assertions.checkArgument(allocationCheckpoints.length >= 2);
|
return 31 * (int) totalBandwidth + (int) allocatedBandwidth;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,15 @@ import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
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.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
import com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
|
||||||
import com.google.android.exoplayer2.testutil.FakeClock;
|
import com.google.android.exoplayer2.testutil.FakeClock;
|
||||||
import com.google.android.exoplayer2.testutil.FakeMediaChunk;
|
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.upstream.BandwidthMeter;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
@ -312,7 +317,7 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
trackGroup,
|
trackGroup,
|
||||||
mockBandwidthMeter,
|
mockBandwidthMeter,
|
||||||
/* tracks= */ new int[] {0, 1},
|
/* tracks= */ new int[] {0, 1},
|
||||||
/* totalFixedTrackBandwidth= */ 0);
|
/* adaptationCheckpoints= */ ImmutableList.of());
|
||||||
|
|
||||||
// Make initial selection.
|
// Make initial selection.
|
||||||
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);
|
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);
|
||||||
|
|
@ -380,6 +385,199 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
assertThat(adaptiveTrackSelection.getSelectedFormat()).isAnyOf(format1, format2);
|
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) {
|
private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup) {
|
||||||
return adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(
|
return adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(
|
||||||
trackGroup, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS);
|
trackGroup, AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS);
|
||||||
|
|
@ -392,12 +590,12 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
trackGroup,
|
trackGroup,
|
||||||
selectedAllTracksInGroup(trackGroup),
|
selectedAllTracksInGroup(trackGroup),
|
||||||
mockBandwidthMeter,
|
mockBandwidthMeter,
|
||||||
/* reservedBandwidth= */ 0,
|
|
||||||
minDurationForQualityIncreaseMs,
|
minDurationForQualityIncreaseMs,
|
||||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||||
/* bandwidthFraction= */ 1.0f,
|
/* bandwidthFraction= */ 1.0f,
|
||||||
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
||||||
|
/* adaptationCheckpoints= */ ImmutableList.of(),
|
||||||
fakeClock));
|
fakeClock));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,12 +606,12 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
trackGroup,
|
trackGroup,
|
||||||
selectedAllTracksInGroup(trackGroup),
|
selectedAllTracksInGroup(trackGroup),
|
||||||
mockBandwidthMeter,
|
mockBandwidthMeter,
|
||||||
/* reservedBandwidth= */ 0,
|
|
||||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||||
maxDurationForQualityDecreaseMs,
|
maxDurationForQualityDecreaseMs,
|
||||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||||
/* bandwidthFraction= */ 1.0f,
|
/* bandwidthFraction= */ 1.0f,
|
||||||
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
||||||
|
/* adaptationCheckpoints= */ ImmutableList.of(),
|
||||||
fakeClock));
|
fakeClock));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,12 +622,28 @@ public final class AdaptiveTrackSelectionTest {
|
||||||
trackGroup,
|
trackGroup,
|
||||||
selectedAllTracksInGroup(trackGroup),
|
selectedAllTracksInGroup(trackGroup),
|
||||||
mockBandwidthMeter,
|
mockBandwidthMeter,
|
||||||
/* reservedBandwidth= */ 0,
|
|
||||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
|
||||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||||
durationToRetainAfterDiscardMs,
|
durationToRetainAfterDiscardMs,
|
||||||
/* bandwidthFraction= */ 1.0f,
|
/* bandwidthFraction= */ 1.0f,
|
||||||
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
|
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));
|
fakeClock));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue