Fix SampleSource limbo state by introducing explicit "selecting tracks" state.

1. SampleSource now has an explicit track selection state. This state is entered
   after the source is prepared, and also by calling startTrackSelection.
2. endTrackSelection commits selection changes, and is responsible for doing the
   right thing w.r.t starting/stopping/restarting load operations.
3. All sources now start or restart a load in the case of a new track selection.
   This fixes a problem where a source could be advanced by repeatedly disabling
   and re-enabling whilst paused. Some sources didn't restart a load in this case,
   since the position was unchanged, however the downstream renderer would then
   consume media up to the first keyframe in order to render something. Hence
   each disable/re-enable would advance by a keyframe.
4. This change will enable a subsequent change where we'll discard media for
   non-selected tracks earlier than we do currently (i.e. we'll hook the extractor
   to a dummy track output, so the samples will never be written to a rolling
   buffer). This will enable a further subsequent change where buffer contributions
   are per-renderer rather than per-source.

Issue: #1041
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=118024436
This commit is contained in:
olly 2016-03-24 06:30:11 -07:00 committed by Oliver Woodman
parent 24b2c09287
commit b3ce415e88
8 changed files with 465 additions and 298 deletions

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer.TrackSelector.InvalidationListener; import com.google.android.exoplayer.TrackSelector.InvalidationListener;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.PriorityHandlerThread; import com.google.android.exoplayer.util.PriorityHandlerThread;
import com.google.android.exoplayer.util.TraceUtil; import com.google.android.exoplayer.util.TraceUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -31,7 +32,6 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
@ -68,12 +68,12 @@ import java.util.concurrent.atomic.AtomicInteger;
private final StandaloneMediaClock standaloneMediaClock; private final StandaloneMediaClock standaloneMediaClock;
private final long minBufferUs; private final long minBufferUs;
private final long minRebufferUs; private final long minRebufferUs;
private final TrackSelection[] trackSelections;
private final Handler handler; private final Handler handler;
private final HandlerThread internalPlaybackThread; private final HandlerThread internalPlaybackThread;
private final Handler eventHandler; private final Handler eventHandler;
private final AtomicInteger pendingSeekCount; private final AtomicInteger pendingSeekCount;
private TrackSelectionArray trackSelections;
private TrackRenderer rendererMediaClockSource; private TrackRenderer rendererMediaClockSource;
private MediaClock rendererMediaClock; private MediaClock rendererMediaClock;
private SampleSource source; private SampleSource source;
@ -104,7 +104,6 @@ import java.util.concurrent.atomic.AtomicInteger;
this.bufferedPositionUs = C.UNKNOWN_TIME_US; this.bufferedPositionUs = C.UNKNOWN_TIME_US;
standaloneMediaClock = new StandaloneMediaClock(); standaloneMediaClock = new StandaloneMediaClock();
pendingSeekCount = new AtomicInteger(); pendingSeekCount = new AtomicInteger();
trackSelections = new TrackSelection[renderers.length];
enabledRenderers = new TrackRenderer[0]; enabledRenderers = new TrackRenderer[0];
trackSelector.init(this); trackSelector.init(this);
@ -281,7 +280,7 @@ import java.util.concurrent.atomic.AtomicInteger;
durationUs = source.getDurationUs(); durationUs = source.getDurationUs();
bufferedPositionUs = source.getBufferedPositionUs(); bufferedPositionUs = source.getBufferedPositionUs();
selectTracksInternal(); selectTracksInternal(true);
boolean allRenderersEnded = true; boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true; boolean allRenderersReadyOrEnded = true;
@ -478,27 +477,28 @@ import java.util.concurrent.atomic.AtomicInteger;
handler.removeMessages(MSG_DO_SOME_WORK); handler.removeMessages(MSG_DO_SOME_WORK);
handler.removeMessages(MSG_INCREMENTAL_PREPARE); handler.removeMessages(MSG_INCREMENTAL_PREPARE);
rebuffering = false; rebuffering = false;
trackSelections = null;
standaloneMediaClock.stop(); standaloneMediaClock.stop();
if (renderers == null) { rendererMediaClock = null;
return; rendererMediaClockSource = null;
}
for (TrackRenderer renderer : renderers) {
resetRendererInternal(renderer);
}
enabledRenderers = new TrackRenderer[0]; enabledRenderers = new TrackRenderer[0];
Arrays.fill(trackSelections, null); for (TrackRenderer renderer : renderers) {
source = null; try {
} ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
private void resetRendererInternal(TrackRenderer renderer) { renderer.disable();
try { }
ensureDisabled(renderer); } catch (ExoPlaybackException e) {
} catch (ExoPlaybackException e) { // There's nothing we can do.
// There's nothing we can do. Log.e(TAG, "Stop failed.", e);
Log.e(TAG, "Stop failed.", e); } catch (RuntimeException e) {
} catch (RuntimeException e) { // Ditto.
// Ditto. Log.e(TAG, "Stop failed.", e);
Log.e(TAG, "Stop failed.", e); }
}
if (source != null) {
source.release();
source = null;
} }
} }
@ -520,10 +520,24 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
} }
private void selectTracksInternal() throws ExoPlaybackException { private void selectTracksInternal(boolean initialSelection) throws ExoPlaybackException {
TrackGroupArray groups = source.getTrackGroups(); TrackGroupArray groups = source.getTrackGroups();
Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers, groups); Pair<TrackSelectionArray, Object> result = trackSelector.selectTracks(renderers, groups);
TrackSelectionArray newTrackSelections = result.first; TrackSelectionArray newSelections = result.first;
if (newSelections.equals(trackSelections)) {
trackSelector.onSelectionActivated(result.second);
// No changes to the track selections.
return;
}
if (initialSelection) {
Assertions.checkState(source.getState() == SampleSource.STATE_SELECTING_TRACKS);
} else {
Assertions.checkState(source.getState() == SampleSource.STATE_READING);
source.startTrackSelection();
}
// We disable all renderers whose track selections have changed, then enable renderers with new // We disable all renderers whose track selections have changed, then enable renderers with new
// track selections during a second pass. Doing all disables before any enables is necessary // track selections during a second pass. Doing all disables before any enables is necessary
@ -536,37 +550,45 @@ import java.util.concurrent.atomic.AtomicInteger;
int enabledRendererCount = 0; int enabledRendererCount = 0;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i]; TrackRenderer renderer = renderers[i];
TrackSelection previousTrackSelection = trackSelections[i]; TrackSelection oldSelection = trackSelections == null ? null : trackSelections.get(i);
trackSelections[i] = newTrackSelections.get(i); TrackSelection newSelection = newSelections.get(i);
if (trackSelections[i] != null) { if (newSelection != null) {
enabledRendererCount++; enabledRendererCount++;
} }
rendererWasEnabledFlags[i] = renderer.getState() != TrackRenderer.STATE_DISABLED; rendererWasEnabledFlags[i] = renderer.getState() != TrackRenderer.STATE_DISABLED;
if (!Util.areEqual(previousTrackSelection, trackSelections[i])) { if (!Util.areEqual(oldSelection, newSelection)) {
// The track selection has changed for this renderer. // The track selection has changed for this renderer.
if (rendererWasEnabledFlags[i]) { if (rendererWasEnabledFlags[i]) {
// We need to disable the renderer so that we can enable it with its new selection. // We need to disable the renderer so that we can enable it with its new selection.
if (trackSelections[i] == null && renderer == rendererMediaClockSource) { if (renderer == rendererMediaClockSource) {
// We've been using rendererMediaClockSource to advance the current position, but it's // The renderer is providing the media clock.
// being disabled and won't be re-enabled. Sync standaloneMediaClock so that it can take if (newSelection == null) {
// over timing responsibilities. // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); // over timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
}
rendererMediaClock = null;
rendererMediaClockSource = null;
}
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
TrackStream trackStream = renderer.disable();
source.unselectTrack(trackStream);
} }
ensureDisabled(renderer);
} }
} }
} }
// The new selections are being activated.
trackSelector.onSelectionActivated(result.second); trackSelector.onSelectionActivated(result.second);
trackSelections = newSelections;
enabledRenderers = new TrackRenderer[enabledRendererCount]; enabledRenderers = new TrackRenderer[enabledRendererCount];
enabledRendererCount = 0; enabledRendererCount = 0;
// Enable renderers with their new track selections. // Enable renderers with their new track selections.
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i]; TrackRenderer renderer = renderers[i];
TrackSelection trackSelection = trackSelections[i]; TrackSelection newSelection = trackSelections.get(i);
if (trackSelection != null) { if (newSelection != null) {
enabledRenderers[enabledRendererCount++] = renderer; enabledRenderers[enabledRendererCount++] = renderer;
if (renderer.getState() == TrackRenderer.STATE_DISABLED) { if (renderer.getState() == TrackRenderer.STATE_DISABLED) {
// The renderer needs enabling with its new track selection. // The renderer needs enabling with its new track selection.
@ -574,11 +596,11 @@ import java.util.concurrent.atomic.AtomicInteger;
// Consider as joining only if the renderer was previously disabled. // Consider as joining only if the renderer was previously disabled.
boolean joining = !rendererWasEnabledFlags[i] && playing; boolean joining = !rendererWasEnabledFlags[i] && playing;
// Enable the source and obtain the stream for the renderer to consume. // Enable the source and obtain the stream for the renderer to consume.
TrackStream trackStream = source.enable(trackSelection, positionUs); TrackStream trackStream = source.selectTrack(newSelection, positionUs);
// Build an array of formats contained by the new selection. // Build an array of formats contained by the new selection.
Format[] formats = new Format[trackSelection.length]; Format[] formats = new Format[newSelection.length];
for (int j = 0; j < formats.length; j++) { for (int j = 0; j < formats.length; j++) {
formats[j] = groups.get(trackSelections[i].group).getFormat(trackSelection.getTrack(j)); formats[j] = groups.get(newSelection.group).getFormat(newSelection.getTrack(j));
} }
// Enable the renderer. // Enable the renderer.
renderer.enable(formats, trackStream, positionUs, joining); renderer.enable(formats, trackStream, positionUs, joining);
@ -598,6 +620,8 @@ import java.util.concurrent.atomic.AtomicInteger;
} }
} }
} }
source.endTrackSelection(positionUs);
} }
private void reselectTracksInternal() throws ExoPlaybackException { private void reselectTracksInternal() throws ExoPlaybackException {
@ -605,22 +629,10 @@ import java.util.concurrent.atomic.AtomicInteger;
// We don't have tracks yet, so we don't care. // We don't have tracks yet, so we don't care.
return; return;
} }
selectTracksInternal(); selectTracksInternal(false);
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} }
private void ensureDisabled(TrackRenderer renderer) throws ExoPlaybackException {
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
TrackStream trackStream = renderer.disable();
source.disable(trackStream);
if (renderer == rendererMediaClockSource) {
rendererMediaClock = null;
rendererMediaClockSource = null;
}
}
}
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException { private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
if (renderer.getState() == TrackRenderer.STATE_STARTED) { if (renderer.getState() == TrackRenderer.STATE_STARTED) {
renderer.stop(); renderer.stop();

View File

@ -71,7 +71,7 @@ public final class FrameworkSampleSource implements SampleSource {
private final long fileDescriptorOffset; private final long fileDescriptorOffset;
private final long fileDescriptorLength; private final long fileDescriptorLength;
private boolean prepared; private int state;
private long durationUs; private long durationUs;
private MediaExtractor extractor; private MediaExtractor extractor;
private TrackGroupArray tracks; private TrackGroupArray tracks;
@ -97,6 +97,7 @@ public final class FrameworkSampleSource implements SampleSource {
fileDescriptor = null; fileDescriptor = null;
fileDescriptorOffset = 0; fileDescriptorOffset = 0;
fileDescriptorLength = 0; fileDescriptorLength = 0;
state = STATE_UNPREPARED;
} }
/** /**
@ -116,13 +117,17 @@ public final class FrameworkSampleSource implements SampleSource {
context = null; context = null;
uri = null; uri = null;
headers = null; headers = null;
state = STATE_UNPREPARED;
}
@Override
public int getState() {
return state;
} }
@Override @Override
public boolean prepare(long positionUs) throws IOException { public boolean prepare(long positionUs) throws IOException {
if (prepared) { Assertions.checkState(state == STATE_UNPREPARED);
return true;
}
extractor = new MediaExtractor(); extractor = new MediaExtractor();
if (context != null) { if (context != null) {
extractor.setDataSource(context, uri, headers); extractor.setDataSource(context, uri, headers);
@ -132,16 +137,16 @@ public final class FrameworkSampleSource implements SampleSource {
durationUs = C.UNKNOWN_TIME_US; durationUs = C.UNKNOWN_TIME_US;
trackStates = new int[extractor.getTrackCount()]; trackStates = new int[extractor.getTrackCount()];
pendingResets = new boolean[trackStates.length]; pendingResets = new boolean[trackStates.length];
TrackGroup[] tracks = new TrackGroup[trackStates.length]; TrackGroup[] trackArray = new TrackGroup[trackStates.length];
for (int i = 0; i < trackStates.length; i++) { for (int i = 0; i < trackStates.length; i++) {
MediaFormat format = extractor.getTrackFormat(i); MediaFormat format = extractor.getTrackFormat(i);
if (format.containsKey(MediaFormat.KEY_DURATION)) { if (format.containsKey(MediaFormat.KEY_DURATION)) {
durationUs = Math.max(durationUs, format.getLong(MediaFormat.KEY_DURATION)); durationUs = Math.max(durationUs, format.getLong(MediaFormat.KEY_DURATION));
} }
tracks[i] = new TrackGroup(createFormat(i, format)); trackArray[i] = new TrackGroup(createFormat(i, format));
} }
this.tracks = new TrackGroupArray(tracks); tracks = new TrackGroupArray(trackArray);
prepared = true; state = STATE_SELECTING_TRACKS;
return true; return true;
} }
@ -161,8 +166,14 @@ public final class FrameworkSampleSource implements SampleSource {
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
Assertions.checkState(prepared); Assertions.checkState(state == STATE_READING);
state = STATE_SELECTING_TRACKS;
}
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
Assertions.checkState(selection.length == 1); Assertions.checkState(selection.length == 1);
Assertions.checkState(selection.getTrack(0) == 0); Assertions.checkState(selection.getTrack(0) == 0);
int track = selection.group; int track = selection.group;
@ -170,10 +181,29 @@ public final class FrameworkSampleSource implements SampleSource {
enabledTrackCount++; enabledTrackCount++;
trackStates[track] = TRACK_STATE_ENABLED; trackStates[track] = TRACK_STATE_ENABLED;
extractor.selectTrack(track); extractor.selectTrack(track);
seekToUsInternal(positionUs, positionUs != 0);
return new TrackStreamImpl(track); return new TrackStreamImpl(track);
} }
@Override
public void unselectTrack(TrackStream stream) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
int track = ((TrackStreamImpl) stream).track;
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
enabledTrackCount--;
trackStates[track] = TRACK_STATE_DISABLED;
extractor.unselectTrack(track);
pendingResets[track] = false;
}
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
if (enabledTrackCount > 0) {
seekToUsInternal(positionUs, positionUs != 0);
}
}
/* package */ long readReset(int track) { /* package */ long readReset(int track) {
if (pendingResets[track]) { if (pendingResets[track]) {
pendingResets[track] = false; pendingResets[track] = false;
@ -183,7 +213,6 @@ public final class FrameworkSampleSource implements SampleSource {
} }
/* package */ int readData(int track, FormatHolder formatHolder, SampleHolder sampleHolder) { /* package */ int readData(int track, FormatHolder formatHolder, SampleHolder sampleHolder) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
if (pendingResets[track]) { if (pendingResets[track]) {
return TrackStream.NOTHING_READ; return TrackStream.NOTHING_READ;
@ -223,20 +252,8 @@ public final class FrameworkSampleSource implements SampleSource {
} }
} }
@Override
public void disable(TrackStream trackStream) {
Assertions.checkState(prepared);
int track = ((TrackStreamImpl) trackStream).track;
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);
extractor.unselectTrack(track);
trackStates[track] = TRACK_STATE_DISABLED;
pendingResets[track] = false;
enabledTrackCount--;
}
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return; return;
} }
@ -245,7 +262,6 @@ public final class FrameworkSampleSource implements SampleSource {
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return C.END_OF_SOURCE_US; return C.END_OF_SOURCE_US;
} }
@ -261,11 +277,11 @@ public final class FrameworkSampleSource implements SampleSource {
@Override @Override
public void release() { public void release() {
state = STATE_RELEASED;
if (extractor != null) { if (extractor != null) {
extractor.release(); extractor.release();
extractor = null; extractor = null;
} }
prepared = false;
} }
@TargetApi(18) @TargetApi(18)

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.util.Assertions;
import android.util.Pair; import android.util.Pair;
import java.io.IOException; import java.io.IOException;
@ -28,45 +30,54 @@ public final class MultiSampleSource implements SampleSource {
private final SampleSource[] sources; private final SampleSource[] sources;
private final IdentityHashMap<TrackStream, Integer> trackStreamSourceIndices; private final IdentityHashMap<TrackStream, Integer> trackStreamSourceIndices;
private boolean prepared; private int state;
private long durationUs; private long durationUs;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
public MultiSampleSource(SampleSource... sources) { public MultiSampleSource(SampleSource... sources) {
this.sources = sources; this.sources = sources;
trackStreamSourceIndices = new IdentityHashMap<>(); trackStreamSourceIndices = new IdentityHashMap<>();
state = STATE_UNPREPARED;
}
@Override
public int getState() {
return state;
} }
@Override @Override
public boolean prepare(long positionUs) throws IOException { public boolean prepare(long positionUs) throws IOException {
if (prepared) { Assertions.checkState(state == SampleSource.STATE_UNPREPARED);
return true;
}
boolean sourcesPrepared = true; boolean sourcesPrepared = true;
for (SampleSource source : sources) { for (SampleSource source : sources) {
sourcesPrepared &= source.prepare(positionUs); if (source.getState() == SampleSource.STATE_UNPREPARED) {
} sourcesPrepared &= source.prepare(positionUs);
if (sourcesPrepared) {
prepared = true;
durationUs = C.UNKNOWN_TIME_US;
int totalTrackGroupCount = 0;
for (SampleSource source : sources) {
totalTrackGroupCount += source.getTrackGroups().length;
if (source.getDurationUs() > durationUs) {
durationUs = source.getDurationUs();
}
} }
TrackGroup[] trackGroups = new TrackGroup[totalTrackGroupCount];
int trackGroupIndex = 0;
for (SampleSource source : sources) {
int sourceTrackGroupCount = source.getTrackGroups().length;
for (int j = 0; j < sourceTrackGroupCount; j++) {
trackGroups[trackGroupIndex++] = source.getTrackGroups().get(j);
}
}
this.trackGroups = new TrackGroupArray(trackGroups);
} }
return prepared; if (!sourcesPrepared) {
return false;
}
durationUs = 0;
int totalTrackGroupCount = 0;
for (SampleSource source : sources) {
totalTrackGroupCount += source.getTrackGroups().length;
if (durationUs != C.UNKNOWN_TIME_US) {
long sourceDurationUs = source.getDurationUs();
durationUs = sourceDurationUs == C.UNKNOWN_TIME_US
? C.UNKNOWN_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);
state = STATE_SELECTING_TRACKS;
return true;
} }
@Override @Override
@ -75,18 +86,38 @@ public final class MultiSampleSource implements SampleSource {
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
Assertions.checkState(state == STATE_READING);
state = STATE_SELECTING_TRACKS;
for (SampleSource source : sources) {
source.startTrackSelection();
}
}
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
Pair<Integer, Integer> sourceAndGroup = getSourceAndTrackGroupIndices(selection.group); Pair<Integer, Integer> sourceAndGroup = getSourceAndTrackGroupIndices(selection.group);
TrackStream trackStream = sources[sourceAndGroup.first].enable( TrackStream trackStream = sources[sourceAndGroup.first].selectTrack(
new TrackSelection(sourceAndGroup.second, selection.getTracks()), positionUs); new TrackSelection(sourceAndGroup.second, selection.getTracks()), positionUs);
trackStreamSourceIndices.put(trackStream, sourceAndGroup.first); trackStreamSourceIndices.put(trackStream, sourceAndGroup.first);
return trackStream; return trackStream;
} }
@Override @Override
public void disable(TrackStream trackStream) { public void unselectTrack(TrackStream trackStream) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
int sourceIndex = trackStreamSourceIndices.remove(trackStream); int sourceIndex = trackStreamSourceIndices.remove(trackStream);
sources[sourceIndex].disable(trackStream); sources[sourceIndex].unselectTrack(trackStream);
}
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
for (SampleSource source : sources) {
source.endTrackSelection(positionUs);
}
} }
@Override @Override
@ -129,7 +160,7 @@ public final class MultiSampleSource implements SampleSource {
for (SampleSource source : sources) { for (SampleSource source : sources) {
source.release(); source.release();
} }
prepared = false; state = STATE_RELEASED;
} }
private Pair<Integer, Integer> getSourceAndTrackGroupIndices(int group) { private Pair<Integer, Integer> getSourceAndTrackGroupIndices(int group) {

View File

@ -22,11 +22,39 @@ import java.io.IOException;
*/ */
public interface SampleSource { public interface SampleSource {
/**
* The source has not yet been prepared.
*/
int STATE_UNPREPARED = 0;
/**
* The source is prepared and in the track selection state.
*/
int STATE_SELECTING_TRACKS = 1;
/**
* The source is prepared and in the reading state.
*/
int STATE_READING = 2;
/**
* The source has been released.
*/
int STATE_RELEASED = 3;
/**
* Returns the state of the source.
*
* @return The state of the source. One of {@link #STATE_UNPREPARED},
* {@link #STATE_SELECTING_TRACKS}, {@link #STATE_READING} and {@link #STATE_RELEASED}.
*/
int getState();
/** /**
* Prepares the source. * Prepares the source.
* <p> * <p>
* If preparation cannot complete immediately then the call will return {@code false} rather than * If preparation cannot complete immediately then the call will return {@code false} rather than
* block. The method can be called repeatedly until the return value indicates success. * block and the state will remain unchanged. If true is returned the state will have changed
* to {@link #STATE_SELECTING_TRACKS} for the initial track selection to take place.
* <p>
* This method should only be called when the state is {@link #STATE_UNPREPARED}.
* *
* @param positionUs The player's current playback position. * @param positionUs The player's current playback position.
* @return True if the source was prepared, false otherwise. * @return True if the source was prepared, false otherwise.
@ -37,7 +65,8 @@ public interface SampleSource {
/** /**
* Returns the duration of the source. * Returns the duration of the source.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called when the state is {@link #STATE_SELECTING_TRACKS} or
* {@link #STATE_READING}.
* *
* @return The duration of the source in microseconds, or {@link C#UNKNOWN_TIME_US} if the * @return The duration of the source in microseconds, or {@link C#UNKNOWN_TIME_US} if the
* duration is not known. * duration is not known.
@ -47,16 +76,63 @@ public interface SampleSource {
/** /**
* Returns the {@link TrackGroup}s exposed by the source. * Returns the {@link TrackGroup}s exposed by the source.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called when the state is {@link #STATE_SELECTING_TRACKS} or
* {@link #STATE_READING}.
* *
* @return The {@link TrackGroup}s. * @return The {@link TrackGroup}s.
*/ */
TrackGroupArray getTrackGroups(); TrackGroupArray getTrackGroups();
/**
* Enters the track selection state.
* <p>
* The selected tracks are initially unchanged, but may be modified by calls to
* {@link #unselectTrack(TrackStream)} and {@link #selectTrack(TrackSelection, long)}, followed by
* a call to {@link #endTrackSelection(long)}.
* <p>
* This method should only be called when the state is {@link #STATE_READING}.
*/
void startTrackSelection();
/**
* Selects a track defined by a {@link TrackSelection}. A {@link TrackStream} is returned through
* which the track's data can be read.
* <p>
* The {@link TrackSelection} must have a {@link TrackSelection#group} index distinct from those
* of other enabled tracks, and a {@code TrackSelection#length} of 1 unless
* {@link TrackGroup#adaptive} is true for the selected group.
* <p>
* This method should only be called when the state is {@link #STATE_SELECTING_TRACKS}.
*
* @param selection Defines the track.
* @param positionUs The current playback position in microseconds.
* @return A {@link TrackStream} from which the enabled track's data can be read.
*/
TrackStream selectTrack(TrackSelection selection, long positionUs);
/**
* Unselects a track previously selected by calling {@link #selectTrack(TrackSelection, long)}.
* <p>
* This method should only be called when the state is {@link #STATE_SELECTING_TRACKS}.
*
* @param stream The {@link TrackStream} obtained from the corresponding call to
* {@link #selectTrack(TrackSelection, long)}.
*/
void unselectTrack(TrackStream stream);
/**
* Exits the track selection state.
* <p>
* This method should only be called when the state is {@link #STATE_SELECTING_TRACKS}.
*
* @param positionUs The current playback position in microseconds.
*/
void endTrackSelection(long positionUs);
/** /**
* Indicates to the source that it should continue buffering data for its enabled tracks. * Indicates to the source that it should continue buffering data for its enabled tracks.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called when the state is {@link #STATE_READING}.
* *
* @param positionUs The current playback position. * @param positionUs The current playback position.
*/ */
@ -65,7 +141,7 @@ public interface SampleSource {
/** /**
* Returns an estimate of the position up to which data is buffered for the enabled tracks. * Returns an estimate of the position up to which data is buffered for the enabled tracks.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called when the state is {@link #STATE_READING}.
* *
* @return An estimate of the absolute position in microseconds up to which data is buffered, * @return An estimate of the absolute position in microseconds up to which data is buffered,
* or {@link C#END_OF_SOURCE_US} if the track is fully buffered, or {@link C#UNKNOWN_TIME_US} * or {@link C#END_OF_SOURCE_US} if the track is fully buffered, or {@link C#UNKNOWN_TIME_US}
@ -77,38 +153,17 @@ public interface SampleSource {
/** /**
* Seeks to the specified time in microseconds. * Seeks to the specified time in microseconds.
* <p> * <p>
* This method should only be called after the source has been prepared. * This method should only be called when the state is {@link #STATE_READING}.
* *
* @param positionUs The seek position in microseconds. * @param positionUs The seek position in microseconds.
*/ */
void seekToUs(long positionUs); void seekToUs(long positionUs);
/**
* Enables the source to read a track defined by a {@link TrackSelection}. A {@link TrackStream}
* is returned through which the track's data can be read.
* <p>
* This method should only be called after the source has been prepared, and when there are no
* other enabled tracks with the same {@link TrackSelection#group} index. Note that
* {@code TrackSelection#tracks} must be of length 1 unless {@link TrackGroup#adaptive} is true
* for the group.
*
* @param selection Defines the track.
* @param positionUs The current playback position in microseconds.
* @return A {@link TrackStream} from which the enabled track's data can be read.
*/
TrackStream enable(TrackSelection selection, long positionUs);
/**
* Disables a {@link TrackStream} previously obtained from {@link #enable(TrackSelection, long)}.
*
* @param trackStream The {@link TrackStream} to disable.
*/
void disable(TrackStream trackStream);
/** /**
* Releases the source. * Releases the source.
* <p> * <p>
* This method should be called when the source is no longer required. * This method should be called when the source is no longer required. It may be called in any
* state.
*/ */
void release(); void release();

View File

@ -59,9 +59,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
*/ */
private static final int INITIAL_SAMPLE_SIZE = 1; private static final int INITIAL_SAMPLE_SIZE = 1;
private static final int STATE_SEND_FORMAT = 0; private static final int STREAM_STATE_SEND_FORMAT = 0;
private static final int STATE_SEND_SAMPLE = 1; private static final int STREAM_STATE_SEND_SAMPLE = 1;
private static final int STATE_END_OF_STREAM = 2; private static final int STREAM_STATE_END_OF_STREAM = 2;
private final Uri uri; private final Uri uri;
private final DataSource dataSource; private final DataSource dataSource;
@ -74,9 +74,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
private final int eventSourceId; private final int eventSourceId;
private int state; private int state;
private byte[] sampleData;
private int sampleSize;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private boolean loadingFinished; private boolean loadingFinished;
private Loader loader; private Loader loader;
@ -84,6 +81,10 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
private int streamState;
private byte[] sampleData;
private int sampleSize;
public SingleSampleSource(Uri uri, DataSource dataSource, Format format, long durationUs) { public SingleSampleSource(Uri uri, DataSource dataSource, Format format, long durationUs) {
this(uri, dataSource, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); this(uri, dataSource, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
} }
@ -106,6 +107,12 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
this.eventSourceId = eventSourceId; this.eventSourceId = eventSourceId;
tracks = new TrackGroupArray(new TrackGroup(format)); tracks = new TrackGroupArray(new TrackGroup(format));
sampleData = new byte[INITIAL_SAMPLE_SIZE]; sampleData = new byte[INITIAL_SAMPLE_SIZE];
state = STATE_UNPREPARED;
}
@Override
public int getState() {
return state;
} }
@Override @Override
@ -117,9 +124,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
@Override @Override
public boolean prepare(long positionUs) { public boolean prepare(long positionUs) {
if (loader == null) { Assertions.checkState(state == STATE_UNPREPARED);
loader = new Loader("Loader:" + format.sampleMimeType); state = STATE_SELECTING_TRACKS;
} loader = new Loader("Loader:" + format.sampleMimeType);
return true; return true;
} }
@ -134,14 +141,33 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
state = STATE_SEND_FORMAT; Assertions.checkState(state == STATE_READING);
state = STATE_SELECTING_TRACKS;
}
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
streamState = STREAM_STATE_SEND_FORMAT;
pendingResetPositionUs = NO_RESET; pendingResetPositionUs = NO_RESET;
clearCurrentLoadableException(); clearCurrentLoadableException();
maybeStartLoading(); maybeStartLoading();
return this; return this;
} }
@Override
public void unselectTrack(TrackStream stream) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
streamState = STREAM_STATE_END_OF_STREAM;
}
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
}
@Override @Override
public void continueBuffering(long positionUs) { public void continueBuffering(long positionUs) {
maybeStartLoading(); maybeStartLoading();
@ -161,16 +187,16 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
@Override @Override
public int readData(FormatHolder formatHolder, SampleHolder sampleHolder) { public int readData(FormatHolder formatHolder, SampleHolder sampleHolder) {
if (state == STATE_END_OF_STREAM) { if (streamState == STREAM_STATE_END_OF_STREAM) {
sampleHolder.addFlag(C.SAMPLE_FLAG_END_OF_STREAM); sampleHolder.addFlag(C.SAMPLE_FLAG_END_OF_STREAM);
return END_OF_STREAM; return END_OF_STREAM;
} else if (state == STATE_SEND_FORMAT) { } else if (streamState == STREAM_STATE_SEND_FORMAT) {
formatHolder.format = format; formatHolder.format = format;
state = STATE_SEND_SAMPLE; streamState = STREAM_STATE_SEND_SAMPLE;
return FORMAT_READ; return FORMAT_READ;
} }
Assertions.checkState(state == STATE_SEND_SAMPLE); Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE);
if (!loadingFinished) { if (!loadingFinished) {
return NOTHING_READ; return NOTHING_READ;
} else { } else {
@ -179,31 +205,28 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
sampleHolder.addFlag(C.SAMPLE_FLAG_SYNC); sampleHolder.addFlag(C.SAMPLE_FLAG_SYNC);
sampleHolder.ensureSpaceForWrite(sampleHolder.size); sampleHolder.ensureSpaceForWrite(sampleHolder.size);
sampleHolder.data.put(sampleData, 0, sampleSize); sampleHolder.data.put(sampleData, 0, sampleSize);
state = STATE_END_OF_STREAM; streamState = STREAM_STATE_END_OF_STREAM;
return SAMPLE_READ; return SAMPLE_READ;
} }
} }
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
if (state == STATE_END_OF_STREAM) { if (streamState == STREAM_STATE_END_OF_STREAM) {
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
state = STATE_SEND_SAMPLE; streamState = STREAM_STATE_SEND_SAMPLE;
} }
} }
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
return state == STATE_END_OF_STREAM || loadingFinished ? C.END_OF_SOURCE_US : 0; return streamState == STREAM_STATE_END_OF_STREAM || loadingFinished ? C.END_OF_SOURCE_US : 0;
}
@Override
public void disable(TrackStream trackStream) {
state = STATE_END_OF_STREAM;
} }
@Override @Override
public void release() { public void release() {
state = STATE_RELEASED;
streamState = STREAM_STATE_END_OF_STREAM;
if (loader != null) { if (loader != null) {
loader.release(); loader.release();
loader = null; loader = null;
@ -213,7 +236,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load
// Private methods. // Private methods.
private void maybeStartLoading() { private void maybeStartLoading() {
if (loadingFinished || state == STATE_END_OF_STREAM || loader.isLoading()) { if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) {
return; return;
} }
if (currentLoadableException != null) { if (currentLoadableException != null) {

View File

@ -54,10 +54,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
*/ */
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
private static final int STATE_IDLE = 0;
private static final int STATE_PREPARED = 1;
private static final int STATE_ENABLED = 2;
private static final long NO_RESET_PENDING = Long.MIN_VALUE; private static final long NO_RESET_PENDING = Long.MIN_VALUE;
private final int eventSourceId; private final int eventSourceId;
@ -83,8 +79,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
private long durationUs; private long durationUs;
private Loader loader; private Loader loader;
private boolean loadingFinished; private boolean loadingFinished;
private boolean trackEnabled;
private IOException currentLoadableException; private IOException currentLoadableException;
private int enabledTrackCount;
private int currentLoadableExceptionCount; private int currentLoadableExceptionCount;
private long currentLoadableExceptionTimestamp; private long currentLoadableExceptionTimestamp;
private long currentLoadStartTimeMs; private long currentLoadStartTimeMs;
@ -143,27 +139,30 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
mediaChunks = new LinkedList<>(); mediaChunks = new LinkedList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator());
state = STATE_IDLE;
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
state = STATE_UNPREPARED;
}
@Override
public int getState() {
return state;
} }
@Override @Override
public boolean prepare(long positionUs) throws IOException { public boolean prepare(long positionUs) throws IOException {
if (state != STATE_IDLE) { Assertions.checkState(state == STATE_UNPREPARED);
return true;
}
if (!chunkSource.prepare()) { if (!chunkSource.prepare()) {
return false; return false;
} }
durationUs = chunkSource.getDurationUs(); durationUs = chunkSource.getDurationUs();
TrackGroup trackGroup = chunkSource.getTracks(); TrackGroup tracks = chunkSource.getTracks();
if (trackGroup != null) { if (tracks != null) {
loader = new Loader("Loader:" + trackGroup.getFormat(0).containerMimeType); loader = new Loader("Loader:" + tracks.getFormat(0).containerMimeType);
trackGroups = new TrackGroupArray(trackGroup); trackGroups = new TrackGroupArray(tracks);
} else { } else {
trackGroups = new TrackGroupArray(); trackGroups = new TrackGroupArray();
} }
state = STATE_PREPARED; state = STATE_SELECTING_TRACKS;
return true; return true;
} }
@ -174,15 +173,20 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public TrackGroupArray getTrackGroups() { public TrackGroupArray getTrackGroups() {
Assertions.checkState(state != STATE_IDLE);
return trackGroups; return trackGroups;
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(state == STATE_READING);
Assertions.checkState(enabledTrackCount++ == 0); state = STATE_SELECTING_TRACKS;
state = STATE_ENABLED; }
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
Assertions.checkState(!trackEnabled);
trackEnabled = true;
chunkSource.enable(selection.getTracks()); chunkSource.enable(selection.getTracks());
loadControl.register(this, bufferSizeContribution); loadControl.register(this, bufferSizeContribution);
downstreamFormat = null; downstreamFormat = null;
@ -195,10 +199,10 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
@Override @Override
public void disable(TrackStream trackStream) { public void unselectTrack(TrackStream stream) {
Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(state == STATE_SELECTING_TRACKS);
Assertions.checkState(--enabledTrackCount == 0); Assertions.checkState(trackEnabled);
state = STATE_PREPARED; trackEnabled = false;
try { try {
chunkSource.disable(); chunkSource.disable();
} finally { } finally {
@ -214,12 +218,14 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
} }
} }
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
}
@Override @Override
public void continueBuffering(long positionUs) { public void continueBuffering(long positionUs) {
Assertions.checkState(state != STATE_IDLE);
if (state == STATE_PREPARED) {
return;
}
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
chunkSource.continueBuffering(positionUs); chunkSource.continueBuffering(positionUs);
updateLoadControl(); updateLoadControl();
@ -227,7 +233,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public boolean isReady() { public boolean isReady() {
Assertions.checkState(state == STATE_ENABLED);
return loadingFinished || !sampleQueue.isEmpty(); return loadingFinished || !sampleQueue.isEmpty();
} }
@ -242,7 +247,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public int readData(FormatHolder formatHolder, SampleHolder sampleHolder) { public int readData(FormatHolder formatHolder, SampleHolder sampleHolder) {
Assertions.checkState(state == STATE_ENABLED);
if (pendingReset || isPendingReset()) { if (pendingReset || isPendingReset()) {
return NOTHING_READ; return NOTHING_READ;
} }
@ -292,10 +296,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(state != STATE_IDLE);
if (state == STATE_PREPARED) {
return;
}
downstreamPositionUs = positionUs; downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the sample queue. // If we're not pending a reset, see if we can seek within the sample queue.
@ -326,8 +326,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(state != STATE_IDLE); if (!trackEnabled || loadingFinished) {
if (state != STATE_ENABLED || loadingFinished) {
return C.END_OF_SOURCE_US; return C.END_OF_SOURCE_US;
} else if (isPendingReset()) { } else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
@ -340,12 +339,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override @Override
public void release() { public void release() {
Assertions.checkState(state != STATE_ENABLED); state = STATE_RELEASED;
trackEnabled = false;
if (loader != null) { if (loader != null) {
loader.release(); loader.release();
loader = null; loader = null;
} }
state = STATE_IDLE;
} }
@Override @Override
@ -371,7 +370,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
Chunk currentLoadable = currentLoadableHolder.chunk; Chunk currentLoadable = currentLoadableHolder.chunk;
notifyLoadCanceled(currentLoadable.bytesLoaded()); notifyLoadCanceled(currentLoadable.bytesLoaded());
clearCurrentLoadable(); clearCurrentLoadable();
if (state == STATE_ENABLED) { if (trackEnabled) {
restartFrom(pendingResetPositionUs); restartFrom(pendingResetPositionUs);
} else { } else {
sampleQueue.clear(); sampleQueue.clear();

View File

@ -208,13 +208,15 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
private volatile SeekMap seekMap; private volatile SeekMap seekMap;
private volatile DrmInitData drmInitData; private volatile DrmInitData drmInitData;
private boolean prepared; private int state;
private int enabledTrackCount; private int enabledTrackCount;
private TrackGroupArray tracks; private TrackGroupArray tracks;
private long durationUs; private long durationUs;
private boolean[] pendingMediaFormat; private boolean[] pendingMediaFormat;
private boolean[] pendingResets; private boolean[] pendingResets;
private boolean[] trackEnabledStates; private boolean[] trackEnabledStates;
private boolean isFirstTrackSelection;
private boolean newTracksSelected;
private long downstreamPositionUs; private long downstreamPositionUs;
private long lastSeekPositionUs; private long lastSeekPositionUs;
@ -325,35 +327,39 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
extractorHolder = new ExtractorHolder(extractors, this); extractorHolder = new ExtractorHolder(extractors, this);
sampleQueues = new SparseArray<>(); sampleQueues = new SparseArray<>();
pendingResetPositionUs = NO_RESET_PENDING; pendingResetPositionUs = NO_RESET_PENDING;
state = STATE_UNPREPARED;
}
@Override
public int getState() {
return state;
} }
@Override @Override
public boolean prepare(long positionUs) { public boolean prepare(long positionUs) {
if (prepared) { Assertions.checkState(state == STATE_UNPREPARED);
return true;
}
if (loader == null) { if (loader == null) {
loader = new Loader("Loader:ExtractorSampleSource"); loader = new Loader("Loader:ExtractorSampleSource");
} }
maybeStartLoading(); maybeStartLoading();
if (seekMap == null || !tracksBuilt || !haveFormatsForAllTracks()) {
if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { return false;
int trackCount = sampleQueues.size();
TrackGroup[] tracks = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount];
pendingResets = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
tracks[i] = new TrackGroup(sampleQueues.valueAt(i).getFormat());
}
this.tracks = new TrackGroupArray(tracks);
prepared = true;
return true;
} }
return false; int trackCount = sampleQueues.size();
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackEnabledStates = new boolean[trackCount];
pendingResets = new boolean[trackCount];
pendingMediaFormat = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getFormat());
}
tracks = new TrackGroupArray(trackArray);
isFirstTrackSelection = true;
state = STATE_SELECTING_TRACKS;
return true;
} }
@Override @Override
@ -367,8 +373,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
Assertions.checkState(prepared); Assertions.checkState(state == STATE_READING);
state = STATE_SELECTING_TRACKS;
newTracksSelected = false;
isFirstTrackSelection = false;
}
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
Assertions.checkState(selection.length == 1); Assertions.checkState(selection.length == 1);
Assertions.checkState(selection.getTrack(0) == 0); Assertions.checkState(selection.getTrack(0) == 0);
int track = selection.group; int track = selection.group;
@ -377,23 +391,23 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
trackEnabledStates[track] = true; trackEnabledStates[track] = true;
pendingMediaFormat[track] = true; pendingMediaFormat[track] = true;
pendingResets[track] = false; pendingResets[track] = false;
if (enabledTrackCount == 1) { newTracksSelected = true;
// Treat all enables in non-seekable media as being from t=0.
positionUs = !seekMap.isSeekable() ? 0 : positionUs;
downstreamPositionUs = positionUs;
lastSeekPositionUs = positionUs;
restartFrom(positionUs);
}
return new TrackStreamImpl(track); return new TrackStreamImpl(track);
} }
@Override @Override
public void disable(TrackStream trackStream) { public void unselectTrack(TrackStream stream) {
Assertions.checkState(prepared); Assertions.checkState(state == STATE_SELECTING_TRACKS);
int track = ((TrackStreamImpl) trackStream).track; int track = ((TrackStreamImpl) stream).track;
Assertions.checkState(trackEnabledStates[track]); Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--; enabledTrackCount--;
trackEnabledStates[track] = false; trackEnabledStates[track] = false;
}
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
downstreamPositionUs = Long.MIN_VALUE; downstreamPositionUs = Long.MIN_VALUE;
if (loader.isLoading()) { if (loader.isLoading()) {
@ -402,12 +416,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
clearState(); clearState();
allocator.trim(0); allocator.trim(0);
} }
} else if (!isFirstTrackSelection && newTracksSelected) {
seekToInternal(positionUs);
} }
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public void continueBuffering(long playbackPositionUs) {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return; return;
} }
@ -420,7 +435,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
} }
/* package */ boolean isReady(int track) { /* package */ boolean isReady(int track) {
Assertions.checkState(prepared);
Assertions.checkState(trackEnabledStates[track]); Assertions.checkState(trackEnabledStates[track]);
return !sampleQueues.valueAt(track).isEmpty(); return !sampleQueues.valueAt(track).isEmpty();
@ -490,13 +504,18 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return; return;
} }
seekToInternal(positionUs);
}
private void seekToInternal(long positionUs) {
// Treat all seeks into non-seekable media as being to t=0. // Treat all seeks into non-seekable media as being to t=0.
downstreamPositionUs = !seekMap.isSeekable() ? 0 : positionUs; positionUs = !seekMap.isSeekable() ? 0 : positionUs;
lastSeekPositionUs = downstreamPositionUs; lastSeekPositionUs = positionUs;
downstreamPositionUs = positionUs;
Arrays.fill(pendingResets, true);
// If we're not pending a reset, see if we can seek within the sample queues. // If we're not pending a reset, see if we can seek within the sample queues.
boolean seekInsideBuffer = !isPendingReset(); boolean seekInsideBuffer = !isPendingReset();
for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) { for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) {
@ -506,9 +525,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
if (!seekInsideBuffer) { if (!seekInsideBuffer) {
restartFrom(positionUs); restartFrom(positionUs);
} }
// Either way, we need to send discontinuities to the downstream components.
Arrays.fill(pendingResets, true);
} }
@Override @Override
@ -530,11 +546,12 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
@Override @Override
public void release() { public void release() {
state = STATE_RELEASED;
enabledTrackCount = 0;
if (loader != null) { if (loader != null) {
loader.release(); loader.release();
loader = null; loader = null;
} }
prepared = false;
} }
// Loader.Callback implementation. // Loader.Callback implementation.
@ -617,7 +634,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
currentLoadableException = null; currentLoadableException = null;
if (!prepared) { if (state == STATE_UNPREPARED) {
// We don't know whether we're playing an on-demand or a live stream. For a live stream // We don't know whether we're playing an on-demand or a live stream. For a live stream
// we need to load from the start, as outlined below. Since we might be playing a live // we need to load from the start, as outlined below. Since we might be playing a live
// stream, play it safe and load from the start. // stream, play it safe and load from the start.
@ -654,7 +671,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
sampleTimeOffsetUs = 0; sampleTimeOffsetUs = 0;
havePendingNextSampleUs = false; havePendingNextSampleUs = false;
if (!prepared) { if (state == STATE_UNPREPARED) {
loadable = createLoadableFromStart(); loadable = createLoadableFromStart();
} else { } else {
Assertions.checkState(isPendingReset()); Assertions.checkState(isPendingReset());
@ -836,6 +853,8 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu
allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize); allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize);
result = extractor.read(input, positionHolder); result = extractor.read(input, positionHolder);
// TODO: Implement throttling to stop us from buffering data too often. // TODO: Implement throttling to stop us from buffering data too often.
// TODO: Block buffering between the point at which we have sufficient data for
// preparation to complete and the first call to endTrackSelection.
} }
} finally { } finally {
if (result == Extractor.RESULT_SEEK) { if (result == Extractor.RESULT_SEEK) {

View File

@ -73,7 +73,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private boolean prepared; private int state;
private boolean loadControlRegistered; private boolean loadControlRegistered;
private int enabledTrackCount; private int enabledTrackCount;
@ -84,6 +84,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private int primaryTrackGroupIndex; private int primaryTrackGroupIndex;
private int[] primarySelectedTracks; private int[] primarySelectedTracks;
private boolean primarySelectedTracksChanged;
// Indexed by group. // Indexed by group.
private boolean[] groupEnabledStates; private boolean[] groupEnabledStates;
private boolean[] pendingResets; private boolean[] pendingResets;
@ -131,15 +132,20 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
chunkOperationHolder = new ChunkOperationHolder(); chunkOperationHolder = new ChunkOperationHolder();
} }
@Override
public int getState() {
return state;
}
@Override @Override
public boolean prepare(long positionUs) throws IOException { public boolean prepare(long positionUs) throws IOException {
if (prepared) { Assertions.checkState(state == STATE_UNPREPARED);
return true; if (!chunkSource.prepare()) {
} else if (!chunkSource.prepare()) {
return false; return false;
} else if (chunkSource.getTrackCount() == 0) { }
if (chunkSource.getTrackCount() == 0) {
trackGroups = new TrackGroupArray(); trackGroups = new TrackGroupArray();
prepared = true; state = STATE_SELECTING_TRACKS;
return true; return true;
} }
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
@ -148,7 +154,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
HlsExtractorWrapper extractor = extractors.getFirst(); HlsExtractorWrapper extractor = extractors.getFirst();
if (extractor.isPrepared()) { if (extractor.isPrepared()) {
buildTracks(extractor); buildTracks(extractor);
prepared = true; state = STATE_SELECTING_TRACKS;
maybeStartLoading(); // Update the load control. maybeStartLoading(); // Update the load control.
return true; return true;
} else if (extractors.size() > 1) { } else if (extractors.size() > 1) {
@ -183,74 +189,82 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public TrackGroupArray getTrackGroups() { public TrackGroupArray getTrackGroups() {
Assertions.checkState(prepared);
return trackGroups; return trackGroups;
} }
@Override @Override
public TrackStream enable(TrackSelection selection, long positionUs) { public void startTrackSelection() {
Assertions.checkState(prepared); Assertions.checkState(state == STATE_READING);
state = STATE_SELECTING_TRACKS;
primarySelectedTracksChanged = false;
}
@Override
public TrackStream selectTrack(TrackSelection selection, long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
int group = selection.group; int group = selection.group;
int[] tracks = selection.getTracks(); int[] tracks = selection.getTracks();
setTrackGroupEnabledState(group, true); setTrackGroupEnabledState(group, true);
downstreamSampleFormats[group] = null; downstreamSampleFormats[group] = null;
pendingResets[group] = false; pendingResets[group] = false;
downstreamFormat = null;
boolean wasLoadControlRegistered = loadControlRegistered;
if (!loadControlRegistered) {
loadControl.register(this, bufferSizeContribution);
loadControlRegistered = true;
}
// Treat enabling of a live stream as occurring at t=0 in both of the blocks below.
positionUs = chunkSource.isLive() ? 0 : positionUs;
if (group == primaryTrackGroupIndex && !Arrays.equals(tracks, primarySelectedTracks)) { if (group == primaryTrackGroupIndex && !Arrays.equals(tracks, primarySelectedTracks)) {
// This is a primary track whose corresponding chunk source track is different to the one
// currently selected. We need to change the selection and restart. Since other exposed tracks
// may be enabled too, we need to implement the restart as a seek so that all downstream
// renderers receive a discontinuity event.
chunkSource.selectTracks(tracks);
primarySelectedTracks = tracks; primarySelectedTracks = tracks;
seekToInternal(positionUs); primarySelectedTracksChanged = true;
} else if (enabledTrackCount == 1) {
lastSeekPositionUs = positionUs;
if (wasLoadControlRegistered && downstreamPositionUs == positionUs) {
// TODO: Address [Internal: b/21743989] to remove the need for this kind of hack.
// This is the first track to be enabled after preparation and the position is the same as
// was passed to prepare. In this case we can avoid restarting, which would reload the same
// chunks as were loaded during preparation.
maybeStartLoading();
} else {
downstreamPositionUs = positionUs;
restartFrom(positionUs);
}
} }
return new TrackStreamImpl(group); return new TrackStreamImpl(group);
} }
@Override @Override
public void disable(TrackStream trackStream) { public void unselectTrack(TrackStream stream) {
Assertions.checkState(prepared); Assertions.checkState(state == STATE_SELECTING_TRACKS);
int group = ((TrackStreamImpl) trackStream).group; int group = ((TrackStreamImpl) stream).group;
setTrackGroupEnabledState(group, false); setTrackGroupEnabledState(group, false);
}
@Override
public void endTrackSelection(long positionUs) {
Assertions.checkState(state == STATE_SELECTING_TRACKS);
state = STATE_READING;
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
chunkSource.reset(); chunkSource.reset();
downstreamPositionUs = Long.MIN_VALUE; downstreamPositionUs = Long.MIN_VALUE;
if (loadControlRegistered) { downstreamFormat = null;
loadControl.unregister(this); if (loader != null) {
loadControlRegistered = false; if (loadControlRegistered) {
loadControl.unregister(this);
loadControlRegistered = false;
}
if (loader.isLoading()) {
loader.cancelLoading();
} else {
clearState();
loadControl.trimAllocator();
}
} }
if (loader.isLoading()) { } else {
loader.cancelLoading(); if (!loadControlRegistered) {
loadControl.register(this, bufferSizeContribution);
loadControlRegistered = true;
}
// Treat enabling of a live stream as occurring at t=0 in both of the blocks below.
positionUs = chunkSource.isLive() ? 0 : positionUs;
if (primarySelectedTracksChanged) {
// If the primary tracks change then this will affect other exposed tracks that are enabled
// as well. Hence we implement the restart as a seek so that all downstream renderers
// receive a discontinuity event.
chunkSource.selectTracks(primarySelectedTracks);
seekToInternal(positionUs);
} else { } else {
clearState(); lastSeekPositionUs = positionUs;
loadControl.trimAllocator(); downstreamPositionUs = positionUs;
restartFrom(positionUs);
} }
} }
} }
@Override @Override
public void continueBuffering(long playbackPositionUs) { public void continueBuffering(long playbackPositionUs) {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return; return;
} }
@ -290,8 +304,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
} }
/* package */ int readData(int group, FormatHolder formatHolder, SampleHolder sampleHolder) { /* package */ int readData(int group, FormatHolder formatHolder, SampleHolder sampleHolder) {
Assertions.checkState(prepared);
if (pendingResets[group] || isPendingReset()) { if (pendingResets[group] || isPendingReset()) {
return TrackStream.NOTHING_READ; return TrackStream.NOTHING_READ;
} }
@ -355,7 +367,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return; return;
} }
@ -364,7 +375,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
Assertions.checkState(prepared);
if (enabledTrackCount == 0) { if (enabledTrackCount == 0) {
return C.END_OF_SOURCE_US; return C.END_OF_SOURCE_US;
} else if (isPendingReset()) { } else if (isPendingReset()) {
@ -391,6 +401,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
@Override @Override
public void release() { public void release() {
state = STATE_RELEASED;
enabledTrackCount = 0;
if (loader != null) { if (loader != null) {
if (loadControlRegistered) { if (loadControlRegistered) {
loadControl.unregister(this); loadControl.unregister(this);
@ -399,7 +411,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
loader.release(); loader.release();
loader = null; loader = null;
} }
prepared = false;
} }
// Loader.Callback implementation. // Loader.Callback implementation.
@ -680,7 +691,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
return; return;
} }
if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { if (loader.isLoading() || !nextLoader
|| (state != STATE_UNPREPARED && enabledTrackCount == 0)) {
return; return;
} }
@ -730,7 +742,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback {
if (isPendingReset()) { if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} else { } else {
return loadingFinished || (prepared && enabledTrackCount == 0) ? -1 return loadingFinished || (state != STATE_UNPREPARED && enabledTrackCount == 0) ? -1
: currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs;
} }
} }