diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkHolder.java similarity index 62% rename from library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java rename to library/src/main/java/com/google/android/exoplayer/chunk/ChunkHolder.java index 1e2087178b..7fac1fde51 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkHolder.java @@ -16,20 +16,9 @@ package com.google.android.exoplayer.chunk; /** - * Holds a chunk operation, which consists of a either: - *
- * This method should only be called when the source is enabled. + * Removing {@link MediaChunk}s from the back of the queue can be useful if they could be replaced + * with chunks of a significantly higher quality (e.g. because the available bandwidth has + * substantially increased). * - * @param queue A representation of the currently buffered {@link MediaChunk}s. - * @param playbackPositionUs The current playback position. If the queue is empty then this + * @param playbackPositionUs The current playback position. + * @param queue The queue of buffered {@link MediaChunk}s. + * @return The preferred queue size. + */ + int getPreferredQueueSize(long playbackPositionUs, List extends MediaChunk> queue); + + /** + * Gets the next chunk to load. + *
+ * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has + * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the + * end of the stream has not been reached, the {@link ChunkHolder} is not modified. + * + * @param previous The most recently loaded media chunk. + * @param playbackPositionUs The current playback position. If {@code previous} is null then this * parameter is the position from which playback is expected to start (or restart) and hence * should be interpreted as a seek position. - * @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is - * initially set to false, whose {@link ChunkOperationHolder#queueSize} is initially equal to - * the length of the queue, and whose {@link ChunkOperationHolder#chunk} is initially equal to - * null or a {@link Chunk} previously supplied by the {@link ChunkSource} that the caller has - * not yet finished loading. In the latter case the chunk can either be replaced or left - * unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than - * replacing it with a new but identical chunk. + * @param out A holder to populate. */ - void getChunkOperation(List extends MediaChunk> queue, long playbackPositionUs, - ChunkOperationHolder out); + void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out); /** * Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java index 7b2420add9..50ea402f5a 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java @@ -42,26 +42,30 @@ public interface FormatEvaluator { * Update the supplied evaluation. *
* When invoked, {@code evaluation} must contain the currently selected format (null for an - * initial evaluation), the most recent trigger (@link Chunk#TRIGGER_INITIAL} for an initial - * evaluation) and the size of {@code queue}. The invocation will update the format and trigger, - * and may also reduce {@link Evaluation#queueSize} to indicate that chunks should be discarded - * from the end of the queue to allow re-buffering in a different format. The evaluation will - * always retain the first chunk in the queue, if there is one. + * initial evaluation) and the most recent trigger {@link Chunk#TRIGGER_INITIAL} for an initial + * evaluation). * - * @param queue A read only representation of currently buffered chunks. Must not be empty unless - * the evaluation is at the start of playback or immediately follows a seek. All but the first - * chunk may be discarded. A caller may pass a singleton list containing only the most - * recently buffered chunk in the case that it does not support discarding of chunks. - * @param playbackPositionUs The current playback position in microseconds. - * @param switchingOverlapUs If switching format requires downloading overlapping media then this - * is the duration of the required overlap in microseconds. 0 otherwise. + * @param bufferedDurationUs The duration of media currently buffered in microseconds. * @param blacklistFlags An array whose length is equal to the number of available formats. A * {@code true} element indicates that a format is currently blacklisted and should not be * selected by the evaluation. At least one element must be {@code false}. * @param evaluation The evaluation to be updated. */ - void evaluate(List extends MediaChunk> queue, long playbackPositionUs, - long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation); + void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, + Evaluation evaluation); + + /** + * Evaluates whether to discard {@link MediaChunk}s from the queue. + * + * @param playbackPositionUs The current playback position in microseconds. + * @param queue The queue of buffered {@link MediaChunk}s. + * @param blacklistFlags An array whose length is equal to the number of available formats. A + * {@code true} element indicates that a format is currently blacklisted and should not be + * selected by the evaluation. At least one element must be {@code false}. + * @return The preferred queue size. + */ + int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue, + boolean[] blacklistFlags); /** * A format evaluation. @@ -78,11 +82,6 @@ public interface FormatEvaluator { */ public int trigger; - /** - * The desired size of the queue. - */ - public int queueSize; - public Evaluation() { trigger = Chunk.TRIGGER_INITIAL; } @@ -128,8 +127,8 @@ public interface FormatEvaluator { } @Override - public void evaluate(List extends MediaChunk> queue, long playbackPositionUs, - long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) { + public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, + Evaluation evaluation) { // Count the number of non-blacklisted formats. int nonBlacklistedFormatCount = 0; for (int i = 0; i < blacklistFlags.length; i++) { @@ -156,6 +155,12 @@ public interface FormatEvaluator { evaluation.format = newFormat; } + @Override + public int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue, + boolean[] blacklistFlags) { + return queue.size(); + } + } /** @@ -235,43 +240,18 @@ public interface FormatEvaluator { } @Override - public void evaluate(List extends MediaChunk> queue, long playbackPositionUs, - long switchingOverlapUs, boolean[] blacklistFlags, Evaluation evaluation) { - long bufferedDurationUs = queue.isEmpty() ? 0 - : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; - if (switchingOverlapUs > 0) { - bufferedDurationUs = Math.max(0, bufferedDurationUs - switchingOverlapUs); - } + public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, + Evaluation evaluation) { Format current = evaluation.format; Format ideal = determineIdealFormat(formats, blacklistFlags, bandwidthMeter.getBitrateEstimate()); - boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; - boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; - if (isHigher) { - if (bufferedDurationUs < minDurationForQualityIncreaseUs) { - // The ideal format is a higher quality, but we have insufficient buffer to - // safely switch up. Defer switching up for now. - ideal = current; - } else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) { - // We're switching from an SD stream to a stream of higher resolution. Consider - // discarding already buffered media chunks. Specifically, discard media chunks starting - // from the first one that is of lower bandwidth, lower resolution and that is not HD. - for (int i = 1; i < queue.size(); i++) { - MediaChunk thisChunk = queue.get(i); - long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; - if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs - && thisChunk.format.bitrate < ideal.bitrate - && thisChunk.format.height < ideal.height - && thisChunk.format.height < 720 - && thisChunk.format.width < 1280) { - // Discard chunks from this one onwards. - evaluation.queueSize = i; - break; - } - } - } - } else if (isLower && current != null - && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { + boolean isHigher = current != null && ideal.bitrate > current.bitrate; + boolean isLower = current != null && ideal.bitrate < current.bitrate; + if (isHigher && bufferedDurationUs < minDurationForQualityIncreaseUs) { + // The ideal format is a higher quality, but we have insufficient buffer to safely switch + // up. Defer switching up for now. + ideal = current; + } else if (isLower && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { // The ideal format is a lower quality, but we have sufficient buffer to defer switching // down for now. ideal = current; @@ -282,6 +262,40 @@ public interface FormatEvaluator { evaluation.format = ideal; } + @Override + public int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue, + boolean[] blacklistFlags) { + if (queue.isEmpty()) { + return 0; + } + int queueSize = queue.size(); + long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; + if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { + return queueSize; + } + Format current = queue.get(queueSize - 1).format; + Format ideal = determineIdealFormat(formats, blacklistFlags, + bandwidthMeter.getBitrateEstimate()); + if (ideal.bitrate <= current.bitrate) { + return queueSize; + } + // Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution + // and bitrate are both lower than the ideal format. + for (int i = 0; i < queueSize; i++) { + MediaChunk thisChunk = queue.get(i); + long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; + if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs + && thisChunk.format.bitrate < ideal.bitrate + && thisChunk.format.height < ideal.height + && thisChunk.format.height < 720 + && thisChunk.format.width < 1280) { + // Discard chunks from this one onwards. + return i; + } + } + return queueSize; + } + /** * Compute the ideal format ignoring buffer health. */ diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 02f2a9a8ba..cfe50bfba4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer.TimeRange.StaticTimeRange; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; -import com.google.android.exoplayer.chunk.ChunkOperationHolder; +import com.google.android.exoplayer.chunk.ChunkHolder; import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ContainerMediaChunk; import com.google.android.exoplayer.chunk.FormatEvaluator; @@ -297,17 +297,24 @@ public class DashChunkSource implements ChunkSource { } @Override - public final void getChunkOperation(List extends MediaChunk> queue, long playbackPositionUs, - ChunkOperationHolder out) { + public int getPreferredQueueSize(long playbackPositionUs, List extends MediaChunk> queue) { + if (fatalError != null || enabledFormats.length < 2) { + return queue.size(); + } + return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, + adaptiveFormatBlacklistFlags); + } + + @Override + public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { if (fatalError != null) { - out.chunk = null; return; } - evaluation.queueSize = queue.size(); if (evaluation.format == null || !lastChunkWasInitialization) { if (enabledFormats.length > 1) { - adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, 0, adaptiveFormatBlacklistFlags, + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, evaluation); } else { evaluation.format = enabledFormats[0]; @@ -316,26 +323,15 @@ public class DashChunkSource implements ChunkSource { } Format selectedFormat = evaluation.format; - out.queueSize = evaluation.queueSize; - if (selectedFormat == null) { - out.chunk = null; - return; - } else if (out.queueSize == queue.size() && out.chunk != null - && out.chunk.format == selectedFormat) { - // We already have a chunk, and the evaluation hasn't changed either the format or the size - // of the queue. Leave unchanged. return; } - // In all cases where we return before instantiating a new chunk, we want out.chunk to be null. - out.chunk = null; - boolean startingNewPeriod; PeriodHolder periodHolder; availableRange.getCurrentBoundsUs(availableRangeValues); - if (queue.isEmpty()) { + if (previous == null) { if (live) { if (startAtLiveEdge) { // We want live streams to start at the live edge instead of the beginning of the @@ -358,7 +354,6 @@ public class DashChunkSource implements ChunkSource { startAtLiveEdge = false; } - MediaChunk previous = queue.get(out.queueSize - 1); long nextSegmentStartTimeUs = previous.endTimeUs; if (live && nextSegmentStartTimeUs < availableRangeValues[0]) { // This is before the first chunk in the current manifest. @@ -433,9 +428,9 @@ public class DashChunkSource implements ChunkSource { return; } - int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(playbackPositionUs) + int segmentNum = previous == null ? representationHolder.getSegmentNum(playbackPositionUs) : startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum() - : queue.get(out.queueSize - 1).getNextChunkIndex(); + : previous.getNextChunkIndex(); Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource, selectedFormat, sampleFormat, segmentNum, evaluation.trigger); lastChunkWasInitialization = false; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 093c79f1aa..e506c2c16a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.C; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.chunk.Chunk; -import com.google.android.exoplayer.chunk.ChunkOperationHolder; +import com.google.android.exoplayer.chunk.ChunkHolder; import com.google.android.exoplayer.chunk.DataChunk; import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; @@ -317,21 +317,22 @@ public class HlsChunkSource { } /** - * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should - * be performed by the calling {@link HlsSampleSource}. + * Gets the next chunk to load. + *
+ * If a chunk is available then {@link ChunkHolder#chunk} is set. If the end of the stream has
+ * been reached then {@link ChunkHolder#endOfStream} is set. If a chunk is not available but the
+ * end of the stream has not been reached, the {@link ChunkHolder} is not modified.
*
- * @param previousTsChunk The previously loaded chunk that the next chunk should follow.
- * @param playbackPositionUs The current playback position. If previousTsChunk is null then this
+ * @param previous The most recently loaded media chunk.
+ * @param playbackPositionUs The current playback position. If {@code previous} is null then this
* parameter is the position from which playback is expected to start (or restart) and hence
* should be interpreted as a seek position.
- * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is
- * unused.
+ * @param out A holder to populate.
*/
- public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs,
- ChunkOperationHolder out) {
- int variantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
- boolean switchingVariant = previousTsChunk != null
- && variants[variantIndex].format != previousTsChunk.format;
+ public void getNextChunk(TsChunk previous, long playbackPositionUs, ChunkHolder out) {
+ int variantIndex = getNextVariantIndex(previous, playbackPositionUs);
+ boolean switchingVariant = previous != null
+ && variants[variantIndex].format != previous.format;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
if (mediaPlaylist == null) {
@@ -342,11 +343,10 @@ public class HlsChunkSource {
int chunkMediaSequence = 0;
if (live) {
- if (previousTsChunk == null) {
+ if (previous == null) {
chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
} else {
- chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
- : previousTsChunk.chunkIndex + 1;
+ chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
@@ -354,12 +354,11 @@ public class HlsChunkSource {
}
} else {
// Not live.
- if (previousTsChunk == null) {
+ if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
} else {
- chunkMediaSequence = switchingVariant ? previousTsChunk.chunkIndex
- : previousTsChunk.chunkIndex + 1;
+ chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
}
}
@@ -398,12 +397,12 @@ public class HlsChunkSource {
// Compute start and end times, and the sequence number of the next chunk.
long startTimeUs;
if (live) {
- if (previousTsChunk == null) {
+ if (previous == null) {
startTimeUs = 0;
} else if (switchingVariant) {
- startTimeUs = previousTsChunk.startTimeUs;
+ startTimeUs = previous.startTimeUs;
} else {
- startTimeUs = previousTsChunk.endTimeUs;
+ startTimeUs = previous.endTimeUs;
}
} else /* Not live */ {
startTimeUs = segment.startTimeUs;
@@ -439,9 +438,9 @@ public class HlsChunkSource {
Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster);
extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor,
switchingVariant);
- } else if (previousTsChunk == null
- || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
- || format != previousTsChunk.format) {
+ } else if (previous == null
+ || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber
+ || format != previous.format) {
// MPEG-2 TS segments, but we need a new extractor.
PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(true,
segment.discontinuitySequenceNumber, startTimeUs);
@@ -467,7 +466,7 @@ public class HlsChunkSource {
switchingVariant);
} else {
// MPEG-2 TS segments, and we need to continue using the same extractor.
- extractorWrapper = previousTsChunk.extractorWrapper;
+ extractorWrapper = previous.extractorWrapper;
}
out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs,
@@ -596,20 +595,19 @@ public class HlsChunkSource {
return false;
}
- private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
+ private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) {
clearStaleBlacklistedVariants();
- long switchingOverlapUs;
- List