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 e56cc041aa..54024ae274 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -17,7 +17,6 @@ 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; @@ -32,6 +31,7 @@ import android.util.Log; import android.util.Pair; import java.io.IOException; +import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; /** @@ -281,7 +281,7 @@ import java.util.concurrent.atomic.AtomicInteger; } durationUs = source.getDurationUs(); - selectTracksInternal(true); + selectTracksInternal(); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; @@ -531,38 +531,28 @@ import java.util.concurrent.atomic.AtomicInteger; } } - private void selectTracksInternal(boolean initialSelection) throws ExoPlaybackException { + private void selectTracksInternal() throws ExoPlaybackException { TrackGroupArray groups = source.getTrackGroups(); Pair result = trackSelector.selectTracks(renderers, groups); - TrackSelectionArray newSelections = result.first; + TrackSelectionArray newTrackSelections = result.first; - if (newSelections.equals(trackSelections)) { + if (newTrackSelections.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 - // because the new track selection for some renderer X may have the same track group as the old - // selection for some other renderer Y. Trying to enable X before disabling Y would fail, since - // sources do not support being enabled more than once per track group at a time. - - // Disable renderers whose track selections have changed. + // Disable any renderers whose selections have changed, adding the corresponding TrackStream + // instances to oldStreams. Where we need to obtain a new TrackStream instance for a renderer, + // we add the corresponding TrackSelection to newSelections. + ArrayList oldStreams = new ArrayList<>(); + ArrayList newSelections = new ArrayList<>(); boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; int enabledRendererCount = 0; for (int i = 0; i < renderers.length; i++) { TrackRenderer renderer = renderers[i]; TrackSelection oldSelection = trackSelections == null ? null : trackSelections.get(i); - TrackSelection newSelection = newSelections.get(i); + TrackSelection newSelection = newTrackSelections.get(i); if (newSelection != null) { enabledRendererCount++; } @@ -584,18 +574,23 @@ import java.util.concurrent.atomic.AtomicInteger; ensureStopped(renderer); if (renderer.getState() == TrackRenderer.STATE_ENABLED) { TrackStream trackStream = renderer.disable(); - source.unselectTrack(trackStream); + oldStreams.add(trackStream); } } + if (newSelection != null) { + newSelections.add(newSelection); + } } } + // Update the source selection. + TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs); trackSelector.onSelectionActivated(result.second); - trackSelections = newSelections; + trackSelections = newTrackSelections; + + // Enable renderers with their new selections. 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 newSelection = trackSelections.get(i); @@ -606,15 +601,14 @@ import java.util.concurrent.atomic.AtomicInteger; boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; // 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.selectTrack(newSelection, positionUs); // Build an array of formats contained by the new selection. Format[] formats = new Format[newSelection.length]; for (int j = 0; j < formats.length; j++) { formats[j] = groups.get(newSelection.group).getFormat(newSelection.getTrack(j)); } // Enable the renderer. - renderer.enable(formats, trackStream, positionUs, joining); + int newStreamIndex = newSelections.indexOf(newSelection); + renderer.enable(formats, newStreams[newStreamIndex], positionUs, joining); MediaClock mediaClock = renderer.getMediaClock(); if (mediaClock != null) { if (rendererMediaClock != null) { @@ -632,7 +626,6 @@ import java.util.concurrent.atomic.AtomicInteger; } } - source.endTrackSelection(positionUs); updateBufferedPositionUs(); } @@ -641,7 +634,7 @@ import java.util.concurrent.atomic.AtomicInteger; // We don't have tracks yet, so we don't care. return; } - selectTracksInternal(false); + selectTracksInternal(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } 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 8bb5d71814..b9a8668f22 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -34,6 +34,7 @@ import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -71,7 +72,7 @@ public final class FrameworkSampleSource implements SampleSource { private final long fileDescriptorOffset; private final long fileDescriptorLength; - private int state; + private boolean prepared; private long durationUs; private MediaExtractor extractor; private TrackGroupArray tracks; @@ -97,7 +98,6 @@ public final class FrameworkSampleSource implements SampleSource { fileDescriptor = null; fileDescriptorOffset = 0; fileDescriptorLength = 0; - state = STATE_UNPREPARED; } /** @@ -117,17 +117,13 @@ 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 { - Assertions.checkState(state == STATE_UNPREPARED); + if (prepared) { + return true; + } extractor = new MediaExtractor(); if (context != null) { extractor.setDataSource(context, uri, headers); @@ -146,7 +142,7 @@ public final class FrameworkSampleSource implements SampleSource { trackArray[i] = new TrackGroup(createFormat(i, format)); } tracks = new TrackGroupArray(trackArray); - state = STATE_SELECTING_TRACKS; + prepared = true; return true; } @@ -166,42 +162,36 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - 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; - Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); - enabledTrackCount++; - trackStates[track] = TRACK_STATE_ENABLED; - extractor.selectTrack(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; + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + // Unselect old tracks. + for (int i = 0; i < oldStreams.size(); i++) { + int track = ((TrackStreamImpl) oldStreams.get(i)).track; + Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); + enabledTrackCount--; + trackStates[track] = TRACK_STATE_DISABLED; + extractor.unselectTrack(track); + pendingResets[track] = false; + } + // Select new tracks. + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + for (int i = 0; i < newStreams.length; i++) { + TrackSelection selection = newSelections.get(i); + Assertions.checkState(selection.length == 1); + Assertions.checkState(selection.getTrack(0) == 0); + int track = selection.group; + Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); + enabledTrackCount++; + trackStates[track] = TRACK_STATE_ENABLED; + extractor.selectTrack(track); + newStreams[i] = new TrackStreamImpl(track); + } + // Seek if necessary. if (enabledTrackCount > 0) { seekToUsInternal(positionUs, positionUs != 0); } + return newStreams; } /* package */ long readReset(int track) { @@ -277,7 +267,6 @@ public final class FrameworkSampleSource implements SampleSource { @Override public void release() { - state = STATE_RELEASED; if (extractor != null) { extractor.release(); extractor = null; 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 b5f15b0b5e..f25071bdcb 100644 --- a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java @@ -20,8 +20,9 @@ import com.google.android.exoplayer.util.Assertions; import android.util.Pair; import java.io.IOException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.IdentityHashMap; +import java.util.List; /** * Combines multiple {@link SampleSource} instances. @@ -29,34 +30,29 @@ import java.util.IdentityHashMap; public final class MultiSampleSource implements SampleSource { private final SampleSource[] sources; - private final int[] sourceEnabledTrackCounts; - private final IdentityHashMap trackStreamSourceIndices; + private final IdentityHashMap trackStreamSources; + private final int[] selectedTrackCounts; - private int state; + private boolean prepared; + private boolean seenFirstTrackSelection; private long durationUs; private TrackGroupArray trackGroups; private SampleSource[] enabledSources; public MultiSampleSource(SampleSource... sources) { this.sources = sources; - sourceEnabledTrackCounts = new int[sources.length]; - trackStreamSourceIndices = new IdentityHashMap<>(); - state = STATE_UNPREPARED; - } - - @Override - public int getState() { - return state; + trackStreamSources = new IdentityHashMap<>(); + selectedTrackCounts = new int[sources.length]; } @Override public boolean prepare(long positionUs) throws IOException { - Assertions.checkState(state == SampleSource.STATE_UNPREPARED); + if (prepared) { + return true; + } boolean sourcesPrepared = true; for (SampleSource source : sources) { - if (source.getState() == SampleSource.STATE_UNPREPARED) { - sourcesPrepared &= source.prepare(positionUs); - } + sourcesPrepared &= source.prepare(positionUs); } if (!sourcesPrepared) { return false; @@ -80,7 +76,7 @@ public final class MultiSampleSource implements SampleSource { } } trackGroups = new TrackGroupArray(trackGroupArray); - state = STATE_SELECTING_TRACKS; + prepared = true; return true; } @@ -90,47 +86,29 @@ public final class MultiSampleSource implements SampleSource { } @Override - 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].selectTrack( - new TrackSelection(sourceAndGroup.second, selection.getTracks()), positionUs); - int sourceIndex = sourceAndGroup.first; - sourceEnabledTrackCounts[sourceIndex]++; - trackStreamSourceIndices.put(trackStream, sourceIndex); - return trackStream; - } - - @Override - public void unselectTrack(TrackStream trackStream) { - Assertions.checkState(state == STATE_SELECTING_TRACKS); - int sourceIndex = trackStreamSourceIndices.remove(trackStream); - sources[sourceIndex].unselectTrack(trackStream); - sourceEnabledTrackCounts[sourceIndex]--; - } - - @Override - public void endTrackSelection(long positionUs) { - Assertions.checkState(state == STATE_SELECTING_TRACKS); - state = STATE_READING; - int newEnabledSourceCount = 0; - SampleSource[] newEnabledSources = new SampleSource[sources.length]; + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + // Select tracks for each source. + int enabledSourceCount = 0; for (int i = 0; i < sources.length; i++) { - sources[i].endTrackSelection(positionUs); - if (sourceEnabledTrackCounts[i] > 0) { - newEnabledSources[newEnabledSourceCount++] = sources[i]; + selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs, + newStreams); + if (selectedTrackCounts[i] > 0) { + enabledSourceCount++; } } - enabledSources = Arrays.copyOf(newEnabledSources, newEnabledSourceCount); + // Update the enabled sources. + enabledSources = new SampleSource[enabledSourceCount]; + enabledSourceCount = 0; + for (int i = 0; i < sources.length; i++) { + if (selectedTrackCounts[i] > 0) { + enabledSources[enabledSourceCount++] = sources[i]; + } + } + seenFirstTrackSelection = true; + return newStreams; } @Override @@ -173,15 +151,49 @@ public final class MultiSampleSource implements SampleSource { for (SampleSource source : sources) { source.release(); } - state = STATE_RELEASED; } - private Pair getSourceAndTrackGroupIndices(int group) { + private int selectTracks(SampleSource source, List allOldStreams, + List allNewSelections, long positionUs, TrackStream[] allNewStreams) { + // Get the subset of the old streams for the source. + ArrayList oldStreams = new ArrayList<>(); + for (int i = 0; i < allOldStreams.size(); i++) { + TrackStream stream = allOldStreams.get(i); + if (trackStreamSources.get(stream) == source) { + trackStreamSources.remove(stream); + oldStreams.add(stream); + } + } + // Get the subset of the new selections for the source. + ArrayList newSelections = new ArrayList<>(); + int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; + for (int i = 0; i < allNewSelections.size(); i++) { + TrackSelection selection = allNewSelections.get(i); + Pair sourceAndGroup = getSourceAndGroup(selection.group); + if (sourceAndGroup.first == source) { + newSelectionOriginalIndices[newSelections.size()] = i; + newSelections.add(new TrackSelection(sourceAndGroup.second, selection.getTracks())); + } + } + // Do nothing if nothing has changed, except during the first selection. + if (seenFirstTrackSelection && oldStreams.isEmpty() && newSelections.isEmpty()) { + return 0; + } + // Perform the selection. + TrackStream[] newStreams = source.selectTracks(oldStreams, newSelections, positionUs); + for (int j = 0; j < newStreams.length; j++) { + allNewStreams[newSelectionOriginalIndices[j]] = newStreams[j]; + trackStreamSources.put(newStreams[j], source); + } + return newSelections.size() - oldStreams.size(); + } + + private Pair getSourceAndGroup(int group) { int totalTrackGroupCount = 0; for (int i = 0; i < sources.length; i++) { int sourceTrackGroupCount = sources[i].getTrackGroups().length; if (group < totalTrackGroupCount + sourceTrackGroupCount) { - return Pair.create(i, group - totalTrackGroupCount); + return Pair.create(sources[i], group - totalTrackGroupCount); } totalTrackGroupCount += sourceTrackGroupCount; } 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 cf2259881c..53b28f0f0a 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer; import java.io.IOException; +import java.util.List; /** * A source of media. @@ -23,41 +24,14 @@ 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. + * Prepares the source, or does nothing if the source is already prepared. *

- * If preparation cannot complete immediately then the call will return {@code false} rather than - * 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}. + * {@link #selectTracks(List, List, long)} must be called after the source is prepared to + * make an initial track selection. This is true even if the caller does not wish to select any + * tracks. * * @param positionUs The player's current playback position. - * @return True if the source was prepared, false otherwise. + * @return True if the source is prepared, false otherwise. * @throws IOException If there's an error preparing the source. */ boolean prepare(long positionUs) throws IOException; @@ -65,8 +39,7 @@ public interface SampleSource { /** * Returns the duration of the source. *

- * This method should only be called when the state is {@link #STATE_SELECTING_TRACKS} or - * {@link #STATE_READING}. + * This method should only be called after the source has been prepared. * * @return The duration of the source in microseconds, or {@link C#UNKNOWN_TIME_US} if the * duration is not known. @@ -76,64 +49,36 @@ public interface SampleSource { /** * Returns the {@link TrackGroup}s exposed by the source. *

- * This method should only be called when the state is {@link #STATE_SELECTING_TRACKS} or - * {@link #STATE_READING}. + * This method should only be called after the source has been prepared. * * @return The {@link TrackGroup}s. */ TrackGroupArray getTrackGroups(); /** - * Enters the track selection state. + * Modifies the selected tracks. *

- * 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)}. + * {@link TrackStream}s corresponding to tracks being unselected are passed in {@code oldStreams}. + * Tracks being selected are specified in {@code newSelections}. Each new {@link TrackSelection} + * must have a {@link TrackSelection#group} index distinct from those of currently enabled tracks, + * except for those being unselected. *

- * 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}. + * This method should only be called after the source has been prepared. * - * @param selection Defines the track. + * @param oldStreams {@link TrackStream}s corresponding to tracks being unselected. May be empty + * but must not be null. + * @param newSelections {@link TrackSelection}s that define tracks being selected. May be empty + * but must not be null. * @param positionUs The current playback position in microseconds. - * @return A {@link TrackStream} from which the enabled track's data can be read. + * @return The {@link TrackStream}s corresponding to each of the newly selected tracks. */ - 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); + TrackStream[] selectTracks(List oldStreams, List newSelections, + long positionUs); /** * Indicates to the source that it should continue buffering data for its enabled tracks. *

- * This method should only be called when the state is {@link #STATE_READING} and at least one - * track is selected. + * This method should only be called when at least one track is selected. * * @param positionUs The current playback position. */ @@ -142,8 +87,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 when the state is {@link #STATE_READING} and at least one - * track is selected. + * This method should only be called when at least one track is selected. * * @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} @@ -154,8 +98,7 @@ public interface SampleSource { /** * Seeks to the specified time in microseconds. *

- * This method should only be called when the state is {@link #STATE_READING} and at least one - * track is selected. + * This method should only be called when at least one track is selected. * * @param positionUs The seek position in microseconds. */ 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 c5678a0824..d5e8e1ddbc 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -27,6 +27,7 @@ import android.os.SystemClock; import java.io.IOException; import java.util.Arrays; +import java.util.List; /** * A {@link SampleSource} that loads the data at a given {@link Uri} as a single sample. @@ -73,7 +74,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load private final EventListener eventListener; private final int eventSourceId; - private int state; + private boolean prepared; private long pendingResetPositionUs; private boolean loadingFinished; private Loader loader; @@ -107,12 +108,6 @@ 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 @@ -124,9 +119,11 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load @Override public boolean prepare(long positionUs) { - Assertions.checkState(state == STATE_UNPREPARED); - state = STATE_SELECTING_TRACKS; + if (prepared) { + return true; + } loader = new Loader("Loader:" + format.sampleMimeType); + prepared = true; return true; } @@ -141,31 +138,25 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load } @Override - 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; + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(oldStreams.size() <= 1); + Assertions.checkState(newSelections.size() <= 1); + // Unselect old tracks. + if (!oldStreams.isEmpty()) { + streamState = STREAM_STATE_END_OF_STREAM; + } + // Select new tracks. + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + if (!newSelections.isEmpty()) { + newStreams[0] = this; + streamState = STREAM_STATE_SEND_FORMAT; + pendingResetPositionUs = NO_RESET; + clearCurrentLoadableException(); + maybeStartLoading(); + } + return newStreams; } @Override @@ -225,7 +216,6 @@ public final class SingleSampleSource implements SampleSource, TrackStream, Load @Override public void release() { - state = STATE_RELEASED; streamState = STREAM_STATE_END_OF_STREAM; if (loader != null) { loader.release(); 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 093b7da863..0678016232 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 @@ -68,12 +68,13 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call private final EventListener eventListener; private final int minLoadableRetryCount; - private int state; + private boolean prepared; private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private long lastPerformedBufferOperation; private boolean pendingReset; + private boolean loadControlRegistered; private TrackGroupArray trackGroups; private long durationUs; @@ -140,17 +141,13 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); pendingResetPositionUs = NO_RESET_PENDING; - state = STATE_UNPREPARED; - } - - @Override - public int getState() { - return state; } @Override public boolean prepare(long positionUs) throws IOException { - Assertions.checkState(state == STATE_UNPREPARED); + if (prepared) { + return true; + } if (!chunkSource.prepare()) { return false; } @@ -162,7 +159,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call } else { trackGroups = new TrackGroupArray(); } - state = STATE_SELECTING_TRACKS; + prepared = true; return true; } @@ -177,36 +174,31 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call } @Override - 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; - downstreamSampleFormat = null; - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - pendingReset = false; - restartFrom(positionUs); - return this; - } - - @Override - public void unselectTrack(TrackStream stream) { - Assertions.checkState(state == STATE_SELECTING_TRACKS); - Assertions.checkState(trackEnabled); - trackEnabled = false; - try { + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(oldStreams.size() <= 1); + Assertions.checkState(newSelections.size() <= 1); + // Unselect old tracks. + if (!oldStreams.isEmpty()) { + Assertions.checkState(trackEnabled); + trackEnabled = false; chunkSource.disable(); - } finally { - loadControl.unregister(this); + } + // Select new tracks. + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + if (!newSelections.isEmpty()) { + Assertions.checkState(!trackEnabled); + trackEnabled = true; + chunkSource.enable(newSelections.get(0).getTracks()); + newStreams[0] = this; + } + // Cancel or start requests as necessary. + if (!trackEnabled && loader != null) { + if (loadControlRegistered) { + loadControl.unregister(this); + loadControlRegistered = false; + } if (loader.isLoading()) { loader.cancelLoading(); } else { @@ -215,13 +207,19 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call clearCurrentLoadable(); loadControl.trimAllocator(); } + } else if (trackEnabled) { + if (!loadControlRegistered) { + loadControl.register(this, bufferSizeContribution); + loadControlRegistered = true; + } + downstreamFormat = null; + downstreamSampleFormat = null; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + pendingReset = false; + restartFrom(positionUs); } - } - - @Override - public void endTrackSelection(long positionUs) { - Assertions.checkState(state == STATE_SELECTING_TRACKS); - state = STATE_READING; + return newStreams; } @Override @@ -339,7 +337,7 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call @Override public void release() { - state = STATE_RELEASED; + prepared = false; trackEnabled = false; if (loader != null) { loader.release(); 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 8f77a68a46..b4f9037c16 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,15 +208,14 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private volatile SeekMap seekMap; private volatile DrmInitData drmInitData; - private int state; + private boolean prepared; + private boolean seenFirstTrackSelection; 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; @@ -327,26 +326,20 @@ 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) { - Assertions.checkState(state == STATE_UNPREPARED); + if (prepared) { + return true; + } if (loader == null) { loader = new Loader("Loader:ExtractorSampleSource"); } - maybeStartLoading(); if (seekMap == null || !tracksBuilt || !haveFormatsForAllTracks()) { return false; } - int trackCount = sampleQueues.size(); TrackGroup[] trackArray = new TrackGroup[trackCount]; trackEnabledStates = new boolean[trackCount]; @@ -357,8 +350,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getFormat()); } tracks = new TrackGroupArray(trackArray); - isFirstTrackSelection = true; - state = STATE_SELECTING_TRACKS; + prepared = true; return true; } @@ -373,41 +365,31 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu } @Override - 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; - Assertions.checkState(!trackEnabledStates[track]); - enabledTrackCount++; - trackEnabledStates[track] = true; - pendingMediaFormat[track] = true; - pendingResets[track] = false; - newTracksSelected = true; - return new TrackStreamImpl(track); - } - - @Override - 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; + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + // Unselect old tracks. + for (int i = 0; i < oldStreams.size(); i++) { + int track = ((TrackStreamImpl) oldStreams.get(i)).track; + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + } + // Select new tracks. + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + for (int i = 0; i < newStreams.length; i++) { + TrackSelection selection = newSelections.get(i); + Assertions.checkState(selection.length == 1); + Assertions.checkState(selection.getTrack(0) == 0); + int track = selection.group; + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + pendingMediaFormat[track] = true; + pendingResets[track] = false; + newStreams[i] = new TrackStreamImpl(track); + } + // Cancel or start requests as necessary. if (enabledTrackCount == 0) { downstreamPositionUs = Long.MIN_VALUE; if (loader.isLoading()) { @@ -416,9 +398,11 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu clearState(); allocator.trim(0); } - } else if (isFirstTrackSelection ? positionUs != 0 : newTracksSelected) { + } else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { seekToInternal(positionUs); } + seenFirstTrackSelection = true; + return newStreams; } @Override @@ -540,7 +524,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void release() { - state = STATE_RELEASED; enabledTrackCount = 0; if (loader != null) { loader.release(); @@ -628,7 +611,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { currentLoadableException = null; - if (state == STATE_UNPREPARED) { + if (!prepared) { // 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. @@ -665,7 +648,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu sampleTimeOffsetUs = 0; havePendingNextSampleUs = false; - if (state == STATE_UNPREPARED) { + if (!prepared) { loadable = createLoadableFromStart(); } else { Assertions.checkState(isPendingReset()); 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 7478971bf1..7e36eefbba 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 @@ -39,6 +39,7 @@ import android.os.SystemClock; import java.io.IOException; import java.util.Arrays; import java.util.LinkedList; +import java.util.List; /** * A {@link SampleSource} for HLS streams. @@ -73,7 +74,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private final Handler eventHandler; private final EventListener eventListener; - private int state; + private boolean prepared; + private boolean seenFirstTrackSelection; private boolean loadControlRegistered; private int enabledTrackCount; @@ -83,9 +85,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; private int primaryTrackGroupIndex; - private boolean isFirstTrackSelection; - private boolean newTracksSelected; - private boolean primaryTracksDeselected; // Indexed by group. private boolean[] groupEnabledStates; private boolean[] pendingResets; @@ -133,21 +132,17 @@ 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 { - Assertions.checkState(state == STATE_UNPREPARED); + if (prepared) { + return true; + } if (!chunkSource.prepare()) { return false; } if (chunkSource.getTrackCount() == 0) { trackGroups = new TrackGroupArray(); - state = STATE_SELECTING_TRACKS; - isFirstTrackSelection = true; + prepared = true; return true; } if (!extractors.isEmpty()) { @@ -156,9 +151,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { HlsExtractorWrapper extractor = extractors.getFirst(); if (extractor.isPrepared()) { buildTracks(extractor); - state = STATE_SELECTING_TRACKS; - isFirstTrackSelection = true; maybeStartLoading(); // Update the load control. + prepared = true; return true; } else if (extractors.size() > 1) { extractors.removeFirst().clear(); @@ -195,42 +189,31 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return trackGroups; } - @Override - public void startTrackSelection() { - Assertions.checkState(state == STATE_READING); - state = STATE_SELECTING_TRACKS; - isFirstTrackSelection = false; - newTracksSelected = false; - primaryTracksDeselected = 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; - newTracksSelected = true; - if (group == primaryTrackGroupIndex) { - primaryTracksDeselected |= chunkSource.selectTracks(tracks); + public TrackStream[] selectTracks(List oldStreams, + List newSelections, long positionUs) { + Assertions.checkState(prepared); + // Unselect old tracks. + for (int i = 0; i < oldStreams.size(); i++) { + int group = ((TrackStreamImpl) oldStreams.get(i)).group; + setTrackGroupEnabledState(group, false); } - return new TrackStreamImpl(group); - } - - @Override - 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; + // Select new tracks. + boolean primaryTracksDeselected = false; + TrackStream[] newStreams = new TrackStream[newSelections.size()]; + for (int i = 0; i < newStreams.length; i++) { + TrackSelection selection = newSelections.get(i); + int group = selection.group; + int[] tracks = selection.getTracks(); + setTrackGroupEnabledState(group, true); + downstreamSampleFormats[group] = null; + pendingResets[group] = false; + if (group == primaryTrackGroupIndex) { + primaryTracksDeselected |= chunkSource.selectTracks(tracks); + } + newStreams[i] = new TrackStreamImpl(group); + } + // Cancel or start requests as necessary. if (enabledTrackCount == 0) { chunkSource.reset(); downstreamPositionUs = Long.MIN_VALUE; @@ -247,13 +230,15 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { loadControl.trimAllocator(); } } - } else if (primaryTracksDeselected || (!isFirstTrackSelection && newTracksSelected)) { + } else if (primaryTracksDeselected || (seenFirstTrackSelection && newStreams.length > 0)) { if (!loadControlRegistered) { loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } seekToInternal(positionUs); } + seenFirstTrackSelection = true; + return newStreams; } @Override @@ -386,7 +371,6 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void release() { - state = STATE_RELEASED; enabledTrackCount = 0; if (loader != null) { if (loadControlRegistered) { @@ -678,8 +662,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return; } - if (loader.isLoading() || !nextLoader - || (state != STATE_UNPREPARED && enabledTrackCount == 0)) { + if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { return; } @@ -729,7 +712,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished || (state != STATE_UNPREPARED && enabledTrackCount == 0) ? -1 + return loadingFinished || (prepared && enabledTrackCount == 0) ? -1 : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; } }