diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 407d70e850..2f0f4789ec 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.AspectRatioFrameLayout; import com.google.android.exoplayer.C; import com.google.android.exoplayer.ConcatenatingSampleSourceProvider; -import com.google.android.exoplayer.DefaultBufferingPolicy; +import com.google.android.exoplayer.DefaultBufferingControl; import com.google.android.exoplayer.DefaultTrackSelectionPolicy; import com.google.android.exoplayer.DefaultTrackSelector; import com.google.android.exoplayer.DefaultTrackSelector.TrackInfo; @@ -268,8 +268,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, trackSelector.addListener(this); trackSelector.addListener(eventLogger); trackSelectionHelper = new TrackSelectionHelper(trackSelector); - player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultBufferingPolicy(), - drmSessionManager, preferExtensionDecoders); + player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, + new DefaultBufferingControl(), drmSessionManager, preferExtensionDecoders); player.addListener(this); player.addListener(eventLogger); player.setDebugListener(eventLogger); diff --git a/library/src/main/java/com/google/android/exoplayer/BufferingControl.java b/library/src/main/java/com/google/android/exoplayer/BufferingControl.java new file mode 100644 index 0000000000..57e7e539a9 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/BufferingControl.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** + * Controls buffering of media. + */ +public interface BufferingControl { + + /** + * Invoked by the player when a track selection occurs. + * + * @param renderers The renderers. + * @param trackGroups The available {@link TrackGroup}s. + * @param trackSelections The {@link TrackSelection}s that were made. + */ + void onTrackSelections(TrackRenderer[] renderers, TrackGroupArray trackGroups, + TrackSelectionArray trackSelections); + + /** + * Invoked by the player when a reset occurs, meaning all renderers have been disabled. + */ + void reset(); + + /** + * Gets the {@link Allocator} that should be used to obtain media buffer allocations. + * + * @return The {@link Allocator}. + */ + Allocator getAllocator(); + + /** + * Invoked by the player to determine whether sufficient media is buffered for playback to be + * started or resumed. + * + * @param bufferedDurationUs The duration of media that's currently buffered. + * @param rebuffering Whether the player is re-buffering. + * @return True if playback should be allowed to start or resume. False otherwise. + */ + boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering); + + /** + * Invoked by the player to determine whether buffering should continue. + * + * @param bufferedDurationUs The duration of media that's currently buffered. + * @return True if the buffering should continue. False otherwise. + */ + boolean shouldContinueBuffering(long bufferedDurationUs); + +} diff --git a/library/src/main/java/com/google/android/exoplayer/BufferingPolicy.java b/library/src/main/java/com/google/android/exoplayer/BufferingPolicy.java deleted file mode 100644 index 1b122dbd36..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/BufferingPolicy.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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; - -/** - * A media buffering policy. - */ -public interface BufferingPolicy { - - /** - * Invoked by the player to update the playback position. - * - * @param playbackPositionUs The current playback position in microseconds. - */ - void setPlaybackPosition(long playbackPositionUs); - - /** - * Invoked by the player to determine whether sufficient media is buffered for playback to be - * started or resumed. - * - * @param bufferedPositionUs The position up to which media is buffered. - * @param rebuffering Whether the player is re-buffering. - * @return True if playback should be allowed to start or resume. False otherwise. - */ - boolean haveSufficientBuffer(long bufferedPositionUs, boolean rebuffering); - - /** - * Invoked by the player when a track selection occurs. - * - * @param renderers The renderers. - * @param trackGroups The available {@link TrackGroup}s. - * @param trackSelections The {@link TrackSelection}s that were made. - */ - void onTrackSelections(TrackRenderer[] renderers, TrackGroupArray trackGroups, - TrackSelectionArray trackSelections); - - /** - * Invoked by the player when a reset occurs, meaning all renderers have been disabled. - */ - void reset(); - - /** - * Returns a {@link LoadControl} that a {@link SampleSource} can use to control loads according to - * this policy. - * - * @return The {@link LoadControl}. - */ - LoadControl getLoadControl(); - - /** - * Coordinates multiple loaders of time series data. - */ - interface LoadControl { - - /** - * Registers a loader. - * - * @param loader The loader being registered. - */ - void register(Object loader); - - /** - * Unregisters a loader. - * - * @param loader The loader being unregistered. - */ - void unregister(Object loader); - - /** - * Gets the {@link Allocator} that loaders should use to obtain memory allocations into which - * data can be loaded. - * - * @return The {@link Allocator} to use. - */ - Allocator getAllocator(); - - /** - * Invoked by a loader to update the control with its current state. - *

- * This method must be called by a registered loader whenever its state changes. This is true - * even if the registered loader does not itself wish to start its next load (since the state of - * the loader will still affect whether other registered loaders are allowed to proceed). - * - * @param loader The loader invoking the update. - * @param nextLoadPositionUs The loader's next load position, or {@link C#UNSET_TIME_US} if - * finished, failed, or if the next load position is not yet known. - * @param loading Whether the loader is currently loading data. - * @return True if the loader is allowed to start its next load. False otherwise. - */ - boolean update(Object loader, long nextLoadPositionUs, boolean loading); - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/CompositeSequenceableLoader.java b/library/src/main/java/com/google/android/exoplayer/CompositeSequenceableLoader.java new file mode 100644 index 0000000000..c6f494b34f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/CompositeSequenceableLoader.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s. + */ +public final class CompositeSequenceableLoader implements SequenceableLoader { + + private final SequenceableLoader[] loaders; + + public CompositeSequenceableLoader(SequenceableLoader[] loaders) { + this.loaders = loaders; + } + + @Override + public long getNextLoadPositionUs() { + long nextLoadPositionUs = Long.MAX_VALUE; + for (SequenceableLoader loader : loaders) { + long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); + if (loaderNextLoadPositionUs != C.END_OF_SOURCE_US) { + nextLoadPositionUs = Math.min(nextLoadPositionUs, loaderNextLoadPositionUs); + } + } + return nextLoadPositionUs == Long.MAX_VALUE ? C.END_OF_SOURCE_US : nextLoadPositionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + boolean madeProgress = false; + boolean madeProgressThisIteration; + do { + madeProgressThisIteration = false; + long nextLoadPositionUs = getNextLoadPositionUs(); + if (nextLoadPositionUs == C.END_OF_SOURCE_US) { + break; + } + for (SequenceableLoader loader : loaders) { + if (loader.getNextLoadPositionUs() == nextLoadPositionUs) { + madeProgressThisIteration |= loader.continueLoading(positionUs); + } + } + madeProgress |= madeProgressThisIteration; + } while (madeProgressThisIteration); + return madeProgress; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultBufferingControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultBufferingControl.java new file mode 100644 index 0000000000..b7abb3bf6c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/DefaultBufferingControl.java @@ -0,0 +1,204 @@ +/* + * 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.DefaultAllocator; +import com.google.android.exoplayer.util.Util; + +import android.os.Handler; + +/** + * The default {@link BufferingControl} implementation. + */ +public final class DefaultBufferingControl implements BufferingControl { + + /** + * Interface definition for a callback to be notified of {@link DefaultBufferingControl} events. + */ + public interface EventListener { + + /** + * Invoked when the control transitions from a buffering to a draining state or vice versa. + * + * @param buffering Whether the control is now in the buffering state. + */ + void onBufferingChanged(boolean buffering); + + } + + /** + * The default minimum duration of media that the player will attempt to ensure is buffered at all + * times, in milliseconds. + */ + public static final int DEFAULT_MIN_BUFFER_MS = 15000; + + /** + * The default maximum duration of media that the player will attempt to buffer, in milliseconds. + */ + public static final int DEFAULT_MAX_BUFFER_MS = 30000; + + /** + * The default duration of media that must be buffered for playback to start or resume following a + * user action such as a seek, in milliseconds. + */ + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; + + /** + * The default duration of media that must be buffered for playback to resume after a + * player-invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion rather than a user + * action), in milliseconds. + */ + public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + + 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 DefaultAllocator allocator; + private final Handler eventHandler; + private final EventListener eventListener; + + private final long minBufferUs; + private final long maxBufferUs; + private final long bufferForPlaybackUs; + private final long bufferForPlaybackAfterRebufferUs; + + private int targetBufferSize; + private boolean isBuffering; + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + */ + public DefaultBufferingControl() { + this(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); + } + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @param allocator The {@link DefaultAllocator} used by the loader. + */ + public DefaultBufferingControl(DefaultAllocator allocator) { + this(allocator, null, null); + } + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @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 DefaultBufferingControl(DefaultAllocator allocator, Handler eventHandler, + EventListener eventListener) { + this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, eventHandler, eventListener); + } + + /** + * Constructs a new instance. + * + * @param allocator The {@link DefaultAllocator} used by the loader. + * @param minBufferMs The minimum duration of media that the player will attempt to ensure is + * buffered at all times, in milliseconds. + * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in + * milliseconds. + * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or + * resume following a user action such as a seek, in milliseconds. + * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for + * playback to resume after a player-invoked rebuffer (i.e. a rebuffer that occurs due to + * buffer depletion rather than a user action), in milliseconds. + * @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 DefaultBufferingControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, + long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, Handler eventHandler, + EventListener eventListener) { + this.allocator = allocator; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + minBufferUs = minBufferMs * 1000L; + maxBufferUs = maxBufferMs * 1000L; + bufferForPlaybackUs = bufferForPlaybackMs * 1000L; + bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + } + + @Override + public void onTrackSelections(TrackRenderer[] renderers, TrackGroupArray trackGroups, + TrackSelectionArray trackSelections) { + targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelections.get(i) != null) { + targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + } + } + allocator.setTargetBufferSize(targetBufferSize); + } + + @Override + public void reset() { + targetBufferSize = 0; + setBuffering(false); + } + + @Override + public Allocator getAllocator() { + return allocator; + } + + @Override + public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { + long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; + return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + } + + @Override + public boolean shouldContinueBuffering(long bufferedDurationUs) { + int bufferTimeState = getBufferTimeState(bufferedDurationUs); + boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; + boolean shouldBuffer = bufferTimeState == BELOW_LOW_WATERMARK + || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + setBuffering(shouldBuffer); + return shouldBuffer; + } + + private void setBuffering(boolean isBuffering) { + if (this.isBuffering != isBuffering) { + this.isBuffering = isBuffering; + notifyBufferingChanged(isBuffering); + } + } + + private int getBufferTimeState(long bufferedDurationUs) { + return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK + : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); + } + + private void notifyBufferingChanged(final boolean buffering) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onBufferingChanged(buffering); + } + }); + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultBufferingPolicy.java b/library/src/main/java/com/google/android/exoplayer/DefaultBufferingPolicy.java deleted file mode 100644 index f760306e58..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/DefaultBufferingPolicy.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * 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.BufferingPolicy.LoadControl; -import com.google.android.exoplayer.upstream.Allocator; -import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.upstream.NetworkLock; -import com.google.android.exoplayer.util.Util; - -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. - *

- * 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. - *

- * 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 final class DefaultBufferingPolicy implements BufferingPolicy, LoadControl { - - /** - * Interface definition for a callback to be notified of {@link DefaultBufferingPolicy} 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); - - } - - /** - * The default minimum duration of media that the player will attempt to ensure is buffered at all - * times, in milliseconds. - */ - public static final int DEFAULT_MIN_BUFFER_MS = 15000; - - /** - * The default maximum duration of media that the player will attempt to buffer, in milliseconds. - */ - public static final int DEFAULT_MAX_BUFFER_MS = 30000; - - /** - * The default duration of media that must be buffered for playback to start or resume following a - * user action such as a seek, in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; - - /** - * The default duration of media that must be buffered for playback to resume after a - * player-invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion rather than a user - * action), in milliseconds. - */ - public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - - 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 DefaultAllocator allocator; - private final List loaders; - private final HashMap loaderStates; - private final Handler eventHandler; - private final EventListener eventListener; - - private final long minBufferUs; - private final long maxBufferUs; - private final long bufferForPlaybackUs; - private final long bufferForPlaybackAfterRebufferUs; - - private int targetBufferSize; - private boolean targetBufferSizeReached; - private boolean fillingBuffers; - private boolean streamingPrioritySet; - - private long playbackPositionUs; - private long maxLoadStartPositionUs; - - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - */ - public DefaultBufferingPolicy() { - this(null, null); - } - - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - * - * @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 DefaultBufferingPolicy(Handler eventHandler, EventListener eventListener) { - this(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE), eventHandler, eventListener); - } - - /** - * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. - * - * @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 DefaultBufferingPolicy(DefaultAllocator allocator, Handler eventHandler, - EventListener eventListener) { - this(allocator, eventHandler, eventListener, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_MS, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); - } - - /** - * Constructs a new instance. - * - * @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. - * @param minBufferMs The minimum duration of media that the player will attempt to ensure is - * buffered at all times, in milliseconds. - * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in - * milliseconds. - * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or - * resume following a user action such as a seek, in milliseconds. - * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for - * playback to resume after a player-invoked rebuffer (i.e. a rebuffer that occurs due to - * buffer depletion rather than a user action), in milliseconds. - */ - public DefaultBufferingPolicy(DefaultAllocator allocator, Handler eventHandler, - EventListener eventListener, int minBufferMs, int maxBufferMs, long bufferForPlaybackMs, - long bufferForPlaybackAfterRebufferMs) { - this.allocator = allocator; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.minBufferUs = minBufferMs * 1000L; - this.maxBufferUs = maxBufferMs * 1000L; - this.bufferForPlaybackUs = bufferForPlaybackMs * 1000L; - this.bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; - loaders = new ArrayList<>(); - loaderStates = new HashMap<>(); - } - - // BufferingPolicy implementation. - - @Override - public void setPlaybackPosition(long playbackPositionUs) { - this.playbackPositionUs = playbackPositionUs; - } - - @Override - public boolean haveSufficientBuffer(long bufferedPositionUs, boolean rebuffering) { - long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 - || bufferedPositionUs == C.END_OF_SOURCE_US - || bufferedPositionUs >= playbackPositionUs + minBufferDurationUs; - } - - @Override - public void onTrackSelections(TrackRenderer[] renderers, TrackGroupArray trackGroups, - TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } - allocator.setTargetBufferSize(targetBufferSize); - targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - updateControlState(); - } - - @Override - public void reset() { - targetBufferSize = 0; - } - - @Override - public LoadControl getLoadControl() { - return this; - } - - // LoadControl implementation. - - @Override - public void register(Object loader) { - loaders.add(loader); - loaderStates.put(loader, new LoaderState()); - } - - @Override - public void unregister(Object loader) { - loaders.remove(loader); - loaderStates.remove(loader); - } - - @Override - public Allocator getAllocator() { - return allocator; - } - - @Override - public boolean update(Object loader, long nextLoadPositionUs, boolean loading) { - // 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; - if (loaderStateChanged) { - loaderState.bufferState = loaderBufferState; - loaderState.nextLoadPositionUs = nextLoadPositionUs; - loaderState.loading = loading; - } - - // Update the buffer state. - boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; - boolean bufferStateChanged = this.targetBufferSizeReached != targetBufferSizeReached; - if (bufferStateChanged) { - this.targetBufferSizeReached = targetBufferSizeReached; - } - - // If either of the individual states have changed, update the shared control state. - if (loaderStateChanged || bufferStateChanged) { - updateControlState(); - } - - return nextLoadPositionUs != C.UNSET_TIME_US && nextLoadPositionUs <= maxLoadStartPositionUs; - } - - private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) { - if (nextLoadPositionUs == C.UNSET_TIME_US) { - return ABOVE_HIGH_WATERMARK; - } else { - long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs; - return timeUntilNextLoadPosition > maxBufferUs ? ABOVE_HIGH_WATERMARK : - timeUntilNextLoadPosition < minBufferUs ? BELOW_LOW_WATERMARK : - BETWEEN_WATERMARKS; - } - } - - private void updateControlState() { - boolean loading = false; - boolean haveNextLoadPosition = false; - 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; - worstLoaderState = Math.max(worstLoaderState, loaderState.bufferState); - } - - fillingBuffers = !loaders.isEmpty() && (loading || haveNextLoadPosition) - && (worstLoaderState == BELOW_LOW_WATERMARK - || (worstLoaderState == BETWEEN_WATERMARKS && fillingBuffers && !targetBufferSizeReached)); - 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 = C.UNSET_TIME_US; - 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 != C.UNSET_TIME_US - && (maxLoadStartPositionUs == C.UNSET_TIME_US || 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 int bufferState; - public boolean loading; - public long nextLoadPositionUs; - - public LoaderState() { - bufferState = ABOVE_HIGH_WATERMARK; - loading = false; - nextLoadPositionUs = C.UNSET_TIME_US; - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerFactory.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerFactory.java index c2d6692170..736cb8164a 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerFactory.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerFactory.java @@ -42,7 +42,7 @@ public final class ExoPlayerFactory { * @param trackSelector The {@link TrackSelector} that will be used by the instance. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { - return newSimpleInstance(context, trackSelector, new DefaultBufferingPolicy(), null); + return newSimpleInstance(context, trackSelector, new DefaultBufferingControl(), null); } /** @@ -52,13 +52,13 @@ public final class ExoPlayerFactory { * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param bufferingPolicy The {@link BufferingPolicy} that will be used by the instance. + * @param bufferingControl The {@link BufferingControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy, DrmSessionManager drmSessionManager) { - return newSimpleInstance(context, trackSelector, bufferingPolicy, drmSessionManager, false); + BufferingControl bufferingControl, DrmSessionManager drmSessionManager) { + return newSimpleInstance(context, trackSelector, bufferingControl, drmSessionManager, false); } /** @@ -68,7 +68,7 @@ public final class ExoPlayerFactory { * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param bufferingPolicy The {@link BufferingPolicy} that will be used by the instance. + * @param bufferingControl The {@link BufferingControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param preferExtensionDecoders True to prefer {@link TrackRenderer} instances defined in @@ -76,9 +76,9 @@ public final class ExoPlayerFactory { * included in the application build for setting this flag to have any effect. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy, DrmSessionManager drmSessionManager, + BufferingControl bufferingControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders) { - return newSimpleInstance(context, trackSelector, bufferingPolicy, drmSessionManager, + return newSimpleInstance(context, trackSelector, bufferingControl, drmSessionManager, preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } @@ -89,7 +89,7 @@ public final class ExoPlayerFactory { * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param bufferingPolicy The {@link BufferingPolicy} that will be used by the instance. + * @param bufferingControl The {@link BufferingControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. * @param preferExtensionDecoders True to prefer {@link TrackRenderer} instances defined in @@ -99,9 +99,9 @@ public final class ExoPlayerFactory { * seamlessly join an ongoing playback. */ public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy, DrmSessionManager drmSessionManager, + BufferingControl bufferingControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { - return new SimpleExoPlayer(context, trackSelector, bufferingPolicy, drmSessionManager, + return new SimpleExoPlayer(context, trackSelector, bufferingControl, drmSessionManager, preferExtensionDecoders, allowedVideoJoiningTimeMs); } @@ -114,7 +114,7 @@ public final class ExoPlayerFactory { * @param trackSelector The {@link TrackSelector} that will be used by the instance. */ public static ExoPlayer newInstance(TrackRenderer[] renderers, TrackSelector trackSelector) { - return newInstance(renderers, trackSelector, new DefaultBufferingPolicy()); + return newInstance(renderers, trackSelector, new DefaultBufferingControl()); } /** @@ -124,11 +124,11 @@ public final class ExoPlayerFactory { * * @param renderers The {@link TrackRenderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param bufferingPolicy The {@link BufferingPolicy} that will be used by the instance. + * @param bufferingControl The {@link BufferingControl} that will be used by the instance. */ public static ExoPlayer newInstance(TrackRenderer[] renderers, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy) { - return new ExoPlayerImpl(renderers, trackSelector, bufferingPolicy); + BufferingControl bufferingControl) { + return new ExoPlayerImpl(renderers, trackSelector, bufferingControl); } } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java index bab2257ebc..3de3022d92 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java @@ -55,11 +55,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * * @param renderers The {@link TrackRenderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param bufferingPolicy The {@link BufferingPolicy} that will be used by the instance. + * @param bufferingControl The {@link BufferingControl} that will be used by the instance. */ @SuppressLint("HandlerLeak") public ExoPlayerImpl(TrackRenderer[] renderers, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy) { + BufferingControl bufferingControl) { Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); Assertions.checkNotNull(renderers); Assertions.checkState(renderers.length > 0); @@ -72,7 +72,7 @@ import java.util.concurrent.CopyOnWriteArraySet; ExoPlayerImpl.this.handleEvent(msg); } }; - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, bufferingPolicy, + internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, bufferingControl, playWhenReady, eventHandler); playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 9ce501f18f..087af4de51 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -77,8 +77,9 @@ import java.util.ArrayList; private static final int MSG_STOP = 4; private static final int MSG_RELEASE = 5; private static final int MSG_SOURCE_PREPARED = 6; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 7; - private static final int MSG_CUSTOM = 8; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 7; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 8; + private static final int MSG_CUSTOM = 9; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -92,7 +93,7 @@ import java.util.ArrayList; private static final int MAXIMUM_BUFFER_AHEAD_SOURCES = 100; private final TrackSelector trackSelector; - private final BufferingPolicy bufferingPolicy; + private final BufferingControl bufferingControl; private final StandaloneMediaClock standaloneMediaClock; private final Handler handler; private final HandlerThread internalPlaybackThread; @@ -115,9 +116,9 @@ import java.util.ArrayList; private long internalPositionUs; public ExoPlayerImplInternal(TrackRenderer[] renderers, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy, boolean playWhenReady, Handler eventHandler) { + BufferingControl bufferingControl, boolean playWhenReady, Handler eventHandler) { this.trackSelector = trackSelector; - this.bufferingPolicy = bufferingPolicy; + this.bufferingControl = bufferingControl; this.playWhenReady = playWhenReady; this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; @@ -211,6 +212,11 @@ import java.util.ArrayList; handler.obtainMessage(MSG_SOURCE_PREPARED, source).sendToTarget(); } + @Override + public void onContinueLoadingRequested(SampleSource source) { + handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget(); + } + // Handler.Callback implementation. @Override @@ -245,6 +251,10 @@ import java.util.ArrayList; timeline.handleSourcePrepared((SampleSource) msg.obj); return true; } + case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { + timeline.handleContinueLoadingRequested((SampleSource) msg.obj); + return true; + } case MSG_TRACK_SELECTION_INVALIDATED: { reselectTracksInternal(); return true; @@ -356,7 +366,6 @@ import java.util.ArrayList; } playbackInfo.positionUs = positionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; - bufferingPolicy.setPlaybackPosition(positionUs); // Update the buffered position. long bufferedPositionUs; @@ -409,7 +418,7 @@ import java.util.ArrayList; stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady) - && bufferingPolicy.haveSufficientBuffer(playbackInfo.bufferedPositionUs, rebuffering)) { + && timeline.haveSufficientBuffer(rebuffering)) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); @@ -456,19 +465,16 @@ import java.util.ArrayList; stopRenderers(); rebuffering = false; - timeline.seekToSource(sourceIndex); - SampleSource sampleSource = timeline.getSampleSource(); - if (sampleSource != null && enabledRenderers.length > 0) { - seekPositionUs = sampleSource.seekToUs(seekPositionUs); - } - resetInternalPosition(seekPositionUs); - + seekPositionUs = timeline.seekTo(sourceIndex, seekPositionUs); if (sourceIndex != playbackInfo.sourceIndex) { playbackInfo = new PlaybackInfo(sourceIndex); - updatePlaybackPositions(); + playbackInfo.positionUs = seekPositionUs; eventHandler.obtainMessage(MSG_SOURCE_CHANGED, playbackInfo).sendToTarget(); + } else { + playbackInfo.positionUs = seekPositionUs; } + updatePlaybackPositions(); if (sampleSourceProvider != null) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -478,7 +484,8 @@ import java.util.ArrayList; } private void resetInternalPosition(long sourcePositionUs) throws ExoPlaybackException { - internalPositionUs = timeline.playingSource.offsetUs + sourcePositionUs; + long sourceOffsetUs = timeline.playingSource == null ? 0 : timeline.playingSource.offsetUs; + internalPositionUs = sourceOffsetUs + sourcePositionUs; standaloneMediaClock.setPositionUs(internalPositionUs); for (TrackRenderer renderer : enabledRenderers) { renderer.reset(internalPositionUs); @@ -517,7 +524,7 @@ import java.util.ArrayList; enabledRenderers = new TrackRenderer[0]; sampleSourceProvider = null; timeline.reset(); - bufferingPolicy.reset(); + bufferingControl.reset(); } private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { @@ -579,6 +586,24 @@ import java.util.ArrayList; return playingSource == null ? null : playingSource.sampleSource; } + public boolean haveSufficientBuffer(boolean rebuffering) { + if (bufferingSource == null) { + return false; + } + long positionUs = internalPositionUs - bufferingSource.offsetUs; + long bufferedPositionUs = !bufferingSource.prepared ? 0 + : bufferingSource.sampleSource.getBufferedPositionUs(); + if (bufferedPositionUs == C.END_OF_SOURCE_US) { + int sourceCount = sampleSourceProvider.getSourceCount(); + if (sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT + && bufferingSource.index == sourceCount - 1) { + return true; + } + bufferedPositionUs = bufferingSource.sampleSource.getDurationUs(); + } + return bufferingControl.shouldStartPlayback(bufferedPositionUs - positionUs, rebuffering); + } + public void maybeThrowSourcePrepareError() throws IOException { if (bufferingSource != null && !bufferingSource.prepared && (readingSource == null || readingSource.nextSource == bufferingSource)) { @@ -611,14 +636,14 @@ import java.util.ArrayList; } bufferingSource = newSource; long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0; - sampleSource.prepare(ExoPlayerImplInternal.this, bufferingPolicy.getLoadControl(), - startPositionUs); + bufferingSource.sampleSource.prepare(ExoPlayerImplInternal.this, + bufferingControl.getAllocator(), startPositionUs); } } } - if (bufferingSource != null && bufferingSource.hasEnabledTracks) { - long sourcePositionUs = internalPositionUs - bufferingSource.offsetUs; - bufferingSource.sampleSource.continueBuffering(sourcePositionUs); + + if (bufferingSource != null && bufferingSource.needsContinueLoading) { + maybeContinueLoading(); } if (playingSource == null) { @@ -687,22 +712,45 @@ import java.util.ArrayList; } } - public void handleSourcePrepared(SampleSource sampleSource) throws ExoPlaybackException { - if (bufferingSource == null || bufferingSource.sampleSource != sampleSource) { + public void handleSourcePrepared(SampleSource source) throws ExoPlaybackException { + if (bufferingSource == null || bufferingSource.sampleSource != source) { // Stale event. return; } long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0; - bufferingSource.handlePrepared(startPositionUs, bufferingPolicy); + bufferingSource.handlePrepared(startPositionUs, bufferingControl); if (playingSource == null) { // This is the first prepared source, so start playing it. readingSource = bufferingSource; setPlayingSource(readingSource); updateTimelineState(); } + maybeContinueLoading(); } - public void seekToSource(int sourceIndex) throws ExoPlaybackException { + public void handleContinueLoadingRequested(SampleSource source) { + if (bufferingSource == null || bufferingSource.sampleSource != source) { + return; + } + maybeContinueLoading(); + } + + private void maybeContinueLoading() { + long nextLoadPositionUs = bufferingSource.sampleSource.getNextLoadPositionUs(); + if (nextLoadPositionUs != C.END_OF_SOURCE_US) { + long positionUs = internalPositionUs - bufferingSource.offsetUs; + long bufferedDurationUs = nextLoadPositionUs - positionUs; + boolean continueBuffering = bufferingControl.shouldContinueBuffering(bufferedDurationUs); + if (continueBuffering) { + bufferingSource.needsContinueLoading = false; + bufferingSource.sampleSource.continueLoading(positionUs); + } else { + bufferingSource.needsContinueLoading = true; + } + } + } + + public long seekTo(int sourceIndex, long seekPositionUs) throws ExoPlaybackException { // Clear the timeline, but keep the requested source if it is already prepared. Source source = playingSource; Source newPlayingSource = null; @@ -721,6 +769,11 @@ import java.util.ArrayList; updateTimelineState(); readingSource = playingSource; bufferingSource = playingSource; + if (playingSource.hasEnabledTracks) { + seekPositionUs = playingSource.sampleSource.seekToUs(seekPositionUs); + } + resetInternalPosition(seekPositionUs); + maybeContinueLoading(); } else { for (TrackRenderer renderer : enabledRenderers) { ensureStopped(renderer); @@ -731,7 +784,9 @@ import java.util.ArrayList; readingSource = null; bufferingSource = null; pendingSourceIndex = sourceIndex; + resetInternalPosition(seekPositionUs); } + return seekPositionUs; } public void reselectTracks() throws ExoPlaybackException { @@ -769,7 +824,7 @@ import java.util.ArrayList; // Update streams for the new selection, recreating all streams if reading ahead. boolean recreateStreams = readingSource != playingSource; TrackSelectionArray playingSourceOldTrackSelections = playingSource.sourceTrackSelections; - playingSource.updateSourceTrackSelection(playbackInfo.positionUs, bufferingPolicy, + playingSource.updateSourceTrackSelection(playbackInfo.positionUs, bufferingControl, recreateStreams); int enabledRendererCount = 0; @@ -810,10 +865,10 @@ import java.util.ArrayList; source = source.nextSource; } bufferingSource.nextSource = null; - long positionUs = Math.max(0, internalPositionUs - bufferingSource.offsetUs); - bufferingSource.updateSourceTrackSelection(positionUs, bufferingPolicy, false); + bufferingSource.updateSourceTrackSelection(positionUs, bufferingControl, false); } + maybeContinueLoading(); } public void reset() { @@ -930,6 +985,7 @@ import java.util.ArrayList; public boolean hasEnabledTracks; public long offsetUs; public Source nextSource; + public boolean needsContinueLoading; private Object trackSelectionData; private TrackSelectionArray trackSelections; @@ -955,11 +1011,11 @@ import java.util.ArrayList; || sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US); } - public void handlePrepared(long positionUs, BufferingPolicy bufferingPolicy) + public void handlePrepared(long positionUs, BufferingControl bufferingControl) throws ExoPlaybackException { prepared = true; selectTracks(); - updateSourceTrackSelection(positionUs, bufferingPolicy, false); + updateSourceTrackSelection(positionUs, bufferingControl, false); } public boolean selectTracks() throws ExoPlaybackException { @@ -974,7 +1030,7 @@ import java.util.ArrayList; return true; } - public void updateSourceTrackSelection(long positionUs, BufferingPolicy bufferingPolicy, + public void updateSourceTrackSelection(long positionUs, BufferingControl bufferingControl, boolean forceRecreateStreams) throws ExoPlaybackException { // Populate lists of streams that are being disabled/newly enabled. ArrayList oldStreams = new ArrayList<>(); @@ -1013,7 +1069,7 @@ import java.util.ArrayList; } // The track selection has changed. - bufferingPolicy.onTrackSelections(renderers, sampleSource.getTrackGroups(), trackSelections); + bufferingControl.onTrackSelections(renderers, sampleSource.getTrackGroups(), trackSelections); } public void release() { diff --git a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java index 7b10e6ee9f..543df7eb7a 100644 --- a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; +import com.google.android.exoplayer.upstream.Allocator; import android.util.Pair; @@ -40,6 +40,7 @@ public final class MultiSampleSource implements SampleSource, SampleSource.Callb private boolean seenFirstTrackSelection; private SampleSource[] enabledSources; + private SequenceableLoader sequenceableLoader; public MultiSampleSource(SampleSource... sources) { this.sources = sources; @@ -49,10 +50,10 @@ public final class MultiSampleSource implements SampleSource, SampleSource.Callb } @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; for (SampleSource source : sources) { - source.prepare(this, loadControl, positionUs); + source.prepare(this, allocator, positionUs); } } @@ -86,6 +87,7 @@ public final class MultiSampleSource implements SampleSource, SampleSource.Callb enabledSourceCount++; } } + seenFirstTrackSelection = true; // Update the enabled sources. enabledSources = new SampleSource[enabledSourceCount]; enabledSourceCount = 0; @@ -94,15 +96,18 @@ public final class MultiSampleSource implements SampleSource, SampleSource.Callb enabledSources[enabledSourceCount++] = sources[i]; } } - seenFirstTrackSelection = true; + sequenceableLoader = new CompositeSequenceableLoader(enabledSources); return newStreams; } @Override - public void continueBuffering(long positionUs) { - for (SampleSource source : enabledSources) { - source.continueBuffering(positionUs); - } + public boolean continueLoading(long positionUs) { + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return sequenceableLoader.getNextLoadPositionUs(); } @Override @@ -185,6 +190,11 @@ public final class MultiSampleSource implements SampleSource, SampleSource.Callb callback.onSourcePrepared(this); } + @Override + public void onContinueLoadingRequested(SampleSource ignored) { + callback.onContinueLoadingRequested(this); + } + // Internal methods. private int selectTracks(SampleSource source, List allOldStreams, diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 8d84d7de74..26bb5392be 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; +import com.google.android.exoplayer.upstream.Allocator; import java.io.IOException; import java.util.List; @@ -23,12 +23,12 @@ import java.util.List; /** * A source of media. */ -public interface SampleSource { +public interface SampleSource extends SequenceableLoader { /** * A callback to be notified of {@link SampleSource} events. */ - interface Callback { + interface Callback extends SequenceableLoader.Callback { /** * Invoked by the source when preparation completes. @@ -50,11 +50,10 @@ public interface SampleSource { * invoked. * * @param callback A callback to receive updates from the source. - * @param loadControl A {@link LoadControl} to determine when to load data. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The player's current playback position. - * @return True if the source is prepared, false otherwise. */ - void prepare(Callback callback, LoadControl loadControl, long positionUs); + void prepare(Callback callback, Allocator allocator, long positionUs); /** * Throws an error that's preventing the source from becoming prepared. Does nothing if no such @@ -108,15 +107,6 @@ public interface SampleSource { TrackStream[] selectTracks(List oldStreams, List newSelections, long positionUs); - /** - * Indicates to the source that it should continue buffering data for its enabled tracks. - *

- * This method should only be called when at least one track is selected. - * - * @param positionUs The current playback position. - */ - void continueBuffering(long positionUs); - /** * Attempts to read a discontinuity. *

diff --git a/library/src/main/java/com/google/android/exoplayer/SequenceableLoader.java b/library/src/main/java/com/google/android/exoplayer/SequenceableLoader.java new file mode 100644 index 0000000000..640acce41f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/SequenceableLoader.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * An loader that can proceed in approximate synchronization with other loaders. + */ +public interface SequenceableLoader { + + /** + * A callback to be notified of {@link SequenceableLoader} events. + */ + interface Callback { + + /** + * Invoked by the loader to indicate that it wishes for its {@link #continueLoading(long)} + * method to be called when it can continue to load data. + */ + void onContinueLoadingRequested(T source); + + } + + /** + * Returns the next load time, or {@link C#END_OF_SOURCE_US} if loading has finished. + */ + long getNextLoadPositionUs(); + + /** + * Attempts to continue loading. + * + * @param positionUs The current playback position. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return + * a different value than prior to the call. False otherwise. + */ + boolean continueLoading(long positionUs); + +} diff --git a/library/src/main/java/com/google/android/exoplayer/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer/SimpleExoPlayer.java index c6834215ae..2c1e97cc43 100644 --- a/library/src/main/java/com/google/android/exoplayer/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer/SimpleExoPlayer.java @@ -111,7 +111,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private CodecCounters audioCodecCounters; /* package */ SimpleExoPlayer(Context context, TrackSelector trackSelector, - BufferingPolicy bufferingPolicy, DrmSessionManager drmSessionManager, + BufferingControl bufferingControl, DrmSessionManager drmSessionManager, boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { mainHandler = new Handler(); bandwidthMeter = new DefaultBandwidthMeter(); @@ -145,7 +145,7 @@ public final class SimpleExoPlayer implements ExoPlayer { this.audioRendererCount = audioRendererCount; // Build the player and associated objects. - player = new ExoPlayerImpl(renderers, trackSelector, bufferingPolicy); + player = new ExoPlayerImpl(renderers, trackSelector, bufferingControl); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java index 61c00ff0d3..b4bf769791 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; +import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader; @@ -109,7 +109,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, // SampleSource implementation. @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { callback.onSourcePrepared(this); } @@ -133,26 +133,27 @@ public final class SingleSampleSource implements SampleSource, TrackStream, List newSelections, long positionUs) { Assertions.checkState(oldStreams.size() <= 1); Assertions.checkState(newSelections.size() <= 1); - // Unselect old tracks. - if (!oldStreams.isEmpty()) { - streamState = STREAM_STATE_END_OF_STREAM; - if (loader.isLoading()) { - loader.cancelLoading(); - } - } // Select new tracks. TrackStream[] newStreams = new TrackStream[newSelections.size()]; if (!newSelections.isEmpty()) { newStreams[0] = this; streamState = STREAM_STATE_SEND_FORMAT; - maybeStartLoading(); } return newStreams; } @Override - public void continueBuffering(long positionUs) { - // Do nothing. + public boolean continueLoading(long positionUs) { + if (loadingFinished || loader.isLoading()) { + return false; + } + loader.startLoading(this, this, minLoadableRetryCount); + return true; + } + + @Override + public long getNextLoadPositionUs() { + return loadingFinished || loader.isLoading() ? C.END_OF_SOURCE_US : 0; } @Override @@ -170,6 +171,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, @Override public void release() { + sampleData = null; loader.release(); } @@ -225,9 +227,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, @Override public void onLoadCanceled(SingleSampleSource loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (!released) { - maybeStartLoading(); - } + // Never happens. } @Override @@ -272,13 +272,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, // Internal methods. - private void maybeStartLoading() { - if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) { - return; - } - loader.startLoading(this, this, minLoadableRetryCount); - } - private void notifyLoadError(final IOException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { 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 5becab3308..9e2d2d33f6 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 @@ -16,18 +16,17 @@ package com.google.android.exoplayer.chunk; import com.google.android.exoplayer.AdaptiveSourceEventListener.EventDispatcher; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.SequenceableLoader; import com.google.android.exoplayer.TrackStream; import com.google.android.exoplayer.extractor.DefaultTrackOutput; +import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.util.Assertions; -import android.os.SystemClock; - import java.io.IOException; import java.util.Collections; import java.util.LinkedList; @@ -36,12 +35,12 @@ import java.util.List; /** * A {@link TrackStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. */ -public class ChunkTrackStream implements TrackStream, +public class ChunkTrackStream implements TrackStream, SequenceableLoader, Loader.Callback { private final int trackType; private final T chunkSource; - private final LoadControl loadControl; + private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; private final int minLoadableRetryCount; private final LinkedList mediaChunks; @@ -50,11 +49,8 @@ public class ChunkTrackStream implements TrackStream, private final ChunkHolder nextChunkHolder; private final Loader loader; - private boolean readingEnabled; - private long lastPreferredQueueSizeEvaluationTimeMs; private Format downstreamFormat; - private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -63,47 +59,29 @@ public class ChunkTrackStream implements TrackStream, /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. - * @param loadControl Controls when the source is permitted to load data. + * @param callback An {@link Callback} for the stream. + * @param allocator An {@link Allocator} from which allocations can be obtained. * @param positionUs The position from which to start loading media. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkTrackStream(int trackType, T chunkSource, LoadControl loadControl, long positionUs, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + public ChunkTrackStream(int trackType, T chunkSource, + SequenceableLoader.Callback> callback, Allocator allocator, + long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) { this.trackType = trackType; this.chunkSource = chunkSource; - this.loadControl = loadControl; + this.callback = callback; this.eventDispatcher = eventDispatcher; this.minLoadableRetryCount = minLoadableRetryCount; loader = new Loader("Loader:ChunkTrackStream"); nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); - sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); + sampleQueue = new DefaultTrackOutput(allocator); pendingResetPositionUs = C.UNSET_TIME_US; - readingEnabled = true; - downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; - loadControl.register(this); - restartFrom(positionUs); - } - - /** - * Enables or disables reading of data from {@link #readData(FormatHolder, DecoderInputBuffer)}. - * - * @param readingEnabled Whether reading should be enabled. - */ - public void setReadingEnabled(boolean readingEnabled) { - this.readingEnabled = readingEnabled; - } - - // TODO[REFACTOR]: Find a way to get rid of this. - public void continueBuffering(long positionUs) { - downstreamPositionUs = positionUs; - if (!loader.isLoading()) { - maybeStartLoading(); - } + pendingResetPositionUs = positionUs; } /** @@ -127,7 +105,7 @@ public class ChunkTrackStream implements TrackStream, } else if (isPendingReset()) { return pendingResetPositionUs; } else { - long bufferedPositionUs = downstreamPositionUs; + long bufferedPositionUs = lastSeekPositionUs; BaseMediaChunk lastMediaChunk = mediaChunks.getLast(); BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; @@ -144,7 +122,6 @@ public class ChunkTrackStream implements TrackStream, * @param positionUs The seek position in microseconds. */ public void seekToUs(long positionUs) { - downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the sample queue. boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); @@ -156,7 +133,14 @@ public class ChunkTrackStream implements TrackStream, } } else { // We failed, and need to restart. - restartFrom(positionUs); + pendingResetPositionUs = positionUs; + loadingFinished = false; + mediaChunks.clear(); + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + sampleQueue.reset(true); + } } } @@ -168,7 +152,6 @@ public class ChunkTrackStream implements TrackStream, public void release() { chunkSource.release(); sampleQueue.disable(); - loadControl.unregister(this); loader.release(); } @@ -189,7 +172,7 @@ public class ChunkTrackStream implements TrackStream, @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (!readingEnabled || isPendingReset()) { + if (isPendingReset()) { return NOTHING_READ; } @@ -218,7 +201,7 @@ public class ChunkTrackStream implements TrackStream, eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.format, loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); - maybeStartLoading(); + callback.onContinueLoadingRequested(this); } @Override @@ -228,7 +211,8 @@ public class ChunkTrackStream implements TrackStream, loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { - restartFrom(pendingResetPositionUs); + sampleQueue.reset(true); + callback.onContinueLoadingRequested(this); } } @@ -255,43 +239,23 @@ public class ChunkTrackStream implements TrackStream, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, canceled); if (canceled) { - maybeStartLoading(); + callback.onContinueLoadingRequested(this); return Loader.DONT_RETRY; } else { return Loader.RETRY; } } - // Internal methods. + // SequenceableLoader implementation - private void restartFrom(long positionUs) { - pendingResetPositionUs = positionUs; - loadingFinished = false; - mediaChunks.clear(); + @Override + public boolean continueLoading(long positionUs) { if (loader.isLoading()) { - loader.cancelLoading(); - } else { - sampleQueue.reset(true); - maybeStartLoading(); - } - } - - private void maybeStartLoading() { - 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; - } - - boolean isNext = loadControl.update(this, getNextLoadPositionUs(), false); - if (!isNext) { - return; + return false; } chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.UNSET_TIME_US ? pendingResetPositionUs : downstreamPositionUs, + pendingResetPositionUs != C.UNSET_TIME_US ? pendingResetPositionUs : positionUs, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; @@ -299,12 +263,11 @@ public class ChunkTrackStream implements TrackStream, if (endOfStream) { loadingFinished = true; - loadControl.update(this, C.UNSET_TIME_US, false); - return; + return true; } if (loadable == null) { - return; + return false; } if (isMediaChunk(loadable)) { @@ -317,22 +280,32 @@ public class ChunkTrackStream implements TrackStream, eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.format, loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs); - // Update the load control again to indicate that we're now loading. - loadControl.update(this, getNextLoadPositionUs(), true); + return true; } - /** - * Gets the next load time, assuming that the next load starts where the previous chunk ended (or - * from the pending reset time, if there is one). - */ - private long getNextLoadPositionUs() { + @Override + public long getNextLoadPositionUs() { if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.UNSET_TIME_US : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.END_OF_SOURCE_US : mediaChunks.getLast().endTimeUs; } } + // Internal methods + + // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming. + /** + * Discards media chunks from the back of the buffer if conditions have changed such that it's + * preferable to re-buffer the media at a different quality. + * + * @param positionUs The current playback position in microseconds. + */ + private void maybeDiscardUpstream(long positionUs) { + int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + discardUpstreamMediaChunks(Math.max(1, queueSize)); + } + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java index 9f8e0909ca..635860bc83 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java @@ -17,11 +17,12 @@ package com.google.android.exoplayer.dash; import com.google.android.exoplayer.AdaptiveSourceEventListener; import com.google.android.exoplayer.AdaptiveSourceEventListener.EventDispatcher; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.CompositeSequenceableLoader; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SequenceableLoader; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; @@ -35,6 +36,7 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.UtcTimingElement; +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; @@ -61,7 +63,8 @@ import java.util.TimeZone; /** * A {@link SampleSource} for DASH media. */ -public final class DashSampleSource implements SampleSource { +public final class DashSampleSource implements SampleSource, + SequenceableLoader.Callback> { private static final String TAG = "DashSampleSource"; @@ -84,13 +87,16 @@ public final class DashSampleSource implements SampleSource { private MediaPresentationDescription manifest; private Callback callback; - private LoadControl loadControl; + private Allocator allocator; + private Handler manifestRefreshHandler; private boolean prepared; private long durationUs; private long elapsedRealtimeOffset; private TrackGroupArray trackGroups; private int[] trackGroupAdaptationSetIndices; + private ChunkTrackStream[] trackStreams; + private CompositeSequenceableLoader sequenceableLoader; public DashSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, @@ -99,17 +105,19 @@ public final class DashSampleSource implements SampleSource { this.dataSourceFactory = dataSourceFactory; this.bandwidthMeter = bandwidthMeter; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - loader = new Loader("Loader:DashSampleSource"); dataSource = dataSourceFactory.createDataSource(); + loader = new Loader("Loader:DashSampleSource"); manifestParser = new MediaPresentationDescriptionParser(); manifestCallback = new ManifestCallback(); trackStreams = newTrackStreamArray(0); + sequenceableLoader = new CompositeSequenceableLoader(trackStreams); } @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; - this.loadControl = loadControl; + this.allocator = allocator; + manifestRefreshHandler = new Handler(); startLoadingManifest(); } @@ -154,28 +162,18 @@ public final class DashSampleSource implements SampleSource { } trackStreams = newTrackStreams; + sequenceableLoader = new CompositeSequenceableLoader(trackStreams); return streamsToReturn; } @Override - public void continueBuffering(long positionUs) { - if (manifest.dynamic) { - long minUpdatePeriod = manifest.minUpdatePeriod; - if (minUpdatePeriod == 0) { - // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where - // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit - // signaling in the stream, according to: - // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ - minUpdatePeriod = 5000; - } - if (!loader.isLoading() - && SystemClock.elapsedRealtime() > manifestLoadStartTimestamp + minUpdatePeriod) { - startLoadingManifest(); - } - } - for (ChunkTrackStream trackStream : trackStreams) { - trackStream.continueBuffering(positionUs); - } + public boolean continueLoading(long positionUs) { + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return sequenceableLoader.getNextLoadPositionUs(); } @Override @@ -205,12 +203,23 @@ public final class DashSampleSource implements SampleSource { @Override public void release() { + if (manifestRefreshHandler != null) { + manifestRefreshHandler.removeCallbacksAndMessages(null); + manifestRefreshHandler = null; + } loader.release(); for (ChunkTrackStream trackStream : trackStreams) { trackStream.release(); } } + // SequenceableLoader.Callback implementation. + + @Override + public void onContinueLoadingRequested(ChunkTrackStream trackStream) { + callback.onContinueLoadingRequested(this); + } + // Loadable callbacks. /* package */ void onManifestLoadCompleted(ParsingLoadable loadable, @@ -229,13 +238,14 @@ public final class DashSampleSource implements SampleSource { if (manifest.utcTiming != null) { resolveUtcTimingElement(manifest.utcTiming); } else { - prepared = true; - callback.onSourcePrepared(this); + finishPrepare(); } } else { for (ChunkTrackStream trackStream : trackStreams) { trackStream.getChunkSource().updateManifest(manifest); } + callback.onContinueLoadingRequested(this); + scheduleManifestRefresh(); } } @@ -307,15 +317,41 @@ public final class DashSampleSource implements SampleSource { private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs; - prepared = true; - callback.onSourcePrepared(this); + finishPrepare(); } private void onUtcTimestampResolutionError(IOException error) { Log.e(TAG, "Failed to resolve UtcTiming element.", error); // Be optimistic and continue in the hope that the device clock is correct. + finishPrepare(); + } + + private void finishPrepare() { prepared = true; callback.onSourcePrepared(this); + scheduleManifestRefresh(); + } + + private void scheduleManifestRefresh() { + if (!manifest.dynamic) { + return; + } + long minUpdatePeriod = manifest.minUpdatePeriod; + if (minUpdatePeriod == 0) { + // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where + // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit + // signaling in the stream, according to: + // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ + minUpdatePeriod = 5000; + } + long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod; + long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); + manifestRefreshHandler.postDelayed(new Runnable() { + @Override + public void run() { + startLoadingManifest(); + } + }, delayUntilNextLoad); } private void startLoading(ParsingLoadable loadable, @@ -365,7 +401,7 @@ public final class DashSampleSource implements SampleSource { DashChunkSource chunkSource = new DashChunkSource(loader, manifest, adaptationSetIndex, trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator, elapsedRealtimeOffset); - return new ChunkTrackStream<>(adaptationSetType, chunkSource, loadControl, positionUs, + return new ChunkTrackStream<>(adaptationSetType, chunkSource, this, allocator, positionUs, MIN_LOADABLE_RETRY_COUNT, eventDispatcher); } 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 388dbe4729..de437ccb24 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 @@ -15,18 +15,19 @@ */ package com.google.android.exoplayer.extractor; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SequenceableLoader; 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.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; +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; @@ -34,10 +35,10 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.ConditionVariable; import com.google.android.exoplayer.util.Util; import android.net.Uri; -import android.os.ConditionVariable; import android.os.Handler; import java.io.EOFException; @@ -133,7 +134,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private final ExtractorHolder extractorHolder; private Callback callback; - private LoadControl loadControl; + private Allocator allocator; private SeekMap seekMap; private boolean tracksBuilt; private boolean prepared; @@ -308,9 +309,9 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // SampleSource implementation. @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; - this.loadControl = loadControl; + this.allocator = allocator; loadCondition.open(); startLoading(); } @@ -334,7 +335,6 @@ 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; @@ -364,38 +364,34 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } } } - // Cancel or start requests as necessary. if (enabledTrackCount == 0) { - if (tracksWereEnabled) { - loadControl.unregister(this); - loadCondition.close(); - } + notifyReset = false; if (loader.isLoading()) { loader.cancelLoading(); } - } else { - if (!tracksWereEnabled) { - loadControl.register(this); - loadCondition.open(); - } - if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { - long seekPositionUs = seekToUs(positionUs); - if (seekPositionUs != positionUs) { - notifyReset = true; - } - } + } else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { + seekToUs(positionUs); } seenFirstTrackSelection = true; return newStreams; } @Override - public void continueBuffering(long playbackPositionUs) { - if (loadControl.update(this, getBufferedPositionUs(), loader.isLoading())) { - loadCondition.open(); - } else { - loadCondition.close(); + public boolean continueLoading(long playbackPositionUs) { + if (loadingFinished) { + return false; } + boolean continuedLoading = loadCondition.open(); + if (!loader.isLoading()) { + startLoading(); + continuedLoading = true; + } + return continuedLoading; + } + + @Override + public long getNextLoadPositionUs() { + return getBufferedPositionUs(); } @Override @@ -434,7 +430,15 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } // If we failed to seek within the sample queues, we need to restart. if (!seekInsideBuffer) { - restartFrom(positionUs); + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } + } } notifyReset = false; return positionUs; @@ -445,9 +449,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu for (DefaultTrackOutput sampleQueue : sampleQueues) { sampleQueue.disable(); } - if (enabledTrackCount > 0) { - loadControl.unregister(this); - } loader.release(); } @@ -458,15 +459,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } /* package */ void maybeThrowError() throws IOException { - int minRetryCount = minLoadableRetryCount; - if (minRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { - // We assume on-demand before we're prepared. - minRetryCount = !prepared || length != C.LENGTH_UNBOUNDED - || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US) - ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND - : DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; - } - loader.maybeThrowError(minRetryCount); + loader.maybeThrowError(); } /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { @@ -496,7 +489,10 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu long loadDurationMs, boolean released) { copyLengthFromLoader(loadable); if (!released && enabledTrackCount > 0) { - restartFrom(pendingResetPositionUs); + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } + callback.onContinueLoadingRequested(this); } } @@ -520,7 +516,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(loadControl.getAllocator()); + DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); sampleQueue.setUpstreamFormatChangeListener(this); sampleQueues[sampleQueues.length - 1] = sampleQueue; return sampleQueue; @@ -575,19 +571,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } } - private void restartFrom(long positionUs) { - pendingResetPositionUs = positionUs; - loadingFinished = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); - } - startLoading(); - } - } - private void startLoading() { ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, loadCondition); @@ -602,7 +585,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu pendingResetPositionUs = C.UNSET_TIME_US; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - loader.startLoading(loadable, this, 0); + + int minRetryCount = minLoadableRetryCount; + if (minRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { + // We assume on-demand before we're prepared. + minRetryCount = !prepared || length != C.LENGTH_UNBOUNDED + || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US) + ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND + : DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; + } + loader.startLoading(loadable, this, minRetryCount); } private void configureRetry(ExtractingLoadable loadable) { @@ -616,6 +608,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // available media. For this case there's no way to continue loading from where a // previous load finished, so it's necessary to load from the start whenever commencing // a new load. + lastSeekPositionUs = 0; notifyReset = prepared; for (int i = 0; i < sampleQueues.length; i++) { sampleQueues[i].reset(trackEnabledStates[i]); @@ -688,7 +681,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu /** * Loads the media stream and extracts sample data from it. */ - /* package */ static final class ExtractingLoadable implements Loadable { + /* package */ final class ExtractingLoadable implements Loadable { + + /** + * The number of bytes that should be loaded between each each invocation of + * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; private final Uri uri; private final DataSource dataSource; @@ -745,10 +744,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu pendingExtractorSeek = false; } while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - // TODO: Prevent the loader from loading too much data between evaluations of the - // buffering condition. loadCondition.block(); result = extractor.read(input, positionHolder); + if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { + position = input.getPosition(); + loadCondition.close(); + callback.onContinueLoadingRequested(ExtractorSampleSource.this); + } } } finally { if (result == Extractor.RESULT_SEEK) { 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 e7ac1f062b..460134c442 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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.AdaptiveSourceEventListener; import com.google.android.exoplayer.AdaptiveSourceEventListener.EventDispatcher; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.CompositeSequenceableLoader; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; @@ -32,6 +32,7 @@ import com.google.android.exoplayer.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer.hls.playlist.HlsPlaylist; import com.google.android.exoplayer.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer.hls.playlist.Variant; +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; @@ -71,7 +72,7 @@ public final class HlsSampleSource implements SampleSource, private final HlsPlaylistParser manifestParser; private Callback callback; - private LoadControl loadControl; + private Allocator allocator; private long preparePositionUs; private int pendingPrepareCount; @@ -82,6 +83,7 @@ public final class HlsSampleSource implements SampleSource, private int[] selectedTrackCounts; private HlsTrackStreamWrapper[] trackStreamWrappers; private HlsTrackStreamWrapper[] enabledTrackStreamWrappers; + private CompositeSequenceableLoader sequenceableLoader; public HlsSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, @@ -100,9 +102,9 @@ public final class HlsSampleSource implements SampleSource, } @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; - this.loadControl = loadControl; + this.allocator = allocator; this.preparePositionUs = positionUs; ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); @@ -147,6 +149,7 @@ public final class HlsSampleSource implements SampleSource, } // Update the enabled wrappers. enabledTrackStreamWrappers = new HlsTrackStreamWrapper[enabledTrackStreamWrapperCount]; + sequenceableLoader = new CompositeSequenceableLoader(enabledTrackStreamWrappers); enabledTrackStreamWrapperCount = 0; for (int i = 0; i < trackStreamWrappers.length; i++) { if (selectedTrackCounts[i] > 0) { @@ -161,10 +164,13 @@ public final class HlsSampleSource implements SampleSource, } @Override - public void continueBuffering(long positionUs) { - for (HlsTrackStreamWrapper trackStreamWrapper : enabledTrackStreamWrappers) { - trackStreamWrapper.continueBuffering(positionUs); - } + public boolean continueLoading(long positionUs) { + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return sequenceableLoader.getNextLoadPositionUs(); } @Override @@ -190,7 +196,7 @@ public final class HlsSampleSource implements SampleSource, positionUs = isLive ? 0 : positionUs; timestampAdjusterProvider.reset(); for (HlsTrackStreamWrapper trackStreamWrapper : enabledTrackStreamWrappers) { - trackStreamWrapper.restartFrom(positionUs); + trackStreamWrapper.seekTo(positionUs); } return positionUs; } @@ -239,7 +245,7 @@ public final class HlsSampleSource implements SampleSource, return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } - // HlsTrackStreamWrapper callback. + // HlsTrackStreamWrapper.Callback implementation. @Override public void onPrepared() { @@ -267,6 +273,11 @@ public final class HlsSampleSource implements SampleSource, callback.onSourcePrepared(this); } + @Override + public void onContinueLoadingRequested(HlsTrackStreamWrapper trackStreanWrapper) { + callback.onContinueLoadingRequested(this); + } + // Internal methods. private List buildTrackStreamWrappers(HlsPlaylist playlist) { @@ -343,7 +354,7 @@ public final class HlsSampleSource implements SampleSource, DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, timestampAdjusterProvider, formatEvaluator); - return new HlsTrackStreamWrapper(trackType, this, defaultChunkSource, loadControl, + return new HlsTrackStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT, eventDispatcher); } 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 9751918934..4fbe13e2d8 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 @@ -16,11 +16,11 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.AdaptiveSourceEventListener.EventDispatcher; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.FormatHolder; +import com.google.android.exoplayer.SequenceableLoader; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; @@ -31,6 +31,7 @@ import com.google.android.exoplayer.extractor.DefaultTrackOutput; import com.google.android.exoplayer.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; @@ -45,13 +46,13 @@ import java.util.List; * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides * {@link TrackStream}s from which the loaded media can be consumed. */ -/* package */ final class HlsTrackStreamWrapper implements Loader.Callback, ExtractorOutput, - UpstreamFormatChangedListener { +/* package */ final class HlsTrackStreamWrapper implements Loader.Callback, + SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { /** * A callback to be notified of events. */ - public interface Callback { + public interface Callback extends SequenceableLoader.Callback { /** * Invoked when the wrapper has been prepared. @@ -68,7 +69,7 @@ import java.util.List; private final int trackType; private final Callback callback; private final HlsChunkSource chunkSource; - private final LoadControl loadControl; + private final Allocator allocator; private final Format muxedAudioFormat; private final Format muxedCaptionFormat; private final int minLoadableRetryCount; @@ -81,7 +82,6 @@ import java.util.List; private volatile boolean sampleQueuesBuilt; private boolean prepared; - private boolean readingEnabled; private int enabledTrackCount; private Format downstreamFormat; @@ -92,7 +92,6 @@ import java.util.List; // Indexed by group. private boolean[] groupEnabledStates; - private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -102,7 +101,7 @@ import java.util.List; * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param callback A callback for the wrapper. * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. - * @param loadControl Controls when the source is permitted to load data. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, * this is the audio {@link Format} as defined by the playlist. @@ -113,12 +112,12 @@ import java.util.List; * @param eventDispatcher A dispatcher to notify of events. */ public HlsTrackStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, - LoadControl loadControl, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, + Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, int minLoadableRetryCount, EventDispatcher eventDispatcher) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; - this.loadControl = loadControl; + this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; this.muxedCaptionFormat = muxedCaptionFormat; this.minLoadableRetryCount = minLoadableRetryCount; @@ -127,13 +126,12 @@ import java.util.List; nextChunkHolder = new ChunkHolder(); sampleQueues = new SparseArray<>(); mediaChunks = new LinkedList<>(); - readingEnabled = true; + lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; - downstreamPositionUs = positionUs; } public void prepare() { - maybeStartLoading(); + continueLoading(lastSeekPositionUs); } public void maybeThrowPrepareError() throws IOException { @@ -155,7 +153,6 @@ import java.util.List; public TrackStream[] selectTracks(List oldStreams, List newSelections, boolean isFirstTrackSelection) { Assertions.checkState(prepared); - boolean tracksWereEnabled = enabledTrackCount > 0; // Unselect old tracks. for (int i = 0; i < oldStreams.size(); i++) { int group = ((TrackStreamImpl) oldStreams.get(i)).group; @@ -187,24 +184,17 @@ import java.util.List; // 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 if (!tracksWereEnabled) { - loadControl.register(this); } return newStreams; } - public void restartFrom(long positionUs) { + public void seekTo(long positionUs) { lastSeekPositionUs = positionUs; - downstreamPositionUs = positionUs; pendingResetPositionUs = positionUs; loadingFinished = false; mediaChunks.clear(); @@ -215,29 +205,16 @@ import java.util.List; 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; - if (!loader.isLoading()) { - maybeStartLoading(); - } - } - - public void setReadingEnabled(boolean readingEnabled) { - this.readingEnabled = readingEnabled; - } - public long getBufferedPositionUs() { if (loadingFinished) { return C.END_OF_SOURCE_US; } else if (isPendingReset()) { return pendingResetPositionUs; } else { - long bufferedPositionUs = downstreamPositionUs; + long bufferedPositionUs = lastSeekPositionUs; HlsMediaChunk lastMediaChunk = mediaChunks.getLast(); HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; @@ -258,9 +235,6 @@ import java.util.List; for (int i = 0; i < sampleQueueCount; i++) { sampleQueues.valueAt(i).disable(); } - if (enabledTrackCount > 0) { - loadControl.unregister(this); - } loader.release(); } @@ -276,13 +250,14 @@ import java.util.List; } /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (!readingEnabled || isPendingReset()) { + if (isPendingReset()) { return TrackStream.NOTHING_READ; } - while (mediaChunks.size() > 1 && mediaChunks.get(1).startTimeUs <= downstreamPositionUs) { - mediaChunks.removeFirst(); - } + // TODO[REFACTOR]: Restore this. + // while (mediaChunks.size() > 1 && mediaChunks.get(1).startTimeUs <= downstreamPositionUs) { + // mediaChunks.removeFirst(); + // } HlsMediaChunk currentChunk = mediaChunks.getFirst(); Format format = currentChunk.format; if (!format.equals(downstreamFormat)) { @@ -296,6 +271,52 @@ import java.util.List; lastSeekPositionUs); } + // SequenceableLoader implementation + + @Override + public boolean continueLoading(long positionUs) { + if (loader.isLoading()) { + return false; + } + + chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), + pendingResetPositionUs != C.UNSET_TIME_US ? pendingResetPositionUs : positionUs, + nextChunkHolder); + boolean endOfStream = nextChunkHolder.endOfStream; + Chunk loadable = nextChunkHolder.chunk; + nextChunkHolder.clear(); + + if (endOfStream) { + loadingFinished = true; + return true; + } + + if (loadable == null) { + return false; + } + + if (isMediaChunk(loadable)) { + pendingResetPositionUs = C.UNSET_TIME_US; + HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; + mediaChunk.init(this); + mediaChunks.add(mediaChunk); + } + long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.format, + loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, + loadable.endTimeUs, elapsedRealtimeMs); + return true; + } + + @Override + public long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished ? C.END_OF_SOURCE_US : mediaChunks.getLast().endTimeUs; + } + } + // Loader.Callback implementation. @Override @@ -304,7 +325,11 @@ import java.util.List; eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.format, loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); - maybeStartLoading(); + if (!prepared) { + continueLoading(lastSeekPositionUs); + } else { + callback.onContinueLoadingRequested(this); + } } @Override @@ -313,8 +338,12 @@ import java.util.List; eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.format, loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); - if (!released && enabledTrackCount > 0) { - restartFrom(pendingResetPositionUs); + if (!released) { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + } + callback.onContinueLoadingRequested(this); } } @@ -340,7 +369,7 @@ import java.util.List; loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error, canceled); if (canceled) { - maybeStartLoading(); + callback.onContinueLoadingRequested(this); return Loader.DONT_RETRY; } else { return Loader.RETRY; @@ -365,7 +394,7 @@ import java.util.List; if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } - DefaultTrackOutput trackOutput = new DefaultTrackOutput(loadControl.getAllocator()); + DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); trackOutput.setUpstreamFormatChangeListener(this); sampleQueues.put(id, trackOutput); return trackOutput; @@ -525,60 +554,6 @@ import java.util.List; containerFormat.language); } - private void maybeStartLoading() { - boolean shouldStartLoading = !prepared || (enabledTrackCount > 0 - && loadControl.update(this, getNextLoadPositionUs(), false)); - if (!shouldStartLoading) { - return; - } - - chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.UNSET_TIME_US ? pendingResetPositionUs : downstreamPositionUs, - nextChunkHolder); - boolean endOfStream = nextChunkHolder.endOfStream; - Chunk loadable = nextChunkHolder.chunk; - nextChunkHolder.clear(); - - if (endOfStream) { - loadingFinished = true; - if (prepared) { - loadControl.update(this, C.UNSET_TIME_US, false); - } - return; - } - - if (loadable == null) { - return; - } - - if (isMediaChunk(loadable)) { - pendingResetPositionUs = C.UNSET_TIME_US; - HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable; - mediaChunk.init(this); - mediaChunks.add(mediaChunk); - } - long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.format, - loadable.formatEvaluatorTrigger, loadable.formatEvaluatorData, loadable.startTimeUs, - loadable.endTimeUs, elapsedRealtimeMs); - if (prepared) { - // Update the load control again to indicate that we're now loading. - loadControl.update(this, getNextLoadPositionUs(), true); - } - } - - /** - * Gets the next load time, assuming that the next load starts where the previous chunk ended (or - * from the pending reset time, if there is one). - */ - private long getNextLoadPositionUs() { - if (isPendingReset()) { - return pendingResetPositionUs; - } else { - return loadingFinished ? C.UNSET_TIME_US : mediaChunks.getLast().endTimeUs; - } - } - private boolean isMediaChunk(Chunk chunk) { return chunk instanceof HlsMediaChunk; } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java index e0e7f59d3d..77c1c5fe50 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java @@ -17,11 +17,12 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.AdaptiveSourceEventListener; import com.google.android.exoplayer.AdaptiveSourceEventListener.EventDispatcher; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.CompositeSequenceableLoader; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SequenceableLoader; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; @@ -32,6 +33,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +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; @@ -52,6 +54,7 @@ import java.util.List; * A {@link SampleSource} for SmoothStreaming media. */ public final class SmoothStreamingSampleSource implements SampleSource, + SequenceableLoader.Callback>, Loader.Callback> { /** @@ -74,7 +77,8 @@ public final class SmoothStreamingSampleSource implements SampleSource, private SmoothStreamingManifest manifest; private Callback callback; - private LoadControl loadControl; + private Allocator allocator; + private Handler manifestRefreshHandler; private boolean prepared; private long durationUs; private TrackEncryptionBox[] trackEncryptionBoxes; @@ -82,6 +86,7 @@ public final class SmoothStreamingSampleSource implements SampleSource, private int[] trackGroupElementIndices; private ChunkTrackStream[] trackStreams; + private CompositeSequenceableLoader sequenceableLoader; public SmoothStreamingSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, @@ -92,15 +97,17 @@ public final class SmoothStreamingSampleSource implements SampleSource, this.bandwidthMeter = bandwidthMeter; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); trackStreams = newTrackStreamArray(0); + sequenceableLoader = new CompositeSequenceableLoader(trackStreams); manifestDataSource = dataSourceFactory.createDataSource(); manifestParser = new SmoothStreamingManifestParser(); manifestLoader = new Loader("Loader:Manifest"); } @Override - public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + public void prepare(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; - this.loadControl = loadControl; + this.allocator = allocator; + manifestRefreshHandler = new Handler(); startLoadingManifest(); } @@ -145,25 +152,18 @@ public final class SmoothStreamingSampleSource implements SampleSource, } trackStreams = newTrackStreams; + sequenceableLoader = new CompositeSequenceableLoader(trackStreams); return streamsToReturn; } @Override - public void continueBuffering(long positionUs) { - if (manifest.isLive) { - if (!manifestLoader.isLoading() && SystemClock.elapsedRealtime() - > manifestLoadStartTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS) { - for (ChunkTrackStream trackStream : trackStreams) { - if (trackStream.getChunkSource().needManifestRefresh()) { - startLoadingManifest(); - break; - } - } - } - } - for (ChunkTrackStream trackStream : trackStreams) { - trackStream.continueBuffering(positionUs); - } + public boolean continueLoading(long positionUs) { + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return sequenceableLoader.getNextLoadPositionUs(); } @Override @@ -193,12 +193,23 @@ public final class SmoothStreamingSampleSource implements SampleSource, @Override public void release() { + if (manifestRefreshHandler != null) { + manifestRefreshHandler.removeCallbacksAndMessages(null); + manifestRefreshHandler = null; + } manifestLoader.release(); for (ChunkTrackStream trackStream : trackStreams) { trackStream.release(); } } + // SequenceableLoader.Callback implementation + + @Override + public void onContinueLoadingRequested(ChunkTrackStream trackStream) { + callback.onContinueLoadingRequested(this); + } + // Loader.Callback implementation @Override @@ -223,7 +234,9 @@ public final class SmoothStreamingSampleSource implements SampleSource, for (ChunkTrackStream trackStream : trackStreams) { trackStream.getChunkSource().updateManifest(manifest); } + callback.onContinueLoadingRequested(this); } + scheduleManifestRefresh(); } @Override @@ -244,6 +257,20 @@ public final class SmoothStreamingSampleSource implements SampleSource, // Internal methods + private void scheduleManifestRefresh() { + if (!manifest.isLive) { + return; + } + long nextLoadTimestamp = manifestLoadStartTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS; + long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); + manifestRefreshHandler.postDelayed(new Runnable() { + @Override + public void run() { + startLoadingManifest(); + } + }, delayUntilNextLoad); + } + private void startLoadingManifest() { ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); @@ -285,7 +312,7 @@ public final class SmoothStreamingSampleSource implements SampleSource, SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(manifestLoader, manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator, trackEncryptionBoxes); - return new ChunkTrackStream<>(streamElementType, chunkSource, loadControl, positionUs, + return new ChunkTrackStream<>(streamElementType, chunkSource, this, allocator, positionUs, MIN_LOADABLE_RETRY_COUNT, eventDispatcher); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ConditionVariable.java b/library/src/main/java/com/google/android/exoplayer/util/ConditionVariable.java new file mode 100644 index 0000000000..2d801d668c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/ConditionVariable.java @@ -0,0 +1,62 @@ +/* + * 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.util; + +/** + * A condition variable whose {@link #open()} and {@link #close()} methods return whether they + * resulted in a change of state. + */ +public final class ConditionVariable { + + private boolean isOpen; + + /** + * Opens the condition and releases all threads that are blocked. + * + * @return True if the condition variable was opened. False if it was already open. + */ + public synchronized boolean open() { + if (isOpen) { + return false; + } + isOpen = true; + notifyAll(); + return true; + } + + /** + * Closes the condition. + * + * @return True if the condition variable was closed. False if it was already closed. + */ + public synchronized boolean close() { + boolean wasOpen = isOpen; + isOpen = false; + return wasOpen; + } + + /** + * Blocks until the condition is opened. + * + * @throws InterruptedException If the thread is interrupted. + */ + public synchronized void block() throws InterruptedException { + while (!isOpen) { + wait(); + } + } + +}