Gradle dependencies
compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-exoplayer
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A bandwidth based adaptive ExoTrackSelection, whose selected track is updated to be the
one of highest quality given the current network conditions and the state of the buffer.
Summary
Constructors |
---|
public | AdaptiveTrackSelection(TrackGroup group, int[] tracks[], BandwidthMeter bandwidthMeter)
|
protected | AdaptiveTrackSelection(TrackGroup group, int[] tracks[], int type, BandwidthMeter bandwidthMeter, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, int maxWidthToDiscard, int maxHeightToDiscard, float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease, java.util.List<AdaptiveTrackSelection.AdaptationCheckpoint> adaptationCheckpoints, Clock clock)
|
Methods |
---|
protected boolean | canSelectFormat(Format format, int trackBitrate, long effectiveBitrate)
Called when updating the selected track to determine whether a candidate track can be selected. |
public void | disable()
|
public void | enable()
|
public int | evaluateQueueSize(long playbackPositionUs, java.util.List<MediaChunk> queue)
|
public long | getLatestBitrateEstimate()
|
protected long | getMinDurationToRetainAfterDiscardUs()
Called from AdaptiveTrackSelection.evaluateQueueSize(long, List extends MediaChunk>) to determine the minimum duration of buffer
to retain after discarding chunks. |
public int | getSelectedIndex()
|
public java.lang.Object | getSelectionData()
|
public int | getSelectionReason()
|
public void | onPlaybackSpeed(float playbackSpeed)
|
protected boolean | shouldEvaluateQueueSize(long nowMs, java.util.List<MediaChunk> queue)
Called from AdaptiveTrackSelection.evaluateQueueSize(long, List extends MediaChunk>) to determine whether an evaluation should be
performed. |
public void | updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs, java.util.List<MediaChunk> queue, MediaChunkIterator mediaChunkIterators[])
|
from BaseTrackSelection | equals, excludeTrack, getFormat, getIndexInTrackGroup, getSelectedFormat, getSelectedIndexInTrackGroup, getTrackGroup, getType, hashCode, indexOf, indexOf, isTrackExcluded, length |
from java.lang.Object | clone, finalize, getClass, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MSpublic static final int
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MSpublic static final int
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MSpublic static final int
DEFAULT_MAX_WIDTH_TO_DISCARDpublic static final int
DEFAULT_MAX_HEIGHT_TO_DISCARDpublic static final float
DEFAULT_BANDWIDTH_FRACTIONpublic static final float
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASEConstructors
Parameters:
group: The TrackGroup.
tracks: The indices of the selected tracks within the TrackGroup. Must not be
empty. May be in any order.
bandwidthMeter: Provides an estimate of the currently available bandwidth.
protected
AdaptiveTrackSelection(
TrackGroup group, int[] tracks[], int type,
BandwidthMeter bandwidthMeter, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, int maxWidthToDiscard, int maxHeightToDiscard, float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease, java.util.List<AdaptiveTrackSelection.AdaptationCheckpoint> adaptationCheckpoints,
Clock clock)
Parameters:
group: The TrackGroup.
tracks: The indices of the selected tracks within the TrackGroup. Must not be
empty. May be in any order.
type: The type that will be returned from TrackSelection.getType().
bandwidthMeter: Provides an estimate of the currently available bandwidth.
minDurationForQualityIncreaseMs: The minimum duration of buffered data required for the
selected track to switch to one of higher quality.
maxDurationForQualityDecreaseMs: The maximum duration of buffered data required for the
selected track to switch to one of lower quality.
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 minDurationForQualityIncreaseMs.
maxWidthToDiscard: The maximum video width that the selector may discard from the buffer
to speed up switching to a higher quality.
maxHeightToDiscard: The maximum video height that the selector may discard from the
buffer to speed up switching to a higher quality.
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.
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 minDurationForQualityIncreaseMs, which would otherwise prevent switching to a higher
quality from happening.
adaptationCheckpoints: The checkpoints that can be used to
calculate available bandwidth for this selection.
clock: The Clock.
Methods
public void
onPlaybackSpeed(float playbackSpeed)
public void
updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs, java.util.List<MediaChunk> queue,
MediaChunkIterator mediaChunkIterators[])
public int
getSelectedIndex()
public int
getSelectionReason()
public java.lang.Object
getSelectionData()
public int
evaluateQueueSize(long playbackPositionUs, java.util.List<MediaChunk> queue)
public long
getLatestBitrateEstimate()
protected boolean
canSelectFormat(
Format format, int trackBitrate, long effectiveBitrate)
Called when updating the selected track to determine whether a candidate track can be selected.
Parameters:
format: The Format of the candidate track.
trackBitrate: The estimated bitrate of the track. May differ from Format.bitrate
if a more accurate estimate of the current track bitrate is available.
effectiveBitrate: The bitrate available to this selection.
Returns:
Whether this Format can be selected.
protected boolean
shouldEvaluateQueueSize(long nowMs, java.util.List<MediaChunk> queue)
Called from AdaptiveTrackSelection.evaluateQueueSize(long, List extends MediaChunk>) to determine whether an evaluation should be
performed.
Parameters:
nowMs: The current value of Clock.elapsedRealtime().
queue: The queue of buffered MediaChunks. Must not be modified.
Returns:
Whether an evaluation should be performed.
protected long
getMinDurationToRetainAfterDiscardUs()
Called from AdaptiveTrackSelection.evaluateQueueSize(long, List extends MediaChunk>) to determine the minimum duration of buffer
to retain after discarding chunks.
Returns:
The minimum duration of buffer to retain after discarding chunks, in microseconds.
Source
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.trackselection;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
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;
/**
* 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.
*/
@UnstableApi
public class AdaptiveTrackSelection extends BaseTrackSelection {
private static final String TAG = "AdaptiveTrackSelection";
/** Factory for {@link AdaptiveTrackSelection} instances. */
public static class Factory implements ExoTrackSelection.Factory {
private final int minDurationForQualityIncreaseMs;
private final int maxDurationForQualityDecreaseMs;
private final int minDurationToRetainAfterDiscardMs;
private final int maxWidthToDiscard;
private final int maxHeightToDiscard;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final Clock clock;
/** Creates an adaptive track selection factory with default parameters. */
public Factory() {
this(
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);
}
/**
* Creates an adaptive track selection factory.
*
* @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 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.
*/
public Factory(
int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
float bandwidthFraction) {
this(
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
DEFAULT_MAX_WIDTH_TO_DISCARD,
DEFAULT_MAX_HEIGHT_TO_DISCARD,
bandwidthFraction,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
Clock.DEFAULT);
}
/**
* Creates an adaptive track selection factory.
*
* @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.
*/
public Factory(
int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
int maxWidthToDiscard,
int maxHeightToDiscard,
float bandwidthFraction) {
this(
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
maxWidthToDiscard,
maxHeightToDiscard,
bandwidthFraction,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
Clock.DEFAULT);
}
/**
* Creates an adaptive track selection factory.
*
* @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 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 clock A {@link Clock}.
*/
public Factory(
int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
Clock clock) {
this(
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
DEFAULT_MAX_WIDTH_TO_DISCARD,
DEFAULT_MAX_HEIGHT_TO_DISCARD,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
clock);
}
/**
* Creates an adaptive track selection factory.
*
* @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 clock A {@link Clock}.
*/
public Factory(
int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
int maxWidthToDiscard,
int maxHeightToDiscard,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
Clock clock) {
this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
this.maxWidthToDiscard = maxWidthToDiscard;
this.maxHeightToDiscard = maxHeightToDiscard;
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
this.clock = clock;
}
@Override
public final @NullableType ExoTrackSelection[] createTrackSelections(
@NullableType Definition[] definitions,
BandwidthMeter bandwidthMeter,
MediaPeriodId mediaPeriodId,
Timeline timeline) {
ImmutableList<ImmutableList<AdaptationCheckpoint>> adaptationCheckpoints =
getAdaptationCheckpoints(definitions);
ExoTrackSelection[] selections = new ExoTrackSelection[definitions.length];
for (int i = 0; i < definitions.length; i++) {
@Nullable Definition definition = definitions[i];
if (definition == null || definition.tracks.length == 0) {
continue;
}
selections[i] =
definition.tracks.length == 1
? new FixedTrackSelection(
definition.group,
/* track= */ definition.tracks[0],
/* type= */ definition.type)
: createAdaptiveTrackSelection(
definition.group,
definition.tracks,
definition.type,
bandwidthMeter,
adaptationCheckpoints.get(i));
}
return selections;
}
/**
* Creates a single adaptive selection for the given group, bandwidth meter and tracks.
*
* @param group The {@link TrackGroup}.
* @param tracks The indices of the selected tracks in the track group.
* @param type The type that will be returned from {@link TrackSelection#getType()}.
* @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
* @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,
int[] tracks,
int type,
BandwidthMeter bandwidthMeter,
ImmutableList<AdaptationCheckpoint> adaptationCheckpoints) {
return new AdaptiveTrackSelection(
group,
tracks,
type,
bandwidthMeter,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
maxWidthToDiscard,
maxHeightToDiscard,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
adaptationCheckpoints,
clock);
}
}
public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10_000;
public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25_000;
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25_000;
public static final int DEFAULT_MAX_WIDTH_TO_DISCARD = 1279;
public static final int DEFAULT_MAX_HEIGHT_TO_DISCARD = 719;
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f;
public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
private static final long MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 1000;
private final BandwidthMeter bandwidthMeter;
private final long minDurationForQualityIncreaseUs;
private final long maxDurationForQualityDecreaseUs;
private final long minDurationToRetainAfterDiscardUs;
private final int maxWidthToDiscard;
private final int maxHeightToDiscard;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final ImmutableList<AdaptationCheckpoint> adaptationCheckpoints;
private final Clock clock;
private float playbackSpeed;
private int selectedIndex;
private @C.SelectionReason int reason;
private long lastBufferEvaluationMs;
@Nullable private MediaChunk lastBufferEvaluationMediaChunk;
private long latestBitrateEstimate;
/**
* @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 bandwidthMeter Provides an estimate of the currently available bandwidth.
*/
public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) {
this(
group,
tracks,
TrackSelection.TYPE_UNSET,
bandwidthMeter,
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
DEFAULT_MAX_WIDTH_TO_DISCARD,
DEFAULT_MAX_HEIGHT_TO_DISCARD,
DEFAULT_BANDWIDTH_FRACTION,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
/* adaptationCheckpoints= */ ImmutableList.of(),
Clock.DEFAULT);
}
/**
* @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}.
*/
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) {
super(group, tracks, type);
if (minDurationToRetainAfterDiscardMs < minDurationForQualityIncreaseMs) {
Log.w(
TAG,
"Adjusting minDurationToRetainAfterDiscardMs to be at least"
+ " minDurationForQualityIncreaseMs");
minDurationToRetainAfterDiscardMs = minDurationForQualityIncreaseMs;
}
this.bandwidthMeter = bandwidthMeter;
this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
this.maxWidthToDiscard = maxWidthToDiscard;
this.maxHeightToDiscard = maxHeightToDiscard;
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;
latestBitrateEstimate = C.RATE_UNSET_INT;
}
@CallSuper
@Override
public void enable() {
lastBufferEvaluationMs = C.TIME_UNSET;
lastBufferEvaluationMediaChunk = null;
}
@CallSuper
@Override
public void disable() {
// Avoid keeping a reference to a MediaChunk in case it prevents garbage collection.
lastBufferEvaluationMediaChunk = null;
}
@Override
public void onPlaybackSpeed(float playbackSpeed) {
this.playbackSpeed = playbackSpeed;
}
@Override
public void updateSelectedTrack(
long playbackPositionUs,
long bufferedDurationUs,
long availableDurationUs,
List<? extends MediaChunk> queue,
MediaChunkIterator[] mediaChunkIterators) {
long nowMs = clock.elapsedRealtime();
long chunkDurationUs = getNextChunkDurationUs(mediaChunkIterators, queue);
// Make initial selection
if (reason == C.SELECTION_REASON_UNKNOWN) {
reason = C.SELECTION_REASON_INITIAL;
selectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
return;
}
int previousSelectedIndex = selectedIndex;
@C.SelectionReason int previousReason = reason;
int formatIndexOfPreviousChunk =
queue.isEmpty() ? C.INDEX_UNSET : indexOf(Iterables.getLast(queue).trackFormat);
if (formatIndexOfPreviousChunk != C.INDEX_UNSET) {
previousSelectedIndex = formatIndexOfPreviousChunk;
previousReason = Iterables.getLast(queue).trackSelectionReason;
}
int newSelectedIndex = determineIdealSelectedIndex(nowMs, chunkDurationUs);
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
&& bufferedDurationUs < minDurationForQualityIncreaseUs) {
// The selected track is a higher quality, but we have insufficient buffer to safely switch
// up. Defer switching up for now.
newSelectedIndex = previousSelectedIndex;
} else if (selectedFormat.bitrate < currentFormat.bitrate
&& bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
// The selected track is a lower quality, but we have sufficient buffer to defer switching
// down for now.
newSelectedIndex = previousSelectedIndex;
}
}
// If we adapted, update the trigger.
reason =
newSelectedIndex == previousSelectedIndex ? previousReason : C.SELECTION_REASON_ADAPTIVE;
selectedIndex = newSelectedIndex;
}
@Override
public int getSelectedIndex() {
return selectedIndex;
}
@Override
public @C.SelectionReason int getSelectionReason() {
return reason;
}
@Override
@Nullable
public Object getSelectionData() {
return null;
}
@Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
long nowMs = clock.elapsedRealtime();
if (!shouldEvaluateQueueSize(nowMs, queue)) {
return queue.size();
}
lastBufferEvaluationMs = nowMs;
lastBufferEvaluationMediaChunk = queue.isEmpty() ? null : Iterables.getLast(queue);
if (queue.isEmpty()) {
return 0;
}
int queueSize = queue.size();
MediaChunk lastChunk = queue.get(queueSize - 1);
long playoutBufferedDurationBeforeLastChunkUs =
Util.getPlayoutDurationForMediaDuration(
lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs();
if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
int idealSelectedIndex = determineIdealSelectedIndex(nowMs, 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
// are less than or equal to maxWidthToDiscard and maxHeightToDiscard respectively.
for (int i = 0; i < queueSize; i++) {
MediaChunk chunk = queue.get(i);
Format format = chunk.trackFormat;
long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
long playoutDurationBeforeThisChunkUs =
Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed);
if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
&& format.bitrate < idealFormat.bitrate
&& format.height != Format.NO_VALUE
&& format.height <= maxHeightToDiscard
&& format.width != Format.NO_VALUE
&& format.width <= maxWidthToDiscard
&& format.height < idealFormat.height) {
return i;
}
}
return queueSize;
}
@Override
public long getLatestBitrateEstimate() {
return latestBitrateEstimate;
}
/**
* Called when updating the selected track to determine whether a candidate track can be selected.
*
* @param format The {@link Format} of the candidate track.
* @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate}
* if a more accurate estimate of the current track bitrate is available.
* @param effectiveBitrate The bitrate available to this selection.
* @return Whether this {@link Format} can be selected.
*/
@SuppressWarnings("unused")
protected boolean canSelectFormat(Format format, int trackBitrate, long effectiveBitrate) {
return trackBitrate <= effectiveBitrate;
}
/**
* Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be
* performed.
*
* @param nowMs The current value of {@link Clock#elapsedRealtime()}.
* @param queue The queue of buffered {@link MediaChunk MediaChunks}. Must not be modified.
* @return Whether an evaluation should be performed.
*/
protected boolean shouldEvaluateQueueSize(long nowMs, List<? extends MediaChunk> queue) {
return lastBufferEvaluationMs == C.TIME_UNSET
|| nowMs - lastBufferEvaluationMs >= MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS
|| (!queue.isEmpty() && !Iterables.getLast(queue).equals(lastBufferEvaluationMediaChunk));
}
/**
* Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer
* to retain after discarding chunks.
*
* @return The minimum duration of buffer to retain after discarding chunks, in microseconds.
*/
protected long getMinDurationToRetainAfterDiscardUs() {
return minDurationToRetainAfterDiscardUs;
}
/**
* Computes the ideal selected index ignoring buffer health.
*
* @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.
*/
private int determineIdealSelectedIndex(long nowMs, long chunkDurationUs) {
long effectiveBitrate = getAllocatedBandwidth(chunkDurationUs);
int lowestBitrateAllowedIndex = 0;
for (int i = 0; i < length; i++) {
if (nowMs == Long.MIN_VALUE || !isTrackExcluded(i, nowMs)) {
Format format = getFormat(i);
if (canSelectFormat(format, format.bitrate, effectiveBitrate)) {
return i;
} else {
lowestBitrateAllowedIndex = i;
}
}
}
return lowestBitrateAllowedIndex;
}
private long minDurationForQualityIncreaseUs(long availableDurationUs, long chunkDurationUs) {
if (availableDurationUs == C.TIME_UNSET) {
// We are not in a live stream. Use the configured value.
return minDurationForQualityIncreaseUs;
}
if (chunkDurationUs != C.TIME_UNSET) {
// We are currently selecting a new live chunk. Even under perfect conditions, the buffered
// duration can't include the last chunk duration yet because we are still selecting a track
// for this or a previous chunk. Hence, we subtract one chunk duration from the total
// available live duration to ensure we only compare the buffered duration against what is
// actually achievable.
availableDurationUs -= chunkDurationUs;
}
long adjustedMinDurationForQualityIncreaseUs =
(long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease);
return min(adjustedMinDurationForQualityIncreaseUs, minDurationForQualityIncreaseUs);
}
/**
* Returns a best estimate of the duration of the next chunk, in microseconds, or {@link
* C#TIME_UNSET} if an estimate could not be determined.
*/
private long getNextChunkDurationUs(
MediaChunkIterator[] mediaChunkIterators, List<? extends MediaChunk> queue) {
// Try to get the next chunk duration for the currently selected format.
if (selectedIndex < mediaChunkIterators.length && mediaChunkIterators[selectedIndex].next()) {
MediaChunkIterator iterator = mediaChunkIterators[selectedIndex];
return iterator.getChunkEndTimeUs() - iterator.getChunkStartTimeUs();
}
// Try to get the next chunk duration for another format, on the assumption that chunks
// belonging to different formats are likely to have identical or similar durations.
for (MediaChunkIterator iterator : mediaChunkIterators) {
if (iterator.next()) {
return iterator.getChunkEndTimeUs() - iterator.getChunkStartTimeUs();
}
}
// Try to get chunk duration for last chunk in the queue, on the assumption that the next chunk
// is likely to have a similar duration.
return getLastChunkDurationUs(queue);
}
/**
* Returns the duration of the last chunk in the queue, in microseconds, or {@link C#TIME_UNSET}
* if the queue is empty or if the last chunk has an undefined start or end time.
*/
private long getLastChunkDurationUs(List<? extends MediaChunk> queue) {
if (queue.isEmpty()) {
return C.TIME_UNSET;
}
MediaChunk lastChunk = Iterables.getLast(queue);
return lastChunk.startTimeUs != C.TIME_UNSET && lastChunk.endTimeUs != C.TIME_UNSET
? lastChunk.endTimeUs - lastChunk.startTimeUs
: C.TIME_UNSET;
}
private long getAllocatedBandwidth(long chunkDurationUs) {
long totalBandwidth = getTotalAllocatableBandwidth(chunkDurationUs);
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 long getTotalAllocatableBandwidth(long chunkDurationUs) {
latestBitrateEstimate = bandwidthMeter.getBitrateEstimate();
long cautiousBandwidthEstimate = (long) (latestBitrateEstimate * bandwidthFraction);
long timeToFirstByteEstimateUs = bandwidthMeter.getTimeToFirstByteEstimateUs();
if (timeToFirstByteEstimateUs == C.TIME_UNSET || chunkDurationUs == C.TIME_UNSET) {
return (long) (cautiousBandwidthEstimate / playbackSpeed);
}
float availableTimeToLoadUs =
max(chunkDurationUs / playbackSpeed - timeToFirstByteEstimateUs, 0);
return (long) (cautiousBandwidthEstimate * availableTimeToLoadUs / chunkDurationUs);
}
/**
* 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();
}
/** 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++) {
long bitrate = definition.group.getFormat(definition.tracks[j]).bitrate;
trackBitrates[i][j] = bitrate == Format.NO_VALUE ? 0 : bitrate;
}
Arrays.sort(trackBitrates[i]);
}
return trackBitrates;
}
/**
* 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((double) 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());
}
/**
* 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. */
public 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 boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AdaptationCheckpoint)) {
return false;
}
AdaptationCheckpoint that = (AdaptationCheckpoint) o;
return totalBandwidth == that.totalBandwidth && allocatedBandwidth == that.allocatedBandwidth;
}
@Override
public int hashCode() {
return 31 * (int) totalBandwidth + (int) allocatedBandwidth;
}
}
}