Optimize in-buffer seeking for HLS

Also move to using an array to hold the SampleQueues,
as we've moved to doing in ExtractorMediaPeriod.

Issue: #551

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=161972990
This commit is contained in:
olly 2017-07-14 10:24:30 -07:00 committed by Oliver Woodman
parent a2ffcec200
commit bf5495f2f5
3 changed files with 197 additions and 145 deletions

View File

@ -320,10 +320,11 @@ import java.util.Arrays;
positionUs = seekMap.isSeekable() ? positionUs : 0; positionUs = seekMap.isSeekable() ? positionUs : 0;
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
notifyDiscontinuity = false; notifyDiscontinuity = false;
// If we're not pending a reset, see if we can seek within the sample queues. // If we're not pending a reset, see if we can seek within the buffer.
boolean seekInsideBuffer = !isPendingReset() && seekInsideBufferUs(positionUs); if (!isPendingReset() && seekInsideBufferUs(positionUs)) {
// If we failed to seek within the sample queues, we need to restart. return positionUs;
if (!seekInsideBuffer) { }
// We were unable to seek within the buffer, so need to reset.
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
loadingFinished = false; loadingFinished = false;
if (loader.isLoading()) { if (loader.isLoading()) {
@ -333,7 +334,6 @@ import java.util.Arrays;
sampleQueue.reset(); sampleQueue.reset();
} }
} }
}
return positionUs; return positionUs;
} }

View File

@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; 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.Collections; import java.util.Collections;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
@ -53,9 +54,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private final Handler continueLoadingHandler; private final Handler continueLoadingHandler;
private Callback callback; private Callback callback;
private long preparePositionUs;
private int pendingPrepareCount; private int pendingPrepareCount;
private boolean seenFirstTrackSelection;
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] sampleStreamWrappers;
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
@ -71,34 +70,31 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
streamWrapperIndices = new IdentityHashMap<>(); streamWrapperIndices = new IdentityHashMap<>();
timestampAdjusterProvider = new TimestampAdjusterProvider(); timestampAdjusterProvider = new TimestampAdjusterProvider();
continueLoadingHandler = new Handler(); continueLoadingHandler = new Handler();
sampleStreamWrappers = new HlsSampleStreamWrapper[0];
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
} }
public void release() { public void release() {
playlistTracker.removeListener(this); playlistTracker.removeListener(this);
continueLoadingHandler.removeCallbacksAndMessages(null); continueLoadingHandler.removeCallbacksAndMessages(null);
if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.release(); sampleStreamWrapper.release();
} }
} }
}
@Override @Override
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
this.callback = callback; this.callback = callback;
playlistTracker.addListener(this); playlistTracker.addListener(this);
preparePositionUs = positionUs;
buildAndPrepareSampleStreamWrappers(positionUs); buildAndPrepareSampleStreamWrappers(positionUs);
} }
@Override @Override
public void maybeThrowPrepareError() throws IOException { public void maybeThrowPrepareError() throws IOException {
if (sampleStreamWrappers != null) {
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.maybeThrowPrepareError(); sampleStreamWrapper.maybeThrowPrepareError();
} }
} }
}
@Override @Override
public TrackGroupArray getTrackGroups() { public TrackGroupArray getTrackGroups() {
@ -125,23 +121,24 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
} }
} }
// We'll always need to seek if this is a first selection to a position other than the prepare
// position. boolean forceReset = false;
boolean seekRequired = !seenFirstTrackSelection && positionUs != preparePositionUs;
streamWrapperIndices.clear(); streamWrapperIndices.clear();
// Select tracks for each child, copying the resulting streams back into a new streams array. // Select tracks for each child, copying the resulting streams back into a new streams array.
SampleStream[] newStreams = new SampleStream[selections.length]; SampleStream[] newStreams = new SampleStream[selections.length];
SampleStream[] childStreams = new SampleStream[selections.length]; SampleStream[] childStreams = new SampleStream[selections.length];
TrackSelection[] childSelections = new TrackSelection[selections.length]; TrackSelection[] childSelections = new TrackSelection[selections.length];
ArrayList<HlsSampleStreamWrapper> enabledSampleStreamWrapperList = new ArrayList<>( int newEnabledSampleStreamWrapperCount = 0;
sampleStreamWrappers.length); HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers =
new HlsSampleStreamWrapper[sampleStreamWrappers.length];
for (int i = 0; i < sampleStreamWrappers.length; i++) { for (int i = 0; i < sampleStreamWrappers.length; i++) {
for (int j = 0; j < selections.length; j++) { for (int j = 0; j < selections.length; j++) {
childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
} }
seekRequired |= sampleStreamWrappers[i].selectTracks(childSelections, mayRetainStreamFlags, HlsSampleStreamWrapper sampleStreamWrapper = sampleStreamWrappers[i];
childStreams, streamResetFlags, positionUs, seenFirstTrackSelection, seekRequired); boolean wasReset = sampleStreamWrapper.selectTracks(childSelections, mayRetainStreamFlags,
childStreams, streamResetFlags, positionUs, forceReset);
boolean wrapperEnabled = false; boolean wrapperEnabled = false;
for (int j = 0; j < selections.length; j++) { for (int j = 0; j < selections.length; j++) {
if (selectionChildIndices[j] == i) { if (selectionChildIndices[j] == i) {
@ -156,37 +153,29 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
} }
} }
if (wrapperEnabled) { if (wrapperEnabled) {
enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
if (newEnabledSampleStreamWrapperCount++ == 0) {
// The first enabled wrapper is responsible for initializing timestamp adjusters. This
// way, if enabled, variants are responsible. Else audio renditions. Else text renditions.
sampleStreamWrapper.setIsTimestampMaster(true);
if (wasReset || enabledSampleStreamWrappers.length == 0
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
// The wrapper responsible for initializing the timestamp adjusters was reset or
// changed. We need to reset the timestamp adjuster provider and all other wrappers.
timestampAdjusterProvider.reset();
forceReset = true;
}
} else {
sampleStreamWrapper.setIsTimestampMaster(false);
}
} }
} }
// Copy the new streams back into the streams array. // Copy the new streams back into the streams array.
System.arraycopy(newStreams, 0, streams, 0, newStreams.length); System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
// Update the local state. // Update the local state.
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; enabledSampleStreamWrappers = Arrays.copyOf(newEnabledSampleStreamWrappers,
enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); newEnabledSampleStreamWrapperCount);
// The first enabled sample stream wrapper is responsible for intializing the timestamp
// adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are.
// If only subtitles are present, then text renditions are used for timestamp adjustment
// initialization.
if (enabledSampleStreamWrappers.length > 0) {
enabledSampleStreamWrappers[0].setIsTimestampMaster(true);
for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {
enabledSampleStreamWrappers[i].setIsTimestampMaster(false);
}
}
sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers);
if (seekRequired) {
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 positionUs; return positionUs;
} }
@ -226,9 +215,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
@Override @Override
public long seekToUs(long positionUs) { public long seekToUs(long positionUs) {
if (enabledSampleStreamWrappers.length > 0) {
// We need to reset all wrappers if the one responsible for initializing timestamp adjusters
// is reset. Else each wrapper can decide whether to reset independently.
boolean forceReset = enabledSampleStreamWrappers[0].seekToUs(positionUs, false);
for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {
enabledSampleStreamWrappers[i].seekToUs(positionUs, forceReset);
}
if (forceReset) {
timestampAdjusterProvider.reset(); timestampAdjusterProvider.reset();
for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { }
sampleStreamWrapper.seekTo(positionUs);
} }
return positionUs; return positionUs;
} }
@ -348,6 +344,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrapper.prepareSingleTrack(url.format);
sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
} }
// All wrappers are enabled during preparation.
enabledSampleStreamWrappers = sampleStreamWrappers;
} }
private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,

View File

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.hls;
import android.os.Handler; import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
@ -40,6 +39,7 @@ import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.util.Assertions; 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.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
/** /**
@ -81,11 +81,12 @@ import java.util.LinkedList;
private final Loader loader; private final Loader loader;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
private final SparseArray<SampleQueue> sampleQueues;
private final LinkedList<HlsMediaChunk> mediaChunks; private final LinkedList<HlsMediaChunk> mediaChunks;
private final Runnable maybeFinishPrepareRunnable; private final Runnable maybeFinishPrepareRunnable;
private final Handler handler; private final Handler handler;
private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds;
private boolean sampleQueuesBuilt; private boolean sampleQueuesBuilt;
private boolean prepared; private boolean prepared;
private int enabledTrackCount; private int enabledTrackCount;
@ -97,12 +98,14 @@ import java.util.LinkedList;
// Indexed by track (as exposed by this source). // Indexed by track (as exposed by this source).
private TrackGroupArray trackGroups; private TrackGroupArray trackGroups;
private int primaryTrackGroupIndex; private int primaryTrackGroupIndex;
// Indexed by group. private boolean haveAudioVideoTrackGroups;
private boolean[] groupEnabledStates; // Indexed by track group.
private boolean[] trackGroupEnabledStates;
private boolean[] trackGroupIsAudioVideoFlags;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private boolean seenFirstTrackSelection;
private boolean loadingFinished; private boolean loadingFinished;
/** /**
@ -128,7 +131,8 @@ import java.util.LinkedList;
this.eventDispatcher = eventDispatcher; this.eventDispatcher = eventDispatcher;
loader = new Loader("Loader:HlsSampleStreamWrapper"); loader = new Loader("Loader:HlsSampleStreamWrapper");
nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
sampleQueues = new SparseArray<>(); sampleQueueTrackIds = new int[0];
sampleQueues = new SampleQueue[0];
mediaChunks = new LinkedList<>(); mediaChunks = new LinkedList<>();
maybeFinishPrepareRunnable = new Runnable() { maybeFinishPrepareRunnable = new Runnable() {
@Override @Override
@ -177,16 +181,13 @@ import java.util.LinkedList;
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * @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. * 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.
* @param seenFirstTrackSelection Whether we've already had the first track selection, meaning * @param forceReset If true then a reset is forced (i.e. a seek will be performed with in-buffer
* this is a subsequent selection. * seeking disabled).
* @param seekRequired Whether the parent {@link HlsMediaPeriod} is already guaranteed to perform
* a seek as part of the track selection
* @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as
* part of the track selection. * part of the track selection.
*/ */
public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs, SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) {
boolean seenFirstTrackSelection, boolean seekRequired) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
int oldEnabledTrackCount = enabledTrackCount; int oldEnabledTrackCount = enabledTrackCount;
// Deselect old tracks. // Deselect old tracks.
@ -197,24 +198,27 @@ import java.util.LinkedList;
streams[i] = null; streams[i] = null;
} }
} }
// We'll always need to seek if we're making a selection having previously disabled all tracks. // We'll always need to seek if we're being forced to reset, or if this is a first selection to
seekRequired |= seenFirstTrackSelection && oldEnabledTrackCount == 0; // a position other than the one we started preparing with, or if we're making a selection
// having previously disabled all tracks.
boolean seekRequired = forceReset
|| (seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != lastSeekPositionUs);
// Select new tracks. // Select new tracks.
TrackSelection primaryTrackSelection = null; TrackSelection primaryTrackSelection = null;
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (streams[i] == null && selections[i] != null) { if (streams[i] == null && selections[i] != null) {
TrackSelection selection = selections[i]; TrackSelection selection = selections[i];
int group = trackGroups.indexOf(selection.getTrackGroup()); int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
setTrackGroupEnabledState(group, true); setTrackGroupEnabledState(trackGroupIndex, true);
if (group == primaryTrackGroupIndex) { if (trackGroupIndex == primaryTrackGroupIndex) {
primaryTrackSelection = selection; primaryTrackSelection = selection;
chunkSource.selectTracks(selection); chunkSource.selectTracks(selection);
} }
streams[i] = new HlsSampleStream(this, group); streams[i] = new HlsSampleStream(this, trackGroupIndex);
streamResetFlags[i] = true; streamResetFlags[i] = true;
// If there's still a chance of avoiding a seek, try and seek within the sample queue. // If there's still a chance of avoiding a seek, try and seek within the sample queue.
if (!seekRequired) { if (!seekRequired) {
SampleQueue sampleQueue = sampleQueues.valueAt(group); SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
sampleQueue.rewind(); sampleQueue.rewind();
// A seek can be avoided if we're able to advance to the current playback position in the // A seek can be avoided if we're able to advance to the current playback position in the
// sample queue, or if we haven't read anything from the queue since the previous seek // sample queue, or if we haven't read anything from the queue since the previous seek
@ -230,57 +234,77 @@ import java.util.LinkedList;
chunkSource.reset(); chunkSource.reset();
downstreamTrackFormat = null; downstreamTrackFormat = null;
mediaChunks.clear(); mediaChunks.clear();
int sampleQueueCount = sampleQueues.size();
if (loader.isLoading()) { if (loader.isLoading()) {
// Discard as much as we can synchronously. // Discard as much as we can synchronously.
for (int i = 0; i < sampleQueueCount; i++) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueues.valueAt(i).discardToEnd(); sampleQueue.discardToEnd();
} }
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
for (int i = 0; i < sampleQueueCount; i++) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueues.valueAt(i).reset(); sampleQueue.reset();
} }
} }
return false; } else {
} if (!forceReset && !seenFirstTrackSelection && primaryTrackSelection != null
// If this is the first selection and the chunk loaded during preparation does not match the
// selection, we call seekTo to discard it. Note that if seekRequired is true then the wrapping
// HlsMediaPeriod will call seekTo regardless, and so we do not need to perform the selection
// check here.
if (!seekRequired && !seenFirstTrackSelection && primaryTrackSelection != null
&& !mediaChunks.isEmpty()) { && !mediaChunks.isEmpty()) {
primaryTrackSelection.updateSelectedTrack(0); primaryTrackSelection.updateSelectedTrack(0);
int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat);
if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) {
// The loaded preparation chunk does not match the selection, so discard it. // This is the first selection and the chunk loaded during preparation does not match the
seekTo(positionUs); // selection. We need to reset to discard it.
forceReset = true;
seekRequired = true;
} }
} }
if (seekRequired) {
seekToUs(positionUs, forceReset);
// 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 seekRequired; return seekRequired;
} }
public void discardBuffer(long positionUs) { public void discardBuffer(long positionUs) {
int sampleQueueCount = sampleQueues.size(); int sampleQueueCount = sampleQueues.length;
for (int i = 0; i < sampleQueueCount; i++) { for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues.valueAt(i).discardTo(positionUs, false, groupEnabledStates[i]); sampleQueues[i].discardTo(positionUs, false, trackGroupEnabledStates[i]);
} }
} }
public void seekTo(long positionUs) { /**
* Attempts to seek to the specified position in microseconds.
*
* @param positionUs The seek position in microseconds.
* @param forceReset If true then a reset is forced (i.e. in-buffer seeking is disabled).
* @return Whether the wrapper was reset, meaning the wrapped sample queues were reset. If false,
* an in-buffer seek was performed.
*/
public boolean seekToUs(long positionUs, boolean forceReset) {
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
// If we're not forced to reset nor have a pending reset, see if we can seek within the buffer.
if (!forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) {
return false;
}
// We were unable to seek within the buffer, so need to reset.
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
loadingFinished = false; loadingFinished = false;
mediaChunks.clear(); mediaChunks.clear();
if (loader.isLoading()) { if (loader.isLoading()) {
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) { sampleQueue.reset();
sampleQueues.valueAt(i).reset();
} }
} }
return true;
} }
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
@ -296,10 +320,9 @@ import java.util.LinkedList;
if (lastCompletedMediaChunk != null) { if (lastCompletedMediaChunk != null) {
bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
} }
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) {
bufferedPositionUs = Math.max(bufferedPositionUs, bufferedPositionUs = Math.max(bufferedPositionUs,
sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); sampleQueue.getLargestQueuedTimestampUs());
} }
return bufferedPositionUs; return bufferedPositionUs;
} }
@ -310,9 +333,8 @@ import java.util.LinkedList;
if (prepared && !releasedSynchronously) { if (prepared && !releasedSynchronously) {
// Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
// sampleQueues may still be being modified by the loading thread. // sampleQueues may still be being modified by the loading thread.
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) { sampleQueue.discardToEnd();
sampleQueues.valueAt(i).discardToEnd();
} }
} }
handler.removeCallbacksAndMessages(null); handler.removeCallbacksAndMessages(null);
@ -321,9 +343,8 @@ import java.util.LinkedList;
@Override @Override
public void onLoaderReleased() { public void onLoaderReleased() {
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) { sampleQueue.reset();
sampleQueues.valueAt(i).reset();
} }
} }
@ -337,8 +358,8 @@ import java.util.LinkedList;
// SampleStream implementation. // SampleStream implementation.
/* package */ boolean isReady(int group) { /* package */ boolean isReady(int trackGroupIndex) {
return loadingFinished || (!isPendingReset() && sampleQueues.valueAt(group).hasNextSample()); return loadingFinished || (!isPendingReset() && sampleQueues[trackGroupIndex].hasNextSample());
} }
/* package */ void maybeThrowError() throws IOException { /* package */ void maybeThrowError() throws IOException {
@ -346,8 +367,8 @@ import java.util.LinkedList;
chunkSource.maybeThrowError(); chunkSource.maybeThrowError();
} }
/* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, /* package */ int readData(int trackGroupIndex, FormatHolder formatHolder,
boolean requireFormat) { DecoderInputBuffer buffer, boolean requireFormat) {
if (isPendingReset()) { if (isPendingReset()) {
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
@ -366,12 +387,12 @@ import java.util.LinkedList;
downstreamTrackFormat = trackFormat; downstreamTrackFormat = trackFormat;
} }
return sampleQueues.valueAt(group).read(formatHolder, buffer, requireFormat, loadingFinished, return sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, loadingFinished,
lastSeekPositionUs); lastSeekPositionUs);
} }
/* package */ void skipData(int group, long positionUs) { /* package */ void skipData(int trackGroupIndex, long positionUs) {
SampleQueue sampleQueue = sampleQueues.valueAt(group); SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
sampleQueue.advanceToEnd(); sampleQueue.advanceToEnd();
} else { } else {
@ -381,8 +402,8 @@ import java.util.LinkedList;
private boolean finishedReadingChunk(HlsMediaChunk chunk) { private boolean finishedReadingChunk(HlsMediaChunk chunk) {
int chunkUid = chunk.uid; int chunkUid = chunk.uid;
for (int i = 0; i < sampleQueues.size(); i++) { for (int i = 0; i < sampleQueues.length; i++) {
if (groupEnabledStates[i] && sampleQueues.valueAt(i).peekSourceId() == chunkUid) { if (trackGroupEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) {
return false; return false;
} }
} }
@ -462,9 +483,8 @@ import java.util.LinkedList;
loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
if (!released) { if (!released) {
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) { sampleQueue.reset();
sampleQueues.valueAt(i).reset();
} }
if (enabledTrackCount > 0) { if (enabledTrackCount > 0) {
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
@ -516,12 +536,12 @@ import java.util.LinkedList;
*/ */
public void init(int chunkUid, boolean shouldSpliceIn) { public void init(int chunkUid, boolean shouldSpliceIn) {
upstreamChunkUid = chunkUid; upstreamChunkUid = chunkUid;
for (int i = 0; i < sampleQueues.size(); i++) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueues.valueAt(i).sourceId(chunkUid); sampleQueue.sourceId(chunkUid);
} }
if (shouldSpliceIn) { if (shouldSpliceIn) {
for (int i = 0; i < sampleQueues.size(); i++) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueues.valueAt(i).splice(); sampleQueue.splice();
} }
} }
} }
@ -530,14 +550,19 @@ import java.util.LinkedList;
@Override @Override
public SampleQueue track(int id, int type) { public SampleQueue track(int id, int type) {
if (sampleQueues.indexOfKey(id) >= 0) { int trackCount = sampleQueues.length;
return sampleQueues.get(id); for (int i = 0; i < trackCount; i++) {
if (sampleQueueTrackIds[i] == id) {
return sampleQueues[i];
} }
SampleQueue sampleQueue = new SampleQueue(allocator); }
sampleQueue.setUpstreamFormatChangeListener(this); SampleQueue trackOutput = new SampleQueue(allocator);
sampleQueue.sourceId(upstreamChunkUid); trackOutput.setUpstreamFormatChangeListener(this);
sampleQueues.put(id, sampleQueue); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
return sampleQueue; sampleQueueTrackIds[trackCount] = id;
sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1);
sampleQueues[trackCount] = trackOutput;
return trackOutput;
} }
@Override @Override
@ -564,9 +589,8 @@ import java.util.LinkedList;
if (released || prepared || !sampleQueuesBuilt) { if (released || prepared || !sampleQueuesBuilt) {
return; return;
} }
int sampleQueueCount = sampleQueues.size(); for (SampleQueue sampleQueue : sampleQueues) {
for (int i = 0; i < sampleQueueCount; i++) { if (sampleQueue.getUpstreamFormat() == null) {
if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
return; return;
} }
} }
@ -609,9 +633,9 @@ import java.util.LinkedList;
// of the single track of this type. // of the single track of this type.
int primaryExtractorTrackType = PRIMARY_TYPE_NONE; int primaryExtractorTrackType = PRIMARY_TYPE_NONE;
int primaryExtractorTrackIndex = C.INDEX_UNSET; int primaryExtractorTrackIndex = C.INDEX_UNSET;
int extractorTrackCount = sampleQueues.size(); int extractorTrackCount = sampleQueues.length;
for (int i = 0; i < extractorTrackCount; i++) { for (int i = 0; i < extractorTrackCount; i++) {
String sampleMimeType = sampleQueues.valueAt(i).getUpstreamFormat().sampleMimeType; String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType;
int trackType; int trackType;
if (MimeTypes.isVideo(sampleMimeType)) { if (MimeTypes.isVideo(sampleMimeType)) {
trackType = PRIMARY_TYPE_VIDEO; trackType = PRIMARY_TYPE_VIDEO;
@ -638,12 +662,17 @@ import java.util.LinkedList;
// Instantiate the necessary internal data-structures. // Instantiate the necessary internal data-structures.
primaryTrackGroupIndex = C.INDEX_UNSET; primaryTrackGroupIndex = C.INDEX_UNSET;
groupEnabledStates = new boolean[extractorTrackCount]; trackGroupEnabledStates = new boolean[extractorTrackCount];
trackGroupIsAudioVideoFlags = new boolean[extractorTrackCount];
// Construct the set of exposed track groups. // Construct the set of exposed track groups.
TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
for (int i = 0; i < extractorTrackCount; i++) { for (int i = 0; i < extractorTrackCount; i++) {
Format sampleFormat = sampleQueues.valueAt(i).getUpstreamFormat(); Format sampleFormat = sampleQueues[i].getUpstreamFormat();
String mimeType = sampleFormat.sampleMimeType;
boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
trackGroupIsAudioVideoFlags[i] = isAudioVideo;
haveAudioVideoTrackGroups |= isAudioVideo;
if (i == primaryExtractorTrackIndex) { if (i == primaryExtractorTrackIndex) {
Format[] formats = new Format[chunkSourceTrackCount]; Format[] formats = new Format[chunkSourceTrackCount];
for (int j = 0; j < chunkSourceTrackCount; j++) { for (int j = 0; j < chunkSourceTrackCount; j++) {
@ -663,12 +692,12 @@ import java.util.LinkedList;
/** /**
* Enables or disables a specified track group. * Enables or disables a specified track group.
* *
* @param group The index of the track group. * @param trackGroupIndex The index of the track group.
* @param enabledState True if the group is being enabled, or false if it's being disabled. * @param enabledState True if the group is being enabled, or false if it's being disabled.
*/ */
private void setTrackGroupEnabledState(int group, boolean enabledState) { private void setTrackGroupEnabledState(int trackGroupIndex, boolean enabledState) {
Assertions.checkState(groupEnabledStates[group] != enabledState); Assertions.checkState(trackGroupEnabledStates[trackGroupIndex] != enabledState);
groupEnabledStates[group] = enabledState; trackGroupEnabledStates[trackGroupIndex] = enabledState;
enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1);
} }
@ -704,6 +733,30 @@ import java.util.LinkedList;
return pendingResetPositionUs != C.TIME_UNSET; return pendingResetPositionUs != C.TIME_UNSET;
} }
/**
* Attempts to seek to the specified position within the sample queues.
*
* @param positionUs The seek position in microseconds.
* @return Whether the in-buffer seek was successful.
*/
private boolean seekInsideBufferUs(long positionUs) {
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
SampleQueue sampleQueue = sampleQueues[i];
sampleQueue.rewind();
boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
// If we have AV tracks then an in-queue seek is successful if the seek into every AV queue
// is successful. We ignore whether seeks within non-AV queues are successful in this case, as
// they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
// successful only if the seek into every queue succeeds.
if (!seekInsideQueue && (trackGroupIsAudioVideoFlags[i] || !haveAudioVideoTrackGroups)) {
return false;
}
sampleQueue.discardToRead();
}
return true;
}
private static String getAudioCodecs(String codecs) { private static String getAudioCodecs(String codecs) {
return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO); return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO);
} }