diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 3dcc6f8e75..5ebba53527 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; 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.TraceUtil; import com.google.android.exoplayer.util.Util; @@ -31,7 +32,6 @@ import android.util.Log; import android.util.Pair; import java.io.IOException; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; /** @@ -68,12 +68,12 @@ import java.util.concurrent.atomic.AtomicInteger; private final StandaloneMediaClock standaloneMediaClock; private final long minBufferUs; private final long minRebufferUs; - private final TrackSelection[] trackSelections; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final AtomicInteger pendingSeekCount; + private TrackSelectionArray trackSelections; private TrackRenderer rendererMediaClockSource; private MediaClock rendererMediaClock; private SampleSource source; @@ -104,7 +104,6 @@ import java.util.concurrent.atomic.AtomicInteger; this.bufferedPositionUs = C.UNKNOWN_TIME_US; standaloneMediaClock = new StandaloneMediaClock(); pendingSeekCount = new AtomicInteger(); - trackSelections = new TrackSelection[renderers.length]; enabledRenderers = new TrackRenderer[0]; trackSelector.init(this); @@ -281,7 +280,7 @@ import java.util.concurrent.atomic.AtomicInteger; durationUs = source.getDurationUs(); bufferedPositionUs = source.getBufferedPositionUs(); - selectTracksInternal(); + selectTracksInternal(true); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; @@ -478,27 +477,28 @@ import java.util.concurrent.atomic.AtomicInteger; handler.removeMessages(MSG_DO_SOME_WORK); handler.removeMessages(MSG_INCREMENTAL_PREPARE); rebuffering = false; + trackSelections = null; standaloneMediaClock.stop(); - if (renderers == null) { - return; - } - for (TrackRenderer renderer : renderers) { - resetRendererInternal(renderer); - } + rendererMediaClock = null; + rendererMediaClockSource = null; enabledRenderers = new TrackRenderer[0]; - Arrays.fill(trackSelections, null); - source = null; - } - - private void resetRendererInternal(TrackRenderer renderer) { - try { - ensureDisabled(renderer); - } catch (ExoPlaybackException e) { - // There's nothing we can do. - Log.e(TAG, "Stop failed.", e); - } catch (RuntimeException e) { - // Ditto. - Log.e(TAG, "Stop failed.", e); + for (TrackRenderer renderer : renderers) { + try { + ensureStopped(renderer); + if (renderer.getState() == TrackRenderer.STATE_ENABLED) { + renderer.disable(); + } + } catch (ExoPlaybackException e) { + // There's nothing we can do. + Log.e(TAG, "Stop failed.", e); + } catch (RuntimeException e) { + // Ditto. + 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(); + Pair 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 // 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; for (int i = 0; i < renderers.length; i++) { TrackRenderer renderer = renderers[i]; - TrackSelection previousTrackSelection = trackSelections[i]; - trackSelections[i] = newTrackSelections.get(i); - if (trackSelections[i] != null) { + TrackSelection oldSelection = trackSelections == null ? null : trackSelections.get(i); + TrackSelection newSelection = newSelections.get(i); + if (newSelection != null) { enabledRendererCount++; } 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. if (rendererWasEnabledFlags[i]) { // We need to disable the renderer so that we can enable it with its new selection. - if (trackSelections[i] == null && renderer == rendererMediaClockSource) { - // We've been using rendererMediaClockSource to advance the current position, but it's - // being disabled and won't be re-enabled. Sync standaloneMediaClock so that it can take - // over timing responsibilities. - standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + if (renderer == rendererMediaClockSource) { + // The renderer is providing the media clock. + if (newSelection == null) { + // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take + // 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); + trackSelections = newSelections; enabledRenderers = new TrackRenderer[enabledRendererCount]; enabledRendererCount = 0; // Enable renderers with their new track selections. for (int i = 0; i < renderers.length; i++) { TrackRenderer renderer = renderers[i]; - TrackSelection trackSelection = trackSelections[i]; - if (trackSelection != null) { + TrackSelection newSelection = trackSelections.get(i); + if (newSelection != null) { enabledRenderers[enabledRendererCount++] = renderer; if (renderer.getState() == TrackRenderer.STATE_DISABLED) { // 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. boolean joining = !rendererWasEnabledFlags[i] && playing; // 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. - Format[] formats = new Format[trackSelection.length]; + Format[] formats = new Format[newSelection.length]; 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. renderer.enable(formats, trackStream, positionUs, joining); @@ -598,6 +620,8 @@ import java.util.concurrent.atomic.AtomicInteger; } } } + + source.endTrackSelection(positionUs); } 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. return; } - selectTracksInternal(); + selectTracksInternal(false); 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 { if (renderer.getState() == TrackRenderer.STATE_STARTED) { renderer.stop(); diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index d736ac2260..1b4c192be0 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -71,7 +71,7 @@ public final class FrameworkSampleSource implements SampleSource { private final long fileDescriptorOffset; private final long fileDescriptorLength; - private boolean prepared; + private int state; private long durationUs; private MediaExtractor extractor; private TrackGroupArray tracks; @@ -97,6 +97,7 @@ public final class FrameworkSampleSource implements SampleSource { fileDescriptor = null; fileDescriptorOffset = 0; fileDescriptorLength = 0; + state = STATE_UNPREPARED; } /** @@ -116,13 +117,17 @@ public final class FrameworkSampleSource implements SampleSource { context = null; uri = null; headers = null; + state = STATE_UNPREPARED; + } + + @Override + public int getState() { + return state; } @Override public boolean prepare(long positionUs) throws IOException { - if (prepared) { - return true; - } + Assertions.checkState(state == STATE_UNPREPARED); extractor = new MediaExtractor(); if (context != null) { extractor.setDataSource(context, uri, headers); @@ -132,16 +137,16 @@ public final class FrameworkSampleSource implements SampleSource { durationUs = C.UNKNOWN_TIME_US; trackStates = new int[extractor.getTrackCount()]; 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++) { MediaFormat format = extractor.getTrackFormat(i); if (format.containsKey(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); - prepared = true; + tracks = new TrackGroupArray(trackArray); + state = STATE_SELECTING_TRACKS; return true; } @@ -161,8 +166,14 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public TrackStream enable(TrackSelection selection, long positionUs) { - Assertions.checkState(prepared); + public void startTrackSelection() { + 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.getTrack(0) == 0); int track = selection.group; @@ -170,10 +181,29 @@ public final class FrameworkSampleSource implements SampleSource { enabledTrackCount++; trackStates[track] = TRACK_STATE_ENABLED; extractor.selectTrack(track); - seekToUsInternal(positionUs, positionUs != 0); 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) { if (pendingResets[track]) { pendingResets[track] = false; @@ -183,7 +213,6 @@ public final class FrameworkSampleSource implements SampleSource { } /* package */ int readData(int track, FormatHolder formatHolder, SampleHolder sampleHolder) { - Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); if (pendingResets[track]) { 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 public void seekToUs(long positionUs) { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return; } @@ -245,7 +262,6 @@ public final class FrameworkSampleSource implements SampleSource { @Override public long getBufferedPositionUs() { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return C.END_OF_SOURCE_US; } @@ -261,11 +277,11 @@ public final class FrameworkSampleSource implements SampleSource { @Override public void release() { + state = STATE_RELEASED; if (extractor != null) { extractor.release(); extractor = null; } - prepared = false; } @TargetApi(18) diff --git a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java index 6f6b97e0d5..f6db3863b0 100644 --- a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.util.Assertions; + import android.util.Pair; import java.io.IOException; @@ -28,45 +30,54 @@ public final class MultiSampleSource implements SampleSource { private final SampleSource[] sources; private final IdentityHashMap trackStreamSourceIndices; - private boolean prepared; + private int state; private long durationUs; private TrackGroupArray trackGroups; public MultiSampleSource(SampleSource... sources) { this.sources = sources; trackStreamSourceIndices = new IdentityHashMap<>(); + state = STATE_UNPREPARED; + } + + @Override + public int getState() { + return state; } @Override public boolean prepare(long positionUs) throws IOException { - if (prepared) { - return true; - } + Assertions.checkState(state == SampleSource.STATE_UNPREPARED); boolean sourcesPrepared = true; for (SampleSource source : sources) { - 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(); - } + if (source.getState() == SampleSource.STATE_UNPREPARED) { + sourcesPrepared &= source.prepare(positionUs); } - 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 @@ -75,18 +86,38 @@ public final class MultiSampleSource implements SampleSource { } @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 sourceAndGroup = getSourceAndTrackGroupIndices(selection.group); - TrackStream trackStream = sources[sourceAndGroup.first].enable( + TrackStream trackStream = sources[sourceAndGroup.first].selectTrack( new TrackSelection(sourceAndGroup.second, selection.getTracks()), positionUs); trackStreamSourceIndices.put(trackStream, sourceAndGroup.first); return trackStream; } @Override - public void disable(TrackStream trackStream) { + public void unselectTrack(TrackStream trackStream) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); 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 @@ -129,7 +160,7 @@ public final class MultiSampleSource implements SampleSource { for (SampleSource source : sources) { source.release(); } - prepared = false; + state = STATE_RELEASED; } private Pair getSourceAndTrackGroupIndices(int group) { diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 37e4a39b60..9a7931b10f 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -22,11 +22,39 @@ import java.io.IOException; */ 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. *

* 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. + *

+ * This method should only be called when the state is {@link #STATE_UNPREPARED}. * * @param positionUs The player's current playback position. * @return True if the source was prepared, false otherwise. @@ -37,7 +65,8 @@ public interface SampleSource { /** * Returns the duration of the source. *

- * 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 * duration is not known. @@ -47,16 +76,63 @@ public interface SampleSource { /** * Returns the {@link TrackGroup}s exposed by the source. *

- * 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. */ TrackGroupArray getTrackGroups(); + /** + * Enters the track selection state. + *

+ * 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)}. + *

+ * 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. + *

+ * 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. + *

+ * 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)}. + *

+ * 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. + *

+ * 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. *

- * 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. */ @@ -65,7 +141,7 @@ public interface SampleSource { /** * Returns an estimate of the position up to which data is buffered for the enabled tracks. *

- * 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, * 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. *

- * 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. */ 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. - *

- * 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. *

- * 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(); diff --git a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java index afa96f5107..0bd1e7734c 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -59,9 +59,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load */ private static final int INITIAL_SAMPLE_SIZE = 1; - private static final int STATE_SEND_FORMAT = 0; - private static final int STATE_SEND_SAMPLE = 1; - private static final int STATE_END_OF_STREAM = 2; + private static final int STREAM_STATE_SEND_FORMAT = 0; + private static final int STREAM_STATE_SEND_SAMPLE = 1; + private static final int STREAM_STATE_END_OF_STREAM = 2; private final Uri uri; private final DataSource dataSource; @@ -74,9 +74,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load private final int eventSourceId; private int state; - private byte[] sampleData; - private int sampleSize; - private long pendingResetPositionUs; private boolean loadingFinished; private Loader loader; @@ -84,6 +81,10 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; + private int streamState; + private byte[] sampleData; + private int sampleSize; + public SingleSampleSource(Uri uri, DataSource dataSource, Format format, long durationUs) { 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; tracks = new TrackGroupArray(new TrackGroup(format)); sampleData = new byte[INITIAL_SAMPLE_SIZE]; + state = STATE_UNPREPARED; + } + + @Override + public int getState() { + return state; } @Override @@ -117,9 +124,9 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load @Override public boolean prepare(long positionUs) { - if (loader == null) { - loader = new Loader("Loader:" + format.sampleMimeType); - } + Assertions.checkState(state == STATE_UNPREPARED); + state = STATE_SELECTING_TRACKS; + loader = new Loader("Loader:" + format.sampleMimeType); return true; } @@ -134,14 +141,33 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load } @Override - public TrackStream enable(TrackSelection selection, long positionUs) { - state = STATE_SEND_FORMAT; + public void startTrackSelection() { + 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; clearCurrentLoadableException(); maybeStartLoading(); 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 public void continueBuffering(long positionUs) { maybeStartLoading(); @@ -161,16 +187,16 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load @Override 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); return END_OF_STREAM; - } else if (state == STATE_SEND_FORMAT) { + } else if (streamState == STREAM_STATE_SEND_FORMAT) { formatHolder.format = format; - state = STATE_SEND_SAMPLE; + streamState = STREAM_STATE_SEND_SAMPLE; return FORMAT_READ; } - Assertions.checkState(state == STATE_SEND_SAMPLE); + Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); if (!loadingFinished) { return NOTHING_READ; } else { @@ -179,31 +205,28 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load sampleHolder.addFlag(C.SAMPLE_FLAG_SYNC); sampleHolder.ensureSpaceForWrite(sampleHolder.size); sampleHolder.data.put(sampleData, 0, sampleSize); - state = STATE_END_OF_STREAM; + streamState = STREAM_STATE_END_OF_STREAM; return SAMPLE_READ; } } @Override public void seekToUs(long positionUs) { - if (state == STATE_END_OF_STREAM) { + if (streamState == STREAM_STATE_END_OF_STREAM) { pendingResetPositionUs = positionUs; - state = STATE_SEND_SAMPLE; + streamState = STREAM_STATE_SEND_SAMPLE; } } @Override public long getBufferedPositionUs() { - return state == STATE_END_OF_STREAM || loadingFinished ? C.END_OF_SOURCE_US : 0; - } - - @Override - public void disable(TrackStream trackStream) { - state = STATE_END_OF_STREAM; + return streamState == STREAM_STATE_END_OF_STREAM || loadingFinished ? C.END_OF_SOURCE_US : 0; } @Override public void release() { + state = STATE_RELEASED; + streamState = STREAM_STATE_END_OF_STREAM; if (loader != null) { loader.release(); loader = null; @@ -213,7 +236,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load // Private methods. private void maybeStartLoading() { - if (loadingFinished || state == STATE_END_OF_STREAM || loader.isLoading()) { + if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) { return; } if (currentLoadableException != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index aacb551c99..0e071acf8f 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -54,10 +54,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call */ 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 final int eventSourceId; @@ -83,8 +79,8 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call private long durationUs; private Loader loader; private boolean loadingFinished; + private boolean trackEnabled; private IOException currentLoadableException; - private int enabledTrackCount; private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; private long currentLoadStartTimeMs; @@ -143,27 +139,30 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); - state = STATE_IDLE; pendingResetPositionUs = NO_RESET_PENDING; + state = STATE_UNPREPARED; + } + + @Override + public int getState() { + return state; } @Override public boolean prepare(long positionUs) throws IOException { - if (state != STATE_IDLE) { - return true; - } + Assertions.checkState(state == STATE_UNPREPARED); if (!chunkSource.prepare()) { return false; } durationUs = chunkSource.getDurationUs(); - TrackGroup trackGroup = chunkSource.getTracks(); - if (trackGroup != null) { - loader = new Loader("Loader:" + trackGroup.getFormat(0).containerMimeType); - trackGroups = new TrackGroupArray(trackGroup); + TrackGroup tracks = chunkSource.getTracks(); + if (tracks != null) { + loader = new Loader("Loader:" + tracks.getFormat(0).containerMimeType); + trackGroups = new TrackGroupArray(tracks); } else { trackGroups = new TrackGroupArray(); } - state = STATE_PREPARED; + state = STATE_SELECTING_TRACKS; return true; } @@ -174,15 +173,20 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public TrackGroupArray getTrackGroups() { - Assertions.checkState(state != STATE_IDLE); return trackGroups; } @Override - public TrackStream enable(TrackSelection selection, long positionUs) { - Assertions.checkState(state == STATE_PREPARED); - Assertions.checkState(enabledTrackCount++ == 0); - state = STATE_ENABLED; + public void startTrackSelection() { + 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(!trackEnabled); + trackEnabled = true; chunkSource.enable(selection.getTracks()); loadControl.register(this, bufferSizeContribution); downstreamFormat = null; @@ -195,10 +199,10 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call } @Override - public void disable(TrackStream trackStream) { - Assertions.checkState(state == STATE_ENABLED); - Assertions.checkState(--enabledTrackCount == 0); - state = STATE_PREPARED; + public void unselectTrack(TrackStream stream) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); + Assertions.checkState(trackEnabled); + trackEnabled = false; try { chunkSource.disable(); } 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 public void continueBuffering(long positionUs) { - Assertions.checkState(state != STATE_IDLE); - if (state == STATE_PREPARED) { - return; - } downstreamPositionUs = positionUs; chunkSource.continueBuffering(positionUs); updateLoadControl(); @@ -227,7 +233,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public boolean isReady() { - Assertions.checkState(state == STATE_ENABLED); return loadingFinished || !sampleQueue.isEmpty(); } @@ -242,7 +247,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public int readData(FormatHolder formatHolder, SampleHolder sampleHolder) { - Assertions.checkState(state == STATE_ENABLED); if (pendingReset || isPendingReset()) { return NOTHING_READ; } @@ -292,10 +296,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public void seekToUs(long positionUs) { - Assertions.checkState(state != STATE_IDLE); - if (state == STATE_PREPARED) { - return; - } downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; // 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 public long getBufferedPositionUs() { - Assertions.checkState(state != STATE_IDLE); - if (state != STATE_ENABLED || loadingFinished) { + if (!trackEnabled || loadingFinished) { return C.END_OF_SOURCE_US; } else if (isPendingReset()) { return pendingResetPositionUs; @@ -340,12 +339,12 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public void release() { - Assertions.checkState(state != STATE_ENABLED); + state = STATE_RELEASED; + trackEnabled = false; if (loader != null) { loader.release(); loader = null; } - state = STATE_IDLE; } @Override @@ -371,7 +370,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCanceled(currentLoadable.bytesLoaded()); clearCurrentLoadable(); - if (state == STATE_ENABLED) { + if (trackEnabled) { restartFrom(pendingResetPositionUs); } else { sampleQueue.clear(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index f9b65a6fe6..e2cc007cfb 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -208,13 +208,15 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private volatile SeekMap seekMap; private volatile DrmInitData drmInitData; - private boolean prepared; + private int state; private int enabledTrackCount; private TrackGroupArray tracks; private long durationUs; private boolean[] pendingMediaFormat; private boolean[] pendingResets; private boolean[] trackEnabledStates; + private boolean isFirstTrackSelection; + private boolean newTracksSelected; private long downstreamPositionUs; private long lastSeekPositionUs; @@ -325,35 +327,39 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu extractorHolder = new ExtractorHolder(extractors, this); sampleQueues = new SparseArray<>(); pendingResetPositionUs = NO_RESET_PENDING; + state = STATE_UNPREPARED; + } + + @Override + public int getState() { + return state; } @Override public boolean prepare(long positionUs) { - if (prepared) { - return true; - } + Assertions.checkState(state == STATE_UNPREPARED); if (loader == null) { loader = new Loader("Loader:ExtractorSampleSource"); } maybeStartLoading(); - - if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { - 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; + if (seekMap == null || !tracksBuilt || !haveFormatsForAllTracks()) { + return false; } - 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 @@ -367,8 +373,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } @Override - public TrackStream enable(TrackSelection selection, long positionUs) { - Assertions.checkState(prepared); + public void startTrackSelection() { + 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.getTrack(0) == 0); int track = selection.group; @@ -377,23 +391,23 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu trackEnabledStates[track] = true; pendingMediaFormat[track] = true; pendingResets[track] = false; - if (enabledTrackCount == 1) { - // Treat all enables in non-seekable media as being from t=0. - positionUs = !seekMap.isSeekable() ? 0 : positionUs; - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - restartFrom(positionUs); - } + newTracksSelected = true; return new TrackStreamImpl(track); } @Override - public void disable(TrackStream trackStream) { - Assertions.checkState(prepared); - int track = ((TrackStreamImpl) trackStream).track; + public void unselectTrack(TrackStream stream) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); + int track = ((TrackStreamImpl) stream).track; Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; + } + + @Override + public void endTrackSelection(long positionUs) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); + state = STATE_READING; if (enabledTrackCount == 0) { downstreamPositionUs = Long.MIN_VALUE; if (loader.isLoading()) { @@ -402,12 +416,13 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu clearState(); allocator.trim(0); } + } else if (!isFirstTrackSelection && newTracksSelected) { + seekToInternal(positionUs); } } @Override public void continueBuffering(long playbackPositionUs) { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return; } @@ -420,7 +435,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } /* package */ boolean isReady(int track) { - Assertions.checkState(prepared); Assertions.checkState(trackEnabledStates[track]); return !sampleQueues.valueAt(track).isEmpty(); @@ -490,13 +504,18 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void seekToUs(long positionUs) { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return; } + seekToInternal(positionUs); + } + + private void seekToInternal(long positionUs) { // Treat all seeks into non-seekable media as being to t=0. - downstreamPositionUs = !seekMap.isSeekable() ? 0 : positionUs; - lastSeekPositionUs = downstreamPositionUs; + positionUs = !seekMap.isSeekable() ? 0 : positionUs; + lastSeekPositionUs = positionUs; + downstreamPositionUs = positionUs; + Arrays.fill(pendingResets, true); // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) { @@ -506,9 +525,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu if (!seekInsideBuffer) { restartFrom(positionUs); } - - // Either way, we need to send discontinuities to the downstream components. - Arrays.fill(pendingResets, true); } @Override @@ -530,11 +546,12 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void release() { + state = STATE_RELEASED; + enabledTrackCount = 0; if (loader != null) { loader.release(); loader = null; } - prepared = false; } // Loader.Callback implementation. @@ -617,7 +634,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { 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 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. @@ -654,7 +671,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu sampleTimeOffsetUs = 0; havePendingNextSampleUs = false; - if (!prepared) { + if (state == STATE_UNPREPARED) { loadable = createLoadableFromStart(); } else { Assertions.checkState(isPendingReset()); @@ -836,6 +853,8 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize); result = extractor.read(input, positionHolder); // 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 { if (result == Extractor.RESULT_SEEK) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index c0679e6216..1065cfc249 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -73,7 +73,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private final Handler eventHandler; private final EventListener eventListener; - private boolean prepared; + private int state; private boolean loadControlRegistered; private int enabledTrackCount; @@ -84,6 +84,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private TrackGroupArray trackGroups; private int primaryTrackGroupIndex; private int[] primarySelectedTracks; + private boolean primarySelectedTracksChanged; // Indexed by group. private boolean[] groupEnabledStates; private boolean[] pendingResets; @@ -131,15 +132,20 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { chunkOperationHolder = new ChunkOperationHolder(); } + @Override + public int getState() { + return state; + } + @Override public boolean prepare(long positionUs) throws IOException { - if (prepared) { - return true; - } else if (!chunkSource.prepare()) { + Assertions.checkState(state == STATE_UNPREPARED); + if (!chunkSource.prepare()) { return false; - } else if (chunkSource.getTrackCount() == 0) { + } + if (chunkSource.getTrackCount() == 0) { trackGroups = new TrackGroupArray(); - prepared = true; + state = STATE_SELECTING_TRACKS; return true; } if (!extractors.isEmpty()) { @@ -148,7 +154,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { HlsExtractorWrapper extractor = extractors.getFirst(); if (extractor.isPrepared()) { buildTracks(extractor); - prepared = true; + state = STATE_SELECTING_TRACKS; maybeStartLoading(); // Update the load control. return true; } else if (extractors.size() > 1) { @@ -183,74 +189,82 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public TrackGroupArray getTrackGroups() { - Assertions.checkState(prepared); return trackGroups; } + @Override - public TrackStream enable(TrackSelection selection, long positionUs) { - Assertions.checkState(prepared); + public void startTrackSelection() { + 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[] tracks = selection.getTracks(); setTrackGroupEnabledState(group, true); downstreamSampleFormats[group] = null; 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)) { - // 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; - seekToInternal(positionUs); - } 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); - } + primarySelectedTracksChanged = true; } return new TrackStreamImpl(group); } @Override - public void disable(TrackStream trackStream) { - Assertions.checkState(prepared); - int group = ((TrackStreamImpl) trackStream).group; + public void unselectTrack(TrackStream stream) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); + int group = ((TrackStreamImpl) stream).group; setTrackGroupEnabledState(group, false); + } + + @Override + public void endTrackSelection(long positionUs) { + Assertions.checkState(state == STATE_SELECTING_TRACKS); + state = STATE_READING; if (enabledTrackCount == 0) { chunkSource.reset(); downstreamPositionUs = Long.MIN_VALUE; - if (loadControlRegistered) { - loadControl.unregister(this); - loadControlRegistered = false; + downstreamFormat = null; + if (loader != null) { + if (loadControlRegistered) { + loadControl.unregister(this); + loadControlRegistered = false; + } + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + loadControl.trimAllocator(); + } } - if (loader.isLoading()) { - loader.cancelLoading(); + } else { + 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 { - clearState(); - loadControl.trimAllocator(); + lastSeekPositionUs = positionUs; + downstreamPositionUs = positionUs; + restartFrom(positionUs); } } } @Override public void continueBuffering(long playbackPositionUs) { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return; } @@ -290,8 +304,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { } /* package */ int readData(int group, FormatHolder formatHolder, SampleHolder sampleHolder) { - Assertions.checkState(prepared); - if (pendingResets[group] || isPendingReset()) { return TrackStream.NOTHING_READ; } @@ -355,7 +367,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void seekToUs(long positionUs) { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return; } @@ -364,7 +375,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public long getBufferedPositionUs() { - Assertions.checkState(prepared); if (enabledTrackCount == 0) { return C.END_OF_SOURCE_US; } else if (isPendingReset()) { @@ -391,6 +401,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void release() { + state = STATE_RELEASED; + enabledTrackCount = 0; if (loader != null) { if (loadControlRegistered) { loadControl.unregister(this); @@ -399,7 +411,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { loader.release(); loader = null; } - prepared = false; } // Loader.Callback implementation. @@ -680,7 +691,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return; } - if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { + if (loader.isLoading() || !nextLoader + || (state != STATE_UNPREPARED && enabledTrackCount == 0)) { return; } @@ -730,7 +742,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished || (prepared && enabledTrackCount == 0) ? -1 + return loadingFinished || (state != STATE_UNPREPARED && enabledTrackCount == 0) ? -1 : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; } }