Event based preparation.

- Removes the load delay that was previously present after
  source preparation.
- Prevents premature failure in the case that the buffering
  source fails to prepare.
- SampleSource.Callback will get a second method in a
  subsequent CL, approximately meaning "tell me if I can load
  more stuff". This will remove the need for LoadControl and
  the complexity that comes with it.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=126172573
This commit is contained in:
olly 2016-06-29 02:55:31 -07:00 committed by Oliver Woodman
parent 76f426e944
commit ca81444f95
13 changed files with 401 additions and 260 deletions

View File

@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer;
import com.google.android.exoplayer.BufferingPolicy.LoadControl;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerMessage;
import com.google.android.exoplayer.TrackSelector.InvalidationListener;
import com.google.android.exoplayer.util.PriorityHandlerThread;
@ -38,7 +37,8 @@ import java.util.ArrayList;
*/
// TODO[REFACTOR]: Make sure renderer errors that will prevent prepare from being called again are
// always propagated properly.
/* package */ final class ExoPlayerImplInternal implements Handler.Callback, InvalidationListener {
/* package */ final class ExoPlayerImplInternal implements Handler.Callback, SampleSource.Callback,
InvalidationListener {
/**
* Playback position information which is read on the application's thread by
@ -76,8 +76,9 @@ import java.util.ArrayList;
private static final int MSG_SEEK_TO = 3;
private static final int MSG_STOP = 4;
private static final int MSG_RELEASE = 5;
private static final int MSG_TRACK_SELECTION_INVALIDATED = 6;
private static final int MSG_CUSTOM = 7;
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 PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
@ -203,6 +204,13 @@ import java.util.ArrayList;
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
}
// SampleSource.Callback implementation.
@Override
public void onSourcePrepared(SampleSource source) {
handler.obtainMessage(MSG_SOURCE_PREPARED, source).sendToTarget();
}
// Handler.Callback implementation.
@Override
@ -233,14 +241,18 @@ import java.util.ArrayList;
releaseInternal();
return true;
}
case MSG_CUSTOM: {
sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
case MSG_SOURCE_PREPARED: {
timeline.handleSourcePrepared((SampleSource) msg.obj);
return true;
}
case MSG_TRACK_SELECTION_INVALIDATED: {
reselectTracksInternal();
return true;
}
case MSG_CUSTOM: {
sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
return true;
}
default:
return false;
}
@ -368,7 +380,8 @@ import java.util.ArrayList;
timeline.updateSources();
if (timeline.getSampleSource() == null) {
// We're still waiting for the source to be prepared.
// We're still waiting for the first source to be prepared.
timeline.maybeThrowSourcePrepareError();
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
return;
}
@ -393,15 +406,16 @@ import java.util.ArrayList;
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
}
// TODO: Have timeline.updateSources() above return whether the timeline is ready, and remove
// timeline.isReady(). This will avoid any inconsistencies that could arise due to the playback
// position update. We could probably return [ENDED|READY|BUFFERING] and get rid of isEnded too.
if (!allRenderersReadyOrEnded) {
timeline.maybeThrowSourcePrepareError();
}
if (allRenderersEnded && (playbackInfo.durationUs == C.UNSET_TIME_US
|| playbackInfo.durationUs <= playbackInfo.positionUs) && timeline.isEnded()) {
|| playbackInfo.durationUs <= playbackInfo.positionUs) && timeline.isEnded) {
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING) {
if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady())
if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady)
&& bufferingPolicy.haveSufficientBuffer(playbackInfo.bufferedPositionUs, rebuffering)) {
setState(ExoPlayer.STATE_READY);
if (playWhenReady) {
@ -409,7 +423,7 @@ import java.util.ArrayList;
}
}
} else if (state == ExoPlayer.STATE_READY) {
if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !timeline.isReady()) {
if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !timeline.isReady) {
rebuffering = playWhenReady;
setState(ExoPlayer.STATE_BUFFERING);
stopRenderers();
@ -558,6 +572,9 @@ import java.util.ArrayList;
private final ArrayList<TrackStream> oldStreams;
private final ArrayList<TrackSelection> newSelections;
public boolean isReady;
public boolean isEnded;
private Source playingSource;
private Source readingSource;
private Source bufferingSource;
@ -577,19 +594,16 @@ import java.util.ArrayList;
return playingSource == null ? null : playingSource.sampleSource;
}
public boolean isEnded() {
if (playingSource == null) {
return false;
public void maybeThrowSourcePrepareError() throws IOException {
if (bufferingSource != null && !bufferingSource.prepared
&& (readingSource == null || readingSource.nextSource == bufferingSource)) {
for (TrackRenderer renderer : enabledRenderers) {
if (!renderer.hasReadStreamToEnd()) {
return;
}
int sourceCount = sampleSourceProvider.getSourceCount();
return sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT
&& playingSource.index == sourceCount - 1;
}
public boolean isReady() {
return playingSourceEndPositionUs == C.UNSET_TIME_US
|| internalPositionUs < playingSourceEndPositionUs
|| (playingSource.nextSource != null && playingSource.nextSource.prepared);
bufferingSource.sampleSource.maybeThrowPrepareError();
}
}
public void updateSources() throws ExoPlaybackException, IOException {
@ -611,36 +625,24 @@ import java.util.ArrayList;
bufferingSource.setNextSource(newSource);
}
bufferingSource = newSource;
}
}
}
if (bufferingSource != null) {
if (!bufferingSource.prepared) {
// Continue preparation.
// TODO[playlists]: Add support for setting the start position to play in a source.
long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0;
if (bufferingSource.prepare(startPositionUs, bufferingPolicy.getLoadControl())) {
Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers,
bufferingSource.sampleSource.getTrackGroups());
bufferingSource.selectTracks(result.first, result.second, startPositionUs,
bufferingPolicy, renderers);
if (playingSource == null) {
// This is the first prepared source, so start playing it.
readingSource = bufferingSource;
setPlayingSource(readingSource);
sampleSource.prepare(ExoPlayerImplInternal.this, bufferingPolicy.getLoadControl(),
startPositionUs);
}
}
}
if (bufferingSource.hasEnabledTracks) {
long sourcePositionUs = internalPositionUs - bufferingSource.offsetUs;
bufferingSource.sampleSource.continueBuffering(sourcePositionUs);
}
}
// Update the playing and reading sources.
if (bufferingSource != null && bufferingSource.hasEnabledTracks) {
long sourcePositionUs = internalPositionUs - bufferingSource.offsetUs;
bufferingSource.sampleSource.continueBuffering(sourcePositionUs);
}
if (playingSource == null) {
// We're waiting for the first source to be prepared.
return;
}
// Update the playing and reading sources.
if (playingSourceEndPositionUs == C.UNSET_TIME_US && playingSource.isFullyBuffered()) {
playingSourceEndPositionUs = playingSource.offsetUs
+ playingSource.sampleSource.getDurationUs();
@ -656,6 +658,7 @@ import java.util.ArrayList;
updatePlaybackPositions();
eventHandler.obtainMessage(MSG_SOURCE_CHANGED, playbackInfo).sendToTarget();
}
updateTimelineState();
if (readingSource == null) {
return;
}
@ -698,6 +701,23 @@ import java.util.ArrayList;
}
}
public void handleSourcePrepared(SampleSource sampleSource) throws ExoPlaybackException {
if (bufferingSource == null || bufferingSource.sampleSource != sampleSource) {
// Stale event.
return;
}
long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0;
Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers,
bufferingSource.sampleSource.getTrackGroups());
bufferingSource.handlePrepared(result.first, result.second, startPositionUs,
bufferingPolicy, renderers);
if (playingSource == null) {
// This is the first prepared source, so start playing it.
setPlayingSource(bufferingSource);
updateTimelineState();
}
}
public void seekToSource(int sourceIndex) throws ExoPlaybackException {
// Clear the timeline, but keep the requested source if it is already prepared.
Source source = playingSource;
@ -714,6 +734,7 @@ import java.util.ArrayList;
if (newPlayingSource != null) {
newPlayingSource.nextSource = null;
setPlayingSource(newPlayingSource);
updateTimelineState();
readingSource = playingSource;
bufferingSource = playingSource;
} else {
@ -794,6 +815,8 @@ import java.util.ArrayList;
source.release();
source = source.nextSource;
}
isReady = false;
isEnded = false;
playingSource = null;
readingSource = null;
bufferingSource = null;
@ -812,6 +835,15 @@ import java.util.ArrayList;
enableRenderers(source.trackSelections, enabledRendererCount);
}
private void updateTimelineState() {
isReady = playingSourceEndPositionUs == C.UNSET_TIME_US
|| internalPositionUs < playingSourceEndPositionUs
|| (playingSource.nextSource != null && playingSource.nextSource.prepared);
int sourceCount = sampleSourceProvider.getSourceCount();
isEnded = sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT
&& playingSource.index == sourceCount - 1;
}
private int disableRenderers(boolean sourceTransition, TrackSelectionArray newTrackSelections)
throws ExoPlaybackException {
// Disable any renderers whose selections have changed, adding the corresponding TrackStream
@ -924,28 +956,20 @@ import java.util.ArrayList;
trackStreams = new TrackStream[rendererCount];
}
public boolean isFullyBuffered() {
return prepared && (!hasEnabledTracks
|| sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US);
}
public boolean prepare(long startPositionUs, LoadControl loadControl) throws IOException {
if (sampleSource.prepare(startPositionUs, loadControl)) {
prepared = true;
return true;
} else {
return false;
}
}
public void setNextSource(Source nextSource) {
this.nextSource = nextSource;
nextSource.offsetUs = offsetUs + sampleSource.getDurationUs();
}
public void selectTracks(TrackSelectionArray newTrackSelections, Object trackSelectionData,
public boolean isFullyBuffered() {
return prepared && (!hasEnabledTracks
|| sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US);
}
public void handlePrepared(TrackSelectionArray newTrackSelections, Object trackSelectionData,
long positionUs, BufferingPolicy bufferingPolicy, TrackRenderer[] renderers)
throws ExoPlaybackException {
prepared = true;
this.trackSelectionData = trackSelectionData;
if (newTrackSelections.equals(trackSelections)) {
return;

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer;
import com.google.android.exoplayer.BufferingPolicy.LoadControl;
import com.google.android.exoplayer.util.Assertions;
import android.util.Pair;
@ -28,58 +27,41 @@ import java.util.List;
/**
* Combines multiple {@link SampleSource} instances.
*/
public final class MultiSampleSource implements SampleSource {
public final class MultiSampleSource implements SampleSource, SampleSource.Callback {
private final SampleSource[] sources;
private final IdentityHashMap<TrackStream, SampleSource> trackStreamSources;
private final int[] selectedTrackCounts;
private boolean prepared;
private boolean seenFirstTrackSelection;
private Callback callback;
private int pendingChildPrepareCount;
private long durationUs;
private TrackGroupArray trackGroups;
private boolean seenFirstTrackSelection;
private SampleSource[] enabledSources;
public MultiSampleSource(SampleSource... sources) {
this.sources = sources;
pendingChildPrepareCount = sources.length;
trackStreamSources = new IdentityHashMap<>();
selectedTrackCounts = new int[sources.length];
}
@Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException {
if (prepared) {
return true;
}
boolean sourcesPrepared = true;
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
this.callback = callback;
for (SampleSource source : sources) {
sourcesPrepared &= source.prepare(positionUs, loadControl);
source.prepare(this, loadControl, positionUs);
}
if (!sourcesPrepared) {
return false;
}
durationUs = 0;
int totalTrackGroupCount = 0;
@Override
public void maybeThrowPrepareError() throws IOException {
for (SampleSource source : sources) {
totalTrackGroupCount += source.getTrackGroups().length;
if (durationUs != C.UNSET_TIME_US) {
long sourceDurationUs = source.getDurationUs();
durationUs = sourceDurationUs == C.UNSET_TIME_US
? C.UNSET_TIME_US : Math.max(durationUs, sourceDurationUs);
source.maybeThrowPrepareError();
}
}
TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
int trackGroupIndex = 0;
for (SampleSource source : sources) {
int sourceTrackGroupCount = source.getTrackGroups().length;
for (int j = 0; j < sourceTrackGroupCount; j++) {
trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j);
}
}
trackGroups = new TrackGroupArray(trackGroupArray);
prepared = true;
return true;
}
@Override
public long getDurationUs() {
@ -94,13 +76,12 @@ public final class MultiSampleSource implements SampleSource {
@Override
public TrackStream[] selectTracks(List<TrackStream> oldStreams,
List<TrackSelection> newSelections, long positionUs) {
Assertions.checkState(prepared);
TrackStream[] newStreams = new TrackStream[newSelections.size()];
// Select tracks for each source.
int enabledSourceCount = 0;
for (int i = 0; i < sources.length; i++) {
selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs,
newStreams);
newStreams, seenFirstTrackSelection);
if (selectedTrackCounts[i] > 0) {
enabledSourceCount++;
}
@ -166,10 +147,40 @@ public final class MultiSampleSource implements SampleSource {
}
}
// SampleSource.Callback implementation
@Override
public void onSourcePrepared(SampleSource ignored) {
if (--pendingChildPrepareCount > 0) {
return;
}
durationUs = 0;
int totalTrackGroupCount = 0;
for (SampleSource source : sources) {
totalTrackGroupCount += source.getTrackGroups().length;
if (durationUs != C.UNSET_TIME_US) {
long sourceDurationUs = source.getDurationUs();
durationUs = sourceDurationUs == C.UNSET_TIME_US
? C.UNSET_TIME_US : Math.max(durationUs, sourceDurationUs);
}
}
TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
int trackGroupIndex = 0;
for (SampleSource source : sources) {
int sourceTrackGroupCount = source.getTrackGroups().length;
for (int j = 0; j < sourceTrackGroupCount; j++) {
trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j);
}
}
trackGroups = new TrackGroupArray(trackGroupArray);
callback.onSourcePrepared(this);
}
// Internal methods.
private int selectTracks(SampleSource source, List<TrackStream> allOldStreams,
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams) {
List<TrackSelection> allNewSelections, long positionUs, TrackStream[] allNewStreams,
boolean seenFirstTrackSelection) {
// Get the subset of the old streams for the source.
ArrayList<TrackStream> oldStreams = new ArrayList<>();
for (int i = 0; i < allOldStreams.size(); i++) {

View File

@ -26,18 +26,45 @@ import java.util.List;
public interface SampleSource {
/**
* Prepares the source, or does nothing if the source is already prepared.
* <p>
* {@link #selectTracks(List, List, long)} <b>must</b> be called after the source is prepared to
* make an initial track selection. This is true even if the caller does not wish to select any
* tracks.
*
* @param positionUs The player's current playback position.
* @param loadControl A {@link LoadControl} to determine when to load data.
* @return True if the source is prepared, false otherwise.
* @throws IOException If there's an error preparing the source.
* A callback to be notified of {@link SampleSource} events.
*/
boolean prepare(long positionUs, LoadControl loadControl) throws IOException;
interface Callback {
/**
* Invoked by the source when preparation completes.
* <p>
* May be called from any thread. After invoking this method, the source can expect
* {@link #selectTracks(List, List, long)} to be invoked when the initial track selection.
*
* @param source The prepared source.
*/
void onSourcePrepared(SampleSource source);
}
/**
* Starts preparation of the source.
* <p>
* {@link Callback#onSourcePrepared(SampleSource)} is invoked when preparation completes. If
* preparation fails, {@link #maybeThrowPrepareError()} will throw an {@link IOException} if
* invoked.
*
* @param callback A callback to receive updates from the source.
* @param loadControl A {@link LoadControl} to determine when to load data.
* @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);
/**
* Throws an error that's preventing the source from becoming prepared. Does nothing if no such
* error exists.
* <p>
* This method should only be called before the source has completed preparation.
*
* @throws IOException The underlying error.
*/
void maybeThrowPrepareError() throws IOException;
/**
* Returns the duration of the source in microseconds, or {@link C#UNSET_TIME_US} if not known.

View File

@ -109,9 +109,13 @@ public final class SingleSampleSource implements SampleSource, TrackStream,
// SampleSource implementation.
@Override
public boolean prepare(long positionUs, LoadControl loadControl) {
// TODO: Use the load control.
return true;
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
callback.onSourcePrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
// Do nothing.
}
@Override

View File

@ -112,6 +112,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
private boolean readEndOfStream;
private boolean streamIsFinal;
public TrackRenderer() {
readEndOfStream = true;
}
/**
* Sets the index of this renderer within the player.
*

View File

@ -70,8 +70,7 @@ public class ChunkTrackStream<T extends ChunkSource> implements TrackStream,
* @param eventDispatcher A dispatcher to notify of events.
*/
public ChunkTrackStream(int trackType, T chunkSource, LoadControl loadControl, long positionUs,
int minLoadableRetryCount,
EventDispatcher eventDispatcher) {
int minLoadableRetryCount, EventDispatcher eventDispatcher) {
this.trackType = trackType;
this.chunkSource = chunkSource;
this.loadControl = loadControl;

View File

@ -39,7 +39,6 @@ import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSourceFactory;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Callback;
import com.google.android.exoplayer.upstream.ParsingLoadable;
import com.google.android.exoplayer.util.Util;
@ -84,6 +83,7 @@ public final class DashSampleSource implements SampleSource {
private long manifestLoadEndTimestamp;
private MediaPresentationDescription manifest;
private Callback callback;
private LoadControl loadControl;
private boolean prepared;
private long durationUs;
@ -107,16 +107,15 @@ public final class DashSampleSource implements SampleSource {
}
@Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException {
if (prepared) {
return true;
}
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
this.callback = callback;
this.loadControl = loadControl;
loader.maybeThrowError();
if (!loader.isLoading() && manifest == null) {
startLoadingManifest();
}
return false;
@Override
public void maybeThrowPrepareError() throws IOException {
loader.maybeThrowError();
}
@Override
@ -231,6 +230,7 @@ public final class DashSampleSource implements SampleSource {
resolveUtcTimingElement(manifest.utcTiming);
} else {
prepared = true;
callback.onSourcePrepared(this);
}
} else {
for (ChunkTrackStream<DashChunkSource> trackStream : trackStreams) {
@ -308,16 +308,18 @@ public final class DashSampleSource implements SampleSource {
private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {
this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs;
prepared = true;
callback.onSourcePrepared(this);
}
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.
prepared = true;
callback.onSourcePrepared(this);
}
private <T> void startLoading(ParsingLoadable<T> loadable, Callback<ParsingLoadable<T>> callback,
int minRetryCount) {
private <T> void startLoading(ParsingLoadable<T> loadable,
Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}

View File

@ -38,6 +38,20 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public final class DefaultTrackOutput implements TrackOutput {
/**
* A listener for changes to the upstream format.
*/
public interface UpstreamFormatChangedListener {
/**
* Invoked on the loading thread when an upstream format change occurs.
*
* @param format The new upstream format.
*/
void onUpstreamFormatChanged(Format format);
}
private static final int INITIAL_SCRATCH_SIZE = 32;
private static final int STATE_ENABLED = 0;
@ -64,6 +78,7 @@ public final class DefaultTrackOutput implements TrackOutput {
private int lastAllocationOffset;
private boolean needKeyframe;
private boolean pendingSplice;
private UpstreamFormatChangedListener upstreamFormatChangeListener;
/**
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
@ -391,6 +406,15 @@ public final class DefaultTrackOutput implements TrackOutput {
// Called by the loading thread.
/**
* Sets a listener to be notified of changes to the upstream format.
*
* @param listener The listener.
*/
public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
upstreamFormatChangeListener = listener;
}
/**
* Like {@link #format(Format)}, but with an offset that will be added to the timestamps of
* samples subsequently queued to the buffer. The offset is also used to adjust
@ -407,7 +431,11 @@ public final class DefaultTrackOutput implements TrackOutput {
@Override
public void format(Format format) {
infoQueue.format(getAdjustedSampleFormat(format, sampleOffsetUs));
Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
infoQueue.format(adjustedFormat);
if (upstreamFormatChangeListener != null) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
}
}
@Override

View File

@ -18,6 +18,7 @@ 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;
@ -25,6 +26,7 @@ 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.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSourceFactory;
@ -71,7 +73,8 @@ import java.util.List;
* from {@link Extractor#sniff(ExtractorInput)} will be used.
*/
public final class ExtractorSampleSource implements SampleSource, ExtractorOutput,
Loader.Callback<ExtractorSampleSource.ExtractingLoadable> {
Loader.Callback<ExtractorSampleSource.ExtractingLoadable>,
UpstreamFormatChangedListener {
/**
* Interface definition for a callback to be notified of {@link ExtractorSampleSource} events.
@ -129,11 +132,12 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
private final Loader loader;
private final ExtractorHolder extractorHolder;
private volatile boolean tracksBuilt;
private volatile SeekMap seekMap;
private Callback callback;
private LoadControl loadControl;
private SeekMap seekMap;
private boolean tracksBuilt;
private boolean prepared;
private boolean seenFirstTrackSelection;
private boolean notifyReset;
private int enabledTrackCount;
@ -304,31 +308,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
// SampleSource implementation.
@Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException {
if (prepared) {
return true;
}
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
this.callback = callback;
this.loadControl = loadControl;
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) {
loadCondition.close();
int trackCount = sampleQueues.length;
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat());
}
tracks = new TrackGroupArray(trackArray);
prepared = true;
return true;
}
// We're not prepared.
maybeThrowError();
if (!loader.isLoading()) {
loadCondition.open();
startLoading();
}
return false;
@Override
public void maybeThrowPrepareError() throws IOException {
maybeThrowError();
}
@Override
@ -521,6 +510,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
public TrackOutput track(int id) {
sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1);
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
sampleQueue.setUpstreamFormatChangeListener(this);
sampleQueues[sampleQueues.length - 1] = sampleQueue;
return sampleQueue;
}
@ -528,15 +518,46 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
@Override
public void endTracks() {
tracksBuilt = true;
maybeFinishPrepare();
}
@Override
public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap;
maybeFinishPrepare();
}
// UpstreamFormatChangedListener implementation
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
}
// Internal methods.
private void maybeFinishPrepare() {
if (seekMap == null || !tracksBuilt) {
return;
}
for (DefaultTrackOutput sampleQueue : sampleQueues) {
if (sampleQueue.getUpstreamFormat() == null) {
return;
}
}
loadCondition.close();
int trackCount = sampleQueues.length;
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat());
}
tracks = new TrackGroupArray(trackArray);
prepared = true;
callback.onSourcePrepared(this);
}
private void copyLengthFromLoader(ExtractingLoadable loadable) {
if (length == C.LENGTH_UNBOUNDED) {
length = loadable.length;
@ -618,15 +639,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
return largestQueuedTimestampUs;
}
private boolean haveFormatsForAllTracks() {
for (DefaultTrackOutput sampleQueue : sampleQueues) {
if (sampleQueue.getUpstreamFormat() == null) {
return false;
}
}
return true;
}
private boolean isPendingReset() {
return pendingResetPositionUs != C.UNSET_TIME_US;
}

View File

@ -53,7 +53,7 @@ import java.util.List;
* A {@link SampleSource} for HLS streams.
*/
public final class HlsSampleSource implements SampleSource,
Loader.Callback<ParsingLoadable<HlsPlaylist>> {
Loader.Callback<ParsingLoadable<HlsPlaylist>>, HlsTrackStreamWrapper.Callback {
/**
* The minimum number of times to retry loading data prior to failing.
@ -70,7 +70,11 @@ public final class HlsSampleSource implements SampleSource,
private final DataSource manifestDataSource;
private final HlsPlaylistParser manifestParser;
private Callback callback;
private LoadControl loadControl;
private long preparePositionUs;
private int pendingPrepareCount;
private boolean seenFirstTrackSelection;
private long durationUs;
private boolean isLive;
@ -96,50 +100,26 @@ public final class HlsSampleSource implements SampleSource,
}
@Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException {
if (trackGroups != null) {
return true;
}
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
this.callback = callback;
this.loadControl = loadControl;
if (trackStreamWrappers == null) {
manifestFetcher.maybeThrowError();
if (!manifestFetcher.isLoading()) {
this.preparePositionUs = positionUs;
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>(manifestDataSource,
manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this,
MIN_LOADABLE_RETRY_COUNT);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}
return false;
}
boolean trackStreamWrappersPrepared = true;
@Override
public void maybeThrowPrepareError() throws IOException {
if (trackStreamWrappers == null) {
manifestFetcher.maybeThrowError();
} else {
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
trackStreamWrappersPrepared &= trackStreamWrapper.prepare(positionUs);
}
if (!trackStreamWrappersPrepared) {
return false;
}
// The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT.
durationUs = trackStreamWrappers[0].getDurationUs();
isLive = trackStreamWrappers[0].isLive();
int totalTrackGroupCount = 0;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
totalTrackGroupCount += trackStreamWrapper.getTrackGroups().length;
}
TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
int trackGroupIndex = 0;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
int wrapperTrackGroupCount = trackStreamWrapper.getTrackGroups().length;
for (int j = 0; j < wrapperTrackGroupCount; j++) {
trackGroupArray[trackGroupIndex++] = trackStreamWrapper.getTrackGroups().get(j);
trackStreamWrapper.maybeThrowPrepareError();
}
}
trackGroups = new TrackGroupArray(trackGroupArray);
return true;
}
@Override
@ -237,6 +217,10 @@ public final class HlsSampleSource implements SampleSource,
trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()];
trackStreamWrapperList.toArray(trackStreamWrappers);
selectedTrackCounts = new int[trackStreamWrappers.length];
pendingPrepareCount = trackStreamWrappers.length;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
trackStreamWrapper.prepare();
}
}
@Override
@ -255,6 +239,34 @@ public final class HlsSampleSource implements SampleSource,
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
}
// HlsTrackStreamWrapper callback.
@Override
public void onPrepared() {
if (--pendingPrepareCount > 0) {
return;
}
// The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT.
durationUs = trackStreamWrappers[0].getDurationUs();
isLive = trackStreamWrappers[0].isLive();
int totalTrackGroupCount = 0;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
totalTrackGroupCount += trackStreamWrapper.getTrackGroups().length;
}
TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount];
int trackGroupIndex = 0;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
int wrapperTrackGroupCount = trackStreamWrapper.getTrackGroups().length;
for (int j = 0; j < wrapperTrackGroupCount; j++) {
trackGroupArray[trackGroupIndex++] = trackStreamWrapper.getTrackGroups().get(j);
}
}
trackGroups = new TrackGroupArray(trackGroupArray);
callback.onSourcePrepared(this);
}
// Internal methods.
private List<HlsTrackStreamWrapper> buildTrackStreamWrappers(HlsPlaylist playlist) {
@ -296,16 +308,18 @@ public final class HlsSampleSource implements SampleSource,
} else {
// Leave the enabled variants unchanged. They're likely either all video or all audio.
}
if (!selectedVariants.isEmpty()) {
Variant[] variants = new Variant[selectedVariants.size()];
selectedVariants.toArray(variants);
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter), masterPlaylist.muxedAudioFormat,
masterPlaylist.muxedCaptionFormat));
}
// Build the audio stream wrapper if applicable.
List<Variant> audioVariants = masterPlaylist.audios;
if (!audioVariants.isEmpty()) {
variants = new Variant[audioVariants.size()];
Variant[] variants = new Variant[audioVariants.size()];
audioVariants.toArray(variants);
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null,
null, null));
@ -314,7 +328,7 @@ public final class HlsSampleSource implements SampleSource,
// Build the text stream wrapper if applicable.
List<Variant> subtitleVariants = masterPlaylist.subtitles;
if (!subtitleVariants.isEmpty()) {
variants = new Variant[subtitleVariants.size()];
Variant[] variants = new Variant[subtitleVariants.size()];
subtitleVariants.toArray(variants);
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null,
null, null));
@ -329,8 +343,9 @@ public final class HlsSampleSource implements SampleSource,
DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter);
HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource,
timestampAdjusterProvider, formatEvaluator);
return new HlsTrackStreamWrapper(trackType, defaultChunkSource, loadControl, muxedAudioFormat,
muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT, eventDispatcher);
return new HlsTrackStreamWrapper(trackType, this, defaultChunkSource, loadControl,
preparePositionUs, muxedAudioFormat, muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT,
eventDispatcher);
}
private int selectTracks(HlsTrackStreamWrapper trackStreamWrapper,

View File

@ -28,6 +28,7 @@ import com.google.android.exoplayer.TrackStream;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkHolder;
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.Loader;
@ -44,7 +45,20 @@ 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 {
/* package */ final class HlsTrackStreamWrapper implements Loader.Callback<Chunk>, ExtractorOutput,
UpstreamFormatChangedListener {
/**
* A callback to be notified of events.
*/
public interface Callback {
/**
* Invoked when the wrapper has been prepared.
*/
void onPrepared();
}
private static final int PRIMARY_TYPE_NONE = 0;
private static final int PRIMARY_TYPE_TEXT = 1;
@ -52,6 +66,7 @@ import java.util.List;
private static final int PRIMARY_TYPE_VIDEO = 3;
private final int trackType;
private final Callback callback;
private final HlsChunkSource chunkSource;
private final LoadControl loadControl;
private final Format muxedAudioFormat;
@ -85,8 +100,10 @@ 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 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.
* @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed
@ -95,10 +112,11 @@ import java.util.List;
* before propagating an error.
* @param eventDispatcher A dispatcher to notify of events.
*/
public HlsTrackStreamWrapper(int trackType, HlsChunkSource chunkSource, LoadControl loadControl,
Format muxedAudioFormat, Format muxedCaptionFormat, int minLoadableRetryCount,
EventDispatcher eventDispatcher) {
public HlsTrackStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
LoadControl loadControl, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat,
int minLoadableRetryCount, EventDispatcher eventDispatcher) {
this.trackType = trackType;
this.callback = callback;
this.chunkSource = chunkSource;
this.loadControl = loadControl;
this.muxedAudioFormat = muxedAudioFormat;
@ -110,44 +128,16 @@ import java.util.List;
sampleQueues = new SparseArray<>();
mediaChunks = new LinkedList<>();
readingEnabled = true;
pendingResetPositionUs = C.UNSET_TIME_US;
}
public boolean prepare(long positionUs) throws IOException {
if (prepared) {
return true;
}
if (chunkSource.getTrackCount() == 0) {
trackGroups = new TrackGroupArray();
prepared = true;
return true;
}
if (sampleQueuesBuilt) {
boolean canBuildTracks = true;
int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) {
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
canBuildTracks = false;
break;
}
}
if (canBuildTracks) {
buildTracks();
prepared = true;
return true;
}
}
// We're not prepared.
maybeThrowError();
if (!loader.isLoading()) {
// We're going to have to start loading a chunk to get what we need for preparation. We should
// attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk
// in the common case where the renderer is subsequently enabled at this position.
pendingResetPositionUs = positionUs;
downstreamPositionUs = positionUs;
}
public void prepare() {
maybeStartLoading();
}
return false;
public void maybeThrowPrepareError() throws IOException {
maybeThrowError();
}
public long getDurationUs() {
@ -376,6 +366,7 @@ import java.util.List;
return sampleQueues.get(id);
}
DefaultTrackOutput trackOutput = new DefaultTrackOutput(loadControl.getAllocator());
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueues.put(id, trackOutput);
return trackOutput;
}
@ -383,6 +374,7 @@ import java.util.List;
@Override
public void endTracks() {
sampleQueuesBuilt = true;
maybeFinishPrepare();
}
@Override
@ -390,8 +382,30 @@ import java.util.List;
// Do nothing.
}
// UpstreamFormatChangedListener implementation.
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
}
// Internal methods.
private void maybeFinishPrepare() {
if (!sampleQueuesBuilt) {
return;
}
int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) {
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
return;
}
}
buildTracks();
prepared = true;
callback.onPrepared();
}
/**
* Builds tracks that are exposed by this {@link HlsTrackStreamWrapper} instance, as well as
* internal data-structures required for operation.

View File

@ -73,6 +73,7 @@ public final class SmoothStreamingSampleSource implements SampleSource,
private long manifestLoadStartTimestamp;
private SmoothStreamingManifest manifest;
private Callback callback;
private LoadControl loadControl;
private boolean prepared;
private long durationUs;
@ -97,16 +98,15 @@ public final class SmoothStreamingSampleSource implements SampleSource,
}
@Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException {
if (prepared) {
return true;
}
public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
this.callback = callback;
this.loadControl = loadControl;
manifestLoader.maybeThrowError();
if (!manifestLoader.isLoading()) {
startLoadingManifest();
}
return false;
@Override
public void maybeThrowPrepareError() throws IOException {
manifestLoader.maybeThrowError();
}
@Override
@ -218,6 +218,7 @@ public final class SmoothStreamingSampleSource implements SampleSource,
new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)};
}
prepared = true;
callback.onSourcePrepared(this);
} else {
for (ChunkTrackStream<SmoothStreamingChunkSource> trackStream : trackStreams) {
trackStream.getChunkSource().updateManifest(manifest);

View File

@ -46,7 +46,7 @@ public final class Loader {
}
/**
* Interface definition of an object that can be loaded using a {@link Loader}.
* An object that can be loaded using a {@link Loader}.
*/
public interface Loadable {
@ -73,7 +73,7 @@ public final class Loader {
}
/**
* Interface definition for a callback to be notified of {@link Loader} events.
* A callback to be notified of {@link Loader} events.
*/
public interface Callback<T extends Loadable> {