Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.trackselection;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;

Expand All @@ -39,11 +40,12 @@
import com.google.common.collect.MultimapBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
* A bandwidth based adaptive {@link ExoTrackSelection}, whose selected track is updated to be the
* one of highest quality given the current network conditions and the state of the buffer.
* highest priority track given the current network conditions and the state of the buffer.
*/
@UnstableApi
public class AdaptiveTrackSelection extends BaseTrackSelection {
Expand All @@ -61,6 +63,7 @@ public static class Factory implements ExoTrackSelection.Factory {
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final Clock clock;
private Comparator<Format> trackFormatComparator;

/** Creates an adaptive track selection factory with default parameters. */
public Factory() {
Expand Down Expand Up @@ -227,6 +230,24 @@ public Factory(
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
this.clock = clock;
this.trackFormatComparator = BaseTrackSelection.DEFAULT_FORMAT_COMPARATOR;
}

/**
* Sets the comparator used to order formats in adaptive track selections.
* The comparator order controls which formats are considered first during adaptation.
*
* @param trackFormatComparator Comparator used to order selected formats.
* @return This factory, for convenience.
*/
public Factory setTrackFormatComparator(Comparator<Format> trackFormatComparator) {
this.trackFormatComparator = checkNotNull(trackFormatComparator);
return this;
}

/** Returns the comparator used to order formats in adaptive track selections. */
protected final Comparator<Format> getTrackFormatComparator() {
return trackFormatComparator;
}

@Override
Expand Down Expand Up @@ -289,7 +310,8 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection(
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
adaptationCheckpoints,
clock);
clock,
getTrackFormatComparator());
}
}

Expand Down Expand Up @@ -389,7 +411,71 @@ protected AdaptiveTrackSelection(
float bufferedFractionToLiveEdgeForQualityIncrease,
List<AdaptationCheckpoint> adaptationCheckpoints,
Clock clock) {
super(group, tracks, type);
this(
group,
tracks,
type,
bandwidthMeter,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
maxWidthToDiscard,
maxHeightToDiscard,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
adaptationCheckpoints,
clock,
BaseTrackSelection.DEFAULT_FORMAT_COMPARATOR);
}

/**
* @param group The {@link TrackGroup}.
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
* empty. May be in any order.
* @param type The type that will be returned from {@link TrackSelection#getType()}.
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
* selected track to switch to one of higher quality.
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
* selected track to switch to one of lower quality.
* @param minDurationToRetainAfterDiscardMs When switching to a video track of higher quality, the
* selection may indicate that media already buffered at the lower quality can be discarded to
* speed up the switch. This is the minimum duration of media that must be retained at the
* lower quality. It must be at least {@code minDurationForQualityIncreaseMs}.
* @param maxWidthToDiscard The maximum video width that the selector may discard from the buffer
* to speed up switching to a higher quality.
* @param maxHeightToDiscard The maximum video height that the selector may discard from the
* buffer to speed up switching to a higher quality.
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
* consider available for use. Setting to a value less than 1 is recommended to account for
* inaccuracies in the bandwidth estimator.
* @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
* duration from current playback position to the live edge that has to be buffered before the
* selected track can be switched to one of higher quality. This parameter is only applied
* when the playback position is closer to the live edge than {@code
* minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
* quality from happening.
* @param adaptationCheckpoints The {@link AdaptationCheckpoint checkpoints} that can be used to
* calculate available bandwidth for this selection.
* @param clock The {@link Clock}.
* @param trackFormatComparator Comparator used to order selected formats.
*/
protected AdaptiveTrackSelection(
TrackGroup group,
int[] tracks,
@Type int type,
BandwidthMeter bandwidthMeter,
long minDurationForQualityIncreaseMs,
long maxDurationForQualityDecreaseMs,
long minDurationToRetainAfterDiscardMs,
int maxWidthToDiscard,
int maxHeightToDiscard,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
List<AdaptationCheckpoint> adaptationCheckpoints,
Clock clock,
Comparator<Format> trackFormatComparator) {
super(group, tracks, type, trackFormatComparator);
if (minDurationToRetainAfterDiscardMs < minDurationForQualityIncreaseMs) {
Log.w(
TAG,
Expand Down Expand Up @@ -442,11 +528,12 @@ public void updateSelectedTrack(
MediaChunkIterator[] mediaChunkIterators) {
long nowMs = clock.elapsedRealtime();
long chunkDurationUs = getNextChunkDurationUs(mediaChunkIterators, queue);
long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);

// Make initial selection
if (reason == C.SELECTION_REASON_UNKNOWN) {
reason = C.SELECTION_REASON_INITIAL;
selectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
selectedIndex = determineIdealSelectedIndex(nowMs, effectiveBitrate);
return;
}

Expand All @@ -458,22 +545,20 @@ public void updateSelectedTrack(
previousSelectedIndex = formatIndexOfPreviousChunk;
previousReason = Iterables.getLast(queue).trackSelectionReason;
}
int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
int newSelectedIndex = determineIdealSelectedIndex(nowMs, effectiveBitrate);
if (newSelectedIndex != previousSelectedIndex
&& !isTrackExcluded(previousSelectedIndex, nowMs)) {
// Revert back to the previous selection if conditions are not suitable for switching.
Format currentFormat = getFormat(previousSelectedIndex);
Format selectedFormat = getFormat(newSelectedIndex);
long minDurationForQualityIncreaseUs =
minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs);
if (selectedFormat.bitrate > currentFormat.bitrate
if (newSelectedIndex < previousSelectedIndex
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The selected track is a higher quality, but we have insufficient buffer to safely switch
// The selected track is higher priority, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
newSelectedIndex = previousSelectedIndex;
} else if (selectedFormat.bitrate < currentFormat.bitrate
} else if (newSelectedIndex > previousSelectedIndex
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The selected track is a lower quality, but we have sufficient buffer to defer switching
// The selected track is lower priority, but we have sufficient buffer to defer switching
// down for now.
newSelectedIndex = previousSelectedIndex;
}
Expand Down Expand Up @@ -521,7 +606,8 @@ public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk>
if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
int idealSelectedIndex = determineIdealSelectedIndex(nowMs, getLastChunkDurationUs(queue));
int idealSelectedIndex =
determineIdealSelectedIndex(nowMs, getAllocatedBandwidth(getLastChunkDurationUs(queue)));
Format idealFormat = getFormat(idealSelectedIndex);
// If chunks contain video, discard from the first chunk after minDurationToRetainAfterDiscardUs
// whose resolution and bitrate are both lower than the ideal track, and whose width and height
Expand Down Expand Up @@ -593,11 +679,9 @@ protected long getMinDurationToRetainAfterDiscardUs() {
*
* @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
* Long#MIN_VALUE} to ignore track exclusion.
* @param chunkDurationUs The duration of a media chunk in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param effectiveBitrate The bitrate available to this selection.
*/
private int determineIdealSelectedIndex(long nowMs, long chunkDurationUs) {
long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);
private int determineIdealSelectedIndex(long nowMs, long effectiveBitrate) {
int lowestBitrateAllowedIndex = 0;
for (int i = 0; i < length; i++) {
if (nowMs == Long.MIN_VALUE || !isTrackExcluded(i, nowMs)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,32 @@
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/** An abstract base class suitable for most {@link ExoTrackSelection} implementations. */
@UnstableApi
public abstract class BaseTrackSelection implements ExoTrackSelection {

static final Comparator<Format> DEFAULT_FORMAT_COMPARATOR =
(firstFormat, secondFormat) -> Integer.compare(secondFormat.bitrate, firstFormat.bitrate);

/** The selected {@link TrackGroup}. */
protected final TrackGroup group;

/** The number of selected tracks within the {@link TrackGroup}. Always greater than zero. */
protected final int length;

/** The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth. */
/** The indices of the selected tracks in {@link #group}, in selection order. */
protected final int[] tracks;

/** The type of the selection. */
private final @Type int type;

/** The {@link Format}s of the selected tracks, in order of decreasing bandwidth. */
/** The {@link Format}s of the selected tracks, in selection order. */
private final Format[] formats;

/** Selected track exclusion timestamps, in order of decreasing bandwidth. */
/** Selected track exclusion timestamps, in selection order. */
private final long[] excludeUntilTimes;

// Lazily initialized hashcode.
Expand All @@ -75,17 +79,28 @@ public BaseTrackSelection(TrackGroup group, int... tracks) {
* @param type The type that will be returned from {@link TrackSelection#getType()}.
*/
public BaseTrackSelection(TrackGroup group, int[] tracks, @Type int type) {
this(group, tracks, type, DEFAULT_FORMAT_COMPARATOR);
}

/**
* @param group The {@link TrackGroup}. Must not be null.
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
* null or empty. May be in any order.
* @param type The type that will be returned from {@link TrackSelection#getType()}.
* @param formatComparator Comparator that determines the order of selected {@link Format}s.
*/
protected BaseTrackSelection(
TrackGroup group, int[] tracks, @Type int type, Comparator<Format> formatComparator) {
checkState(tracks.length > 0);
this.type = type;
this.group = checkNotNull(group);
this.length = tracks.length;
// Set the formats, sorted in order of decreasing bandwidth.
// Set the formats in selection order.
formats = new Format[length];
for (int i = 0; i < tracks.length; i++) {
formats[i] = group.getFormat(tracks[i]);
}
// Sort in order of decreasing bandwidth.
Arrays.sort(formats, (a, b) -> b.bitrate - a.bitrate);
Arrays.sort(formats, checkNotNull(formatComparator));
// Set the format indices in the same order.
this.tracks = new int[length];
for (int i = 0; i < length; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
* A track selection consisting of a static subset of selected tracks belonging to a {@link
* TrackGroup}.
*
* <p>Tracks belonging to the subset are exposed in decreasing bandwidth order.
* <p>Tracks belonging to the subset are exposed in selection order, which by default is decreasing
* bandwidth order.
*/
@UnstableApi
public interface TrackSelection {
Expand Down
Loading