Commit 88dea59c by hoangtc Committed by Oliver Woodman

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
parent 6606d73b
Showing with 765 additions and 139 deletions
...@@ -2,6 +2,8 @@ ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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;
......
...@@ -124,6 +124,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -124,6 +124,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs + startUs);
}
@Override
public long readDiscontinuity() { public long readDiscontinuity() {
if (isPendingInitialDiscontinuity()) { if (isPendingInitialDiscontinuity()) {
long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs; long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs;
......
...@@ -53,6 +53,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader { ...@@ -53,6 +53,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader {
} }
@Override @Override
public final void reevaluateBuffer(long positionUs) {
for (SequenceableLoader loader : loaders) {
loader.reevaluateBuffer(positionUs);
}
}
@Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
boolean madeProgress = false; boolean madeProgress = false;
boolean madeProgressThisIteration; boolean madeProgressThisIteration;
......
...@@ -120,6 +120,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -120,6 +120,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
mediaPeriod.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
} }
......
...@@ -289,6 +289,11 @@ import java.util.Arrays; ...@@ -289,6 +289,11 @@ import java.util.Arrays;
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long playbackPositionUs) { public boolean continueLoading(long playbackPositionUs) {
if (loadingFinished || (prepared && enabledTrackCount == 0)) { if (loadingFinished || (prepared && enabledTrackCount == 0)) {
return false; return false;
......
...@@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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
...@@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader { ...@@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader {
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
* have been retained but with the requirement that the consuming renderer be reset. * have been retained but with the requirement that the consuming renderer be reset.
* @param positionUs The current playback position in microseconds. If playback of this period has * @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position. * not yet started, the value will be the starting position.
* @return The actual position at which the tracks were enabled, in microseconds. * @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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
...@@ -140,6 +140,11 @@ import java.util.IdentityHashMap; ...@@ -140,6 +140,11 @@ import java.util.IdentityHashMap;
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@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 { ...@@ -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);
} }
...@@ -121,6 +121,11 @@ import java.util.Arrays; ...@@ -121,6 +121,11 @@ import java.util.Arrays;
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
if (loadingFinished || loader.isLoading()) { if (loadingFinished || loader.isLoading()) {
return false; return false;
......
...@@ -319,7 +319,9 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S ...@@ -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 ...@@ -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 ...@@ -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); int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
discardUpstreamMediaChunks(Math.max(1, queueSize)); 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 ...@@ -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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
......
/*
* 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;
}
}
}
...@@ -271,6 +271,11 @@ import java.util.Map; ...@@ -271,6 +271,11 @@ import java.util.Map;
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) { public boolean continueLoading(long positionUs) {
return compositeSequenceableLoader.continueLoading(positionUs); return compositeSequenceableLoader.continueLoading(positionUs);
} }
......
...@@ -196,6 +196,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -196,6 +196,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@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; ...@@ -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
......
...@@ -150,6 +150,11 @@ import java.util.ArrayList; ...@@ -150,6 +150,11 @@ import java.util.ArrayList;
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
compositeSequenceableLoader.reevaluateBuffer(positionUs);
}
@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 { ...@@ -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));
} }
......
...@@ -152,6 +152,11 @@ public class FakeMediaPeriod implements MediaPeriod { ...@@ -152,6 +152,11 @@ public class FakeMediaPeriod implements MediaPeriod {
} }
@Override @Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() { public long readDiscontinuity() {
Assert.assertTrue(prepared); Assert.assertTrue(prepared);
long positionDiscontinuityUs = this.discontinuityPositionUs; long positionDiscontinuityUs = this.discontinuityPositionUs;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment