From 06b3b3ca8d8ee807008e1cbd5d94c6de5cf96df0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 10 Jul 2017 07:23:25 -0700 Subject: [PATCH] Migrate HLS over to new SampleQueue methods ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161391296 --- .../source/ExtractorMediaPeriod.java | 4 + .../source/SampleMetadataQueue.java | 5 - .../exoplayer2/source/SampleQueue.java | 150 +++--------------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 18 ++- .../source/hls/HlsSampleStreamWrapper.java | 132 ++++++++++----- 5 files changed, 134 insertions(+), 175 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 79bc753241b..ea6b105705a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -219,6 +219,10 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF if (!seekRequired) { SampleQueue sampleQueue = sampleQueues[track]; sampleQueue.rewind(); + // 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 + // (this case is common for sparse tracks such as metadata tracks). In all other cases a + // seek is required. seekRequired = !sampleQueue.advanceTo(positionUs, true, true) && sampleQueue.getReadIndex() != 0; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index c9c44ab0148..79efa984b16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -83,11 +83,6 @@ public void clearSampleData() { relativeStartIndex = 0; readPosition = 0; upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - public void resetLargestParsedTimestamps() { largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 1b70b03a291..4af0349fc1e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -29,7 +29,6 @@ import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicInteger; /** * A queue of media samples. @@ -52,16 +51,11 @@ public interface UpstreamFormatChangedListener { private static final int INITIAL_SCRATCH_SIZE = 32; - private static final int STATE_ENABLED = 0; - private static final int STATE_ENABLED_WRITING = 1; - private static final int STATE_DISABLED = 2; - private final Allocator allocator; private final int allocationLength; private final SampleMetadataQueue metadataQueue; private final SampleExtrasHolder extrasHolder; private final ParsableByteArray scratch; - private final AtomicInteger state; // References into the linked list of allocations. private AllocationNode firstAllocationNode; @@ -88,7 +82,6 @@ public SampleQueue(Allocator allocator) { metadataQueue = new SampleMetadataQueue(); extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - state = new AtomicInteger(); firstAllocationNode = new AllocationNode(0, allocationLength); readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; @@ -100,20 +93,13 @@ public SampleQueue(Allocator allocator) { * Resets the output. */ public void reset() { - reset(true); - } - - /** - * @deprecated Use {@link #reset()}. Don't disable sample queues. - */ - @Deprecated - public void reset(boolean enable) { - int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); - clearSampleData(); - metadataQueue.resetLargestParsedTimestamps(); - if (previousState == STATE_DISABLED) { - downstreamFormat = null; - } + metadataQueue.clearSampleData(); + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); } /** @@ -174,16 +160,6 @@ public void discardUpstreamSamples(int discardFromIndex) { // Called by the consuming thread. - /** - * @deprecated Don't disable sample queues. - */ - @Deprecated - public void disable() { - if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) { - clearSampleData(); - } - } - /** * Returns whether a sample is available to be read. */ @@ -265,15 +241,6 @@ public void discardToEnd() { discardDownstreamTo(metadataQueue.discardToEnd()); } - /** - * @deprecated Use {@link #advanceToEnd()} followed by {@link #discardToRead()}. - */ - @Deprecated - public void skipAll() { - advanceToEnd(); - discardToRead(); - } - /** * Advances the read position to the end of the queue. */ @@ -281,17 +248,6 @@ public void advanceToEnd() { metadataQueue.advanceToEnd(); } - /** - * @deprecated Use {@link #advanceTo(long, boolean, boolean)} followed by - * {@link #discardToRead()}. - */ - @Deprecated - public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - boolean success = advanceTo(timeUs, true, allowTimeBeyondBuffer); - discardToRead(); - return success; - } - /** * Attempts to advance the read position to the sample before or at the specified time. * @@ -307,19 +263,6 @@ public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyon return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } - /** - * @deprecated Use {@link #read(FormatHolder, DecoderInputBuffer, boolean, boolean, long)} - * followed by {@link #discardToRead()}. - */ - @Deprecated - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean loadingFinished, long decodeOnlyUntilUs) { - int result = read(formatHolder, buffer, formatRequired, loadingFinished, - decodeOnlyUntilUs); - discardToRead(); - return result; - } - /** * Attempts to read from the queue. * @@ -558,39 +501,21 @@ public void format(Format format) { @Override public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - if (!startWriteOperation()) { - int bytesSkipped = input.skip(length); - if (bytesSkipped == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - return bytesSkipped; - } - try { - length = preAppend(length); - int bytesAppended = input.read(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); + length = preAppend(length); + int bytesAppended = input.read(writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; } - postAppend(bytesAppended); - return bytesAppended; - } finally { - endWriteOperation(); + throw new EOFException(); } + postAppend(bytesAppended); + return bytesAppended; } @Override public void sampleData(ParsableByteArray buffer, int length) { - if (!startWriteOperation()) { - buffer.skipBytes(length); - return; - } while (length > 0) { int bytesAppended = preAppend(length); buffer.readBytes(writeAllocationNode.allocation.data, @@ -598,7 +523,6 @@ public void sampleData(ParsableByteArray buffer, int length) { length -= bytesAppended; postAppend(bytesAppended); } - endWriteOperation(); } @Override @@ -607,47 +531,19 @@ public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int if (pendingFormatAdjustment) { format(lastUnadjustedFormat); } - if (!startWriteOperation()) { - metadataQueue.commitSampleTimestamp(timeUs); - return; - } - try { - if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { - return; - } - pendingSplice = false; + if (pendingSplice) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { + return; } - timeUs += sampleOffsetUs; - long absoluteOffset = totalBytesWritten - size - offset; - metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); - } finally { - endWriteOperation(); + pendingSplice = false; } + timeUs += sampleOffsetUs; + long absoluteOffset = totalBytesWritten - size - offset; + metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } // Private methods. - private boolean startWriteOperation() { - return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING); - } - - private void endWriteOperation() { - if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) { - clearSampleData(); - } - } - - private void clearSampleData() { - metadataQueue.clearSampleData(); - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - totalBytesWritten = 0; - allocator.trim(); - } - /** * Clears allocation nodes starting from {@code fromNode}. * diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 25e48d8ccec..88de8eb71ed 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -53,6 +53,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final Handler continueLoadingHandler; private Callback callback; + private long preparePositionUs; private int pendingPrepareCount; private boolean seenFirstTrackSelection; private TrackGroupArray trackGroups; @@ -84,8 +85,9 @@ public void release() { @Override public void prepare(Callback callback, long positionUs) { - playlistTracker.addListener(this); this.callback = callback; + playlistTracker.addListener(this); + preparePositionUs = positionUs; buildAndPrepareSampleStreamWrappers(positionUs); } @@ -123,7 +125,9 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF } } } - boolean selectedNewTracks = false; + // We'll always need to seek if this is a first selection to a position other than the prepare + // position. + boolean seekRequired = !seenFirstTrackSelection && positionUs != preparePositionUs; streamWrapperIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. SampleStream[] newStreams = new SampleStream[selections.length]; @@ -136,8 +140,8 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF 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); + seekRequired |= sampleStreamWrappers[i].selectTracks(childSelections, mayRetainStreamFlags, + childStreams, streamResetFlags, positionUs, seenFirstTrackSelection, seekRequired); boolean wrapperEnabled = false; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { @@ -173,7 +177,7 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF } sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); - if (seenFirstTrackSelection && selectedNewTracks) { + 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++) { @@ -188,7 +192,9 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF @Override public void discardBuffer(long positionUs) { - // Do nothing. + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + sampleStreamWrapper.discardBuffer(positionUs); + } } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0e8567b846c..def94359b78 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -47,7 +47,7 @@ * {@link SampleStream}s from which the loaded media can be consumed. */ /* package */ final class HlsSampleStreamWrapper implements Loader.Callback, - SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { + Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { /** * A callback to be notified of events. @@ -165,21 +165,42 @@ public TrackGroupArray getTrackGroups() { return trackGroups; } + /** + * Called by the parent {@link HlsMediaPeriod} when a track selection occurs. + * + * @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. + * @param seenFirstTrackSelection Whether we've already had the first track selection, meaning + * this is a subsequent selection. + * @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 + * part of the track selection. + */ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) { + SampleStream[] streams, boolean[] streamResetFlags, long positionUs, + boolean seenFirstTrackSelection, boolean seekRequired) { Assertions.checkState(prepared); - // Disable old tracks. + int oldEnabledTrackCount = enabledTrackCount; + // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { int group = ((HlsSampleStream) streams[i]).group; setTrackGroupEnabledState(group, false); - sampleQueues.valueAt(group).disable(); streams[i] = null; } } - // Enable new tracks. + // We'll always need to seek if we're making a selection having previously disabled all tracks. + seekRequired |= seenFirstTrackSelection && oldEnabledTrackCount == 0; + // Select new tracks. TrackSelection primaryTrackSelection = null; - boolean selectedNewTracks = false; for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; @@ -191,37 +212,60 @@ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStre } streams[i] = new HlsSampleStream(this, group); streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (isFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (!groupEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); - } - } - if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { - primaryTrackSelection.updateSelectedTrack(0); - int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); - if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { - // The loaded preparation chunk does match the selection. We discard it. - seekTo(lastSeekPositionUs); + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues.valueAt(group); + sampleQueue.rewind(); + // 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 + // (this case is common for sparse tracks such as metadata tracks). In all other cases a + // seek is required. + seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + && sampleQueue.getReadIndex() != 0; } } } - // Cancel requests if necessary. + if (enabledTrackCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; mediaChunks.clear(); + int sampleQueueCount = sampleQueues.size(); if (loader.isLoading()) { + // Discard as much as we can synchronously. + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).discardToEnd(); + } loader.cancelLoading(); + } else { + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).reset(); + } } + return false; + } + + // 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()) { + primaryTrackSelection.updateSelectedTrack(0); + int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // The loaded preparation chunk does not match the selection, so discard it. + seekTo(positionUs); + } + } + return seekRequired; + } + + public void discardBuffer(long positionUs) { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).discardTo(positionUs, false, groupEnabledStates[i]); } - return selectedNewTracks; } public void seekTo(long positionUs) { @@ -234,7 +278,7 @@ public void seekTo(long positionUs) { } else { int sampleQueueCount = sampleQueues.size(); for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + sampleQueues.valueAt(i).reset(); } } } @@ -262,15 +306,27 @@ public long getBufferedPositionUs() { } public void release() { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).disable(); + boolean releasedSynchronously = loader.release(this); + if (prepared && !releasedSynchronously) { + // 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. + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).discardToEnd(); + } } - loader.release(); handler.removeCallbacksAndMessages(null); released = true; } + @Override + public void onLoaderReleased() { + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues.valueAt(i).reset(); + } + } + public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } @@ -310,16 +366,16 @@ public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) { downstreamTrackFormat = trackFormat; } - return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, - loadingFinished, lastSeekPositionUs); + return sampleQueues.valueAt(group).read(formatHolder, buffer, requireFormat, loadingFinished, + lastSeekPositionUs); } /* package */ void skipData(int group, long positionUs) { SampleQueue sampleQueue = sampleQueues.valueAt(group); if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } @@ -408,9 +464,11 @@ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDura if (!released) { int sampleQueueCount = sampleQueues.size(); for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + sampleQueues.valueAt(i).reset(); + } + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); } - callback.onContinueLoadingRequested(this); } }