From 153c0aef2b01312c531835870cdd2a5f74a0b29d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 11 Aug 2016 04:10:10 -0700 Subject: [PATCH] Rework MediaPeriod track selection This change allows MediaPeriod instances to replace SampleStream instances when the selection isn't changing. It also allows MediaPeriod instances to retain a SampleStream but indicate that the renderer consuming from it needs to be reset. The change is used to fix the ref'd bug, and is used to do the same thing in HLS without the need for the source to report a discontinuity. Note that reporting discontinuity could cause unnecessary failure when used as a child of MergingMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=129971782 --- .../android/exoplayer2/BaseRenderer.java | 5 + .../exoplayer2/ExoPlayerImplInternal.java | 87 +++++------- .../google/android/exoplayer2/Renderer.java | 5 + .../source/ExtractorMediaSource.java | 61 ++++---- .../exoplayer2/source/MediaPeriod.java | 34 +++-- .../exoplayer2/source/MergingMediaPeriod.java | 105 ++++++-------- .../source/SingleSampleMediaSource.java | 28 ++-- .../source/dash/DashMediaPeriod.java | 46 +++--- .../exoplayer2/source/hls/HlsMediaSource.java | 134 ++++++++---------- .../source/hls/HlsSampleStreamWrapper.java | 52 +++---- .../source/smoothstreaming/SsMediaPeriod.java | 47 +++--- .../trackselection/TrackSelectionArray.java | 7 + 12 files changed, 299 insertions(+), 312 deletions(-) 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) {