diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index fb67b9e7ce..a2b4a8647f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -96,6 +96,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { onStreamChanged(formats); } + @Override + public final SampleStream getStream() { + return stream; + } + @Override public final boolean hasReadStreamToEnd() { return readEndOfStream; diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 1bca073cc3..11e2b1f86a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -37,7 +37,6 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.ArrayList; /** * Implements the internal behavior of {@link ExoPlayerImpl}. @@ -728,35 +727,37 @@ import java.util.ArrayList; // Update streams for the new selection, recreating all streams if reading ahead. boolean recreateStreams = readingPeriod != playingPeriod; - TrackSelectionArray playingPeriodOldTrackSelections = playingPeriod.periodTrackSelections; - playingPeriod.updatePeriodTrackSelection(playbackInfo.positionUs, loadControl, - recreateStreams); + boolean[] streamResetFlags = playingPeriod.updatePeriodTrackSelection(playbackInfo.positionUs, + loadControl, recreateStreams); int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - TrackSelection oldSelection = playingPeriodOldTrackSelections.get(i); - TrackSelection newSelection = playingPeriod.trackSelections.get(i); - if (newSelection != null) { + SampleStream sampleStream = playingPeriod.sampleStreams[i]; + if (sampleStream != null) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] - && (recreateStreams || !Util.areEqual(oldSelection, newSelection))) { - // We need to disable the renderer so that we can enable it with its new stream. - 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()); + if (rendererWasEnabledFlags[i]) { + if (sampleStream != renderer.getStream()) { + // We need to disable the renderer. + if (renderer == rendererMediaClockSource) { + // The renderer is providing the media clock. + if (sampleStream == 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; } - rendererMediaClock = null; - rendererMediaClockSource = null; + ensureStopped(renderer); + renderer.disable(); + } else if (streamResetFlags[i]) { + // The renderer will continue to consume from its current stream, but needs to be reset. + renderer.resetPosition(playbackInfo.positionUs); } - ensureStopped(renderer); - renderer.disable(); } } trackSelector.onSelectionActivated(playingPeriod.trackSelectionData); @@ -1155,9 +1156,11 @@ import java.util.ArrayList; public final MediaPeriod mediaPeriod; public final Object id; - public final SampleStream[] sampleStreams; public final long startPositionUs; + public final SampleStream[] sampleStreams; + public final boolean[] mayRetainStreamFlags; + public int index; public boolean isLast; public boolean prepared; @@ -1183,6 +1186,7 @@ import java.util.ArrayList; this.mediaPeriod = mediaPeriod; this.id = Assertions.checkNotNull(id); sampleStreams = new SampleStream[renderers.length]; + mayRetainStreamFlags = new boolean[renderers.length]; startPositionUs = positionUs; this.index = index; } @@ -1216,46 +1220,33 @@ import java.util.ArrayList; return true; } - public void updatePeriodTrackSelection(long positionUs, LoadControl loadControl, + public boolean[] updatePeriodTrackSelection(long positionUs, LoadControl loadControl, boolean forceRecreateStreams) throws ExoPlaybackException { - // Populate lists of streams that are being disabled/newly enabled. - ArrayList oldStreams = new ArrayList<>(); - ArrayList newSelections = new ArrayList<>(); for (int i = 0; i < trackSelections.length; i++) { - TrackSelection oldSelection = - periodTrackSelections == null ? null : periodTrackSelections.get(i); - TrackSelection newSelection = trackSelections.get(i); - if (forceRecreateStreams || !Util.areEqual(oldSelection, newSelection)) { - if (oldSelection != null) { - oldStreams.add(sampleStreams[i]); - } - if (newSelection != null) { - newSelections.add(newSelection); - } - } + mayRetainStreamFlags[i] = !forceRecreateStreams + && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), + trackSelections.get(i)); } + boolean[] streamResetFlags = new boolean[renderers.length]; + // Disable streams on the period and get new streams for updated/newly-enabled tracks. - SampleStream[] newStreams = mediaPeriod.selectTracks(oldStreams, newSelections, positionUs); + mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, + streamResetFlags, positionUs); periodTrackSelections = trackSelections; + hasEnabledTracks = false; - for (int i = 0; i < trackSelections.length; i++) { - TrackSelection selection = trackSelections.get(i); - if (selection != null) { + for (int i = 0; i < sampleStreams.length; i++) { + if (sampleStreams[i] != null) { hasEnabledTracks = true; - int index = newSelections.indexOf(selection); - if (index != -1) { - sampleStreams[i] = newStreams[index]; - } else { - // This selection/stream is unchanged. - } - } else { - sampleStreams[i] = null; + break; } } // The track selection has changed. loadControl.onTrackSelections(renderers, mediaPeriod.getTrackGroups(), trackSelections); + + return streamResetFlags; } public void release() { diff --git a/library/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/src/main/java/com/google/android/exoplayer2/Renderer.java index 0b0a96b8c8..f4f95b2fad 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -130,6 +130,11 @@ public interface Renderer extends ExoPlayerComponent { void replaceStream(Format[] formats, SampleStream stream, long offsetUs) throws ExoPlaybackException; + /** + * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. + */ + SampleStream getStream(); + /** * Returns whether the renderer has read the current {@link SampleStream} to the end. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 7577b41cdf..5cf72e64dc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; -import java.util.List; /** * Provides a single {@link MediaPeriod} whose data is loaded from a {@link Uri} and extracted using @@ -240,32 +239,39 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { Assertions.checkState(prepared); - // Unselect old tracks. - for (int i = 0; i < oldStreams.size(); i++) { - int track = ((SampleStreamImpl) oldStreams.get(i)).track; - Assertions.checkState(trackEnabledStates[track]); - enabledTrackCount--; - trackEnabledStates[track] = false; - sampleQueues[track].disable(); + // Disable old tracks. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + int track = ((SampleStreamImpl) streams[i]).track; + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + sampleQueues[track].disable(); + streams[i] = null; + } } - // Select new tracks. - SampleStream[] newStreams = new SampleStream[newSelections.size()]; - for (int i = 0; i < newStreams.length; i++) { - TrackSelection selection = newSelections.get(i); - Assertions.checkState(selection.length() == 1); - Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); - int track = tracks.indexOf(selection.getTrackGroup()); - Assertions.checkState(!trackEnabledStates[track]); - enabledTrackCount++; - trackEnabledStates[track] = true; - newStreams[i] = new SampleStreamImpl(track); + // Enable new tracks. + boolean selectedNewTracks = false; + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + Assertions.checkState(selection.length() == 1); + Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); + int track = tracks.indexOf(selection.getTrackGroup()); + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + streams[i] = new SampleStreamImpl(track); + streamResetFlags[i] = true; + selectedNewTracks = true; + } } - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. if (!seenFirstTrackSelection) { + // At the time of the first track selection all queues will be enabled, so we need to disable + // any that are no longer required. for (int i = 0; i < sampleQueues.length; i++) { if (!trackEnabledStates[i]) { sampleQueues[i].disable(); @@ -277,11 +283,16 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, if (loader.isLoading()) { loader.cancelLoading(); } - } else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { + } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { seekToUs(positionUs); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < streams.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } } seenFirstTrackSelection = true; - return newStreams; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index f88d74d1dc..87b78ec0d8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; -import java.util.List; /** * A source of a single period of media. @@ -35,7 +34,8 @@ public interface MediaPeriod extends SequenceableLoader { * Called when preparation completes. *

* May be called from any thread. After invoking this method, the {@link MediaPeriod} can expect - * for {@link #selectTracks(List, List, long)} to be called with the initial track selection. + * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be + * called with the initial track selection. * * @param mediaPeriod The prepared {@link MediaPeriod}. */ @@ -89,24 +89,30 @@ public interface MediaPeriod extends SequenceableLoader { TrackGroupArray getTrackGroups(); /** - * Modifies the selected tracks. + * Performs a track selection. *

- * {@link SampleStream}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. + * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} + * indicating whether the existing {@code SampleStream} can be retained for each selection, and + * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the + * provided selections, clearing, setting and replacing entries as required. If an existing sample + * stream is retained but with the requirement that the consuming renderer be reset, then the + * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set + * if a new sample stream is created. *

* This method should only be called after the period has been prepared. * - * @param oldStreams {@link SampleStream}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 selections The renderer track selections. + * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained + * for each selection. A {@code true} value indicates that the selection is unchanged, and + * that the caller does not require that the sample stream be recreated. + * @param streams The existing sample streams, which will be updated to reflect the provided + * selections. + * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that + * have been retained but with the requirement that the consuming renderer be reset. * @param positionUs The current playback position in microseconds. - * @return The {@link SampleStream}s corresponding to each of the newly selected tracks. */ - SampleStream[] selectTracks(List oldStreams, List newSelections, - long positionUs); + void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs); /** * Attempts to read a discontinuity. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 917ce72f87..6e499e9c77 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import java.util.ArrayList; import java.util.IdentityHashMap; -import java.util.List; /** * Merges multiple {@link MediaPeriod} instances. @@ -29,23 +28,20 @@ import java.util.List; public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { private final MediaPeriod[] periods; - private final IdentityHashMap sampleStreamPeriods; - private final int[] selectedTrackCounts; + private final IdentityHashMap streamPeriodIndices; private Callback callback; private int pendingChildPrepareCount; private long durationUs; private TrackGroupArray trackGroups; - private boolean seenFirstTrackSelection; private MediaPeriod[] enabledPeriods; private SequenceableLoader sequenceableLoader; public MergingMediaPeriod(MediaPeriod... periods) { this.periods = periods; pendingChildPrepareCount = periods.length; - sampleStreamPeriods = new IdentityHashMap<>(); - selectedTrackCounts = new int[periods.length]; + streamPeriodIndices = new IdentityHashMap<>(); } @Override @@ -74,29 +70,54 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - SampleStream[] newStreams = new SampleStream[newSelections.size()]; - // Select tracks for each period. - int enabledPeriodCount = 0; - for (int i = 0; i < periods.length; i++) { - selectedTrackCounts[i] += selectTracks(periods[i], oldStreams, newSelections, positionUs, - newStreams, seenFirstTrackSelection); - if (selectedTrackCounts[i] > 0) { - enabledPeriodCount++; + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + // Map each selection and stream onto a child period index. + int[] streamChildIndices = new int[selections.length]; + int[] selectionChildIndices = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + streamChildIndices[i] = streams[i] == null ? -1 : streamPeriodIndices.get(streams[i]); + selectionChildIndices[i] = -1; + if (selections[i] != null) { + TrackGroup trackGroup = selections[i].getTrackGroup(); + for (int j = 0; j < periods.length; j++) { + if (periods[j].getTrackGroups().indexOf(trackGroup) != -1) { + selectionChildIndices[i] = j; + break; + } + } } } - seenFirstTrackSelection = true; - // Update the enabled periods. - enabledPeriods = new MediaPeriod[enabledPeriodCount]; - enabledPeriodCount = 0; + streamPeriodIndices.clear(); + // Select tracks for each child, copying the resulting streams back into the streams array. + SampleStream[] childStreams = new SampleStream[selections.length]; + TrackSelection[] childSelections = new TrackSelection[selections.length]; + ArrayList enabledPeriodsList = new ArrayList<>(periods.length); for (int i = 0; i < periods.length; i++) { - if (selectedTrackCounts[i] > 0) { - enabledPeriods[enabledPeriodCount++] = periods[i]; + for (int j = 0; j < selections.length; j++) { + childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; + childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; + } + periods[i].selectTracks(childSelections, mayRetainStreamFlags, childStreams, streamResetFlags, + positionUs); + boolean periodEnabled = false; + for (int j = 0; j < selections.length; j++) { + if (selectionChildIndices[j] == i) { + streams[j] = childStreams[j]; + if (childStreams[j] != null) { + periodEnabled = true; + streamPeriodIndices.put(childStreams[j], i); + } + } + } + if (periodEnabled) { + enabledPeriodsList.add(periods[i]); } } + // Update the local state. + enabledPeriods = new MediaPeriod[enabledPeriodsList.size()]; + enabledPeriodsList.toArray(enabledPeriods); sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods); - return newStreams; } @Override @@ -199,42 +220,4 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba callback.onContinueLoadingRequested(this); } - // Internal methods. - - private int selectTracks(MediaPeriod period, List allOldStreams, - List allNewSelections, long positionUs, SampleStream[] allNewStreams, - boolean seenFirstTrackSelection) { - // Get the subset of the old streams for the period. - ArrayList oldStreams = new ArrayList<>(); - for (int i = 0; i < allOldStreams.size(); i++) { - SampleStream stream = allOldStreams.get(i); - if (sampleStreamPeriods.get(stream) == period) { - sampleStreamPeriods.remove(stream); - oldStreams.add(stream); - } - } - // Get the subset of the new selections for the period. - ArrayList newSelections = new ArrayList<>(); - int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; - TrackGroupArray periodTrackGroups = period.getTrackGroups(); - for (int i = 0; i < allNewSelections.size(); i++) { - TrackSelection selection = allNewSelections.get(i); - if (periodTrackGroups.indexOf(selection.getTrackGroup()) != -1) { - newSelectionOriginalIndices[newSelections.size()] = i; - newSelections.add(selection); - } - } - // Do nothing if nothing has changed, except during the first selection. - if (seenFirstTrackSelection && oldStreams.isEmpty() && newSelections.isEmpty()) { - return 0; - } - // Perform the selection. - SampleStream[] newStreams = period.selectTracks(oldStreams, newSelections, positionUs); - for (int j = 0; j < newStreams.length; j++) { - allNewStreams[newSelectionOriginalIndices[j]] = newStreams[j]; - sampleStreamPeriods.put(newStreams[j], period); - } - return newSelections.size() - oldStreams.size(); - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 1ce59ac395..2d919abb70 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; /** * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}. @@ -159,19 +158,20 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - for (int i = 0; i < oldStreams.size(); i++) { - SampleStreamImpl oldStream = (SampleStreamImpl) oldStreams.get(i); - sampleStreams.remove(oldStream); + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + sampleStreams.remove(streams[i]); + streams[i] = null; + } + if (streams[i] == null && selections[i] != null) { + SampleStreamImpl stream = new SampleStreamImpl(); + sampleStreams.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; + } } - SampleStream[] newStreams = new SampleStream[newSelections.size()]; - for (int i = 0; i < newStreams.length; i++) { - SampleStreamImpl newStream = new SampleStreamImpl(); - sampleStreams.add(newStream); - newStreams[i] = newStream; - } - return newStreams; } @Override @@ -246,7 +246,7 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, private void notifyLoadError(final IOException e) { if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { + eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadError(eventSourceId, e); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 25f319f674..4f006be26c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -117,33 +118,30 @@ import java.util.List; } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size(); - ChunkSampleStream[] newSampleStreams = - newSampleStreamArray(newEnabledSourceCount); - int newEnabledSourceIndex = 0; - - // Iterate over currently enabled streams, either releasing them or adding them to the new list. - for (ChunkSampleStream sampleStream : sampleStreams) { - if (oldStreams.contains(sampleStream)) { - sampleStream.release(); - } else { - newSampleStreams[newEnabledSourceIndex++] = sampleStream; + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + ArrayList> sampleStreamsList = new ArrayList<>(); + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + if (selections[i] == null || !mayRetainStreamFlags[i]) { + stream.release(); + streams[i] = null; + } else { + sampleStreamsList.add(stream); + } + } + if (streams[i] == null && selections[i] != null) { + ChunkSampleStream stream = buildSampleStream(selections[i], positionUs); + sampleStreamsList.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; } } - - // Instantiate and return new streams. - SampleStream[] streamsToReturn = new SampleStream[newSelections.size()]; - for (int i = 0; i < newSelections.size(); i++) { - newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs); - streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex]; - newEnabledSourceIndex++; - } - - sampleStreams = newSampleStreams; + sampleStreams = newSampleStreamArray(sampleStreamsList.size()); + sampleStreamsList.toArray(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - return streamsToReturn; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 6d6b53a451..0043d8d62f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -63,7 +63,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; - private final IdentityHashMap sampleStreamSources; + private final IdentityHashMap streamWrapperIndices; private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final HlsPlaylistParser manifestParser; @@ -79,10 +79,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private HlsPlaylist playlist; private boolean seenFirstTrackSelection; private long durationUs; - private long pendingDiscontinuityPositionUs; private boolean isLive; private TrackGroupArray trackGroups; - private int[] selectedTrackCounts; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; @@ -100,9 +98,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - - pendingDiscontinuityPositionUs = C.UNSET_TIME_US; - sampleStreamSources = new IdentityHashMap<>(); + streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); manifestParser = new HlsPlaylistParser(); } @@ -175,34 +171,66 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - SampleStream[] newStreams = new SampleStream[newSelections.size()]; - // Select tracks for each wrapper. - int enabledSampleStreamWrapperCount = 0; - for (int i = 0; i < sampleStreamWrappers.length; i++) { - selectedTrackCounts[i] += selectTracks(sampleStreamWrappers[i], oldStreams, newSelections, - newStreams, positionUs); - if (selectedTrackCounts[i] > 0) { - enabledSampleStreamWrapperCount++; + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + // Map each selection and stream onto a child period index. + int[] streamChildIndices = new int[selections.length]; + int[] selectionChildIndices = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + streamChildIndices[i] = streams[i] == null ? -1 : streamWrapperIndices.get(streams[i]); + selectionChildIndices[i] = -1; + if (selections[i] != null) { + TrackGroup trackGroup = selections[i].getTrackGroup(); + for (int j = 0; j < sampleStreamWrappers.length; j++) { + if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != -1) { + selectionChildIndices[i] = j; + break; + } + } } } - // Update the enabled wrappers. - enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperCount]; + boolean selectedNewTracks = false; + streamWrapperIndices.clear(); + // Select tracks for each child, copying the resulting streams back into the streams array. + SampleStream[] childStreams = new SampleStream[selections.length]; + TrackSelection[] childSelections = new TrackSelection[selections.length]; + ArrayList enabledSampleStreamWrapperList = new ArrayList<>( + sampleStreamWrappers.length); + for (int i = 0; i < sampleStreamWrappers.length; i++) { + for (int j = 0; j < selections.length; j++) { + childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; + childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; + } + selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, + mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); + boolean wrapperEnabled = false; + for (int j = 0; j < selections.length; j++) { + if (selectionChildIndices[j] == i) { + streams[j] = childStreams[j]; + if (childStreams[j] != null) { + wrapperEnabled = true; + streamWrapperIndices.put(childStreams[j], i); + } + } + } + if (wrapperEnabled) { + enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); + } + } + // Update the local state. + enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; + enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); - enabledSampleStreamWrapperCount = 0; - for (int i = 0; i < sampleStreamWrappers.length; i++) { - if (selectedTrackCounts[i] > 0) { - enabledSampleStreamWrappers[enabledSampleStreamWrapperCount++] = sampleStreamWrappers[i]; - } - } - if (enabledSampleStreamWrapperCount == 0) { - pendingDiscontinuityPositionUs = C.UNSET_TIME_US; - } else if (seenFirstTrackSelection && !newSelections.isEmpty()) { + if (seenFirstTrackSelection && selectedNewTracks) { seekToUs(positionUs); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } } seenFirstTrackSelection = true; - return newStreams; } @Override @@ -217,9 +245,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, @Override public long readDiscontinuity() { - long result = pendingDiscontinuityPositionUs; - pendingDiscontinuityPositionUs = C.UNSET_TIME_US; - return result; + return C.UNSET_TIME_US; } @Override @@ -247,7 +273,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, @Override public void releasePeriod() { - sampleStreamSources.clear(); + streamWrapperIndices.clear(); timestampAdjusterProvider.reset(); manifestDataSource = null; if (manifestFetcher != null) { @@ -263,7 +289,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, durationUs = 0; isLive = false; trackGroups = null; - selectedTrackCounts = null; if (sampleStreamWrappers != null) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); @@ -285,7 +310,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, List sampleStreamWrapperList = buildSampleStreamWrappers(); sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()]; sampleStreamWrapperList.toArray(sampleStreamWrappers); - selectedTrackCounts = new int[sampleStreamWrappers.length]; pendingPrepareCount = sampleStreamWrappers.length; for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.prepare(); @@ -430,48 +454,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, eventDispatcher); } - private int selectTracks(HlsSampleStreamWrapper sampleStreamWrapper, - List allOldStreams, List allNewSelections, - SampleStream[] allNewStreams, long positionUs) { - // Get the subset of the old streams for the source. - ArrayList oldStreams = new ArrayList<>(); - for (int i = 0; i < allOldStreams.size(); i++) { - SampleStream stream = allOldStreams.get(i); - if (sampleStreamSources.get(stream) == sampleStreamWrapper) { - sampleStreamSources.remove(stream); - oldStreams.add(stream); - } - } - // Get the subset of the new selections for the wrapper. - ArrayList newSelections = new ArrayList<>(); - TrackGroupArray sampleStreamWrapperTrackGroups = sampleStreamWrapper.getTrackGroups(); - int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; - for (int i = 0; i < allNewSelections.size(); i++) { - TrackSelection selection = allNewSelections.get(i); - if (sampleStreamWrapperTrackGroups.indexOf(selection.getTrackGroup()) != -1) { - newSelectionOriginalIndices[newSelections.size()] = i; - newSelections.add(selection); - } - } - // Do nothing if nothing has changed, except during the first selection. - if (seenFirstTrackSelection && oldStreams.isEmpty() && newSelections.isEmpty()) { - return 0; - } - // If there are other active SampleStreams provided by the wrapper then we need to report a - // discontinuity so that the consuming renderers are reset. - if (sampleStreamWrapper.getEnabledTrackCount() > oldStreams.size()) { - pendingDiscontinuityPositionUs = positionUs; - } - // Perform the selection. - SampleStream[] newStreams = sampleStreamWrapper.selectTracks(oldStreams, newSelections, - !seenFirstTrackSelection); - for (int j = 0; j < newStreams.length; j++) { - allNewStreams[newSelectionOriginalIndices[j]] = newStreams[j]; - sampleStreamSources.put(newStreams[j], sampleStreamWrapper); - } - return newSelections.size() - oldStreams.size(); - } - private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { String codecs = variant.codecs; if (TextUtils.isEmpty(codecs)) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index f16cf7891e..e672b88c18 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.LinkedList; -import java.util.List; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -141,10 +140,6 @@ import java.util.List; return chunkSource.getDurationUs(); } - public int getEnabledTrackCount() { - return enabledTrackCount; - } - public boolean isLive() { return chunkSource.isLive(); } @@ -153,29 +148,36 @@ import java.util.List; return trackGroups; } - public SampleStream[] selectTracks(List oldStreams, - List newSelections, boolean isFirstTrackSelection) { + public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) { Assertions.checkState(prepared); - // Unselect old tracks. - for (int i = 0; i < oldStreams.size(); i++) { - int group = ((SampleStreamImpl) oldStreams.get(i)).group; - setTrackGroupEnabledState(group, false); - sampleQueues.valueAt(group).disable(); - } - // Select new tracks. - SampleStream[] newStreams = new SampleStream[newSelections.size()]; - for (int i = 0; i < newStreams.length; i++) { - TrackSelection selection = newSelections.get(i); - int group = trackGroups.indexOf(selection.getTrackGroup()); - setTrackGroupEnabledState(group, true); - if (group == primaryTrackGroupIndex) { - chunkSource.selectTracks(selection); + // Disable old tracks. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + int group = ((SampleStreamImpl) streams[i]).group; + setTrackGroupEnabledState(group, false); + sampleQueues.valueAt(group).disable(); + streams[i] = null; + } + } + // Enable new tracks. + boolean selectedNewTracks = false; + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + int group = trackGroups.indexOf(selection.getTrackGroup()); + setTrackGroupEnabledState(group, true); + if (group == primaryTrackGroupIndex) { + chunkSource.selectTracks(selection); + } + streams[i] = new SampleStreamImpl(group); + streamResetFlags[i] = true; + selectedNewTracks = true; } - newStreams[i] = new SampleStreamImpl(group); } - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. if (isFirstTrackSelection) { + // At the time of the first track selection all queues will be enabled, so we need to disable + // any that are no longer required. int sampleQueueCount = sampleQueues.size(); for (int i = 0; i < sampleQueueCount; i++) { if (!groupEnabledStates[i]) { @@ -192,7 +194,7 @@ import java.util.List; loader.cancelLoading(); } } - return newStreams; + return selectedNewTracks; } public void seekTo(long positionUs) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 4a9656ce0d..98e2ca9f37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -32,7 +32,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import java.io.IOException; -import java.util.List; +import java.util.ArrayList; /** * A SmoothStreaming {@link MediaPeriod}. @@ -109,33 +109,30 @@ import java.util.List; } @Override - public SampleStream[] selectTracks(List oldStreams, - List newSelections, long positionUs) { - int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size(); - ChunkSampleStream[] newSampleStreams = - newSampleStreamArray(newEnabledSourceCount); - int newEnabledSourceIndex = 0; - - // Iterate over currently enabled streams, either releasing them or adding them to the new list. - for (ChunkSampleStream sampleStream : sampleStreams) { - if (oldStreams.contains(sampleStream)) { - sampleStream.release(); - } else { - newSampleStreams[newEnabledSourceIndex++] = sampleStream; + public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + ArrayList> sampleStreamsList = new ArrayList<>(); + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = (ChunkSampleStream) streams[i]; + if (selections[i] == null || !mayRetainStreamFlags[i]) { + stream.release(); + streams[i] = null; + } else { + sampleStreamsList.add(stream); + } + } + if (streams[i] == null && selections[i] != null) { + ChunkSampleStream stream = buildSampleStream(selections[i], positionUs); + sampleStreamsList.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; } } - - // Instantiate and return new streams. - SampleStream[] streamsToReturn = new SampleStream[newSelections.size()]; - for (int i = 0; i < newSelections.size(); i++) { - newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs); - streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex]; - newEnabledSourceIndex++; - } - - sampleStreams = newSampleStreams; + sampleStreams = newSampleStreamArray(sampleStreamsList.size()); + sampleStreamsList.toArray(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - return streamsToReturn; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 20915f0e08..4d4b4c614e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -50,6 +50,13 @@ public final class TrackSelectionArray { return trackSelections[index]; } + /** + * Returns the selections in a newly allocated array. + */ + public TrackSelection[] getAll() { + return trackSelections.clone(); + } + @Override public int hashCode() { if (hashCode == 0) {