mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
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:
parent
76f426e944
commit
ca81444f95
@ -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;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@ -611,36 +625,24 @@ import java.util.ArrayList;
|
||||
bufferingSource.setNextSource(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) {
|
||||
// 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;
|
||||
|
@ -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,57 +27,40 @@ 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
|
||||
@ -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++) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
startLoadingManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
loader.maybeThrowError();
|
||||
if (!loader.isLoading() && manifest == null) {
|
||||
startLoadingManifest();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
loadCondition.open();
|
||||
startLoading();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
maybeThrowError();
|
||||
if (!loader.isLoading()) {
|
||||
loadCondition.open();
|
||||
startLoading();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
@ -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;
|
||||
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) {
|
||||
manifestFetcher.maybeThrowError();
|
||||
if (!manifestFetcher.isLoading()) {
|
||||
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;
|
||||
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);
|
||||
} else {
|
||||
for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) {
|
||||
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.
|
||||
}
|
||||
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));
|
||||
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,
|
||||
|
@ -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;
|
||||
pendingResetPositionUs = positionUs;
|
||||
downstreamPositionUs = positionUs;
|
||||
}
|
||||
|
||||
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.
|
||||
public void prepare() {
|
||||
maybeStartLoading();
|
||||
}
|
||||
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
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() {
|
||||
@ -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.
|
||||
|
@ -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;
|
||||
startLoadingManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowPrepareError() throws IOException {
|
||||
manifestLoader.maybeThrowError();
|
||||
if (!manifestLoader.isLoading()) {
|
||||
startLoadingManifest();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@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);
|
||||
|
@ -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> {
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user