Allow late HLS sample queue building

Issue:#3149

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177836048
This commit is contained in:
aquilescanta 2017-12-04 10:50:33 -08:00 committed by Oliver Woodman
parent fbfa43f5a3
commit 002df729a5
4 changed files with 156 additions and 53 deletions

View File

@ -1,3 +1,5 @@
*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION ***
Before filing an issue: Before filing an issue:
----------------------- -----------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed.

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.hls; package com.google.android.exoplayer2.source.hls;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
@ -25,33 +26,60 @@ import java.io.IOException;
*/ */
/* package */ final class HlsSampleStream implements SampleStream { /* package */ final class HlsSampleStream implements SampleStream {
public final int sampleQueueIndex; private final int trackGroupIndex;
private final HlsSampleStreamWrapper sampleStreamWrapper; private final HlsSampleStreamWrapper sampleStreamWrapper;
private int sampleQueueIndex;
public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int sampleQueueIndex) { public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) {
this.sampleStreamWrapper = sampleStreamWrapper; this.sampleStreamWrapper = sampleStreamWrapper;
this.sampleQueueIndex = sampleQueueIndex; this.trackGroupIndex = trackGroupIndex;
} }
public void unbindSampleQueue() {
if (sampleQueueIndex != C.INDEX_UNSET) {
sampleStreamWrapper.unbindSampleQueue(trackGroupIndex);
}
}
// SampleStream implementation.
@Override @Override
public boolean isReady() { public boolean isReady() {
return sampleStreamWrapper.isReady(sampleQueueIndex); return ensureBoundSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex);
} }
@Override @Override
public void maybeThrowError() throws IOException { public void maybeThrowError() throws IOException {
if (!ensureBoundSampleQueue()) {
throw new SampleQueueMappingException(
sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType);
}
sampleStreamWrapper.maybeThrowError(); sampleStreamWrapper.maybeThrowError();
} }
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) {
if (!ensureBoundSampleQueue()) {
return C.RESULT_NOTHING_READ;
}
return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat); return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat);
} }
@Override @Override
public int skipData(long positionUs) { public int skipData(long positionUs) {
if (!ensureBoundSampleQueue()) {
return 0;
}
return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs); return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs);
} }
// Internal methods.
private boolean ensureBoundSampleQueue() {
if (sampleQueueIndex != C.INDEX_UNSET) {
return true;
}
sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex);
return sampleQueueIndex != C.INDEX_UNSET;
}
} }

View File

@ -88,13 +88,14 @@ import java.util.Arrays;
private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
private final ArrayList<HlsMediaChunk> mediaChunks; private final ArrayList<HlsMediaChunk> mediaChunks;
private final Runnable maybeFinishPrepareRunnable; private final Runnable maybeFinishPrepareRunnable;
private final Runnable onTracksEndedRunnable;
private final Handler handler; private final Handler handler;
private SampleQueue[] sampleQueues; private SampleQueue[] sampleQueues;
private int[] sampleQueueTrackIds; private int[] sampleQueueTrackIds;
private boolean sampleQueuesBuilt; private boolean sampleQueuesBuilt;
private boolean prepared; private boolean prepared;
private int enabledSampleQueueCount; private int enabledTrackGroupCount;
private Format downstreamTrackFormat; private Format downstreamTrackFormat;
private boolean released; private boolean released;
@ -108,13 +109,16 @@ import java.util.Arrays;
private boolean[] sampleQueuesEnabledStates; private boolean[] sampleQueuesEnabledStates;
private boolean[] sampleQueueIsAudioVideoFlags; private boolean[] sampleQueueIsAudioVideoFlags;
private long sampleOffsetUs;
private long lastSeekPositionUs; private long lastSeekPositionUs;
private long pendingResetPositionUs; private long pendingResetPositionUs;
private boolean pendingResetUpstreamFormats; private boolean pendingResetUpstreamFormats;
private boolean seenFirstTrackSelection; private boolean seenFirstTrackSelection;
private boolean loadingFinished; private boolean loadingFinished;
// Accessed only by the loading thread.
private boolean tracksEnded;
private long sampleOffsetUs;
/** /**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
* @param callback A callback for the wrapper. * @param callback A callback for the wrapper.
@ -143,12 +147,20 @@ import java.util.Arrays;
sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueueIsAudioVideoFlags = new boolean[0];
sampleQueuesEnabledStates = new boolean[0]; sampleQueuesEnabledStates = new boolean[0];
mediaChunks = new ArrayList<>(); mediaChunks = new ArrayList<>();
maybeFinishPrepareRunnable = new Runnable() { maybeFinishPrepareRunnable =
@Override new Runnable() {
public void run() { @Override
maybeFinishPrepare(); public void run() {
} maybeFinishPrepare();
}; }
};
onTracksEndedRunnable =
new Runnable() {
@Override
public void run() {
onTracksEnded();
}
};
handler = new Handler(); handler = new Handler();
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
pendingResetPositionUs = positionUs; pendingResetPositionUs = positionUs;
@ -166,8 +178,8 @@ import java.util.Arrays;
*/ */
public void prepareSingleTrack(Format format) { public void prepareSingleTrack(Format format) {
track(0, C.TRACK_TYPE_UNKNOWN).format(format); track(0, C.TRACK_TYPE_UNKNOWN).format(format);
sampleQueuesBuilt = true; tracksEnded = true;
maybeFinishPrepare(); onTracksEnded();
} }
public void maybeThrowPrepareError() throws IOException { public void maybeThrowPrepareError() throws IOException {
@ -178,6 +190,19 @@ import java.util.Arrays;
return trackGroups; return trackGroups;
} }
public int bindSampleQueueToSampleStream(int trackGroupIndex) {
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
if (sampleQueueIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
setSampleQueueEnabledState(sampleQueueIndex, true);
return sampleQueueIndex;
}
public void unbindSampleQueue(int trackGroupIndex) {
setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false);
}
/** /**
* Called by the parent {@link HlsMediaPeriod} when a track selection occurs. * Called by the parent {@link HlsMediaPeriod} when a track selection occurs.
* *
@ -198,20 +223,23 @@ import java.util.Arrays;
public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) {
Assertions.checkState(prepared); Assertions.checkState(prepared);
int oldEnabledSampleQueueCount = enabledSampleQueueCount; int oldEnabledTrackGroupCount = enabledTrackGroupCount;
// Deselect old tracks. // Deselect old tracks.
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
setSampleQueueEnabledState(((HlsSampleStream) streams[i]).sampleQueueIndex, false); enabledTrackGroupCount--;
((HlsSampleStream) streams[i]).unbindSampleQueue();
streams[i] = null; streams[i] = null;
} }
} }
// We'll always need to seek if we're being forced to reset, or if this is a first selection to // We'll always need to seek if we're being forced to reset, or if this is a first selection to
// a position other than the one we started preparing with, or if we're making a selection // a position other than the one we started preparing with, or if we're making a selection
// having previously disabled all tracks. // having previously disabled all tracks.
boolean seekRequired = forceReset boolean seekRequired =
|| (seenFirstTrackSelection ? oldEnabledSampleQueueCount == 0 forceReset
: positionUs != lastSeekPositionUs); || (seenFirstTrackSelection
? oldEnabledTrackGroupCount == 0
: positionUs != lastSeekPositionUs);
// Get the old (i.e. current before the loop below executes) primary track selection. The new // Get the old (i.e. current before the loop below executes) primary track selection. The new
// primary selection will equal the old one unless it's changed in the loop. // primary selection will equal the old one unless it's changed in the loop.
TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection();
@ -219,19 +247,18 @@ import java.util.Arrays;
// Select new tracks. // Select new tracks.
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) {
enabledTrackGroupCount++;
TrackSelection selection = selections[i]; TrackSelection selection = selections[i];
int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
setSampleQueueEnabledState(sampleQueueIndex, true);
if (trackGroupIndex == primaryTrackGroupIndex) { if (trackGroupIndex == primaryTrackGroupIndex) {
primaryTrackSelection = selection; primaryTrackSelection = selection;
chunkSource.selectTracks(selection); chunkSource.selectTracks(selection);
} }
streams[i] = new HlsSampleStream(this, sampleQueueIndex); 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 (sampleQueuesBuilt && !seekRequired) {
SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[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
@ -243,14 +270,16 @@ import java.util.Arrays;
} }
} }
if (enabledSampleQueueCount == 0) { if (enabledTrackGroupCount == 0) {
chunkSource.reset(); chunkSource.reset();
downstreamTrackFormat = null; downstreamTrackFormat = null;
mediaChunks.clear(); mediaChunks.clear();
if (loader.isLoading()) { if (loader.isLoading()) {
// Discard as much as we can synchronously. if (sampleQueuesBuilt) {
for (SampleQueue sampleQueue : sampleQueues) { // Discard as much as we can synchronously.
sampleQueue.discardToEnd(); for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
} }
loader.cancelLoading(); loader.cancelLoading();
} else { } else {
@ -297,6 +326,9 @@ import java.util.Arrays;
} }
public void discardBuffer(long positionUs, boolean toKeyframe) { public void discardBuffer(long positionUs, boolean toKeyframe) {
if (!sampleQueuesBuilt) {
return;
}
int sampleQueueCount = sampleQueues.length; int sampleQueueCount = sampleQueues.length;
for (int i = 0; i < sampleQueueCount; i++) { for (int i = 0; i < sampleQueueCount; i++) {
sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]); sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]);
@ -314,7 +346,7 @@ import java.util.Arrays;
public boolean seekToUs(long positionUs, boolean forceReset) { 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 we're not forced to reset nor have a pending reset, see if we can seek within the buffer.
if (!forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { if (sampleQueuesBuilt && !forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) {
return false; return false;
} }
// We were unable to seek within the buffer, so need to reset. // We were unable to seek within the buffer, so need to reset.
@ -426,9 +458,11 @@ import java.util.Arrays;
if (lastCompletedMediaChunk != null) { if (lastCompletedMediaChunk != null) {
bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
} }
for (SampleQueue sampleQueue : sampleQueues) { if (sampleQueuesBuilt) {
bufferedPositionUs = Math.max(bufferedPositionUs, for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.getLargestQueuedTimestampUs()); bufferedPositionUs =
Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs());
}
} }
return bufferedPositionUs; return bufferedPositionUs;
} }
@ -513,7 +547,7 @@ import java.util.Arrays;
loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
if (!released) { if (!released) {
resetSampleQueues(); resetSampleQueues();
if (enabledSampleQueueCount > 0) { if (enabledTrackGroupCount > 0) {
callback.onContinueLoadingRequested(this); callback.onContinueLoadingRequested(this);
} }
} }
@ -582,7 +616,7 @@ import java.util.Arrays;
return sampleQueues[i]; return sampleQueues[i];
} }
} }
if (sampleQueuesBuilt) { if (tracksEnded) {
Log.w(TAG, "Unmapped track with id " + id + " of type " + type); Log.w(TAG, "Unmapped track with id " + id + " of type " + type);
return new DummyTrackOutput(); return new DummyTrackOutput();
} }
@ -603,8 +637,8 @@ import java.util.Arrays;
@Override @Override
public void endTracks() { public void endTracks() {
sampleQueuesBuilt = true; tracksEnded = true;
handler.post(maybeFinishPrepareRunnable); handler.post(onTracksEndedRunnable);
} }
@Override @Override
@ -616,9 +650,7 @@ import java.util.Arrays;
@Override @Override
public void onUpstreamFormatChanged(Format format) { public void onUpstreamFormatChanged(Format format) {
if (!prepared) { handler.post(maybeFinishPrepareRunnable);
handler.post(maybeFinishPrepareRunnable);
}
} }
// Called by the loading thread. // Called by the loading thread.
@ -650,6 +682,11 @@ import java.util.Arrays;
pendingResetUpstreamFormats = false; pendingResetUpstreamFormats = false;
} }
private void onTracksEnded() {
sampleQueuesBuilt = true;
maybeFinishPrepare();
}
private void maybeFinishPrepare() { private void maybeFinishPrepare() {
if (released || prepared || !sampleQueuesBuilt) { if (released || prepared || !sampleQueuesBuilt) {
return; return;
@ -739,14 +776,14 @@ import java.util.Arrays;
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++) {
formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat); formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true);
} }
trackGroups[i] = new TrackGroup(formats); trackGroups[i] = new TrackGroup(formats);
primaryTrackGroupIndex = i; primaryTrackGroupIndex = i;
} else { } else {
Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO
&& MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null;
trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false));
} }
} }
this.trackGroups = new TrackGroupArray(trackGroups); this.trackGroups = new TrackGroupArray(trackGroups);
@ -761,7 +798,6 @@ import java.util.Arrays;
private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) {
Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState);
sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; sampleQueuesEnabledStates[sampleQueueIndex] = enabledState;
enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1);
} }
private HlsMediaChunk getLastMediaChunk() { private HlsMediaChunk getLastMediaChunk() {
@ -797,22 +833,30 @@ import java.util.Arrays;
} }
/** /**
* Derives a track format corresponding to a given container format, by combining it with sample * Derives a track format using master playlist and sample format information.
* level information obtained from the samples.
* *
* @param containerFormat The container format for which the track format should be derived. * @param playlistFormat The format information obtained from the master playlist.
* @param sampleFormat A sample format from which to obtain sample level information. * @param sampleFormat The format information obtained from the samples.
* @param propagateBitrate Whether the bitrate from the playlist format should be included in the
* derived format.
* @return The derived track format. * @return The derived track format.
*/ */
private static Format deriveFormat(Format containerFormat, Format sampleFormat) { private static Format deriveFormat(
if (containerFormat == null) { Format playlistFormat, Format sampleFormat, boolean propagateBitrate) {
if (playlistFormat == null) {
return sampleFormat; return sampleFormat;
} }
int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE;
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, return sampleFormat.copyWithContainerInfo(
containerFormat.width, containerFormat.height, containerFormat.selectionFlags, playlistFormat.id,
containerFormat.language); codecs,
bitrate,
playlistFormat.width,
playlistFormat.height,
playlistFormat.selectionFlags,
playlistFormat.language);
} }
private static boolean isMediaChunk(Chunk chunk) { private static boolean isMediaChunk(Chunk chunk) {

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.hls;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.TrackGroup;
import java.io.IOException;
/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */
public final class SampleQueueMappingException extends IOException {
/** @param mimeType The mime type of the track group whose mapping failed. */
public SampleQueueMappingException(String mimeType) {
super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + ".");
}
}