/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.exoplayer; import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.NetworkLock; import android.os.Handler; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents * any loader from getting too far ahead or behind any of the other loaders. * <p> * Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the * duration of buffered media and the buffer utilization both exceed respective thresholds, the * control switches to a draining state during which no loads are permitted to start. During * draining periods, resources such as the device radio have an opportunity to switch into low * power modes. The control reverts back to the loading state when either the duration of buffered * media or the buffer utilization fall below respective thresholds. * <p> * This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering * itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods, * and unregistering itself during draining periods. */ public class DefaultLoadControl implements LoadControl { /** * Interface definition for a callback to be notified of {@link DefaultLoadControl} events. */ public interface EventListener { /** * Invoked when the control transitions from a loading to a draining state, or vice versa. * * @param loading Whether the control is now in a loading state. */ void onLoadingChanged(boolean loading); } public static final int DEFAULT_LOW_WATERMARK_MS = 15000; public static final int DEFAULT_HIGH_WATERMARK_MS = 30000; public static final float DEFAULT_LOW_POOL_LOAD = 0.2f; public static final float DEFAULT_HIGH_POOL_LOAD = 0.8f; private static final int ABOVE_HIGH_WATERMARK = 0; private static final int BETWEEN_WATERMARKS = 1; private static final int BELOW_LOW_WATERMARK = 2; private final Allocator allocator; private final List<Object> loaders; private final HashMap<Object, LoaderState> loaderStates; private final Handler eventHandler; private final EventListener eventListener; private final long lowWatermarkUs; private final long highWatermarkUs; private final float lowPoolLoad; private final float highPoolLoad; private int targetBufferSize; private long maxLoadStartPositionUs; private int bufferPoolState; private boolean fillingBuffers; private boolean streamingPrioritySet; /** * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. * * @param allocator The {@link Allocator} used by the loader. */ public DefaultLoadControl(Allocator 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 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, EventListener eventListener) { this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS, DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_POOL_LOAD, DEFAULT_HIGH_POOL_LOAD); } /** * Constructs a new instance. * * @param allocator The {@link Allocator} 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. * @param lowWatermarkMs The minimum duration of media that can be buffered for the control to * be in the draining state. If less media is buffered, then the control will transition to * the filling state. * @param highWatermarkMs The minimum duration of media that can be buffered for the control to * transition from filling to draining. * @param lowPoolLoad 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 highPoolLoad 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 lowPoolLoad, float highPoolLoad) { this.allocator = allocator; this.eventHandler = eventHandler; this.eventListener = eventListener; this.loaders = new ArrayList<Object>(); this.loaderStates = new HashMap<Object, LoaderState>(); this.lowWatermarkUs = lowWatermarkMs * 1000L; this.highWatermarkUs = highWatermarkMs * 1000L; this.lowPoolLoad = lowPoolLoad; this.highPoolLoad = highPoolLoad; } @Override public void register(Object loader, int bufferSizeContribution) { loaders.add(loader); loaderStates.put(loader, new LoaderState(bufferSizeContribution)); targetBufferSize += bufferSizeContribution; } @Override public void unregister(Object loader) { loaders.remove(loader); LoaderState state = loaderStates.remove(loader); targetBufferSize -= state.bufferSizeContribution; updateControlState(); } @Override public void trimAllocator() { allocator.trim(targetBufferSize); } @Override public Allocator getAllocator() { return allocator; } @Override public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, boolean loading, boolean failed) { // Update the loader state. int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); LoaderState loaderState = loaderStates.get(loader); boolean loaderStateChanged = loaderState.bufferState != loaderBufferState || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading || loaderState.failed != failed; if (loaderStateChanged) { loaderState.bufferState = loaderBufferState; loaderState.nextLoadPositionUs = nextLoadPositionUs; loaderState.loading = loading; loaderState.failed = failed; } // Update the buffer pool state. int allocatedSize = allocator.getAllocatedSize(); int bufferPoolState = getBufferPoolState(allocatedSize); boolean bufferPoolStateChanged = this.bufferPoolState != bufferPoolState; if (bufferPoolStateChanged) { this.bufferPoolState = bufferPoolState; } // If either of the individual states have changed, update the shared control state. if (loaderStateChanged || bufferPoolStateChanged) { updateControlState(); } return allocatedSize < targetBufferSize && nextLoadPositionUs != -1 && nextLoadPositionUs <= maxLoadStartPositionUs; } private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) { if (nextLoadPositionUs == -1) { return ABOVE_HIGH_WATERMARK; } else { long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs; return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK : timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS; } } private int getBufferPoolState(int allocatedSize) { float bufferPoolLoad = (float) allocatedSize / targetBufferSize; return bufferPoolLoad > highPoolLoad ? ABOVE_HIGH_WATERMARK : bufferPoolLoad < lowPoolLoad ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS; } private void updateControlState() { boolean loading = false; boolean failed = false; boolean finished = true; int highestState = bufferPoolState; for (int i = 0; i < loaders.size(); i++) { LoaderState loaderState = loaderStates.get(loaders.get(i)); loading |= loaderState.loading; failed |= loaderState.failed; finished &= loaderState.nextLoadPositionUs == -1; highestState = Math.max(highestState, loaderState.bufferState); } fillingBuffers = !loaders.isEmpty() && !finished && !failed && (highestState == BELOW_LOW_WATERMARK || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); if (fillingBuffers && !streamingPrioritySet) { NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY); streamingPrioritySet = true; notifyLoadingChanged(true); } else if (!fillingBuffers && streamingPrioritySet && !loading) { NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY); streamingPrioritySet = false; notifyLoadingChanged(false); } maxLoadStartPositionUs = -1; if (fillingBuffers) { for (int i = 0; i < loaders.size(); i++) { Object loader = loaders.get(i); LoaderState loaderState = loaderStates.get(loader); long loaderTime = loaderState.nextLoadPositionUs; if (loaderTime != -1 && (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) { maxLoadStartPositionUs = loaderTime; } } } } private void notifyLoadingChanged(final boolean loading) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadingChanged(loading); } }); } } private static class LoaderState { public final int bufferSizeContribution; public int bufferState; public boolean loading; public boolean failed; public long nextLoadPositionUs; public LoaderState(int bufferSizeContribution) { this.bufferSizeContribution = bufferSizeContribution; bufferState = ABOVE_HIGH_WATERMARK; loading = false; failed = false; nextLoadPositionUs = -1; } } }