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

View File

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

View File

@ -26,18 +26,45 @@ import java.util.List;
public interface SampleSource { public interface SampleSource {
/** /**
* Prepares the source, or does nothing if the source is already prepared. * A callback to be notified of {@link SampleSource} events.
* <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.
*/ */
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. * 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. // SampleSource implementation.
@Override @Override
public boolean prepare(long positionUs, LoadControl loadControl) { public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
// TODO: Use the load control. callback.onSourcePrepared(this);
return true; }
@Override
public void maybeThrowPrepareError() throws IOException {
// Do nothing.
} }
@Override @Override

View File

@ -112,6 +112,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent {
private boolean readEndOfStream; private boolean readEndOfStream;
private boolean streamIsFinal; private boolean streamIsFinal;
public TrackRenderer() {
readEndOfStream = true;
}
/** /**
* Sets the index of this renderer within the player. * 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. * @param eventDispatcher A dispatcher to notify of events.
*/ */
public ChunkTrackStream(int trackType, T chunkSource, LoadControl loadControl, long positionUs, public ChunkTrackStream(int trackType, T chunkSource, LoadControl loadControl, long positionUs,
int minLoadableRetryCount, int minLoadableRetryCount, EventDispatcher eventDispatcher) {
EventDispatcher eventDispatcher) {
this.trackType = trackType; this.trackType = trackType;
this.chunkSource = chunkSource; this.chunkSource = chunkSource;
this.loadControl = loadControl; 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.DataSource;
import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DataSourceFactory;
import com.google.android.exoplayer.upstream.Loader; 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.upstream.ParsingLoadable;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -84,6 +83,7 @@ public final class DashSampleSource implements SampleSource {
private long manifestLoadEndTimestamp; private long manifestLoadEndTimestamp;
private MediaPresentationDescription manifest; private MediaPresentationDescription manifest;
private Callback callback;
private LoadControl loadControl; private LoadControl loadControl;
private boolean prepared; private boolean prepared;
private long durationUs; private long durationUs;
@ -107,16 +107,15 @@ public final class DashSampleSource implements SampleSource {
} }
@Override @Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
if (prepared) { this.callback = callback;
return true;
}
this.loadControl = loadControl; this.loadControl = loadControl;
startLoadingManifest();
}
@Override
public void maybeThrowPrepareError() throws IOException {
loader.maybeThrowError(); loader.maybeThrowError();
if (!loader.isLoading() && manifest == null) {
startLoadingManifest();
}
return false;
} }
@Override @Override
@ -231,6 +230,7 @@ public final class DashSampleSource implements SampleSource {
resolveUtcTimingElement(manifest.utcTiming); resolveUtcTimingElement(manifest.utcTiming);
} else { } else {
prepared = true; prepared = true;
callback.onSourcePrepared(this);
} }
} else { } else {
for (ChunkTrackStream<DashChunkSource> trackStream : trackStreams) { for (ChunkTrackStream<DashChunkSource> trackStream : trackStreams) {
@ -308,16 +308,18 @@ public final class DashSampleSource implements SampleSource {
private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {
this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs; this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs;
prepared = true; prepared = true;
callback.onSourcePrepared(this);
} }
private void onUtcTimestampResolutionError(IOException error) { private void onUtcTimestampResolutionError(IOException error) {
Log.e(TAG, "Failed to resolve UtcTiming element.", error); Log.e(TAG, "Failed to resolve UtcTiming element.", error);
// Be optimistic and continue in the hope that the device clock is correct. // Be optimistic and continue in the hope that the device clock is correct.
prepared = true; prepared = true;
callback.onSourcePrepared(this);
} }
private <T> void startLoading(ParsingLoadable<T> loadable, Callback<ParsingLoadable<T>> callback, private <T> void startLoading(ParsingLoadable<T> loadable,
int minRetryCount) { Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); 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 { 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 INITIAL_SCRATCH_SIZE = 32;
private static final int STATE_ENABLED = 0; private static final int STATE_ENABLED = 0;
@ -64,6 +78,7 @@ public final class DefaultTrackOutput implements TrackOutput {
private int lastAllocationOffset; private int lastAllocationOffset;
private boolean needKeyframe; private boolean needKeyframe;
private boolean pendingSplice; private boolean pendingSplice;
private UpstreamFormatChangedListener upstreamFormatChangeListener;
/** /**
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained. * @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. // 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 * 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 * samples subsequently queued to the buffer. The offset is also used to adjust
@ -407,7 +431,11 @@ public final class DefaultTrackOutput implements TrackOutput {
@Override @Override
public void format(Format format) { 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 @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.BufferingPolicy.LoadControl;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.DecoderInputBuffer; import com.google.android.exoplayer.DecoderInputBuffer;
import com.google.android.exoplayer.Format;
import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleSource; 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.TrackGroupArray;
import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackSelection;
import com.google.android.exoplayer.TrackStream; 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.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DataSourceFactory;
@ -71,7 +73,8 @@ import java.util.List;
* from {@link Extractor#sniff(ExtractorInput)} will be used. * from {@link Extractor#sniff(ExtractorInput)} will be used.
*/ */
public final class ExtractorSampleSource implements SampleSource, ExtractorOutput, 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. * 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 Loader loader;
private final ExtractorHolder extractorHolder; private final ExtractorHolder extractorHolder;
private volatile boolean tracksBuilt; private Callback callback;
private volatile SeekMap seekMap;
private LoadControl loadControl; private LoadControl loadControl;
private SeekMap seekMap;
private boolean tracksBuilt;
private boolean prepared; private boolean prepared;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private boolean notifyReset; private boolean notifyReset;
private int enabledTrackCount; private int enabledTrackCount;
@ -304,31 +308,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
// SampleSource implementation. // SampleSource implementation.
@Override @Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
if (prepared) { this.callback = callback;
return true;
}
this.loadControl = loadControl; this.loadControl = loadControl;
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { loadCondition.open();
loadCondition.close(); startLoading();
int trackCount = sampleQueues.length; }
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount]; @Override
durationUs = seekMap.getDurationUs(); public void maybeThrowPrepareError() throws IOException {
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(); maybeThrowError();
if (!loader.isLoading()) {
loadCondition.open();
startLoading();
}
return false;
} }
@Override @Override
@ -521,6 +510,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
public TrackOutput track(int id) { public TrackOutput track(int id) {
sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1);
DefaultTrackOutput sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); DefaultTrackOutput sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
sampleQueue.setUpstreamFormatChangeListener(this);
sampleQueues[sampleQueues.length - 1] = sampleQueue; sampleQueues[sampleQueues.length - 1] = sampleQueue;
return sampleQueue; return sampleQueue;
} }
@ -528,15 +518,46 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
@Override @Override
public void endTracks() { public void endTracks() {
tracksBuilt = true; tracksBuilt = true;
maybeFinishPrepare();
} }
@Override @Override
public void seekMap(SeekMap seekMap) { public void seekMap(SeekMap seekMap) {
this.seekMap = seekMap; this.seekMap = seekMap;
maybeFinishPrepare();
}
// UpstreamFormatChangedListener implementation
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
} }
// Internal methods. // 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) { private void copyLengthFromLoader(ExtractingLoadable loadable) {
if (length == C.LENGTH_UNBOUNDED) { if (length == C.LENGTH_UNBOUNDED) {
length = loadable.length; length = loadable.length;
@ -618,15 +639,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
return largestQueuedTimestampUs; return largestQueuedTimestampUs;
} }
private boolean haveFormatsForAllTracks() {
for (DefaultTrackOutput sampleQueue : sampleQueues) {
if (sampleQueue.getUpstreamFormat() == null) {
return false;
}
}
return true;
}
private boolean isPendingReset() { private boolean isPendingReset() {
return pendingResetPositionUs != C.UNSET_TIME_US; return pendingResetPositionUs != C.UNSET_TIME_US;
} }

View File

@ -53,7 +53,7 @@ import java.util.List;
* A {@link SampleSource} for HLS streams. * A {@link SampleSource} for HLS streams.
*/ */
public final class HlsSampleSource implements SampleSource, 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. * 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 DataSource manifestDataSource;
private final HlsPlaylistParser manifestParser; private final HlsPlaylistParser manifestParser;
private Callback callback;
private LoadControl loadControl; private LoadControl loadControl;
private long preparePositionUs;
private int pendingPrepareCount;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private long durationUs; private long durationUs;
private boolean isLive; private boolean isLive;
@ -96,50 +100,26 @@ public final class HlsSampleSource implements SampleSource,
} }
@Override @Override
public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { public void prepare(Callback callback, LoadControl loadControl, long positionUs) {
if (trackGroups != null) { this.callback = callback;
return true;
}
this.loadControl = loadControl; this.loadControl = loadControl;
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);
}
@Override
public void maybeThrowPrepareError() throws IOException {
if (trackStreamWrappers == null) { if (trackStreamWrappers == null) {
manifestFetcher.maybeThrowError(); manifestFetcher.maybeThrowError();
if (!manifestFetcher.isLoading()) { } else {
ParsingLoadable<HlsPlaylist> loadable = new ParsingLoadable<>(manifestDataSource, for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); trackStreamWrapper.maybeThrowPrepareError();
long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this,
MIN_LOADABLE_RETRY_COUNT);
eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
}
return false;
}
boolean trackStreamWrappersPrepared = true;
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);
} }
} }
trackGroups = new TrackGroupArray(trackGroupArray);
return true;
} }
@Override @Override
@ -237,6 +217,10 @@ public final class HlsSampleSource implements SampleSource,
trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()]; trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()];
trackStreamWrapperList.toArray(trackStreamWrappers); trackStreamWrapperList.toArray(trackStreamWrappers);
selectedTrackCounts = new int[trackStreamWrappers.length]; selectedTrackCounts = new int[trackStreamWrappers.length];
pendingPrepareCount = trackStreamWrappers.length;
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
trackStreamWrapper.prepare();
}
} }
@Override @Override
@ -255,6 +239,34 @@ public final class HlsSampleSource implements SampleSource,
return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; 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. // Internal methods.
private List<HlsTrackStreamWrapper> buildTrackStreamWrappers(HlsPlaylist playlist) { private List<HlsTrackStreamWrapper> buildTrackStreamWrappers(HlsPlaylist playlist) {
@ -296,16 +308,18 @@ public final class HlsSampleSource implements SampleSource,
} else { } else {
// Leave the enabled variants unchanged. They're likely either all video or all audio. // Leave the enabled variants unchanged. They're likely either all video or all audio.
} }
Variant[] variants = new Variant[selectedVariants.size()]; if (!selectedVariants.isEmpty()) {
selectedVariants.toArray(variants); Variant[] variants = new Variant[selectedVariants.size()];
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, selectedVariants.toArray(variants);
new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter), masterPlaylist.muxedAudioFormat, trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants,
masterPlaylist.muxedCaptionFormat)); new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter), masterPlaylist.muxedAudioFormat,
masterPlaylist.muxedCaptionFormat));
}
// Build the audio stream wrapper if applicable. // Build the audio stream wrapper if applicable.
List<Variant> audioVariants = masterPlaylist.audios; List<Variant> audioVariants = masterPlaylist.audios;
if (!audioVariants.isEmpty()) { if (!audioVariants.isEmpty()) {
variants = new Variant[audioVariants.size()]; Variant[] variants = new Variant[audioVariants.size()];
audioVariants.toArray(variants); audioVariants.toArray(variants);
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null, trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null,
null, null)); null, null));
@ -314,7 +328,7 @@ public final class HlsSampleSource implements SampleSource,
// Build the text stream wrapper if applicable. // Build the text stream wrapper if applicable.
List<Variant> subtitleVariants = masterPlaylist.subtitles; List<Variant> subtitleVariants = masterPlaylist.subtitles;
if (!subtitleVariants.isEmpty()) { if (!subtitleVariants.isEmpty()) {
variants = new Variant[subtitleVariants.size()]; Variant[] variants = new Variant[subtitleVariants.size()];
subtitleVariants.toArray(variants); subtitleVariants.toArray(variants);
trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null, trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null,
null, null)); null, null));
@ -329,8 +343,9 @@ public final class HlsSampleSource implements SampleSource,
DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter);
HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource,
timestampAdjusterProvider, formatEvaluator); timestampAdjusterProvider, formatEvaluator);
return new HlsTrackStreamWrapper(trackType, defaultChunkSource, loadControl, muxedAudioFormat, return new HlsTrackStreamWrapper(trackType, this, defaultChunkSource, loadControl,
muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT, eventDispatcher); preparePositionUs, muxedAudioFormat, muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT,
eventDispatcher);
} }
private int selectTracks(HlsTrackStreamWrapper trackStreamWrapper, 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.Chunk;
import com.google.android.exoplayer.chunk.ChunkHolder; import com.google.android.exoplayer.chunk.ChunkHolder;
import com.google.android.exoplayer.extractor.DefaultTrackOutput; 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.ExtractorOutput;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.upstream.Loader; 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 * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
* {@link TrackStream}s from which the loaded media can be consumed. * {@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_NONE = 0;
private static final int PRIMARY_TYPE_TEXT = 1; 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 static final int PRIMARY_TYPE_VIDEO = 3;
private final int trackType; private final int trackType;
private final Callback callback;
private final HlsChunkSource chunkSource; private final HlsChunkSource chunkSource;
private final LoadControl loadControl; private final LoadControl loadControl;
private final Format muxedAudioFormat; 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 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 chunkSource A {@link HlsChunkSource} from which chunks to load are obtained.
* @param loadControl Controls when the source is permitted to load data. * @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, * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio,
* this is the audio {@link Format} as defined by the playlist. * this is the audio {@link Format} as defined by the playlist.
* @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed
@ -95,10 +112,11 @@ import java.util.List;
* before propagating an error. * before propagating an error.
* @param eventDispatcher A dispatcher to notify of events. * @param eventDispatcher A dispatcher to notify of events.
*/ */
public HlsTrackStreamWrapper(int trackType, HlsChunkSource chunkSource, LoadControl loadControl, public HlsTrackStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
Format muxedAudioFormat, Format muxedCaptionFormat, int minLoadableRetryCount, LoadControl loadControl, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat,
EventDispatcher eventDispatcher) { int minLoadableRetryCount, EventDispatcher eventDispatcher) {
this.trackType = trackType; this.trackType = trackType;
this.callback = callback;
this.chunkSource = chunkSource; this.chunkSource = chunkSource;
this.loadControl = loadControl; this.loadControl = loadControl;
this.muxedAudioFormat = muxedAudioFormat; this.muxedAudioFormat = muxedAudioFormat;
@ -110,44 +128,16 @@ import java.util.List;
sampleQueues = new SparseArray<>(); sampleQueues = new SparseArray<>();
mediaChunks = new LinkedList<>(); mediaChunks = new LinkedList<>();
readingEnabled = true; readingEnabled = true;
pendingResetPositionUs = C.UNSET_TIME_US; pendingResetPositionUs = positionUs;
downstreamPositionUs = positionUs;
} }
public boolean prepare(long positionUs) throws IOException { public void prepare() {
if (prepared) { maybeStartLoading();
return true; }
}
if (chunkSource.getTrackCount() == 0) { public void maybeThrowPrepareError() throws IOException {
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(); 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;
maybeStartLoading();
}
return false;
} }
public long getDurationUs() { public long getDurationUs() {
@ -376,6 +366,7 @@ import java.util.List;
return sampleQueues.get(id); return sampleQueues.get(id);
} }
DefaultTrackOutput trackOutput = new DefaultTrackOutput(loadControl.getAllocator()); DefaultTrackOutput trackOutput = new DefaultTrackOutput(loadControl.getAllocator());
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueues.put(id, trackOutput); sampleQueues.put(id, trackOutput);
return trackOutput; return trackOutput;
} }
@ -383,6 +374,7 @@ import java.util.List;
@Override @Override
public void endTracks() { public void endTracks() {
sampleQueuesBuilt = true; sampleQueuesBuilt = true;
maybeFinishPrepare();
} }
@Override @Override
@ -390,8 +382,30 @@ import java.util.List;
// Do nothing. // Do nothing.
} }
// UpstreamFormatChangedListener implementation.
@Override
public void onUpstreamFormatChanged(Format format) {
maybeFinishPrepare();
}
// Internal methods. // 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 * Builds tracks that are exposed by this {@link HlsTrackStreamWrapper} instance, as well as
* internal data-structures required for operation. * internal data-structures required for operation.

View File

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