diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java index 7349d03e06..746d7c0e29 100644 --- a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.upstream.Allocator; +import com.google.android.exoplayer.upstream.DefaultAllocator; import com.google.android.exoplayer.upstream.NetworkLock; import android.os.Handler; @@ -64,7 +65,7 @@ public final class DefaultLoadControl implements LoadControl { private static final int BETWEEN_WATERMARKS = 1; private static final int BELOW_LOW_WATERMARK = 2; - private final Allocator allocator; + private final DefaultAllocator allocator; private final List loaders; private final HashMap loaderStates; private final Handler eventHandler; @@ -84,21 +85,21 @@ public final class DefaultLoadControl implements LoadControl { /** * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. * - * @param allocator The {@link Allocator} used by the loader. + * @param allocator The {@link DefaultAllocator} used by the loader. */ - public DefaultLoadControl(Allocator allocator) { + public DefaultLoadControl(DefaultAllocator allocator) { this(allocator, null, null); } /** * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. * - * @param allocator The {@link Allocator} used by the loader. + * @param allocator The {@link DefaultAllocator} used by the loader. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public DefaultLoadControl(Allocator allocator, Handler eventHandler, + public DefaultLoadControl(DefaultAllocator allocator, Handler eventHandler, EventListener eventListener) { this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS, DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_BUFFER_LOAD, DEFAULT_HIGH_BUFFER_LOAD); @@ -107,7 +108,7 @@ public final class DefaultLoadControl implements LoadControl { /** * Constructs a new instance. * - * @param allocator The {@link Allocator} used by the loader. + * @param allocator The {@link DefaultAllocator} used by the loader. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -122,8 +123,9 @@ public final class DefaultLoadControl implements LoadControl { * @param highBufferLoad The minimum fraction of the buffer that must be utilized for the control * to transition from the loading state to the draining state. */ - public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener, - int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, float highBufferLoad) { + public DefaultLoadControl(DefaultAllocator allocator, Handler eventHandler, + EventListener eventListener, int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, + float highBufferLoad) { this.allocator = allocator; this.eventHandler = eventHandler; this.eventListener = eventListener; @@ -140,6 +142,7 @@ public final class DefaultLoadControl implements LoadControl { loaders.add(loader); loaderStates.put(loader, new LoaderState(bufferSizeContribution)); targetBufferSize += bufferSizeContribution; + allocator.setTargetBufferSize(targetBufferSize); } @Override @@ -147,14 +150,10 @@ public final class DefaultLoadControl implements LoadControl { loaders.remove(loader); LoaderState state = loaderStates.remove(loader); targetBufferSize -= state.bufferSizeContribution; + allocator.setTargetBufferSize(targetBufferSize); updateControlState(); } - @Override - public void trimAllocator() { - allocator.trim(targetBufferSize); - } - @Override public Allocator getAllocator() { return allocator; diff --git a/library/src/main/java/com/google/android/exoplayer/LoadControl.java b/library/src/main/java/com/google/android/exoplayer/LoadControl.java index 220abf556e..18a3b35990 100644 --- a/library/src/main/java/com/google/android/exoplayer/LoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/LoadControl.java @@ -47,15 +47,6 @@ public interface LoadControl { */ Allocator getAllocator(); - /** - * Hints to the control that it should consider trimming any unused memory being held in order - * to satisfy allocation requests. - *

- * This method is typically invoked by a recently unregistered loader, once it has released all - * of its allocations back to the {@link Allocator}. - */ - void trimAllocator(); - /** * Invoked by a loader to update the control with its current state. *

diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java index 2af1f4c1bb..18964cdb6d 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java @@ -170,16 +170,11 @@ public class ChunkTrackStream implements TrackStream, * This method should be called when the stream is no longer required. */ public void release() { - chunkSource.release(); - loadControl.unregister(this); - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - clearState(); - loadControl.trimAllocator(); - } - loader.release(); released = true; + chunkSource.release(); + sampleQueue.disable(); + loadControl.unregister(this); + loader.release(); } // TrackStream implementation. @@ -242,9 +237,6 @@ public class ChunkTrackStream implements TrackStream, eventDispatcher.loadCanceled(loadable.bytesLoaded()); if (!released) { restartFrom(pendingResetPositionUs); - } else { - clearState(); - loadControl.trimAllocator(); } } @@ -277,19 +269,15 @@ public class ChunkTrackStream implements TrackStream, private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; + mediaChunks.clear(); if (loader.isLoading()) { loader.cancelLoading(); } else { - clearState(); + sampleQueue.reset(true); maybeStartLoading(); } } - private void clearState() { - sampleQueue.clear(); - mediaChunks.clear(); - } - private void maybeStartLoading() { long now = SystemClock.elapsedRealtime(); if (now - lastPreferredQueueSizeEvaluationTimeMs > 5000) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java index 0072f45052..5c59983953 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java @@ -30,6 +30,7 @@ import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicInteger; /** * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from @@ -39,6 +40,10 @@ public final class DefaultTrackOutput implements TrackOutput { private static final int INITIAL_SCRATCH_SIZE = 32; + private static final int STATE_ENABLED = 0; + private static final int STATE_ENABLED_WRITING = 1; + private static final int STATE_DISABLED = 2; + private final Allocator allocator; private final int allocationLength; @@ -46,6 +51,7 @@ public final class DefaultTrackOutput implements TrackOutput { private final LinkedBlockingDeque dataQueue; private final BufferExtrasHolder extrasHolder; private final ParsableByteArray scratch; + private final AtomicInteger state; // Accessed only by the consuming thread. private long totalBytesDropped; @@ -69,6 +75,7 @@ public final class DefaultTrackOutput implements TrackOutput { dataQueue = new LinkedBlockingDeque<>(); extrasHolder = new BufferExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + state = new AtomicInteger(); lastAllocationOffset = allocationLength; needKeyframe = true; } @@ -76,26 +83,17 @@ public final class DefaultTrackOutput implements TrackOutput { // Called by the consuming thread, but only when there is no loading thread. /** - * Clears the buffer, returning all allocations to the allocator. + * Resets the output. + * + * @param enable True if the output should be enabled. False if it should be disabled. */ - public void clear() { - infoQueue.clear(); - while (!dataQueue.isEmpty()) { - allocator.release(dataQueue.remove()); + public void reset(boolean enable) { + int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); + clearSampleData(); + infoQueue.resetLargestParsedTimestamps(); + if (previousState == STATE_DISABLED) { + downstreamFormat = null; } - totalBytesDropped = 0; - totalBytesWritten = 0; - lastAllocation = null; - lastAllocationOffset = allocationLength; - needKeyframe = true; - } - - /** - * Indicates that {@link #readData(FormatHolder, DecoderInputBuffer, boolean, long)} should - * provide the sample format before any samples, even if it has already been provided. - */ - public void needDownstreamFormat() { - downstreamFormat = null; } /** @@ -151,6 +149,15 @@ public final class DefaultTrackOutput implements TrackOutput { // Called by the consuming thread. + /** + * Disables buffering of sample data and metadata. + */ + public void disable() { + if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) { + clearSampleData(); + } + } + /** * Returns whether the buffer is empty. */ @@ -173,30 +180,19 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Returns the largest sample timestamp that has been queued since the last {@link #clear()}. + * Returns the largest sample timestamp that has been queued since the last {@link #reset}. *

* Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not * considered as having been queued. Samples that were dequeued from the front of the queue are * considered as having been queued. * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. + * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no + * samples have been queued. */ public long getLargestQueuedTimestampUs() { return infoQueue.getLargestQueuedTimestampUs(); } - /** - * Skips all currently buffered samples. - */ - public void skipAllSamples() { - long nextOffset = infoQueue.skipAllSamples(); - if (nextOffset == -1) { - return; - } - dropDownstreamTo(nextOffset); - } - /** * Attempts to skip to the keyframe before the specified time, if it's present in the buffer. * @@ -417,22 +413,40 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - length = prepareForAppend(length); - int bytesAppended = input.read(lastAllocation.data, - lastAllocation.translateOffset(lastAllocationOffset), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; + if (!startWriteOperation()) { + int bytesSkipped = input.skip(length); + if (bytesSkipped == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); } - throw new EOFException(); + return bytesSkipped; + } + try { + length = prepareForAppend(length); + int bytesAppended = input.read(lastAllocation.data, + lastAllocation.translateOffset(lastAllocationOffset), length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + lastAllocationOffset += bytesAppended; + totalBytesWritten += bytesAppended; + return bytesAppended; + } finally { + endWriteOperation(); } - lastAllocationOffset += bytesAppended; - totalBytesWritten += bytesAppended; - return bytesAppended; } @Override public void sampleData(ParsableByteArray buffer, int length) { + if (!startWriteOperation()) { + buffer.skipBytes(length); + return; + } while (length > 0) { int thisAppendLength = prepareForAppend(length); buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset), @@ -441,29 +455,63 @@ public final class DefaultTrackOutput implements TrackOutput { totalBytesWritten += thisAppendLength; length -= thisAppendLength; } + endWriteOperation(); } @Override public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { - if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { - return; - } - // TODO - We should be able to actually remove the data from the rolling buffer after a splice - // succeeds, but doing so is a little bit tricky; it requires moving data written after the - // last committed sample. - pendingSplice = false; + if (!startWriteOperation()) { + infoQueue.commitSampleTimestamp(timeUs); + return; } - if (needKeyframe) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - // TODO - As above, although this case is probably less worthwhile. - return; + try { + if (pendingSplice) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { + return; + } + // TODO - We should be able to actually remove the data from the rolling buffer after a + // splice succeeds, but doing so is a little bit tricky; it requires moving data written + // after the last committed sample. + pendingSplice = false; } - needKeyframe = false; + if (needKeyframe) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // TODO - As above, although this case is probably less worthwhile. + return; + } + needKeyframe = false; + } + timeUs += sampleOffsetUs; + long absoluteOffset = totalBytesWritten - size - offset; + infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); + } finally { + endWriteOperation(); } - timeUs += sampleOffsetUs; - long absoluteOffset = totalBytesWritten - size - offset; - infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); + } + + // Private methods. + + private boolean startWriteOperation() { + return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING); + } + + private void endWriteOperation() { + if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) { + clearSampleData(); + } + } + + private void clearSampleData() { + infoQueue.clearSampleData(); + while (!dataQueue.isEmpty()) { + allocator.release(dataQueue.remove()); + } + allocator.trim(); + totalBytesDropped = 0; + totalBytesWritten = 0; + lastAllocation = null; + lastAllocationOffset = allocationLength; + needKeyframe = true; } /** @@ -533,16 +581,16 @@ public final class DefaultTrackOutput implements TrackOutput { largestQueuedTimestampUs = Long.MIN_VALUE; } - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Clears the queue. - */ - public void clear() { + public void clearSampleData() { absoluteReadIndex = 0; relativeReadIndex = 0; relativeWriteIndex = 0; queueSize = 0; + } + + // Called by the consuming thread, but only when there is no loading thread. + + public void resetLargestParsedTimestamps() { largestDequeuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; } @@ -612,7 +660,7 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Returns the largest sample timestamp that has been queued since the last {@link #clear()}. + * Returns the largest sample timestamp that has been queued since the last {@link #reset}. *

* Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not * considered as having been queued. Samples that were dequeued from the front of the queue are @@ -676,25 +724,6 @@ public final class DefaultTrackOutput implements TrackOutput { return TrackStream.BUFFER_READ; } - /** - * Skips all currently buffered samples. - * - * @return The absolute position of the first byte in the rolling buffer that may still be - * required after skipping the samples, or -1 if no samples were skipped. - */ - public synchronized long skipAllSamples() { - if (queueSize == 0) { - return -1; - } - - relativeReadIndex = (relativeReadIndex + queueSize) % capacity; - absoluteReadIndex += queueSize; - queueSize = 0; - - int lastReadIndex = (relativeReadIndex == 0 ? capacity : relativeReadIndex) - 1; - return sizes[lastReadIndex] + offsets[lastReadIndex]; - } - /** * Attempts to locate the keyframe before the specified time, if it's present in the buffer. * @@ -751,13 +780,13 @@ public final class DefaultTrackOutput implements TrackOutput { public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size, byte[] encryptionKey) { + commitSampleTimestamp(timeUs); timesUs[relativeWriteIndex] = timeUs; offsets[relativeWriteIndex] = offset; sizes[relativeWriteIndex] = size; flags[relativeWriteIndex] = sampleFlags; encryptionKeys[relativeWriteIndex] = encryptionKey; formats[relativeWriteIndex] = upstreamFormat; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); // Increment the write index. queueSize++; if (queueSize == capacity) { @@ -802,6 +831,10 @@ public final class DefaultTrackOutput implements TrackOutput { } } + public synchronized void commitSampleTimestamp(long timeUs) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + } + /** * Attempts to discard samples from the tail of the queue to allow samples starting from the * specified timestamp to be spliced in. diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index c6a9eb98b5..2cdb6d3d69 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -68,7 +68,7 @@ import java.util.List; * from {@link Extractor#sniff(ExtractorInput)} will be used. */ public final class ExtractorSampleSource implements SampleSource, ExtractorOutput, - Loader.Callback { + Loader.Callback { /** * Interface definition for a callback to be notified of {@link ExtractorSampleSource} events. @@ -136,11 +136,9 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private boolean[] trackEnabledStates; private long length; - private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; - private ExtractingLoadable loadable; private int extractedSamplesCountAtStartOfLoad; private boolean loadingFinished; @@ -391,6 +389,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; + sampleQueues[track].disable(); } // Select new tracks. TrackStream[] newStreams = new TrackStream[newSelections.size()]; @@ -402,20 +401,24 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu Assertions.checkState(!trackEnabledStates[track]); enabledTrackCount++; trackEnabledStates[track] = true; - sampleQueues[track].needDownstreamFormat(); newStreams[i] = new TrackStreamImpl(track); } + // At the time of the first track selection all queues will be enabled, so we need to disable + // any that are no longer required. + if (!seenFirstTrackSelection) { + for (int i = 0; i < sampleQueues.length; i++) { + if (!trackEnabledStates[i]) { + sampleQueues[i].disable(); + } + } + } // Cancel or start requests as necessary. if (enabledTrackCount == 0) { - downstreamPositionUs = Long.MIN_VALUE; if (loader.isLoading()) { loader.cancelLoading(); - } else { - clearState(); - allocator.trim(0); } } else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { - seekToInternal(positionUs); + seekToUs(positionUs); } seenFirstTrackSelection = true; return newStreams; @@ -423,8 +426,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void continueBuffering(long playbackPositionUs) { - downstreamPositionUs = playbackPositionUs; - discardSamplesForDisabledTracks(); + // Do nothing. } @Override @@ -448,18 +450,35 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, sampleQueue.getLargestQueuedTimestampUs()); } - return largestQueuedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs + return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs : largestQueuedTimestampUs; } } @Override public void seekToUs(long positionUs) { - seekToInternal(positionUs); + // Treat all seeks into non-seekable media as being to t=0. + positionUs = seekMap.isSeekable() ? positionUs : 0; + lastSeekPositionUs = positionUs; + notifyReset = true; + // If we're not pending a reset, see if we can seek within the sample queues. + boolean seekInsideBuffer = !isPendingReset(); + for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { + if (trackEnabledStates[i]) { + seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); + } + } + // If we failed to seek within the sample queues, we need to restart. + if (!seekInsideBuffer) { + restartFrom(positionUs); + } } @Override public void release() { + for (DefaultTrackOutput sampleQueue : sampleQueues) { + sampleQueue.disable(); + } enabledTrackCount = 0; loader.release(); } @@ -485,32 +504,29 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // Loader.Callback implementation. @Override - public void onLoadCompleted(Loadable loadable, long elapsedMs) { - copyLengthFromLoader(); + public void onLoadCompleted(ExtractingLoadable loadable, long elapsedMs) { + copyLengthFromLoader(loadable); loadingFinished = true; } @Override - public void onLoadCanceled(Loadable loadable, long elapsedMs) { - copyLengthFromLoader(); + public void onLoadCanceled(ExtractingLoadable loadable, long elapsedMs) { + copyLengthFromLoader(loadable); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); - } else { - clearState(); - allocator.trim(0); } } @Override - public int onLoadError(Loadable loadable, long elapsedMs, IOException e) { - copyLengthFromLoader(); + public int onLoadError(ExtractingLoadable loadable, long elapsedMs, IOException e) { + copyLengthFromLoader(loadable); notifyLoadError(e); if (isLoadableExceptionFatal(e)) { return Loader.DONT_RETRY_FATAL; } int extractedSamplesCount = getExtractedSamplesCount(); boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; - configureRetry(); // May clear the sample queues. + configureRetry(loadable); // May reset the sample queues. extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY; } @@ -537,45 +553,28 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // Internal methods. - private void copyLengthFromLoader() { + private void copyLengthFromLoader(ExtractingLoadable loadable) { if (length == C.LENGTH_UNBOUNDED) { length = loadable.length; } } - private void seekToInternal(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = seekMap.isSeekable() ? positionUs : 0; - lastSeekPositionUs = positionUs; - downstreamPositionUs = positionUs; - notifyReset = true; - // If we're not pending a reset, see if we can seek within the sample queues. - boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { - if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); - } - } - // If we failed to seek within the sample queues, we need to restart. - if (!seekInsideBuffer) { - restartFrom(positionUs); - } - } - private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; if (loader.isLoading()) { loader.cancelLoading(); } else { - clearState(); + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } startLoading(); } } private void startLoading() { - loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, - requestedBufferSize); + ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, + allocator, requestedBufferSize); if (prepared) { Assertions.checkState(isPendingReset()); if (durationUs != C.UNSET_TIME_US && pendingResetPositionUs >= durationUs) { @@ -599,8 +598,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu loader.startLoading(loadable, this, minRetryCount); } - private void configureRetry() { - Assertions.checkState(loadable != null); + private void configureRetry(ExtractingLoadable loadable) { if (length != C.LENGTH_UNBOUNDED || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US)) { // We're playing an on-demand stream. Resume the current loadable, which will @@ -612,7 +610,9 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // previous load finished, so it's necessary to load from the start whenever commencing // a new load. notifyReset = prepared; - clearSampleQueues(); + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } loadable.setLoadPosition(0); } } @@ -634,25 +634,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu return true; } - private void discardSamplesForDisabledTracks() { - for (int i = 0; i < trackEnabledStates.length; i++) { - if (!trackEnabledStates[i]) { - sampleQueues[i].skipAllSamples(); - } - } - } - - private void clearState() { - clearSampleQueues(); - loadable = null; - } - - private void clearSampleQueues() { - for (DefaultTrackOutput sampleQueue : sampleQueues) { - sampleQueue.clear(); - } - } - private boolean isPendingReset() { return pendingResetPositionUs != C.UNSET_TIME_US; } @@ -700,7 +681,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu /** * Loads the media stream and extracts sample data from it. */ - private static class ExtractingLoadable implements Loadable { + /* package */ static final class ExtractingLoadable implements Loadable { private final Uri uri; private final DataSource dataSource; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java index ad3de7b79b..f72495aa81 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java @@ -177,6 +177,7 @@ import java.util.List; for (int i = 0; i < oldStreams.size(); i++) { int group = ((TrackStreamImpl) oldStreams.get(i)).group; setTrackGroupEnabledState(group, false); + sampleQueues.valueAt(group).disable(); } // Select new tracks. TrackStream[] newStreams = new TrackStream[newSelections.size()]; @@ -185,25 +186,32 @@ import java.util.List; int group = selection.group; int[] tracks = selection.getTracks(); setTrackGroupEnabledState(group, true); - sampleQueues.valueAt(group).needDownstreamFormat(); if (group == primaryTrackGroupIndex) { chunkSource.selectTracks(tracks, isFirstTrackSelection); } newStreams[i] = new TrackStreamImpl(group); } - // Cancel or start requests as necessary. + // At the time of the first track selection all queues will be enabled, so we need to disable + // any that are no longer required. + if (isFirstTrackSelection) { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + if (!groupEnabledStates[i]) { + sampleQueues.valueAt(i).disable(); + } + } + } + // Cancel requests if necessary. if (enabledTrackCount == 0) { chunkSource.reset(); downstreamPositionUs = Long.MIN_VALUE; downstreamFormat = null; + mediaChunks.clear(); if (tracksWereEnabled) { loadControl.unregister(this); } if (loader.isLoading()) { loader.cancelLoading(); - } else { - clearState(); - loadControl.trimAllocator(); } } else if (!tracksWereEnabled) { loadControl.register(this, bufferSizeContribution); @@ -211,9 +219,26 @@ import java.util.List; return newStreams; } + public void restartFrom(long positionUs) { + lastSeekPositionUs = positionUs; + downstreamPositionUs = positionUs; + pendingResetPositionUs = positionUs; + loadingFinished = false; + mediaChunks.clear(); + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + } + maybeStartLoading(); + } + } + + // TODO[REFACTOR]: Find a way to get rid of this. public void continueBuffering(long playbackPositionUs) { downstreamPositionUs = playbackPositionUs; - discardSamplesForDisabledTracks(); if (!loader.isLoading()) { maybeStartLoading(); } @@ -245,23 +270,14 @@ import java.util.List; } } - public void restartFrom(long positionUs) { - lastSeekPositionUs = positionUs; - downstreamPositionUs = positionUs; - pendingResetPositionUs = positionUs; - loadingFinished = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - clearState(); - maybeStartLoading(); - } - } - public void release() { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).disable(); + } if (enabledTrackCount > 0) { - loadControl.unregister(this); enabledTrackCount = 0; + loadControl.unregister(this); } loader.release(); } @@ -319,9 +335,6 @@ import java.util.List; eventDispatcher.loadCanceled(loadable.bytesLoaded()); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); - } else { - clearState(); - loadControl.trimAllocator(); } } @@ -501,24 +514,6 @@ import java.util.List; containerFormat.width, containerFormat.height, 0, containerFormat.language); } - private void discardSamplesForDisabledTracks() { - if (!prepared) { - return; - } - for (int i = 0; i < groupEnabledStates.length; i++) { - if (!groupEnabledStates[i]) { - sampleQueues.valueAt(i).skipAllSamples(); - } - } - } - - private void clearState() { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).clear(); - } - mediaChunks.clear(); - } - private void maybeStartLoading() { boolean shouldStartLoading = !prepared || (enabledTrackCount > 0 && loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), false)); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java b/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java index db575441b6..724bf46fde 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Allocator.java @@ -39,11 +39,9 @@ public interface Allocator { /** * Hints to the {@link Allocator} that it should make a best effort to release any memory that it - * has allocated, beyond the specified target number of bytes. - * - * @param targetSize The target size in bytes. + * has allocated that it no longer requires. */ - void trim(int targetSize); + void trim(); /** * Blocks execution until the number of bytes allocated is not greater than the limit, or the diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultAllocator.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultAllocator.java index 2327e909f1..3bb61afa6e 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultAllocator.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultAllocator.java @@ -30,6 +30,7 @@ public final class DefaultAllocator implements Allocator { private final int individualAllocationSize; private final byte[] initialAllocationBlock; + private int targetBufferSize; private int allocatedCount; private int availableCount; private Allocation[] availableAllocations; @@ -68,6 +69,10 @@ public final class DefaultAllocator implements Allocator { } } + public synchronized void setTargetBufferSize(int targetBufferSize) { + this.targetBufferSize = targetBufferSize; + } + @Override public synchronized Allocation allocate() { allocatedCount++; @@ -96,8 +101,8 @@ public final class DefaultAllocator implements Allocator { } @Override - public synchronized void trim(int targetSize) { - int targetAllocationCount = Util.ceilDivide(targetSize, individualAllocationSize); + public synchronized void trim() { + int targetAllocationCount = Util.ceilDivide(targetBufferSize, individualAllocationSize); int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount); if (targetAvailableCount >= availableCount) { // We're already at or below the target.