Skip to content

Commit d29702f

Browse files
committed
Add adaptive track format comparator with safe fallback
1 parent 630c1af commit d29702f

File tree

4 files changed

+357
-24
lines changed

4 files changed

+357
-24
lines changed

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/AdaptiveTrackSelection.java

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package androidx.media3.exoplayer.trackselection;
1717

18+
import static com.google.common.base.Preconditions.checkNotNull;
1819
import static java.lang.Math.max;
1920
import static java.lang.Math.min;
2021

@@ -39,11 +40,12 @@
3940
import com.google.common.collect.MultimapBuilder;
4041
import java.util.ArrayList;
4142
import java.util.Arrays;
43+
import java.util.Comparator;
4244
import java.util.List;
4345

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

6568
/** Creates an adaptive track selection factory with default parameters. */
6669
public Factory() {
@@ -227,6 +230,19 @@ public Factory(
227230
this.bufferedFractionToLiveEdgeForQualityIncrease =
228231
bufferedFractionToLiveEdgeForQualityIncrease;
229232
this.clock = clock;
233+
this.trackFormatComparator = BaseTrackSelection.DEFAULT_FORMAT_COMPARATOR;
234+
}
235+
236+
/**
237+
* Sets the comparator used to order formats in adaptive track selections.
238+
* The comparator order controls which formats are considered first during adaptation.
239+
*
240+
* @param trackFormatComparator Comparator used to order selected formats.
241+
* @return This factory, for convenience.
242+
*/
243+
public Factory setTrackFormatComparator(Comparator<Format> trackFormatComparator) {
244+
this.trackFormatComparator = checkNotNull(trackFormatComparator);
245+
return this;
230246
}
231247

232248
@Override
@@ -289,7 +305,8 @@ protected AdaptiveTrackSelection createAdaptiveTrackSelection(
289305
bandwidthFraction,
290306
bufferedFractionToLiveEdgeForQualityIncrease,
291307
adaptationCheckpoints,
292-
clock);
308+
clock,
309+
trackFormatComparator);
293310
}
294311
}
295312

@@ -389,7 +406,71 @@ protected AdaptiveTrackSelection(
389406
float bufferedFractionToLiveEdgeForQualityIncrease,
390407
List<AdaptationCheckpoint> adaptationCheckpoints,
391408
Clock clock) {
392-
super(group, tracks, type);
409+
this(
410+
group,
411+
tracks,
412+
type,
413+
bandwidthMeter,
414+
minDurationForQualityIncreaseMs,
415+
maxDurationForQualityDecreaseMs,
416+
minDurationToRetainAfterDiscardMs,
417+
maxWidthToDiscard,
418+
maxHeightToDiscard,
419+
bandwidthFraction,
420+
bufferedFractionToLiveEdgeForQualityIncrease,
421+
adaptationCheckpoints,
422+
clock,
423+
BaseTrackSelection.DEFAULT_FORMAT_COMPARATOR);
424+
}
425+
426+
/**
427+
* @param group The {@link TrackGroup}.
428+
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
429+
* empty. May be in any order.
430+
* @param type The type that will be returned from {@link TrackSelection#getType()}.
431+
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
432+
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
433+
* selected track to switch to one of higher quality.
434+
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
435+
* selected track to switch to one of lower quality.
436+
* @param minDurationToRetainAfterDiscardMs When switching to a video track of higher quality, the
437+
* selection may indicate that media already buffered at the lower quality can be discarded to
438+
* speed up the switch. This is the minimum duration of media that must be retained at the
439+
* lower quality. It must be at least {@code minDurationForQualityIncreaseMs}.
440+
* @param maxWidthToDiscard The maximum video width that the selector may discard from the buffer
441+
* to speed up switching to a higher quality.
442+
* @param maxHeightToDiscard The maximum video height that the selector may discard from the
443+
* buffer to speed up switching to a higher quality.
444+
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
445+
* consider available for use. Setting to a value less than 1 is recommended to account for
446+
* inaccuracies in the bandwidth estimator.
447+
* @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
448+
* duration from current playback position to the live edge that has to be buffered before the
449+
* selected track can be switched to one of higher quality. This parameter is only applied
450+
* when the playback position is closer to the live edge than {@code
451+
* minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
452+
* quality from happening.
453+
* @param adaptationCheckpoints The {@link AdaptationCheckpoint checkpoints} that can be used to
454+
* calculate available bandwidth for this selection.
455+
* @param clock The {@link Clock}.
456+
* @param trackFormatComparator Comparator used to order selected formats.
457+
*/
458+
protected AdaptiveTrackSelection(
459+
TrackGroup group,
460+
int[] tracks,
461+
@Type int type,
462+
BandwidthMeter bandwidthMeter,
463+
long minDurationForQualityIncreaseMs,
464+
long maxDurationForQualityDecreaseMs,
465+
long minDurationToRetainAfterDiscardMs,
466+
int maxWidthToDiscard,
467+
int maxHeightToDiscard,
468+
float bandwidthFraction,
469+
float bufferedFractionToLiveEdgeForQualityIncrease,
470+
List<AdaptationCheckpoint> adaptationCheckpoints,
471+
Clock clock,
472+
Comparator<Format> trackFormatComparator) {
473+
super(group, tracks, type, trackFormatComparator);
393474
if (minDurationToRetainAfterDiscardMs < minDurationForQualityIncreaseMs) {
394475
Log.w(
395476
TAG,
@@ -442,11 +523,12 @@ public void updateSelectedTrack(
442523
MediaChunkIterator[] mediaChunkIterators) {
443524
long nowMs = clock.elapsedRealtime();
444525
long chunkDurationUs = getNextChunkDurationUs(mediaChunkIterators, queue);
526+
long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);
445527

446528
// Make initial selection
447529
if (reason == C.SELECTION_REASON_UNKNOWN) {
448530
reason = C.SELECTION_REASON_INITIAL;
449-
selectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
531+
selectedIndex = determineIdealSelectedIndexForEffectiveBitrate(nowMs, effectiveBitrate);
450532
return;
451533
}
452534

@@ -458,23 +540,24 @@ public void updateSelectedTrack(
458540
previousSelectedIndex = formatIndexOfPreviousChunk;
459541
previousReason = Iterables.getLast(queue).trackSelectionReason;
460542
}
461-
int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
543+
int newSelectedIndex = determineIdealSelectedIndexForEffectiveBitrate(nowMs, effectiveBitrate);
462544
if (newSelectedIndex != previousSelectedIndex
463545
&& !isTrackExcluded(previousSelectedIndex, nowMs)) {
464-
// Revert back to the previous selection if conditions are not suitable for switching.
465-
Format currentFormat = getFormat(previousSelectedIndex);
466-
Format selectedFormat = getFormat(newSelectedIndex);
546+
// Revert back to the previous selection if conditions are not suitable for switching. Do not
547+
// defer a switch when the previous format no longer fits into available bandwidth.
467548
long minDurationForQualityIncreaseUs =
468549
minDurationForQualityIncreaseUs(availableDurationUs, chunkDurationUs);
469-
if (selectedFormat.bitrate > currentFormat.bitrate
550+
if (newSelectedIndex < previousSelectedIndex
470551
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
471-
// The selected track is a higher quality, but we have insufficient buffer to safely switch
552+
// The selected track is higher priority, but we have insufficient buffer to safely switch
472553
// up. Defer switching up for now.
473554
newSelectedIndex = previousSelectedIndex;
474-
} else if (selectedFormat.bitrate < currentFormat.bitrate
555+
} else if (newSelectedIndex > previousSelectedIndex
556+
&& (isUsingDefaultFormatComparator()
557+
|| isTrackSelectable(previousSelectedIndex, effectiveBitrate))
475558
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
476-
// The selected track is a lower quality, but we have sufficient buffer to defer switching
477-
// down for now.
559+
// The selected track is lower priority, but we have sufficient buffer to defer switching
560+
// down while preserving existing behavior for default ordering.
478561
newSelectedIndex = previousSelectedIndex;
479562
}
480563
}
@@ -597,19 +680,33 @@ protected long getMinDurationToRetainAfterDiscardUs() {
597680
* if unknown.
598681
*/
599682
private int determineIdealSelectedIndex(long nowMs, long chunkDurationUs) {
600-
long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);
601-
int lowestBitrateAllowedIndex = 0;
683+
return determineIdealSelectedIndexForEffectiveBitrate(nowMs, getAllocatedBandwidth(chunkDurationUs));
684+
}
685+
686+
private int determineIdealSelectedIndexForEffectiveBitrate(long nowMs, long effectiveBitrate) {
687+
int lowestBitrateAllowedIndex = C.INDEX_UNSET;
688+
int lowestBitrate = Integer.MAX_VALUE;
602689
for (int i = 0; i < length; i++) {
603690
if (nowMs == Long.MIN_VALUE || !isTrackExcluded(i, nowMs)) {
604691
Format format = getFormat(i);
605692
if (canSelectFormat(format, format.bitrate, effectiveBitrate)) {
606693
return i;
607-
} else {
694+
}
695+
int formatBitrate = format.bitrate == Format.NO_VALUE ? 0 : format.bitrate;
696+
if (formatBitrate <= lowestBitrate) {
697+
// fallback semantics by selecting the lowest bitrate non-excluded track when no track
698+
// is selectable within available bandwidth.
608699
lowestBitrateAllowedIndex = i;
700+
lowestBitrate = formatBitrate;
609701
}
610702
}
611703
}
612-
return lowestBitrateAllowedIndex;
704+
return lowestBitrateAllowedIndex == C.INDEX_UNSET ? 0 : lowestBitrateAllowedIndex;
705+
}
706+
707+
private boolean isTrackSelectable(int index, long effectiveBitrate) {
708+
Format format = getFormat(index);
709+
return canSelectFormat(format, format.bitrate, effectiveBitrate);
613710
}
614711

615712
private long minDurationForQualityIncreaseUs(long availableDurationUs, long chunkDurationUs) {

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/BaseTrackSelection.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,37 @@
2929
import androidx.media3.common.util.Util;
3030
import androidx.media3.exoplayer.source.chunk.MediaChunk;
3131
import java.util.Arrays;
32+
import java.util.Comparator;
3233
import java.util.List;
3334

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

39+
static final Comparator<Format> DEFAULT_FORMAT_COMPARATOR =
40+
(firstFormat, secondFormat) -> Integer.compare(secondFormat.bitrate, firstFormat.bitrate);
41+
3842
/** The selected {@link TrackGroup}. */
3943
protected final TrackGroup group;
4044

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

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

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

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

53-
/** Selected track exclusion timestamps, in order of decreasing bandwidth. */
57+
/** Selected track exclusion timestamps, in selection order. */
5458
private final long[] excludeUntilTimes;
5559

60+
/** Whether selected formats are ordered with the default bitrate comparator. */
61+
private final boolean isUsingDefaultFormatComparator;
62+
5663
// Lazily initialized hashcode.
5764
private int hashCode;
5865

@@ -75,23 +82,42 @@ public BaseTrackSelection(TrackGroup group, int... tracks) {
7582
* @param type The type that will be returned from {@link TrackSelection#getType()}.
7683
*/
7784
public BaseTrackSelection(TrackGroup group, int[] tracks, @Type int type) {
85+
this(group, tracks, type, DEFAULT_FORMAT_COMPARATOR);
86+
}
87+
88+
/**
89+
* @param group The {@link TrackGroup}. Must not be null.
90+
* @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
91+
* null or empty. May be in any order.
92+
* @param type The type that will be returned from {@link TrackSelection#getType()}.
93+
* @param formatComparator Comparator that determines the order of selected {@link Format}s.
94+
*/
95+
protected BaseTrackSelection(
96+
TrackGroup group, int[] tracks, @Type int type, Comparator<Format> formatComparator) {
7897
checkState(tracks.length > 0);
7998
this.type = type;
8099
this.group = checkNotNull(group);
81100
this.length = tracks.length;
82-
// Set the formats, sorted in order of decreasing bandwidth.
101+
// Set the formats in selection order.
83102
formats = new Format[length];
84103
for (int i = 0; i < tracks.length; i++) {
85104
formats[i] = group.getFormat(tracks[i]);
86105
}
87-
// Sort in order of decreasing bandwidth.
88-
Arrays.sort(formats, (a, b) -> b.bitrate - a.bitrate);
106+
Comparator<Format> safeFormatComparator = checkNotNull(formatComparator);
107+
boolean isUsingDefaultFormatComparator = safeFormatComparator == DEFAULT_FORMAT_COMPARATOR;
108+
try {
109+
Arrays.sort(formats, safeFormatComparator);
110+
} catch (Throwable throwable) {
111+
Arrays.sort(formats, DEFAULT_FORMAT_COMPARATOR);
112+
isUsingDefaultFormatComparator = true;
113+
}
89114
// Set the format indices in the same order.
90115
this.tracks = new int[length];
91116
for (int i = 0; i < length; i++) {
92117
this.tracks[i] = group.indexOf(formats[i]);
93118
}
94119
excludeUntilTimes = new long[length];
120+
this.isUsingDefaultFormatComparator = isUsingDefaultFormatComparator;
95121
playWhenReady = false;
96122
}
97123

@@ -208,6 +234,11 @@ protected final boolean getPlayWhenReady() {
208234
return playWhenReady;
209235
}
210236

237+
/** Returns whether formats are ordered with the default bitrate comparator. */
238+
protected final boolean isUsingDefaultFormatComparator() {
239+
return isUsingDefaultFormatComparator;
240+
}
241+
211242
// Object overrides.
212243

213244
@Override

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelection.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
* A track selection consisting of a static subset of selected tracks belonging to a {@link
3232
* TrackGroup}.
3333
*
34-
* <p>Tracks belonging to the subset are exposed in decreasing bandwidth order.
34+
* <p>Tracks belonging to the subset are exposed in selection order, which by default is decreasing
35+
* bandwidth order.
3536
*/
3637
@UnstableApi
3738
public interface TrackSelection {

0 commit comments

Comments
 (0)