From 9282710f046ccd585dd5c3bb42e11236f733ac67 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Apr 2016 03:51:11 -0700 Subject: [PATCH] Decouple next chunk evaluation and queue trimming. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=118925372 --- ...kOperationHolder.java => ChunkHolder.java} | 16 +-- .../exoplayer/chunk/ChunkSampleSource.java | 73 +++++------ .../android/exoplayer/chunk/ChunkSource.java | 35 +++-- .../exoplayer/chunk/FormatEvaluator.java | 124 ++++++++++-------- .../exoplayer/dash/DashChunkSource.java | 37 +++--- .../android/exoplayer/hls/HlsChunkSource.java | 68 +++++----- .../exoplayer/hls/HlsSampleSource.java | 20 ++- .../SmoothStreamingChunkSource.java | 33 ++--- 8 files changed, 196 insertions(+), 210 deletions(-) rename library/src/main/java/com/google/android/exoplayer/chunk/{ChunkOperationHolder.java => ChunkHolder.java} (62%) 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: - * + * Holds a chunk or an indication that the end of the stream has been reached. */ -public final class ChunkOperationHolder { - - /** - * The number of {@link MediaChunk}s to retain in a queue. - */ - public int queueSize; +public final class ChunkHolder { /** * The chunk. @@ -45,7 +34,6 @@ public final class ChunkOperationHolder { * Clears the holder. */ public void clear() { - queueSize = 0; chunk = null; endOfStream = false; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index 2b5183cfde..6c7c0d2a7a 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -55,7 +55,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call private final Loader loader; private final LoadControl loadControl; private final ChunkSource chunkSource; - private final ChunkOperationHolder currentLoadableHolder; + private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; private final DefaultTrackOutput sampleQueue; @@ -66,7 +66,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; - private long lastPerformedBufferOperation; + private long lastPreferredQueueSizeEvaluationTimeMs; private boolean pendingReset; private boolean loadControlRegistered; @@ -76,6 +76,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call private boolean trackEnabled; private long currentLoadStartTimeMs; + private Chunk currentLoadable; private Format downstreamFormat; private Format downstreamSampleFormat; @@ -124,7 +125,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call this.bufferSizeContribution = bufferSizeContribution; loader = new Loader("Loader:ChunkSampleSource", minLoadableRetryCount); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); - currentLoadableHolder = new ChunkOperationHolder(); + nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); @@ -267,9 +268,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); - if (currentLoadableHolder.chunk == null) { - chunkSource.maybeThrowError(); - } + chunkSource.maybeThrowError(); } @Override @@ -336,7 +335,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call public void onLoadCompleted(Loadable loadable) { long now = SystemClock.elapsedRealtime(); long loadDurationMs = now - currentLoadStartTimeMs; - Chunk currentLoadable = currentLoadableHolder.chunk; chunkSource.onChunkLoadCompleted(currentLoadable); if (isMediaChunk(currentLoadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; @@ -353,7 +351,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public void onLoadCanceled(Loadable loadable) { - Chunk currentLoadable = currentLoadableHolder.chunk; eventDispatcher.loadCanceled(currentLoadable.bytesLoaded()); if (trackEnabled) { restartFrom(pendingResetPositionUs); @@ -365,7 +362,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public int onLoadError(Loadable loadable, IOException e) { - Chunk currentLoadable = currentLoadableHolder.chunk; long bytesLoaded = currentLoadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(currentLoadable); boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1; @@ -420,7 +416,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call } private void clearCurrentLoadable() { - currentLoadableHolder.chunk = null; + currentLoadable = null; } private void maybeStartLoading() { @@ -429,44 +425,38 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call } long now = SystemClock.elapsedRealtime(); + if (now - lastPreferredQueueSizeEvaluationTimeMs > 5000) { + int queueSize = chunkSource.getPreferredQueueSize(downstreamPositionUs, readOnlyMediaChunks); + // Never discard the first chunk. + discardUpstreamMediaChunks(Math.max(1, queueSize)); + lastPreferredQueueSizeEvaluationTimeMs = now; + } + long nextLoadPositionUs = getNextLoadPositionUs(); - - // Evaluate the operation if (a) we don't have the next chunk yet and we're not finished, or (b) - // if the last evaluation was over 2000ms ago. - if ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) - || (now - lastPerformedBufferOperation > 2000)) { - // Perform the evaluation. - currentLoadableHolder.endOfStream = false; - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - long playbackPositionUs = pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs - : downstreamPositionUs; - chunkSource.getChunkOperation(readOnlyMediaChunks, playbackPositionUs, currentLoadableHolder); - loadingFinished = currentLoadableHolder.endOfStream; - boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); - lastPerformedBufferOperation = now; - // Update the next load position as appropriate. - if (currentLoadableHolder.chunk == null) { - // Set loadPosition to -1 to indicate that we don't have anything to load. - nextLoadPositionUs = -1; - } else if (chunksDiscarded) { - // Chunks were discarded, so we need to re-evaluate the load position. - nextLoadPositionUs = getNextLoadPositionUs(); - } - } - - boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false); - if (!nextLoader) { - // We're not allowed to start loading yet. + boolean isNext = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, false); + if (!isNext) { return; } - Chunk currentLoadable = currentLoadableHolder.chunk; - if (currentLoadable == null) { - // We're allowed to start loading, but have nothing to load. + chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), + pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, + nextChunkHolder); + boolean endOfStream = nextChunkHolder.endOfStream; + Chunk nextLoadable = nextChunkHolder.chunk; + nextChunkHolder.clear(); + + if (endOfStream) { + loadingFinished = true; + loadControl.update(this, downstreamPositionUs, -1, false); return; } - currentLoadStartTimeMs = SystemClock.elapsedRealtime(); + if (nextLoadable == null) { + return; + } + + currentLoadStartTimeMs = now; + currentLoadable = nextLoadable; if (isMediaChunk(currentLoadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; mediaChunk.init(sampleQueue); @@ -514,6 +504,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call while (mediaChunks.size() > queueLength) { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; + loadingFinished = false; } sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); eventDispatcher.upstreamDiscarded(startTimeUs, endTimeUs); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java index 9b28847e2d..ca0f1815f3 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java @@ -90,25 +90,32 @@ public interface ChunkSource { void continueBuffering(long playbackPositionUs); /** - * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should - * be performed by the calling {@link ChunkSampleSource}. + * Evaluates whether {@link MediaChunk}s should be removed from the back of the queue. *

- * 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 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 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 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 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 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 queue, + boolean[] blacklistFlags) { + return queue.size(); + } + } /** @@ -235,43 +240,18 @@ public interface FormatEvaluator { } @Override - public void evaluate(List 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 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 queue, long playbackPositionUs, - ChunkOperationHolder out) { + public int getPreferredQueueSize(long playbackPositionUs, List 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 queue; - if (previousTsChunk != null) { - switchingOverlapUs = previousTsChunk.endTimeUs - previousTsChunk.startTimeUs; - queue = Collections.singletonList(previousTsChunk); + long bufferedDurationUs; + if (previous != null) { + // Use start time of the previous chunk rather than its end time because switching format will + // require downloading overlapping segments. + bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs); } else { - switchingOverlapUs = 0; - queue = Collections.emptyList(); + bufferedDurationUs = 0; } if (enabledVariants.length > 1) { - adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, switchingOverlapUs, - enabledVariantBlacklistFlags, evaluation); + adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags, + evaluation); } else { evaluation.format = enabledVariants[0].format; evaluation.trigger = Chunk.TRIGGER_MANUAL; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index a484ec4f45..1131496d51 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; 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.ChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; import com.google.android.exoplayer.upstream.Loader; @@ -63,7 +63,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private final HlsChunkSource chunkSource; private final LinkedList extractors; private final int bufferSizeContribution; - private final ChunkOperationHolder chunkOperationHolder; + private final ChunkHolder nextChunkHolder; private final EventDispatcher eventDispatcher; private final LoadControl loadControl; @@ -116,7 +116,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { loader = new Loader("Loader:HLS", minLoadableRetryCount); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); extractors = new LinkedList<>(); - chunkOperationHolder = new ChunkOperationHolder(); + nextChunkHolder = new ChunkHolder(); } // SampleSource implementation. @@ -301,9 +301,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { /* package */ void maybeThrowError() throws IOException { loader.maybeThrowError(); - if (currentLoadable == null) { - chunkSource.maybeThrowError(); - } + chunkSource.maybeThrowError(); } /* package */ long readReset(int group) { @@ -641,12 +639,12 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return; } - chunkSource.getChunkOperation(previousTsLoadable, + chunkSource.getNextChunk(previousTsLoadable, pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, - chunkOperationHolder); - boolean endOfStream = chunkOperationHolder.endOfStream; - Chunk nextLoadable = chunkOperationHolder.chunk; - chunkOperationHolder.clear(); + nextChunkHolder); + boolean endOfStream = nextChunkHolder.endOfStream; + Chunk nextLoadable = nextChunkHolder.chunk; + nextChunkHolder.clear(); if (endOfStream) { loadingFinished = true; diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index e897553456..1c9df96849 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer.Format.DecreasingBandwidthComparator; 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; @@ -208,16 +208,23 @@ public class SmoothStreamingChunkSource implements ChunkSource { } @Override - public final void getChunkOperation(List queue, long playbackPositionUs, - ChunkOperationHolder out) { + public int getPreferredQueueSize(long playbackPositionUs, List 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 (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]; @@ -225,21 +232,10 @@ public class SmoothStreamingChunkSource 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; - StreamElement streamElement = currentManifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { if (currentManifest.isLive) { @@ -251,13 +247,12 @@ public class SmoothStreamingChunkSource implements ChunkSource { } int chunkIndex; - if (queue.isEmpty()) { + if (previous == null) { if (live) { playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs); } chunkIndex = streamElement.getChunkIndex(playbackPositionUs); } else { - MediaChunk previous = queue.get(out.queueSize - 1); chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset; }