Add experimental option for multiple parallel adaptive track selections.

If enabled, DefaultTrackSelector returns multiple groups with more than one
track. AdaptiveTrackSelection then decides on the order in which the tracks
are changed such that the BandwidthProvider for each selection can figure out
the allocated bandwidth.

PiperOrigin-RevId: 240150206
This commit is contained in:
tonihei 2019-03-25 16:03:33 +00:00 committed by Toni
parent 32347362f4
commit 679b628326
2 changed files with 183 additions and 6 deletions

View file

@ -23,8 +23,10 @@ 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 java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@ -230,7 +232,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public @NullableType TrackSelection[] createTrackSelections(
@NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
TrackSelection[] selections = new TrackSelection[definitions.length];
AdaptiveTrackSelection adaptiveSelection = null;
List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>();
int totalFixedBandwidth = 0;
for (int i = 0; i < definitions.length; i++) {
Definition definition = definitions[i];
@ -238,8 +240,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
continue;
}
if (definition.tracks.length > 1) {
adaptiveSelection =
AdaptiveTrackSelection adaptiveSelection =
createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks);
adaptiveSelections.add(adaptiveSelection);
selections[i] = adaptiveSelection;
} else {
selections[i] =
@ -251,8 +254,27 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
}
}
}
if (blockFixedTrackSelectionBandwidth && adaptiveSelection != null) {
adaptiveSelection.experimental_setNonAllocatableBandwidth(totalFixedBandwidth);
if (blockFixedTrackSelectionBandwidth) {
for (int i = 0; i < adaptiveSelections.size(); i++) {
adaptiveSelections.get(i).experimental_setNonAllocatableBandwidth(totalFixedBandwidth);
}
}
if (adaptiveSelections.size() > 1) {
long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][];
for (int i = 0; i < adaptiveSelections.size(); i++) {
AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i);
adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()];
for (int j = 0; j < adaptiveSelection.length(); j++) {
adaptiveTrackBitrates[i][j] =
adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate;
}
}
long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates);
for (int i = 0; i < adaptiveSelections.size(); i++) {
adaptiveSelections
.get(i)
.experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]);
}
}
return selections;
}
@ -430,6 +452,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
.experimental_setNonAllocatableBandwidth(nonAllocatableBandwidth);
}
/**
* Sets checkpoints to determine the allocation bandwidth based on the total bandwidth.
*
* @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0]
* being the total bandwidth and [1] being the allocated bandwidth.
*/
public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) {
((DefaultBandwidthProvider) bandwidthProvider)
.experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints);
}
@Override
public void enable() {
lastBufferEvaluationMs = C.TIME_UNSET;
@ -630,6 +663,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private long nonAllocatableBandwidth;
@Nullable private long[][] allocationCheckpoints;
/* package */ DefaultBandwidthProvider(BandwidthMeter bandwidthMeter, float bandwidthFraction) {
this.bandwidthMeter = bandwidthMeter;
this.bandwidthFraction = bandwidthFraction;
@ -638,11 +673,140 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public long getAllocatedBandwidth() {
long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
return Math.max(0L, totalBandwidth - nonAllocatableBandwidth);
long allocatableBandwidth = Math.max(0L, totalBandwidth - nonAllocatableBandwidth);
if (allocationCheckpoints == null) {
return allocatableBandwidth;
}
int nextIndex = 1;
while (nextIndex < allocationCheckpoints.length - 1
&& allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {
nextIndex++;
}
long[] previous = allocationCheckpoints[nextIndex - 1];
long[] next = allocationCheckpoints[nextIndex];
float fractionBetweenCheckpoints =
(float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]);
return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1]));
}
/* package */ void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) {
this.nonAllocatableBandwidth = nonAllocatableBandwidth;
}
/* package */ void experimental_setBandwidthAllocationCheckpoints(
long[][] allocationCheckpoints) {
Assertions.checkArgument(allocationCheckpoints.length >= 2);
this.allocationCheckpoints = allocationCheckpoints;
}
}
/**
* Returns allocation checkpoints for allocating bandwidth between multiple adaptive track
* selections.
*
* @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate.
* @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total
* bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint.
*/
private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) {
// Algorithm:
// 1. Use log bitrates to treat all resolution update steps equally.
// 2. Distribute switch points for each selection equally in the same [0.0-1.0] range.
// 3. Switch up one format at a time in the order of the switch points.
double[][] logBitrates = getLogArrayValues(trackBitrates);
double[][] switchPoints = getSwitchPoints(logBitrates);
// There will be (count(switch point) + 3) checkpoints:
// [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points,
// [end] = extra point to set slope for additional bitrate.
int checkpointCount = countArrayElements(switchPoints) + 3;
long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2];
int[] currentSelection = new int[logBitrates.length];
setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection);
for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) {
int nextUpdateIndex = 0;
double nextUpdateSwitchPoint = Double.MAX_VALUE;
for (int i = 0; i < logBitrates.length; i++) {
if (currentSelection[i] + 1 == logBitrates[i].length) {
continue;
}
double switchPoint = switchPoints[i][currentSelection[i]];
if (switchPoint < nextUpdateSwitchPoint) {
nextUpdateSwitchPoint = switchPoint;
nextUpdateIndex = i;
}
}
currentSelection[nextUpdateIndex]++;
setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection);
}
for (long[][] points : checkpoints) {
points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0];
points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1];
}
return checkpoints;
}
/** Converts all input values to Math.log(value). */
private static double[][] getLogArrayValues(long[][] values) {
double[][] logValues = new double[values.length][];
for (int i = 0; i < values.length; i++) {
logValues[i] = new double[values[i].length];
for (int j = 0; j < values[i].length; j++) {
logValues[i][j] = 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] = (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;
}
}
}

View file

@ -1283,6 +1283,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private final TrackSelection.Factory trackSelectionFactory;
private final AtomicReference<Parameters> parametersReference;
private boolean allowMultipleAdaptiveSelections;
public DefaultTrackSelector() {
this(new AdaptiveTrackSelection.Factory());
}
@ -1397,6 +1399,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
setParameters(buildUponParameters().setTunnelingAudioSessionId(tunnelingAudioSessionId));
}
/**
* Allows the creation of multiple adaptive track selections.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
public void experimental_allowMultipleAdaptiveSelections() {
this.allowMultipleAdaptiveSelections = true;
}
// MappingTrackSelector implementation.
@Override
@ -1514,13 +1525,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int selectedAudioRendererIndex = C.INDEX_UNSET;
for (int i = 0; i < rendererCount; i++) {
if (C.TRACK_TYPE_AUDIO == mappedTrackInfo.getRendererType(i)) {
boolean enableAdaptiveTrackSelection =
allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks;
Pair<TrackSelection.Definition, AudioTrackScore> audioSelection =
selectAudioTrack(
mappedTrackInfo.getTrackGroups(i),
rendererFormatSupports[i],
rendererMixedMimeTypeAdaptationSupports[i],
params,
!seenVideoRendererWithMappedTracks);
enableAdaptiveTrackSelection);
if (audioSelection != null
&& (selectedAudioTrackScore == null
|| audioSelection.second.compareTo(selectedAudioTrackScore) > 0)) {