Commit 153c0aef by olly Committed by Oliver Woodman

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
parent 9092c566
...@@ -97,6 +97,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -97,6 +97,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
@Override @Override
public final SampleStream getStream() {
return stream;
}
@Override
public final boolean hasReadStreamToEnd() { public final boolean hasReadStreamToEnd() {
return readEndOfStream; return readEndOfStream;
} }
......
...@@ -37,7 +37,6 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; ...@@ -37,7 +37,6 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
/** /**
* Implements the internal behavior of {@link ExoPlayerImpl}. * Implements the internal behavior of {@link ExoPlayerImpl}.
...@@ -728,26 +727,24 @@ import java.util.ArrayList; ...@@ -728,26 +727,24 @@ import java.util.ArrayList;
// Update streams for the new selection, recreating all streams if reading ahead. // Update streams for the new selection, recreating all streams if reading ahead.
boolean recreateStreams = readingPeriod != playingPeriod; boolean recreateStreams = readingPeriod != playingPeriod;
TrackSelectionArray playingPeriodOldTrackSelections = playingPeriod.periodTrackSelections; boolean[] streamResetFlags = playingPeriod.updatePeriodTrackSelection(playbackInfo.positionUs,
playingPeriod.updatePeriodTrackSelection(playbackInfo.positionUs, loadControl, loadControl, recreateStreams);
recreateStreams);
int enabledRendererCount = 0; int enabledRendererCount = 0;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i]; Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
TrackSelection oldSelection = playingPeriodOldTrackSelections.get(i); SampleStream sampleStream = playingPeriod.sampleStreams[i];
TrackSelection newSelection = playingPeriod.trackSelections.get(i); if (sampleStream != null) {
if (newSelection != null) {
enabledRendererCount++; enabledRendererCount++;
} }
if (rendererWasEnabledFlags[i] if (rendererWasEnabledFlags[i]) {
&& (recreateStreams || !Util.areEqual(oldSelection, newSelection))) { if (sampleStream != renderer.getStream()) {
// We need to disable the renderer so that we can enable it with its new stream. // We need to disable the renderer.
if (renderer == rendererMediaClockSource) { if (renderer == rendererMediaClockSource) {
// The renderer is providing the media clock. // The renderer is providing the media clock.
if (newSelection == null) { if (sampleStream == null) {
// The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
// over timing responsibilities. // over timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
...@@ -757,6 +754,10 @@ import java.util.ArrayList; ...@@ -757,6 +754,10 @@ import java.util.ArrayList;
} }
ensureStopped(renderer); ensureStopped(renderer);
renderer.disable(); 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);
}
} }
} }
trackSelector.onSelectionActivated(playingPeriod.trackSelectionData); trackSelector.onSelectionActivated(playingPeriod.trackSelectionData);
...@@ -1155,9 +1156,11 @@ import java.util.ArrayList; ...@@ -1155,9 +1156,11 @@ import java.util.ArrayList;
public final MediaPeriod mediaPeriod; public final MediaPeriod mediaPeriod;
public final Object id; public final Object id;
public final SampleStream[] sampleStreams;
public final long startPositionUs; public final long startPositionUs;
public final SampleStream[] sampleStreams;
public final boolean[] mayRetainStreamFlags;
public int index; public int index;
public boolean isLast; public boolean isLast;
public boolean prepared; public boolean prepared;
...@@ -1183,6 +1186,7 @@ import java.util.ArrayList; ...@@ -1183,6 +1186,7 @@ import java.util.ArrayList;
this.mediaPeriod = mediaPeriod; this.mediaPeriod = mediaPeriod;
this.id = Assertions.checkNotNull(id); this.id = Assertions.checkNotNull(id);
sampleStreams = new SampleStream[renderers.length]; sampleStreams = new SampleStream[renderers.length];
mayRetainStreamFlags = new boolean[renderers.length];
startPositionUs = positionUs; startPositionUs = positionUs;
this.index = index; this.index = index;
} }
...@@ -1216,46 +1220,33 @@ import java.util.ArrayList; ...@@ -1216,46 +1220,33 @@ import java.util.ArrayList;
return true; return true;
} }
public void updatePeriodTrackSelection(long positionUs, LoadControl loadControl, public boolean[] updatePeriodTrackSelection(long positionUs, LoadControl loadControl,
boolean forceRecreateStreams) throws ExoPlaybackException { boolean forceRecreateStreams) throws ExoPlaybackException {
// Populate lists of streams that are being disabled/newly enabled.
ArrayList<SampleStream> oldStreams = new ArrayList<>();
ArrayList<TrackSelection> newSelections = new ArrayList<>();
for (int i = 0; i < trackSelections.length; i++) { for (int i = 0; i < trackSelections.length; i++) {
TrackSelection oldSelection = mayRetainStreamFlags[i] = !forceRecreateStreams
periodTrackSelections == null ? null : periodTrackSelections.get(i); && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i),
TrackSelection newSelection = trackSelections.get(i); trackSelections.get(i));
if (forceRecreateStreams || !Util.areEqual(oldSelection, newSelection)) {
if (oldSelection != null) {
oldStreams.add(sampleStreams[i]);
}
if (newSelection != null) {
newSelections.add(newSelection);
}
}
} }
boolean[] streamResetFlags = new boolean[renderers.length];
// Disable streams on the period and get new streams for updated/newly-enabled tracks. // 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; periodTrackSelections = trackSelections;
hasEnabledTracks = false; hasEnabledTracks = false;
for (int i = 0; i < trackSelections.length; i++) { for (int i = 0; i < sampleStreams.length; i++) {
TrackSelection selection = trackSelections.get(i); if (sampleStreams[i] != null) {
if (selection != null) {
hasEnabledTracks = true; hasEnabledTracks = true;
int index = newSelections.indexOf(selection); break;
if (index != -1) {
sampleStreams[i] = newStreams[index];
} else {
// This selection/stream is unchanged.
}
} else {
sampleStreams[i] = null;
} }
} }
// The track selection has changed. // The track selection has changed.
loadControl.onTrackSelections(renderers, mediaPeriod.getTrackGroups(), trackSelections); loadControl.onTrackSelections(renderers, mediaPeriod.getTrackGroups(), trackSelections);
return streamResetFlags;
} }
public void release() { public void release() {
......
...@@ -131,6 +131,11 @@ public interface Renderer extends ExoPlayerComponent { ...@@ -131,6 +131,11 @@ public interface Renderer extends ExoPlayerComponent {
throws ExoPlaybackException; 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. * Returns whether the renderer has read the current {@link SampleStream} to the end.
* <p> * <p>
* This method may be called when the renderer is in the following states: * This method may be called when the renderer is in the following states:
......
...@@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util; ...@@ -45,7 +45,6 @@ import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
/** /**
* Provides a single {@link MediaPeriod} whose data is loaded from a {@link Uri} and extracted using * 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, ...@@ -240,32 +239,39 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
// Unselect old tracks. // Disable old tracks.
for (int i = 0; i < oldStreams.size(); i++) { for (int i = 0; i < selections.length; i++) {
int track = ((SampleStreamImpl) oldStreams.get(i)).track; if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
int track = ((SampleStreamImpl) streams[i]).track;
Assertions.checkState(trackEnabledStates[track]); Assertions.checkState(trackEnabledStates[track]);
enabledTrackCount--; enabledTrackCount--;
trackEnabledStates[track] = false; trackEnabledStates[track] = false;
sampleQueues[track].disable(); sampleQueues[track].disable();
streams[i] = null;
} }
// Select new tracks. }
SampleStream[] newStreams = new SampleStream[newSelections.size()]; // Enable new tracks.
for (int i = 0; i < newStreams.length; i++) { boolean selectedNewTracks = false;
TrackSelection selection = newSelections.get(i); 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.length() == 1);
Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
int track = tracks.indexOf(selection.getTrackGroup()); int track = tracks.indexOf(selection.getTrackGroup());
Assertions.checkState(!trackEnabledStates[track]); Assertions.checkState(!trackEnabledStates[track]);
enabledTrackCount++; enabledTrackCount++;
trackEnabledStates[track] = true; trackEnabledStates[track] = true;
newStreams[i] = new SampleStreamImpl(track); streams[i] = new SampleStreamImpl(track);
streamResetFlags[i] = true;
selectedNewTracks = true;
}
} }
if (!seenFirstTrackSelection) {
// At the time of the first track selection all queues will be enabled, so we need to disable // At the time of the first track selection all queues will be enabled, so we need to disable
// any that are no longer required. // any that are no longer required.
if (!seenFirstTrackSelection) {
for (int i = 0; i < sampleQueues.length; i++) { for (int i = 0; i < sampleQueues.length; i++) {
if (!trackEnabledStates[i]) { if (!trackEnabledStates[i]) {
sampleQueues[i].disable(); sampleQueues[i].disable();
...@@ -277,11 +283,16 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, ...@@ -277,11 +283,16 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource,
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} }
} else if (seenFirstTrackSelection ? newStreams.length > 0 : positionUs != 0) { } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) {
seekToUs(positionUs); 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; seenFirstTrackSelection = true;
return newStreams;
} }
@Override @Override
......
...@@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; ...@@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException; import java.io.IOException;
import java.util.List;
/** /**
* A source of a single period of media. * A source of a single period of media.
...@@ -35,7 +34,8 @@ public interface MediaPeriod extends SequenceableLoader { ...@@ -35,7 +34,8 @@ public interface MediaPeriod extends SequenceableLoader {
* Called when preparation completes. * Called when preparation completes.
* <p> * <p>
* May be called from any thread. After invoking this method, the {@link MediaPeriod} can expect * 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}. * @param mediaPeriod The prepared {@link MediaPeriod}.
*/ */
...@@ -89,24 +89,30 @@ public interface MediaPeriod extends SequenceableLoader { ...@@ -89,24 +89,30 @@ public interface MediaPeriod extends SequenceableLoader {
TrackGroupArray getTrackGroups(); TrackGroupArray getTrackGroups();
/** /**
* Modifies the selected tracks. * Performs a track selection.
* <p> * <p>
* {@link SampleStream}s corresponding to tracks being unselected are passed in * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* {@code oldStreams}. Tracks being selected are specified in {@code newSelections}. Each new * indicating whether the existing {@code SampleStream} can be retained for each selection, and
* {@link TrackSelection} must have a {@link TrackSelection#group} index distinct from those of * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* currently enabled tracks, except for those being unselected. * 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.
* <p> * <p>
* This method should only be called after the period has been prepared. * 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 * @param selections The renderer track selections.
* but must not be null. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
* @param newSelections {@link TrackSelection}s that define tracks being selected. May be empty * for each selection. A {@code true} value indicates that the selection is unchanged, and
* but must not be null. * 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. * @param positionUs The current playback position in microseconds.
* @return The {@link SampleStream}s corresponding to each of the newly selected tracks.
*/ */
SampleStream[] selectTracks(List<SampleStream> oldStreams, List<TrackSelection> newSelections, void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
long positionUs); SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
/** /**
* Attempts to read a discontinuity. * Attempts to read a discontinuity.
......
...@@ -21,7 +21,6 @@ import com.google.android.exoplayer2.upstream.Allocator; ...@@ -21,7 +21,6 @@ import com.google.android.exoplayer2.upstream.Allocator;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List;
/** /**
* Merges multiple {@link MediaPeriod} instances. * Merges multiple {@link MediaPeriod} instances.
...@@ -29,23 +28,20 @@ import java.util.List; ...@@ -29,23 +28,20 @@ import java.util.List;
public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
private final MediaPeriod[] periods; private final MediaPeriod[] periods;
private final IdentityHashMap<SampleStream, MediaPeriod> sampleStreamPeriods; private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
private final int[] selectedTrackCounts;
private Callback callback; private Callback callback;
private int pendingChildPrepareCount; private int pendingChildPrepareCount;
private long durationUs; private long durationUs;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private boolean seenFirstTrackSelection;
private MediaPeriod[] enabledPeriods; private MediaPeriod[] enabledPeriods;
private SequenceableLoader sequenceableLoader; private SequenceableLoader sequenceableLoader;
public MergingMediaPeriod(MediaPeriod... periods) { public MergingMediaPeriod(MediaPeriod... periods) {
this.periods = periods; this.periods = periods;
pendingChildPrepareCount = periods.length; pendingChildPrepareCount = periods.length;
sampleStreamPeriods = new IdentityHashMap<>(); streamPeriodIndices = new IdentityHashMap<>();
selectedTrackCounts = new int[periods.length];
} }
@Override @Override
...@@ -74,29 +70,54 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba ...@@ -74,29 +70,54 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
SampleStream[] newStreams = new SampleStream[newSelections.size()]; // Map each selection and stream onto a child period index.
// Select tracks for each period. int[] streamChildIndices = new int[selections.length];
int enabledPeriodCount = 0; 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;
}
}
}
}
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<MediaPeriod> enabledPeriodsList = new ArrayList<>(periods.length);
for (int i = 0; i < periods.length; i++) { for (int i = 0; i < periods.length; i++) {
selectedTrackCounts[i] += selectTracks(periods[i], oldStreams, newSelections, positionUs, for (int j = 0; j < selections.length; j++) {
newStreams, seenFirstTrackSelection); childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
if (selectedTrackCounts[i] > 0) { childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
enabledPeriodCount++; }
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);
} }
} }
seenFirstTrackSelection = true;
// Update the enabled periods.
enabledPeriods = new MediaPeriod[enabledPeriodCount];
enabledPeriodCount = 0;
for (int i = 0; i < periods.length; i++) {
if (selectedTrackCounts[i] > 0) {
enabledPeriods[enabledPeriodCount++] = periods[i];
} }
if (periodEnabled) {
enabledPeriodsList.add(periods[i]);
} }
}
// Update the local state.
enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
enabledPeriodsList.toArray(enabledPeriods);
sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods); sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods);
return newStreams;
} }
@Override @Override
...@@ -199,42 +220,4 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba ...@@ -199,42 +220,4 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
} }
// Internal methods.
private int selectTracks(MediaPeriod period, List<SampleStream> allOldStreams,
List<TrackSelection> allNewSelections, long positionUs, SampleStream[] allNewStreams,
boolean seenFirstTrackSelection) {
// Get the subset of the old streams for the period.
ArrayList<SampleStream> 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<TrackSelection> 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();
}
} }
...@@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; 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}. * 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, ...@@ -159,19 +158,20 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource,
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
for (int i = 0; i < oldStreams.size(); i++) { for (int i = 0; i < selections.length; i++) {
SampleStreamImpl oldStream = (SampleStreamImpl) oldStreams.get(i); if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
sampleStreams.remove(oldStream); sampleStreams.remove(streams[i]);
} streams[i] = null;
SampleStream[] newStreams = new SampleStream[newSelections.size()]; }
for (int i = 0; i < newStreams.length; i++) { if (streams[i] == null && selections[i] != null) {
SampleStreamImpl newStream = new SampleStreamImpl(); SampleStreamImpl stream = new SampleStreamImpl();
sampleStreams.add(newStream); sampleStreams.add(stream);
newStreams[i] = newStream; streams[i] = stream;
} streamResetFlags[i] = true;
return newStreams; }
}
} }
@Override @Override
......
...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; ...@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
...@@ -117,33 +118,30 @@ import java.util.List; ...@@ -117,33 +118,30 @@ import java.util.List;
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size(); ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamsList = new ArrayList<>();
ChunkSampleStream<DashChunkSource>[] newSampleStreams = for (int i = 0; i < selections.length; i++) {
newSampleStreamArray(newEnabledSourceCount); if (streams[i] != null) {
int newEnabledSourceIndex = 0; @SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
// Iterate over currently enabled streams, either releasing them or adding them to the new list. if (selections[i] == null || !mayRetainStreamFlags[i]) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) { stream.release();
if (oldStreams.contains(sampleStream)) { streams[i] = null;
sampleStream.release();
} else { } else {
newSampleStreams[newEnabledSourceIndex++] = sampleStream; sampleStreamsList.add(stream);
} }
} }
if (streams[i] == null && selections[i] != null) {
// Instantiate and return new streams. ChunkSampleStream<DashChunkSource> stream = buildSampleStream(selections[i], positionUs);
SampleStream[] streamsToReturn = new SampleStream[newSelections.size()]; sampleStreamsList.add(stream);
for (int i = 0; i < newSelections.size(); i++) { streams[i] = stream;
newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs); streamResetFlags[i] = true;
streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex];
newEnabledSourceIndex++;
} }
}
sampleStreams = newSampleStreams; sampleStreams = newSampleStreamArray(sampleStreamsList.size());
sampleStreamsList.toArray(sampleStreams);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
return streamsToReturn;
} }
@Override @Override
......
...@@ -63,7 +63,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -63,7 +63,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
private final DataSource.Factory dataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final int minLoadableRetryCount; private final int minLoadableRetryCount;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final IdentityHashMap<SampleStream, HlsSampleStreamWrapper> sampleStreamSources; private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final PtsTimestampAdjusterProvider timestampAdjusterProvider;
private final HlsPlaylistParser manifestParser; private final HlsPlaylistParser manifestParser;
...@@ -79,10 +79,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -79,10 +79,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
private HlsPlaylist playlist; private HlsPlaylist playlist;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private long durationUs; private long durationUs;
private long pendingDiscontinuityPositionUs;
private boolean isLive; private boolean isLive;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private int[] selectedTrackCounts;
private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] sampleStreamWrappers;
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
private CompositeSequenceableLoader sequenceableLoader; private CompositeSequenceableLoader sequenceableLoader;
...@@ -100,9 +98,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -100,9 +98,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
streamWrapperIndices = new IdentityHashMap<>();
pendingDiscontinuityPositionUs = C.UNSET_TIME_US;
sampleStreamSources = new IdentityHashMap<>();
timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
manifestParser = new HlsPlaylistParser(); manifestParser = new HlsPlaylistParser();
} }
...@@ -175,34 +171,66 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -175,34 +171,66 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
SampleStream[] newStreams = new SampleStream[newSelections.size()]; // Map each selection and stream onto a child period index.
// Select tracks for each wrapper. int[] streamChildIndices = new int[selections.length];
int enabledSampleStreamWrapperCount = 0; 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;
}
}
}
}
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<HlsSampleStreamWrapper> enabledSampleStreamWrapperList = new ArrayList<>(
sampleStreamWrappers.length);
for (int i = 0; i < sampleStreamWrappers.length; i++) { for (int i = 0; i < sampleStreamWrappers.length; i++) {
selectedTrackCounts[i] += selectTracks(sampleStreamWrappers[i], oldStreams, newSelections, for (int j = 0; j < selections.length; j++) {
newStreams, positionUs); childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
if (selectedTrackCounts[i] > 0) { childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
enabledSampleStreamWrapperCount++;
} }
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);
} }
// Update the enabled wrappers.
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperCount];
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) { if (wrapperEnabled) {
pendingDiscontinuityPositionUs = C.UNSET_TIME_US; enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]);
} else if (seenFirstTrackSelection && !newSelections.isEmpty()) { }
}
// Update the local state.
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()];
enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers);
sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers);
if (seenFirstTrackSelection && selectedNewTracks) {
seekToUs(positionUs); 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; seenFirstTrackSelection = true;
return newStreams;
} }
@Override @Override
...@@ -217,9 +245,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -217,9 +245,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
@Override @Override
public long readDiscontinuity() { public long readDiscontinuity() {
long result = pendingDiscontinuityPositionUs; return C.UNSET_TIME_US;
pendingDiscontinuityPositionUs = C.UNSET_TIME_US;
return result;
} }
@Override @Override
...@@ -247,7 +273,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -247,7 +273,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
@Override @Override
public void releasePeriod() { public void releasePeriod() {
sampleStreamSources.clear(); streamWrapperIndices.clear();
timestampAdjusterProvider.reset(); timestampAdjusterProvider.reset();
manifestDataSource = null; manifestDataSource = null;
if (manifestFetcher != null) { if (manifestFetcher != null) {
...@@ -263,7 +289,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -263,7 +289,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
durationUs = 0; durationUs = 0;
isLive = false; isLive = false;
trackGroups = null; trackGroups = null;
selectedTrackCounts = null;
if (sampleStreamWrappers != null) { if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release(); sampleStreamWrapper.release();
...@@ -285,7 +310,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -285,7 +310,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers(); List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers();
sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()]; sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()];
sampleStreamWrapperList.toArray(sampleStreamWrappers); sampleStreamWrapperList.toArray(sampleStreamWrappers);
selectedTrackCounts = new int[sampleStreamWrappers.length];
pendingPrepareCount = sampleStreamWrappers.length; pendingPrepareCount = sampleStreamWrappers.length;
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.prepare(); sampleStreamWrapper.prepare();
...@@ -430,48 +454,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, ...@@ -430,48 +454,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource,
eventDispatcher); eventDispatcher);
} }
private int selectTracks(HlsSampleStreamWrapper sampleStreamWrapper,
List<SampleStream> allOldStreams, List<TrackSelection> allNewSelections,
SampleStream[] allNewStreams, long positionUs) {
// Get the subset of the old streams for the source.
ArrayList<SampleStream> 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<TrackSelection> 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) { private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
String codecs = variant.codecs; String codecs = variant.codecs;
if (TextUtils.isEmpty(codecs)) { if (TextUtils.isEmpty(codecs)) {
......
...@@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
/** /**
* Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides
...@@ -141,10 +140,6 @@ import java.util.List; ...@@ -141,10 +140,6 @@ import java.util.List;
return chunkSource.getDurationUs(); return chunkSource.getDurationUs();
} }
public int getEnabledTrackCount() {
return enabledTrackCount;
}
public boolean isLive() { public boolean isLive() {
return chunkSource.isLive(); return chunkSource.isLive();
} }
...@@ -153,29 +148,36 @@ import java.util.List; ...@@ -153,29 +148,36 @@ import java.util.List;
return trackGroups; return trackGroups;
} }
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, boolean isFirstTrackSelection) { SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
// Unselect old tracks. // Disable old tracks.
for (int i = 0; i < oldStreams.size(); i++) { for (int i = 0; i < selections.length; i++) {
int group = ((SampleStreamImpl) oldStreams.get(i)).group; if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
int group = ((SampleStreamImpl) streams[i]).group;
setTrackGroupEnabledState(group, false); setTrackGroupEnabledState(group, false);
sampleQueues.valueAt(group).disable(); sampleQueues.valueAt(group).disable();
streams[i] = null;
}
} }
// Select new tracks. // Enable new tracks.
SampleStream[] newStreams = new SampleStream[newSelections.size()]; boolean selectedNewTracks = false;
for (int i = 0; i < newStreams.length; i++) { for (int i = 0; i < selections.length; i++) {
TrackSelection selection = newSelections.get(i); if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i];
int group = trackGroups.indexOf(selection.getTrackGroup()); int group = trackGroups.indexOf(selection.getTrackGroup());
setTrackGroupEnabledState(group, true); setTrackGroupEnabledState(group, true);
if (group == primaryTrackGroupIndex) { if (group == primaryTrackGroupIndex) {
chunkSource.selectTracks(selection); chunkSource.selectTracks(selection);
} }
newStreams[i] = new SampleStreamImpl(group); streams[i] = new SampleStreamImpl(group);
streamResetFlags[i] = true;
selectedNewTracks = true;
} }
}
if (isFirstTrackSelection) {
// At the time of the first track selection all queues will be enabled, so we need to disable // At the time of the first track selection all queues will be enabled, so we need to disable
// any that are no longer required. // any that are no longer required.
if (isFirstTrackSelection) {
int sampleQueueCount = sampleQueues.size(); int sampleQueueCount = sampleQueues.size();
for (int i = 0; i < sampleQueueCount; i++) { for (int i = 0; i < sampleQueueCount; i++) {
if (!groupEnabledStates[i]) { if (!groupEnabledStates[i]) {
...@@ -192,7 +194,7 @@ import java.util.List; ...@@ -192,7 +194,7 @@ import java.util.List;
loader.cancelLoading(); loader.cancelLoading();
} }
} }
return newStreams; return selectedNewTracks;
} }
public void seekTo(long positionUs) { public void seekTo(long positionUs) {
......
...@@ -32,7 +32,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; ...@@ -32,7 +32,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.ArrayList;
/** /**
* A SmoothStreaming {@link MediaPeriod}. * A SmoothStreaming {@link MediaPeriod}.
...@@ -109,33 +109,30 @@ import java.util.List; ...@@ -109,33 +109,30 @@ import java.util.List;
} }
@Override @Override
public SampleStream[] selectTracks(List<SampleStream> oldStreams, public void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
List<TrackSelection> newSelections, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size(); ArrayList<ChunkSampleStream<SsChunkSource>> sampleStreamsList = new ArrayList<>();
ChunkSampleStream<SsChunkSource>[] newSampleStreams = for (int i = 0; i < selections.length; i++) {
newSampleStreamArray(newEnabledSourceCount); if (streams[i] != null) {
int newEnabledSourceIndex = 0; @SuppressWarnings("unchecked")
ChunkSampleStream<SsChunkSource> stream = (ChunkSampleStream<SsChunkSource>) streams[i];
// Iterate over currently enabled streams, either releasing them or adding them to the new list. if (selections[i] == null || !mayRetainStreamFlags[i]) {
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) { stream.release();
if (oldStreams.contains(sampleStream)) { streams[i] = null;
sampleStream.release();
} else { } else {
newSampleStreams[newEnabledSourceIndex++] = sampleStream; sampleStreamsList.add(stream);
} }
} }
if (streams[i] == null && selections[i] != null) {
// Instantiate and return new streams. ChunkSampleStream<SsChunkSource> stream = buildSampleStream(selections[i], positionUs);
SampleStream[] streamsToReturn = new SampleStream[newSelections.size()]; sampleStreamsList.add(stream);
for (int i = 0; i < newSelections.size(); i++) { streams[i] = stream;
newSampleStreams[newEnabledSourceIndex] = buildSampleStream(newSelections.get(i), positionUs); streamResetFlags[i] = true;
streamsToReturn[i] = newSampleStreams[newEnabledSourceIndex];
newEnabledSourceIndex++;
} }
}
sampleStreams = newSampleStreams; sampleStreams = newSampleStreamArray(sampleStreamsList.size());
sampleStreamsList.toArray(sampleStreams);
sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
return streamsToReturn;
} }
@Override @Override
......
...@@ -50,6 +50,13 @@ public final class TrackSelectionArray { ...@@ -50,6 +50,13 @@ public final class TrackSelectionArray {
return trackSelections[index]; return trackSelections[index];
} }
/**
* Returns the selections in a newly allocated array.
*/
public TrackSelection[] getAll() {
return trackSelections.clone();
}
@Override @Override
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment