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) ### ### 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. * Replace `DefaultTrackSelector.Parameters` copy methods with a builder.
* Allow more flexible loading strategy when playing media containing multiple * Allow more flexible loading strategy when playing media containing multiple
sub-streams, by allowing injection of custom `CompositeSequenceableLoader` sub-streams, by allowing injection of custom `CompositeSequenceableLoader`

View file

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

View file

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

View file

@ -52,6 +52,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader {
return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs; 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 @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
boolean madeProgress = false; boolean madeProgress = false;

View file

@ -119,6 +119,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
return mediaPeriod.getNextLoadPositionUs(); return mediaPeriod.getNextLoadPositionUs();
} }
@Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs);
}
@Override @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(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 @Override
public boolean continueLoading(long playbackPositionUs) { public boolean continueLoading(long playbackPositionUs) {
if (loadingFinished || (prepared && enabledTrackCount == 0)) { if (loadingFinished || (prepared && enabledTrackCount == 0)) {

View file

@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader {
/** /**
* Called when preparation completes. * Called when preparation completes.
* <p> *
* Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect * <p>Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
* for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
* called with the initial track selection. * long)} to be called with the initial track selection.
* *
* @param mediaPeriod The prepared {@link MediaPeriod}. * @param mediaPeriod The prepared {@link MediaPeriod}.
*/ */
void onPrepared(MediaPeriod mediaPeriod); void onPrepared(MediaPeriod mediaPeriod);
} }
/** /**
* Prepares this media period asynchronously. * 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}. * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
* <p> *
* If preparation succeeds and results in a source timeline change (e.g. the period duration * <p>If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known), * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline,
* {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be * Object)} will be called before {@code callback.onPrepared}.
* called before {@code callback.onPrepared}.
* *
* @param callback Callback to receive updates from this period, including being notified when * @param callback Callback to receive updates from this period, including being notified when
* preparation completes. * 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 * Throws an error that's preventing the period from becoming prepared. Does nothing if no such
* error exists. * 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. * @throws IOException The underlying error.
*/ */
@ -75,8 +73,8 @@ public interface MediaPeriod extends SequenceableLoader {
/** /**
* Returns the {@link TrackGroup}s exposed by the period. * 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. * @return The {@link TrackGroup}s.
*/ */
@ -84,16 +82,16 @@ public interface MediaPeriod extends SequenceableLoader {
/** /**
* Performs a track selection. * 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 * 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 * 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 * 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 * 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 * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created. * 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 selections The renderer track selections.
* @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
@ -107,13 +105,17 @@ public interface MediaPeriod extends SequenceableLoader {
* 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. * @return The actual position at which the tracks were enabled, in microseconds.
*/ */
long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, long selectTracks(
SampleStream[] streams, boolean[] streamResetFlags, long positionUs); TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs);
/** /**
* Discards buffered media up to the specified position. * 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 positionUs The position in microseconds.
* @param toKeyframe If true then for each track discards samples up to the keyframe before or at * @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. * Attempts to read a discontinuity.
* <p> *
* After this method has returned a value other than {@link C#TIME_UNSET}, all * <p>After this method has returned a value other than {@link C#TIME_UNSET}, all {@link
* {@link SampleStream}s provided by the period are guaranteed to start from a key frame. * 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>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 * @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}. * discontinuity. Else {@link C#TIME_UNSET}.
@ -136,11 +138,11 @@ public interface MediaPeriod extends SequenceableLoader {
/** /**
* Attempts to seek to the specified position in microseconds. * 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. * 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. * @param positionUs The seek position in microseconds.
* @return The actual position to which the period was seeked, 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. * 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 * @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. * {@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. * 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 * <p>This method should only be called after the period has been prepared. It may be called when
* tracks are selected. * no tracks are selected.
*/ */
@Override @Override
long getNextLoadPositionUs(); long getNextLoadPositionUs();
/** /**
* Attempts to continue loading. * Attempts to continue loading.
* <p> *
* This method may be called both during and after the period has been prepared. * <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>A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
* {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be * {@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 * called when the period is permitted to continue loading data. A period may do this both during
* and after preparation. * 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 * @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 * 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. * of any media in previous periods still to be played.
* @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a
* a different value than prior to the call. False otherwise. * different value than prior to the call. False otherwise.
*/ */
@Override @Override
boolean continueLoading(long positionUs); 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 @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs); return compositeSequenceableLoader.continueLoading(positionUs);

View file

@ -60,4 +60,15 @@ public interface SequenceableLoader {
*/ */
boolean continueLoading(long positionUs); 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. // Do nothing.
} }
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
if (loadingFinished || loader.isLoading()) { if (loadingFinished || loader.isLoading()) {

View file

@ -319,7 +319,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
IOException error) { IOException error) {
long bytesLoaded = loadable.bytesLoaded(); long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable); 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; boolean canceled = false;
if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
if (!cancelable) { if (!cancelable) {
@ -327,12 +329,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} else { } else {
canceled = true; canceled = true;
if (isMediaChunk) { if (isMediaChunk) {
BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);
Assertions.checkState(removed == loadable); 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()) { if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs; pendingResetPositionUs = lastSeekPositionUs;
} }
@ -405,35 +403,29 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} }
} }
// Internal methods @Override
public void reevaluateBuffer(long positionUs) {
// TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming. if (loader.isLoading() || isPendingReset()) {
/** return;
* 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) {
int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
discardUpstreamMediaChunks(Math.max(1, queueSize));
} }
int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
discardUpstreamMediaChunks(queueSize);
}
// Internal methods
private boolean isMediaChunk(Chunk chunk) { private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof BaseMediaChunk; return chunk instanceof BaseMediaChunk;
} }
/** /** Returns whether samples have been read from media chunk at given index. */
* Returns whether samples have been read from {@code mediaChunks.getLast()}. private boolean haveReadFromMediaChunk(int mediaChunkIndex) {
*/ BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
private boolean haveReadFromLastMediaChunk() { if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {
BaseMediaChunk lastChunk = getLastMediaChunk();
if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) {
return true; return true;
} }
for (int i = 0; i < embeddedSampleQueues.length; i++) { 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; 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. * @param desiredQueueSize The desired length of the queue. The final queue size after discarding
* @return Whether chunks were discarded. * 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) { private void discardUpstreamMediaChunks(int desiredQueueSize) {
if (mediaChunks.size() <= queueLength) { if (mediaChunks.size() <= desiredQueueSize) {
return false; 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; long endTimeUs = getLastMediaChunk().endTimeUs;
BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength); BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove);
long startTimeUs = firstRemovedChunk.startTimeUs; loadingFinished = false;
Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size()); 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)); primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) { for (int i = 0; i < embeddedSampleQueues.length; i++) {
embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));
} }
loadingFinished = false; return firstRemovedChunk;
eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
return true;
} }
/** /**

View file

@ -15,12 +15,12 @@
*/ */
package com.google.android.exoplayer2.trackselection; package com.google.android.exoplayer2.trackselection;
import android.os.SystemClock;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.List; import java.util.List;
@ -42,17 +42,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final int minDurationToRetainAfterDiscardMs; private final int minDurationToRetainAfterDiscardMs;
private final float bandwidthFraction; private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease; private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final long minTimeBetweenBufferReevaluationMs;
private final Clock clock;
/** /**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/ */
public Factory(BandwidthMeter bandwidthMeter) { public Factory(BandwidthMeter bandwidthMeter) {
this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, this(
bandwidthMeter,
DEFAULT_MAX_INITIAL_BITRATE,
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
DEFAULT_BANDWIDTH_FRACTION, 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, public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, this(
maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, bandwidthMeter,
bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); 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 bandwidthMeter Provides an estimate of the currently available bandwidth.
* @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
* when a bandwidth estimate is unavailable. * bandwidth estimate is unavailable.
* @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
* the selected track to switch to one of higher quality. * selected track to switch to one of higher quality.
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
* the selected track to switch to one of lower quality. * selected track to switch to one of lower quality.
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
* quality, the selection may indicate that media already buffered at the lower quality can * 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 * be discarded to speed up the switch. This is the minimum duration of media that must be
* retained at the lower quality. * retained at the lower quality.
* @param bandwidthFraction The fraction of the available bandwidth that the selection should * @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 * consider available for use. Setting to a value less than 1 is recommended to account for
* for inaccuracies in the bandwidth estimator. * inaccuracies in the bandwidth estimator.
* @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
* the duration from current playback position to the live edge that has to be buffered * duration from current playback position to the live edge that has to be buffered before
* before the selected track can be switched to one of higher quality. This parameter is * the selected track can be switched to one of higher quality. This parameter is only
* only applied when the playback position is closer to the live edge than * applied when the playback position is closer to the live edge than {@code
* {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
* higher quality from happening. * 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, public Factory(
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, BandwidthMeter bandwidthMeter,
int minDurationToRetainAfterDiscardMs, float bandwidthFraction, int maxInitialBitrate,
float bufferedFractionToLiveEdgeForQualityIncrease) { int minDurationForQualityIncreaseMs,
int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
long minTimeBetweenBufferReevaluationMs,
Clock clock) {
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate; this.maxInitialBitrate = maxInitialBitrate;
this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
@ -113,14 +137,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction; this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease = this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease; bufferedFractionToLiveEdgeForQualityIncrease;
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
this.clock = clock;
} }
@Override @Override
public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, return new AdaptiveTrackSelection(
minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, group,
minDurationToRetainAfterDiscardMs, bandwidthFraction, tracks,
bufferedFractionToLiveEdgeForQualityIncrease); 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 int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; 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 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 BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate; private final int maxInitialBitrate;
@ -139,10 +174,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final long minDurationToRetainAfterDiscardUs; private final long minDurationToRetainAfterDiscardUs;
private final float bandwidthFraction; private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease; private final float bufferedFractionToLiveEdgeForQualityIncrease;
private final long minTimeBetweenBufferReevaluationMs;
private final Clock clock;
private float playbackSpeed; private float playbackSpeed;
private int selectedIndex; private int selectedIndex;
private int reason; private int reason;
private long lastBufferEvaluationMs;
/** /**
* @param group The {@link TrackGroup}. * @param group The {@link TrackGroup}.
@ -152,12 +190,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
*/ */
public AdaptiveTrackSelection(TrackGroup group, int[] tracks, public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
BandwidthMeter bandwidthMeter) { 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_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
DEFAULT_BANDWIDTH_FRACTION, 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 * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
* selected track to switch to one of lower quality. * selected track to switch to one of lower quality.
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
* quality, the selection may indicate that media already buffered at the lower quality can * quality, the selection may indicate that media already buffered at the lower quality can be
* be discarded to speed up the switch. This is the minimum duration of media that must be * discarded to speed up the switch. This is the minimum duration of media that must be
* retained at the lower quality. * retained at the lower quality.
* @param bandwidthFraction The fraction of the available bandwidth that the selection should * @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 * consider available for use. Setting to a value less than 1 is recommended to account for
* for inaccuracies in the bandwidth estimator. * inaccuracies in the bandwidth estimator.
* @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
* the duration from current playback position to the live edge that has to be buffered * duration from current playback position to the live edge that has to be buffered before the
* before the selected track can be switched to one of higher quality. This parameter is * selected track can be switched to one of higher quality. This parameter is only applied
* only applied when the playback position is closer to the live edge than * when the playback position is closer to the live edge than {@code
* {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
* higher quality from happening. * 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, public AdaptiveTrackSelection(
int maxInitialBitrate, long minDurationForQualityIncreaseMs, TrackGroup group,
long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, int[] tracks,
float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { BandwidthMeter bandwidthMeter,
int maxInitialBitrate,
long minDurationForQualityIncreaseMs,
long maxDurationForQualityDecreaseMs,
long minDurationToRetainAfterDiscardMs,
float bandwidthFraction,
float bufferedFractionToLiveEdgeForQualityIncrease,
long minTimeBetweenBufferReevaluationMs,
Clock clock) {
super(group, tracks); super(group, tracks);
this.bandwidthMeter = bandwidthMeter; this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate; this.maxInitialBitrate = maxInitialBitrate;
@ -198,9 +254,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction; this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease = this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease; bufferedFractionToLiveEdgeForQualityIncrease;
this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
this.clock = clock;
playbackSpeed = 1f; playbackSpeed = 1f;
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
reason = C.SELECTION_REASON_INITIAL; reason = C.SELECTION_REASON_INITIAL;
lastBufferEvaluationMs = C.TIME_UNSET;
}
@Override
public void enable() {
lastBufferEvaluationMs = C.TIME_UNSET;
} }
@Override @Override
@ -211,7 +275,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override @Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs,
long availableDurationUs) { long availableDurationUs) {
long nowMs = SystemClock.elapsedRealtime(); long nowMs = clock.elapsedRealtime();
// Stash the current selection, then make a new one. // Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex; int currentSelectedIndex = selectedIndex;
selectedIndex = determineIdealSelectedIndex(nowMs); selectedIndex = determineIdealSelectedIndex(nowMs);
@ -258,17 +322,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override @Override
public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) { 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()) { if (queue.isEmpty()) {
return 0; return 0;
} }
int queueSize = queue.size(); int queueSize = queue.size();
long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; MediaChunk lastChunk = queue.get(queueSize - 1);
long playoutBufferedDurationUs = long playoutBufferedDurationBeforeLastChunkUs =
Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed); Util.getPlayoutDurationForMediaDuration(
if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) { lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
return queueSize; return queueSize;
} }
int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
Format idealFormat = getFormat(idealSelectedIndex); Format idealFormat = getFormat(idealSelectedIndex);
// If the chunks contain video, discard from the first SD chunk beyond // If the chunks contain video, discard from the first SD chunk beyond
// minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal // 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. * Computes the ideal selected index ignoring buffer health.
* *
* @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
* {@link Long#MIN_VALUE} to ignore blacklisting. * Long#MIN_VALUE} to ignore blacklisting.
*/ */
private int determineIdealSelectedIndex(long nowMs) { private int determineIdealSelectedIndex(long nowMs) {
long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); long bitrateEstimate = bandwidthMeter.getBitrateEstimate();

View file

@ -265,6 +265,11 @@ public final class CompositeSequenceableLoaderTest {
return loaded; return loaded;
} }
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
private void setNextChunkDurationUs(int nextChunkDurationUs) { private void setNextChunkDurationUs(int nextChunkDurationUs) {
this.nextChunkDurationUs = 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 @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(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 @Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs); return compositeSequenceableLoader.continueLoading(positionUs);

View file

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

View file

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

View file

@ -203,8 +203,20 @@ public class SsManifest {
long timescale, String name, int maxWidth, int maxHeight, int displayWidth, long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes, int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes,
long lastChunkDuration) { long lastChunkDuration) {
this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight, this(
displayWidth, displayHeight, language, formats, chunkStartTimes, baseUri,
chunkTemplate,
type,
subType,
timescale,
name,
maxWidth,
maxHeight,
displayWidth,
displayHeight,
language,
formats,
chunkStartTimes,
Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale), Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale),
Util.scaleLargeTimestamp(lastChunkDuration, 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. // Do nothing.
} }
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override @Override
public long readDiscontinuity() { public long readDiscontinuity() {
Assert.assertTrue(prepared); Assert.assertTrue(prepared);