Add ability for media period to discard buffered media at the back of the queue

In some occasions, we may want to discard a part of the buffered media to
improve playback quality. This CL adds this functionality by allowing the
loading media period to re-evaluate its buffer periodically (every 2s) and discard
chunks as it needs.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177958910
This commit is contained in:
hoangtc 2017-12-05 08:31:17 -08:00 committed by Oliver Woodman
parent 6606d73b29
commit 88dea59cd2
20 changed files with 765 additions and 139 deletions

View file

@ -2,6 +2,8 @@
### dev-v2 (not yet released) ###
* Add ability for `SequenceableLoader` to reevaluate its buffer and discard
buffered media so that it can be re-buffered in a different quality.
* Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
* Allow more flexible loading strategy when playing media containing multiple
sub-streams, by allowing injection of custom `CompositeSequenceableLoader`

View file

@ -1283,6 +1283,7 @@ import java.io.IOException;
// Update the loading period if required.
maybeUpdateLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
} else if (loadingPeriodHolder != null && !isLoading) {
@ -1386,6 +1387,7 @@ import java.io.IOException;
if (loadingPeriodHolder == null) {
info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo);
} else {
loadingPeriodHolder.reevaluateBuffer(rendererPositionUs);
if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered()
|| loadingPeriodHolder.info.durationUs == C.TIME_UNSET) {
return;
@ -1440,6 +1442,7 @@ import java.io.IOException;
// Stale event.
return;
}
loadingPeriodHolder.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
}
@ -1628,13 +1631,18 @@ import java.io.IOException;
info = info.copyWithStartPositionUs(newStartPositionUs);
}
public void reevaluateBuffer(long rendererPositionUs) {
if (prepared) {
mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs));
}
}
public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) {
long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
return false;
} else {
long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
long bufferedDurationUs = nextLoadPositionUs - toPeriodTime(rendererPositionUs);
return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
}
@ -1694,7 +1702,6 @@ import java.io.IOException;
Assertions.checkState(trackSelections.get(i) == null);
}
}
// The track selection has changed.
loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
return positionUs;

View file

@ -123,6 +123,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe);
}
@Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs + startUs);
}
@Override
public long readDiscontinuity() {
if (isPendingInitialDiscontinuity()) {

View file

@ -52,6 +52,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader {
return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs;
}
@Override
public final void reevaluateBuffer(long positionUs) {
for (SequenceableLoader loader : loaders) {
loader.reevaluateBuffer(positionUs);
}
}
@Override
public boolean continueLoading(long positionUs) {
boolean madeProgress = false;

View file

@ -119,6 +119,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
return mediaPeriod.getNextLoadPositionUs();
}
@Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);

View file

@ -288,6 +288,11 @@ import java.util.Arrays;
}
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long playbackPositionUs) {
if (loadingFinished || (prepared && enabledTrackCount == 0)) {

View file

@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Called when preparation completes.
* <p>
* Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect
* for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be
* called with the initial track selection.
*
* <p>Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
* expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
* long)} to be called with the initial track selection.
*
* @param mediaPeriod The prepared {@link MediaPeriod}.
*/
void onPrepared(MediaPeriod mediaPeriod);
}
/**
* Prepares this media period asynchronously.
* <p>
* {@code callback.onPrepared} is called when preparation completes. If preparation fails,
*
* <p>{@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
* <p>
* If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known),
* {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be
* called before {@code callback.onPrepared}.
*
* <p>If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline,
* Object)} will be called before {@code callback.onPrepared}.
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
@ -66,8 +64,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
* error exists.
* <p>
* This method should only be called before the period has completed preparation.
*
* <p>This method should only be called before the period has completed preparation.
*
* @throws IOException The underlying error.
*/
@ -75,8 +73,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the {@link TrackGroup}s exposed by the period.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @return The {@link TrackGroup}s.
*/
@ -84,16 +82,16 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Performs a track selection.
* <p>
* The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
*
* <p>The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* indicating whether the existing {@code SampleStream} can be retained for each selection, and
* the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* provided selections, clearing, setting and replacing entries as required. If an existing sample
* stream is retained but with the requirement that the consuming renderer be reset, then the
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @param selections The renderer track selections.
* @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader {
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
* have been retained but with the requirement that the consuming renderer be reset.
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position.
* not yet started, the value will be the starting position.
* @return The actual position at which the tracks were enabled, in microseconds.
*/
long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
long selectTracks(
TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs);
/**
* Discards buffered media up to the specified position.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>This method should only be called after the period has been prepared.
*
* @param positionUs The position in microseconds.
* @param toKeyframe If true then for each track discards samples up to the keyframe before or at
@ -123,11 +125,11 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Attempts to read a discontinuity.
* <p>
* After this method has returned a value other than {@link C#TIME_UNSET}, all
* {@link SampleStream}s provided by the period are guaranteed to start from a key frame.
* <p>
* This method should only be called after the period has been prepared.
*
* <p>After this method has returned a value other than {@link C#TIME_UNSET}, all {@link
* SampleStream}s provided by the period are guaranteed to start from a key frame.
*
* <p>This method should only be called after the period has been prepared.
*
* @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}.
@ -136,11 +138,11 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Attempts to seek to the specified position in microseconds.
* <p>
* After this method has been called, all {@link SampleStream}s provided by the period are
*
* <p>After this method has been called, all {@link SampleStream}s provided by the period are
* guaranteed to start from a key frame.
* <p>
* This method should only be called when at least one track is selected.
*
* <p>This method should only be called when at least one track is selected.
*
* @param positionUs The seek position in microseconds.
* @return The actual position to which the period was seeked, in microseconds.
@ -151,8 +153,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns an estimate of the position up to which data is buffered for the enabled tracks.
* <p>
* This method should only be called when at least one track is selected.
*
* <p>This method should only be called when at least one track is selected.
*
* @return An estimate of the absolute position in microseconds up to which data is buffered, or
* {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
@ -162,19 +164,19 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
* <p>
* This method should only be called after the period has been prepared. It may be called when no
* tracks are selected.
*
* <p>This method should only be called after the period has been prepared. It may be called when
* no tracks are selected.
*/
@Override
long getNextLoadPositionUs();
/**
* Attempts to continue loading.
* <p>
* This method may be called both during and after the period has been prepared.
* <p>
* A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
*
* <p>This method may be called both during and after the period has been prepared.
*
* <p>A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
* {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
* called when the period is permitted to continue loading data. A period may do this both during
* and after preparation.
@ -182,10 +184,24 @@ public interface MediaPeriod extends SequenceableLoader {
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
* a different value than prior to the call. False otherwise.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a
* different value than prior to the call. False otherwise.
*/
@Override
boolean continueLoading(long positionUs);
/**
* Re-evaluates the buffer given the playback position.
*
* <p>This method should only be called after the period has been prepared.
*
* <p>A period may choose to discard buffered media so that it can be re-buffered in a different
* quality.
*
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
*/
@Override
void reevaluateBuffer(long positionUs);
}

View file

@ -139,6 +139,11 @@ import java.util.IdentityHashMap;
}
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);

View file

@ -60,4 +60,15 @@ public interface SequenceableLoader {
*/
boolean continueLoading(long positionUs);
/**
* Re-evaluates the buffer given the playback position.
*
* <p>Re-evaluation may discard buffered media so that it can be re-buffered in a different
* quality.
*
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
*/
void reevaluateBuffer(long positionUs);
}

View file

@ -120,6 +120,11 @@ import java.util.Arrays;
// Do nothing.
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long positionUs) {
if (loadingFinished || loader.isLoading()) {

View file

@ -319,7 +319,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
IOException error) {
long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable);
boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk();
int lastChunkIndex = mediaChunks.size() - 1;
boolean cancelable =
bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex);
boolean canceled = false;
if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
if (!cancelable) {
@ -327,12 +329,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} else {
canceled = true;
if (isMediaChunk) {
BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1);
BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);
Assertions.checkState(removed == loadable);
primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
}
if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs;
}
@ -405,35 +403,29 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
}
// Internal methods
// TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming.
/**
* Discards media chunks from the back of the buffer if conditions have changed such that it's
* preferable to re-buffer the media at a different quality.
*
* @param positionUs The current playback position in microseconds.
*/
@SuppressWarnings("unused")
private void maybeDiscardUpstream(long positionUs) {
@Override
public void reevaluateBuffer(long positionUs) {
if (loader.isLoading() || isPendingReset()) {
return;
}
int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
discardUpstreamMediaChunks(Math.max(1, queueSize));
discardUpstreamMediaChunks(queueSize);
}
// Internal methods
private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof BaseMediaChunk;
}
/**
* Returns whether samples have been read from {@code mediaChunks.getLast()}.
*/
private boolean haveReadFromLastMediaChunk() {
BaseMediaChunk lastChunk = getLastMediaChunk();
if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) {
/** Returns whether samples have been read from media chunk at given index. */
private boolean haveReadFromMediaChunk(int mediaChunkIndex) {
BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {
return true;
}
for (int i = 0; i < embeddedSampleQueues.length; i++) {
if (embeddedSampleQueues[i].getReadIndex() > lastChunk.getFirstSampleIndex(i + 1)) {
if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) {
return true;
}
}
@ -492,27 +484,51 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
/**
* Discard upstream media chunks until the queue length is equal to the length specified.
* Discard upstream media chunks until the queue length is equal to the length specified, but
* avoid discarding any chunk whose samples have been read by either primary sample stream or
* embedded sample streams.
*
* @param queueLength The desired length of the queue.
* @return Whether chunks were discarded.
* @param desiredQueueSize The desired length of the queue. The final queue size after discarding
* maybe larger than this if there are chunks after the specified position that have been read
* by either primary sample stream or embedded sample streams.
*/
private boolean discardUpstreamMediaChunks(int queueLength) {
if (mediaChunks.size() <= queueLength) {
return false;
private void discardUpstreamMediaChunks(int desiredQueueSize) {
if (mediaChunks.size() <= desiredQueueSize) {
return;
}
int firstIndexToRemove = desiredQueueSize;
for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) {
if (!haveReadFromMediaChunk(i)) {
firstIndexToRemove = i;
break;
}
}
if (firstIndexToRemove == mediaChunks.size()) {
return;
}
long endTimeUs = getLastMediaChunk().endTimeUs;
BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength);
long startTimeUs = firstRemovedChunk.startTimeUs;
Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size());
BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove);
loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs);
}
/**
* Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample
* queues.
*
* @param chunkIndex The index of the first chunk to discard.
* @return The chunk at given index.
*/
private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {
BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);
Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());
primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));
}
loadingFinished = false;
eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
return true;
return firstRemovedChunk;
}
/**

View file

@ -15,12 +15,12 @@
*/
package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
@ -42,17 +42,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final int minDurationToRetainAfterDiscardMs;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final long minTimeBetweenBufferReevaluationMs;
private final Clock clock;
/**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/
public Factory(BandwidthMeter bandwidthMeter) {
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
this(
bandwidthMeter,
DEFAULT_MAX_INITIAL_BITRATE,
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,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
Clock.DEFAULT);
}
/**
@ -74,37 +80,55 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs,
bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
this(
bandwidthMeter,
maxInitialBitrate,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bandwidthFraction,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
Clock.DEFAULT);
}
/**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
* when a bandwidth estimate is unavailable.
* @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 maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
* bandwidth estimate is unavailable.
* @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 track of significantly 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.
* @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.
* 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 minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
* buffer and discard some chunks of lower quality to improve the playback quality if
* network conditions have changed. This is the minimum duration between 2 consecutive
* buffer reevaluation calls.
* @param clock A {@link Clock}.
*/
public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs, float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease) {
public Factory(
BandwidthMeter bandwidthMeter,
int maxInitialBitrate,
int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
long minTimeBetweenBufferReevaluationMs,
Clock clock) {
this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate;
this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
@ -113,14 +137,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
this.clock = clock;
}
@Override
public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate,
minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs, bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease);
return new AdaptiveTrackSelection(
group,
tracks,
bandwidthMeter,
maxInitialBitrate,
minDurationForQualityIncreaseMs,
maxDurationForQualityDecreaseMs,
minDurationToRetainAfterDiscardMs,
bandwidthFraction,
bufferedFractionToLiveEdgeForQualityIncrease,
minTimeBetweenBufferReevaluationMs,
clock);
}
}
@ -131,6 +165,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;
private final BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate;
@ -139,10 +174,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final long minDurationToRetainAfterDiscardUs;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final long minTimeBetweenBufferReevaluationMs;
private final Clock clock;
private float playbackSpeed;
private int selectedIndex;
private int reason;
private long lastBufferEvaluationMs;
/**
* @param group The {@link TrackGroup}.
@ -152,12 +190,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
*/
public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
BandwidthMeter bandwidthMeter) {
this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
this(
group,
tracks,
bandwidthMeter,
DEFAULT_MAX_INITIAL_BITRATE,
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,
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
Clock.DEFAULT);
}
/**
@ -172,23 +216,35 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
* @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 track of significantly 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
* 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.
* @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.
* 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 minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
* buffer and discard some chunks of lower quality to improve the playback quality if network
* condition has changed. This is the minimum duration between 2 consecutive buffer
* reevaluation calls.
*/
public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter,
int maxInitialBitrate, long minDurationForQualityIncreaseMs,
long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs,
float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) {
public AdaptiveTrackSelection(
TrackGroup group,
int[] tracks,
BandwidthMeter bandwidthMeter,
int maxInitialBitrate,
long minDurationForQualityIncreaseMs,
long maxDurationForQualityDecreaseMs,
long minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
long minTimeBetweenBufferReevaluationMs,
Clock clock) {
super(group, tracks);
this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate;
@ -198,9 +254,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
this.clock = clock;
playbackSpeed = 1f;
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
reason = C.SELECTION_REASON_INITIAL;
lastBufferEvaluationMs = C.TIME_UNSET;
}
@Override
public void enable() {
lastBufferEvaluationMs = C.TIME_UNSET;
}
@Override
@ -211,7 +275,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs,
long availableDurationUs) {
long nowMs = SystemClock.elapsedRealtime();
long nowMs = clock.elapsedRealtime();
// Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex;
selectedIndex = determineIdealSelectedIndex(nowMs);
@ -258,17 +322,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
long nowMs = clock.elapsedRealtime();
if (lastBufferEvaluationMs != C.TIME_UNSET
&& nowMs - lastBufferEvaluationMs < minTimeBetweenBufferReevaluationMs) {
return queue.size();
}
lastBufferEvaluationMs = nowMs;
if (queue.isEmpty()) {
return 0;
}
int queueSize = queue.size();
long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
long playoutBufferedDurationUs =
Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed);
if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) {
MediaChunk lastChunk = queue.get(queueSize - 1);
long playoutBufferedDurationBeforeLastChunkUs =
Util.getPlayoutDurationForMediaDuration(
lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
Format idealFormat = getFormat(idealSelectedIndex);
// If the chunks contain video, discard from the first SD chunk beyond
// minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
@ -293,8 +365,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
/**
* Computes the ideal selected index ignoring buffer health.
*
* @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or
* {@link Long#MIN_VALUE} to ignore blacklisting.
* @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
* Long#MIN_VALUE} to ignore blacklisting.
*/
private int determineIdealSelectedIndex(long nowMs) {
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();

View file

@ -265,6 +265,11 @@ public final class CompositeSequenceableLoaderTest {
return loaded;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
private void setNextChunkDurationUs(int nextChunkDurationUs) {
this.nextChunkDurationUs = nextChunkDurationUs;
}

View file

@ -0,0 +1,428 @@
/*
* Copyright (C) 2017 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 com.google.android.exoplayer2.trackselection;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Unit test for {@link AdaptiveTrackSelection}. */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
public final class AdaptiveTrackSelectionTest {
@Mock private BandwidthMeter mockBandwidthMeter;
private FakeClock fakeClock;
private AdaptiveTrackSelection adaptiveTrackSelection;
@Before
public void setUp() {
initMocks(this);
fakeClock = new FakeClock(0);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE);
}
@Test
public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000);
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);
}
@Test
public void testSelectInitialIndexUseBandwidthEstimateIfAvailable() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);
adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000);
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);
}
@Test
public void testUpdateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
// initially bandwidth meter does not have any estimation. The second measurement onward returns
// 2000L, which prompts the track selection to switch up if possible.
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(
trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000);
adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 9_999_000,
/* availableDurationUs= */ C.TIME_UNSET);
// When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate
// format. However, since we only buffered 9_999_000 us, which is smaller than
// minDurationForQualityIncreaseMs, we should defer switch up.
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);
}
@Test
public void testUpdateSelectedTrackSwitchUpIfBufferedEnough() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
// initially bandwidth meter does not have any estimation. The second measurement onward returns
// 2000L, which prompts the track selection to switch up if possible.
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(
trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000);
adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 10_000_000,
/* availableDurationUs= */ C.TIME_UNSET);
// When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate
// format. When we have buffered enough (10_000_000 us, which is equal to
// minDurationForQualityIncreaseMs), we should switch up now.
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);
}
@Test
public void testUpdateSelectedTrackDoNotSwitchDownIfBufferedEnough() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
// initially bandwidth meter does not have any estimation. The second measurement onward returns
// 500L, which prompts the track selection to switch down if necessary.
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(
trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000);
adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 25_000_000,
/* availableDurationUs= */ C.TIME_UNSET);
// When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate
// format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal
// to maxDurationForQualityDecreaseMs), we should defer switch down.
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL);
}
@Test
public void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
// initially bandwidth meter does not have any estimation. The second measurement onward returns
// 500L, which prompts the track selection to switch down if necessary.
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(
trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000);
adaptiveTrackSelection.updateSelectedTrack(
/* playbackPositionUs= */ 0,
/* bufferedDurationUs= */ 24_999_000,
/* availableDurationUs= */ C.TIME_UNSET);
// When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate
// format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than
// maxDurationForQualityDecreaseMs), we should switch down now.
assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1);
assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE);
}
@Test
public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
FakeMediaChunk chunk1 =
new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);
FakeMediaChunk chunk2 =
new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);
FakeMediaChunk chunk3 =
new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);
List<FakeMediaChunk> queue = new ArrayList<>();
queue.add(chunk1);
queue.add(chunk2);
queue.add(chunk3);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);
adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000);
int size = adaptiveTrackSelection.evaluateQueueSize(0, queue);
assertThat(size).isEqualTo(3);
}
@Test
public void testEvaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
FakeMediaChunk chunk1 =
new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);
FakeMediaChunk chunk2 =
new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);
FakeMediaChunk chunk3 =
new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);
List<FakeMediaChunk> queue = new ArrayList<>();
queue.add(chunk1);
queue.add(chunk2);
queue.add(chunk3);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(
trackGroup,
/* initialBitrate= */ 1000,
/* durationToRetainAfterDiscardMs= */ 15_000,
/* minTimeBetweenBufferReevaluationMs= */ 2000);
int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);
fakeClock.advanceTime(1999);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);
// When bandwidth estimation is updated, we can discard chunks at the end of the queue now.
// However, since min duration between buffer reevaluation = 2000, we will not reevaluate
// queue size if time now is only 1999 ms after last buffer reevaluation.
int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);
assertThat(newSize).isEqualTo(initialQueueSize);
}
@Test
public void testEvaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() {
Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240);
Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480);
Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720);
TrackGroup trackGroup = new TrackGroup(format1, format2, format3);
FakeMediaChunk chunk1 =
new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000);
FakeMediaChunk chunk2 =
new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000);
FakeMediaChunk chunk3 =
new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000);
List<FakeMediaChunk> queue = new ArrayList<>();
queue.add(chunk1);
queue.add(chunk2);
queue.add(chunk3);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L);
adaptiveTrackSelection =
adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(
trackGroup,
/* initialBitrate= */ 1000,
/* durationToRetainAfterDiscardMs= */ 15_000,
/* minTimeBetweenBufferReevaluationMs= */ 2000);
int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);
assertThat(initialQueueSize).isEqualTo(3);
fakeClock.advanceTime(2000);
when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L);
// When bandwidth estimation is updated and time has advanced enough, we can discard chunks at
// the end of the queue now.
// However, since duration to retain after discard = 15 000 ms, we need to retain at least the
// first 2 chunks
int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue);
assertThat(newSize).isEqualTo(2);
}
private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup, int initialBitrate) {
return new AdaptiveTrackSelection(
trackGroup,
selectedAllTracksInGroup(trackGroup),
mockBandwidthMeter,
initialBitrate,
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,
AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
fakeClock);
}
private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs(
TrackGroup trackGroup, int initialBitrate, long minDurationForQualityIncreaseMs) {
return new AdaptiveTrackSelection(
trackGroup,
selectedAllTracksInGroup(trackGroup),
mockBandwidthMeter,
initialBitrate,
minDurationForQualityIncreaseMs,
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,
AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
fakeClock);
}
private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs(
TrackGroup trackGroup, int initialBitrate, long maxDurationForQualityDecreaseMs) {
return new AdaptiveTrackSelection(
trackGroup,
selectedAllTracksInGroup(trackGroup),
mockBandwidthMeter,
initialBitrate,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
maxDurationForQualityDecreaseMs,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
/* bandwidthFraction= */ 1.0f,
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
fakeClock);
}
private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs(
TrackGroup trackGroup,
int initialBitrate,
long durationToRetainAfterDiscardMs,
long minTimeBetweenBufferReevaluationMs) {
return new AdaptiveTrackSelection(
trackGroup,
selectedAllTracksInGroup(trackGroup),
mockBandwidthMeter,
initialBitrate,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
durationToRetainAfterDiscardMs,
/* bandwidth fraction= */ 1.0f,
AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
minTimeBetweenBufferReevaluationMs,
fakeClock);
}
private int[] selectedAllTracksInGroup(TrackGroup trackGroup) {
int[] listIndices = new int[trackGroup.length];
for (int i = 0; i < trackGroup.length; i++) {
listIndices[i] = i;
}
return listIndices;
}
private static Format videoFormat(int bitrate, int width, int height) {
return Format.createVideoSampleFormat(
/* id= */ null,
/* sampleMimeType= */ MimeTypes.VIDEO_H264,
/* codecs= */ null,
/* bitrate= */ bitrate,
/* maxInputSize= */ Format.NO_VALUE,
/* width= */ width,
/* height= */ height,
/* frameRate= */ Format.NO_VALUE,
/* initializationData= */ null,
/* drmInitData= */ null);
}
private static final class FakeMediaChunk extends MediaChunk {
private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null);
public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) {
super(
DATA_SOURCE,
new DataSpec(Uri.EMPTY),
trackFormat,
C.SELECTION_REASON_ADAPTIVE,
null,
startTimeUs,
endTimeUs,
0);
}
@Override
public void cancelLoad() {
// Do nothing.
}
@Override
public boolean isLoadCanceled() {
return false;
}
@Override
public void load() throws IOException, InterruptedException {
// Do nothing.
}
@Override
public boolean isLoadCompleted() {
return true;
}
@Override
public long bytesLoaded() {
return 0;
}
}
}

View file

@ -270,6 +270,11 @@ import java.util.Map;
}
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);

View file

@ -195,6 +195,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);

View file

@ -524,6 +524,11 @@ import java.util.Arrays;
return true;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
// Loader.Callback implementation.
@Override

View file

@ -149,6 +149,11 @@ import java.util.ArrayList;
}
}
@Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs);

View file

@ -203,8 +203,20 @@ public class SsManifest {
long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes,
long lastChunkDuration) {
this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight,
displayWidth, displayHeight, language, formats, chunkStartTimes,
this(
baseUri,
chunkTemplate,
type,
subType,
timescale,
name,
maxWidth,
maxHeight,
displayWidth,
displayHeight,
language,
formats,
chunkStartTimes,
Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale),
Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale));
}

View file

@ -151,6 +151,11 @@ public class FakeMediaPeriod implements MediaPeriod {
// Do nothing.
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
Assert.assertTrue(prepared);