diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 9d98f2aae0..80c55c4706 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -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`
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index b0ef675e71..83e7858eaa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -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;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index 36e8e51ffb..539c4841e9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -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()) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
index e9a187a747..c41933b48b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -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;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
index bc29b2fdf1..32a180b956 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
@@ -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);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index 17a6c3bcb8..6b9aeb39da 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -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)) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index 439562e0ab..54b34bc531 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Called when preparation completes.
- *
- * 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.
+ *
+ *
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.
- *
- * {@code callback.onPrepared} is called when preparation completes. If preparation fails,
+ *
+ *
{@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
- *
- * 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}.
+ *
+ *
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.
- *
- * This method should only be called before the period has completed preparation.
+ *
+ *
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.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
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.
- *
- * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
+ *
+ *
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.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
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.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
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.
- *
- * 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.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
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.
+ *
+ *
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.
- *
- * After this method has been called, all {@link SampleStream}s provided by the period are
+ *
+ *
After this method has been called, all {@link SampleStream}s provided by the period are
* guaranteed to start from a key frame.
- *
- * This method should only be called when at least one track is selected.
+ *
+ *
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.
- *
- * This method should only be called when at least one track is selected.
+ *
+ *
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.
- *
- * This method should only be called after the period has been prepared. It may be called when no
- * tracks are selected.
+ *
+ *
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.
- *
- * This method may be called both during and after the period has been prepared.
- *
- * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
+ *
+ *
This method may be called both during and after the period has been prepared.
+ *
+ *
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.
+ *
+ *
This method should only be called after the period has been prepared.
+ *
+ *
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);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index bd37b5efec..5ac9fc8d97 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -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);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
index 6daa1e847a..182f0f17cc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -60,4 +60,15 @@ public interface SequenceableLoader {
*/
boolean continueLoading(long positionUs);
+ /**
+ * Re-evaluates the buffer given the playback position.
+ *
+ *
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);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 0cea0fad66..7b8b54eedc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -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()) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index 20b56e7807..85c4b12241 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -319,7 +319,9 @@ public class ChunkSampleStream 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 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 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 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;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
index ba45b2b186..973155c2e3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -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();
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java
index e3ac104754..f7e29d2b06 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java
@@ -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;
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
new file mode 100644
index 0000000000..ea19c72826
--- /dev/null
+++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java
@@ -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 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 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 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;
+ }
+ }
+}
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
index 8fe10e94ee..f320ad2844 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
@@ -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);
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
index dd596878d2..fd8f2bdbe9 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -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);
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index dbb71329c5..2e69e41d30 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -524,6 +524,11 @@ import java.util.Arrays;
return true;
}
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
// Loader.Callback implementation.
@Override
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
index d418a21dff..564993befe 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
@@ -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);
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
index fbc3726a0e..0df180a5a6 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
@@ -203,8 +203,20 @@ public class SsManifest {
long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, Format[] formats, List 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));
}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
index c1be199b1e..d34c1d1c0c 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
@@ -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);