mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
6606d73b29
commit
88dea59cd2
20 changed files with 765 additions and 139 deletions
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue