Event based buffering.

At a high level, everything that we need to load in sync
with anything else now implements a new SequenceableLoader
class. This includes ChunkTrackStream, since multiple
demuxed tracks in DASH/SS need to be loaded in sync with
one another. At a higher level, SampleSources are also
SequenceableLoaders, which allows them to be kept in sync
by MultiSampleSource.

CompositeSequenceableLoader is able to load multiple
instances SequenceableLoaders in sync with one another,
and is used in various places where this is required.

This change also removes LoadControl registration, which
was complicated and error prone.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=126632861
This commit is contained in:
olly 2016-07-05 06:53:35 -07:00 committed by Oliver Woodman
parent 7b673fe432
commit 7b9e47ec6b
21 changed files with 914 additions and 839 deletions

View File

@ -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);

View File

@ -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);
}

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}
}

View File

@ -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.
* <p>
* Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
* duration of buffered media and the buffer utilization both exceed respective thresholds, the
* control switches to a draining state during which no loads are permitted to start. During
* draining periods, resources such as the device radio have an opportunity to switch into low
* power modes. The control reverts back to the loading state when either the duration of buffered
* media or the buffer utilization fall below respective thresholds.
* <p>
* This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering
* itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods,
* and unregistering itself during draining periods.
*/
public 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<Object> loaders;
private final HashMap<Object, LoaderState> 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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<TrackStream> 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() {

View File

@ -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<TrackStream> allOldStreams,

View File

@ -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<SampleSource> {
/**
* 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<TrackStream> oldStreams, List<TrackSelection> newSelections,
long positionUs);
/**
* Indicates to the source that it should continue buffering data for its enabled tracks.
* <p>
* 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.
* <p>

View File

@ -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<T extends SequenceableLoader> {
/**
* 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);
}

View File

@ -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);
}
/**

View File

@ -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<TrackSelection> 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() {

View File

@ -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<T extends ChunkSource> implements TrackStream,
public class ChunkTrackStream<T extends ChunkSource> implements TrackStream, SequenceableLoader,
Loader.Callback<Chunk> {
private final int trackType;
private final T chunkSource;
private final LoadControl loadControl;
private final SequenceableLoader.Callback<ChunkTrackStream<T>> callback;
private final EventDispatcher eventDispatcher;
private final int minLoadableRetryCount;
private final LinkedList<BaseMediaChunk> mediaChunks;
@ -50,11 +49,8 @@ public class ChunkTrackStream<T extends ChunkSource> 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<T extends ChunkSource> 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<ChunkTrackStream<T>> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> implements TrackStream,
public void release() {
chunkSource.release();
sampleQueue.disable();
loadControl.unregister(this);
loader.release();
}
@ -189,7 +172,7 @@ public class ChunkTrackStream<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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;
}

View File

@ -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<ChunkTrackStream<DashChunkSource>> {
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<DashChunkSource>[] 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<DashChunkSource> 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<DashChunkSource> trackStream : trackStreams) {
trackStream.release();
}
}
// SequenceableLoader.Callback implementation.
@Override
public void onContinueLoadingRequested(ChunkTrackStream<DashChunkSource> trackStream) {
callback.onContinueLoadingRequested(this);
}
// Loadable callbacks.
/* package */ void onManifestLoadCompleted(ParsingLoadable<MediaPresentationDescription> 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<DashChunkSource> 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 <T> void startLoading(ParsingLoadable<T> 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);
}

View File

@ -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<TrackStream> oldStreams,
List<TrackSelection> 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) {

View File

@ -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<HlsPlaylist> 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<HlsTrackStreamWrapper> 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);
}

View File

@ -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<Chunk>, ExtractorOutput,
UpstreamFormatChangedListener {
/* package */ final class HlsTrackStreamWrapper implements Loader.Callback<Chunk>,
SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
/**
* A callback to be notified of events.
*/
public interface Callback {
public interface Callback extends SequenceableLoader.Callback<HlsTrackStreamWrapper> {
/**
* 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<TrackStream> oldStreams,
List<TrackSelection> 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;
}

View File

@ -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<ChunkTrackStream<SmoothStreamingChunkSource>>,
Loader.Callback<ParsingLoadable<SmoothStreamingManifest>> {
/**
@ -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<SmoothStreamingChunkSource>[] 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<SmoothStreamingChunkSource> trackStream : trackStreams) {
if (trackStream.getChunkSource().needManifestRefresh()) {
startLoadingManifest();
break;
}
}
}
}
for (ChunkTrackStream<SmoothStreamingChunkSource> 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<SmoothStreamingChunkSource> trackStream : trackStreams) {
trackStream.release();
}
}
// SequenceableLoader.Callback implementation
@Override
public void onContinueLoadingRequested(ChunkTrackStream<SmoothStreamingChunkSource> trackStream) {
callback.onContinueLoadingRequested(this);
}
// Loader.Callback implementation
@Override
@ -223,7 +234,9 @@ public final class SmoothStreamingSampleSource implements SampleSource,
for (ChunkTrackStream<SmoothStreamingChunkSource> 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<SmoothStreamingManifest> 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);
}

View File

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