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
This commit is contained in:
olly 2016-08-11 04:10:10 -07:00 committed by Oliver Woodman
parent 9092c5665b
commit 153c0aef2b
12 changed files with 299 additions and 312 deletions

View file

@ -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;

View file

@ -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<SampleStream> oldStreams = new ArrayList<>();
ArrayList<TrackSelection> 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() {

View file

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

View file

@ -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<SampleStream> oldStreams,
List<TrackSelection> 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

View file

@ -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.
* <p>
* 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.
* <p>
* {@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.
* <p>
* 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<SampleStream> oldStreams, List<TrackSelection> newSelections,
long positionUs);
void selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
/**
* Attempts to read a discontinuity.

View file

@ -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<SampleStream, MediaPeriod> sampleStreamPeriods;
private final int[] selectedTrackCounts;
private final IdentityHashMap<SampleStream, Integer> 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<SampleStream> oldStreams,
List<TrackSelection> 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<MediaPeriod> 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<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();
}
}

View file

@ -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<SampleStream> oldStreams,
List<TrackSelection> 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);

View file

@ -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<SampleStream> oldStreams,
List<TrackSelection> newSelections, long positionUs) {
int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size();
ChunkSampleStream<DashChunkSource>[] newSampleStreams =
newSampleStreamArray(newEnabledSourceCount);
int newEnabledSourceIndex = 0;
// Iterate over currently enabled streams, either releasing them or adding them to the new list.
for (ChunkSampleStream<DashChunkSource> 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<ChunkSampleStream<DashChunkSource>> sampleStreamsList = new ArrayList<>();
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null) {
@SuppressWarnings("unchecked")
ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) 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<DashChunkSource> 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

View file

@ -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<SampleStream, HlsSampleStreamWrapper> sampleStreamSources;
private final IdentityHashMap<SampleStream, Integer> 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<SampleStream> oldStreams,
List<TrackSelection> 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<HlsSampleStreamWrapper> 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<HlsSampleStreamWrapper> 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<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) {
String codecs = variant.codecs;
if (TextUtils.isEmpty(codecs)) {

View file

@ -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<SampleStream> oldStreams,
List<TrackSelection> 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) {

View file

@ -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<SampleStream> oldStreams,
List<TrackSelection> newSelections, long positionUs) {
int newEnabledSourceCount = sampleStreams.length + newSelections.size() - oldStreams.size();
ChunkSampleStream<SsChunkSource>[] newSampleStreams =
newSampleStreamArray(newEnabledSourceCount);
int newEnabledSourceIndex = 0;
// Iterate over currently enabled streams, either releasing them or adding them to the new list.
for (ChunkSampleStream<SsChunkSource> 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<ChunkSampleStream<SsChunkSource>> sampleStreamsList = new ArrayList<>();
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null) {
@SuppressWarnings("unchecked")
ChunkSampleStream<SsChunkSource> stream = (ChunkSampleStream<SsChunkSource>) 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<SsChunkSource> 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

View file

@ -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) {