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();
+ }
+ }
+
+}