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 746d7c0e29..0e23c872e4 100644 --- a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java @@ -58,8 +58,6 @@ public final class DefaultLoadControl implements LoadControl { public static final int DEFAULT_LOW_WATERMARK_MS = 15000; public static final int DEFAULT_HIGH_WATERMARK_MS = 30000; - public static final float DEFAULT_LOW_BUFFER_LOAD = 0.2f; - public static final float DEFAULT_HIGH_BUFFER_LOAD = 0.8f; private static final int ABOVE_HIGH_WATERMARK = 0; private static final int BETWEEN_WATERMARKS = 1; @@ -73,12 +71,10 @@ public final class DefaultLoadControl implements LoadControl { private final long lowWatermarkUs; private final long highWatermarkUs; - private final float lowBufferLoad; - private final float highBufferLoad; - private int targetBufferSize; private long maxLoadStartPositionUs; - private int bufferState; + private int targetBufferSize; + private boolean targetBufferSizeReached; private boolean fillingBuffers; private boolean streamingPrioritySet; @@ -102,7 +98,7 @@ public final class DefaultLoadControl implements LoadControl { 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); + DEFAULT_HIGH_WATERMARK_MS); } /** @@ -117,15 +113,9 @@ public final class DefaultLoadControl implements LoadControl { * the filling state. * @param highWatermarkMs The minimum duration of media that can be buffered for the control to * transition from filling to draining. - * @param lowBufferLoad The minimum fraction of the buffer that must be utilized for the control - * to be in the draining state. If the utilization is lower, then the control will transition - * to the filling state. - * @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(DefaultAllocator allocator, Handler eventHandler, - EventListener eventListener, int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, - float highBufferLoad) { + EventListener eventListener, int lowWatermarkMs, int highWatermarkMs) { this.allocator = allocator; this.eventHandler = eventHandler; this.eventListener = eventListener; @@ -133,8 +123,6 @@ public final class DefaultLoadControl implements LoadControl { this.loaderStates = new HashMap<>(); this.lowWatermarkUs = lowWatermarkMs * 1000L; this.highWatermarkUs = highWatermarkMs * 1000L; - this.lowBufferLoad = lowBufferLoad; - this.highBufferLoad = highBufferLoad; } @Override @@ -151,6 +139,7 @@ public final class DefaultLoadControl implements LoadControl { LoaderState state = loaderStates.remove(loader); targetBufferSize -= state.bufferSizeContribution; allocator.setTargetBufferSize(targetBufferSize); + targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; updateControlState(); } @@ -174,11 +163,10 @@ public final class DefaultLoadControl implements LoadControl { } // Update the buffer state. - int currentBufferSize = allocator.getTotalBytesAllocated(); - int bufferState = getBufferState(currentBufferSize); - boolean bufferStateChanged = this.bufferState != bufferState; + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; + boolean bufferStateChanged = this.targetBufferSizeReached != targetBufferSizeReached; if (bufferStateChanged) { - this.bufferState = bufferState; + this.targetBufferSizeReached = targetBufferSizeReached; } // If either of the individual states have changed, update the shared control state. @@ -186,8 +174,7 @@ public final class DefaultLoadControl implements LoadControl { updateControlState(); } - return currentBufferSize < targetBufferSize && nextLoadPositionUs != C.UNSET_TIME_US - && nextLoadPositionUs <= maxLoadStartPositionUs; + return nextLoadPositionUs != C.UNSET_TIME_US && nextLoadPositionUs <= maxLoadStartPositionUs; } private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) { @@ -201,27 +188,20 @@ public final class DefaultLoadControl implements LoadControl { } } - private int getBufferState(int currentBufferSize) { - float bufferLoad = (float) currentBufferSize / targetBufferSize; - return bufferLoad > highBufferLoad ? ABOVE_HIGH_WATERMARK - : bufferLoad < lowBufferLoad ? BELOW_LOW_WATERMARK - : BETWEEN_WATERMARKS; - } - private void updateControlState() { boolean loading = false; boolean haveNextLoadPosition = false; - int highestState = bufferState; + int worstLoaderState = ABOVE_HIGH_WATERMARK; for (int i = 0; i < loaders.size(); i++) { LoaderState loaderState = loaderStates.get(loaders.get(i)); loading |= loaderState.loading; haveNextLoadPosition |= loaderState.nextLoadPositionUs != C.UNSET_TIME_US; - highestState = Math.max(highestState, loaderState.bufferState); + worstLoaderState = Math.max(worstLoaderState, loaderState.bufferState); } fillingBuffers = !loaders.isEmpty() && (loading || haveNextLoadPosition) - && (highestState == BELOW_LOW_WATERMARK - || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); + && (worstLoaderState == BELOW_LOW_WATERMARK + || (worstLoaderState == BETWEEN_WATERMARKS && fillingBuffers && !targetBufferSizeReached)); if (fillingBuffers && !streamingPrioritySet) { NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY); streamingPrioritySet = true; 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 b2b856a363..7060fe69ec 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 @@ -17,14 +17,15 @@ package com.google.android.exoplayer.extractor; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; -import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; @@ -36,6 +37,7 @@ import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; import android.net.Uri; +import android.os.ConditionVariable; import android.os.Handler; import java.io.EOFException; @@ -125,7 +127,8 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private final Handler eventHandler; private final EventListener eventListener; private final DataSource dataSource; - private final Allocator allocator; + private final ConditionVariable loadCondition; + private final LoadControl loadControl; private final Loader loader; private final ExtractorHolder extractorHolder; @@ -186,7 +189,8 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu this.eventListener = eventListener; this.eventHandler = eventHandler; dataSource = dataSourceFactory.createDataSource(bandwidthMeter); - allocator = new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE); + loadCondition = new ConditionVariable(); + loadControl = new DefaultLoadControl(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); loader = new Loader("Loader:ExtractorSampleSource"); extractorHolder = new ExtractorHolder(extractors, this); pendingResetPositionUs = C.UNSET_TIME_US; @@ -308,6 +312,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu return true; } if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { + loadCondition.close(); int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackEnabledStates = new boolean[trackCount]; @@ -322,6 +327,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // We're not prepared. maybeThrowError(); if (!loader.isLoading()) { + loadCondition.open(); startLoading(); } return false; @@ -341,6 +347,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu public TrackStream[] selectTracks(List oldStreams, List newSelections, long positionUs) { Assertions.checkState(prepared); + boolean tracksWereEnabled = enabledTrackCount > 0; // Unselect old tracks. for (int i = 0; i < oldStreams.size(); i++) { int track = ((TrackStreamImpl) oldStreams.get(i)).track; @@ -372,11 +379,21 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } // Cancel or start requests as necessary. if (enabledTrackCount == 0) { + if (tracksWereEnabled) { + loadControl.unregister(this); + loadCondition.close(); + } if (loader.isLoading()) { loader.cancelLoading(); } - } else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { - seekToUs(positionUs); + } else { + if (!tracksWereEnabled) { + loadControl.register(this, C.DEFAULT_MUXED_BUFFER_SIZE); + loadCondition.open(); + } + if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { + seekToUs(positionUs); + } } seenFirstTrackSelection = true; return newStreams; @@ -384,7 +401,11 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void continueBuffering(long playbackPositionUs) { - // Do nothing. + if (loadControl.update(this, playbackPositionUs, getBufferedPositionUs(), loader.isLoading())) { + loadCondition.open(); + } else { + loadCondition.close(); + } } @Override @@ -434,6 +455,9 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu for (DefaultTrackOutput sampleQueue : sampleQueues) { sampleQueue.disable(); } + if (enabledTrackCount > 0) { + loadControl.unregister(this); + } loader.release(); } @@ -498,7 +522,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public TrackOutput track(int id) { sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); - DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); + DefaultTrackOutput sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); sampleQueues[sampleQueues.length - 1] = sampleQueue; return sampleQueue; } @@ -536,7 +560,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private void startLoading() { ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, - allocator, C.DEFAULT_MUXED_BUFFER_SIZE); + loadCondition); if (prepared) { Assertions.checkState(isPendingReset()); if (durationUs != C.UNSET_TIME_US && pendingResetPositionUs >= durationUs) { @@ -657,8 +681,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private final Uri uri; private final DataSource dataSource; private final ExtractorHolder extractorHolder; - private final Allocator allocator; - private final int requestedBufferSize; + private final ConditionVariable loadCondition; private final PositionHolder positionHolder; private volatile boolean loadCanceled; @@ -667,12 +690,11 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private long length; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, - Allocator allocator, int requestedBufferSize) { + ConditionVariable loadCondition) { this.uri = Assertions.checkNotNull(uri); this.dataSource = Assertions.checkNotNull(dataSource); this.extractorHolder = Assertions.checkNotNull(extractorHolder); - this.allocator = Assertions.checkNotNull(allocator); - this.requestedBufferSize = requestedBufferSize; + this.loadCondition = loadCondition; this.positionHolder = new PositionHolder(); this.pendingExtractorSeek = true; this.length = C.LENGTH_UNBOUNDED; @@ -711,11 +733,10 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu pendingExtractorSeek = false; } while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize); + // TODO: Prevent the loader from loading too much data between evaluations of the + // buffering condition. + loadCondition.block(); result = extractor.read(input, positionHolder); - // TODO: Implement throttling to stop us from buffering data too often. - // TODO: Block buffering between the point at which we have sufficient data for - // preparation to complete and the first call to endTrackSelection. } } finally { if (result == Extractor.RESULT_SEEK) { 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 724bf46fde..3b86b99458 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,19 +39,10 @@ public interface Allocator { /** * Hints to the {@link Allocator} that it should make a best effort to release any memory that it - * has allocated that it no longer requires. + * has allocated beyond the target buffer size. */ void trim(); - /** - * Blocks execution until the number of bytes allocated is not greater than the limit, or the - * thread is interrupted. - * - * @param limit The limit in bytes. - * @throws InterruptedException If the thread is interrupted. - */ - void blockWhileTotalBytesAllocatedExceeds(int limit) throws InterruptedException; - /** * Returns the total number of bytes currently allocated. */ 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 3bb61afa6e..e989f6f6d6 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 @@ -47,7 +47,7 @@ public final class DefaultAllocator implements Allocator { /** * Constructs a pool with some {@link Allocation}s created up front. *

- * Note: Initial {@link Allocation}s will never be discarded by {@link #trim(int)}. + * Note: Initial {@link Allocation}s will never be discarded by {@link #trim()}. * * @param individualAllocationSize The length of each individual allocation. * @param initialAllocationCount The number of allocations to create up front. @@ -70,7 +70,11 @@ public final class DefaultAllocator implements Allocator { } public synchronized void setTargetBufferSize(int targetBufferSize) { + boolean targetBufferSizeReduced = targetBufferSize < this.targetBufferSize; this.targetBufferSize = targetBufferSize; + if (targetBufferSizeReduced) { + trim(); + } } @Override @@ -147,14 +151,6 @@ public final class DefaultAllocator implements Allocator { return allocatedCount * individualAllocationSize; } - @Override - public synchronized void blockWhileTotalBytesAllocatedExceeds(int limit) - throws InterruptedException { - while (getTotalBytesAllocated() > limit) { - wait(); - } - } - @Override public int getIndividualAllocationLength() { return individualAllocationSize;