From b5bdbedfd5027aade9d2265744401d2f3f5870de Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Apr 2016 10:04:53 -0700 Subject: [PATCH] Move HLS to use a single RollingSampleBuffer per track. Notes: - RollingSampleBuffer will be renamed DefaultTrackOutput in a following CL, and variable naming will be sanitized. - TsChunk will also be renamed to HlsMediaChunk, since it can be used for non-TS containers (e.g. MP3). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=120240243 --- .../extractor/DefaultTrackOutput.java | 193 -------------- .../extractor/RollingSampleBuffer.java | 103 ++++--- .../android/exoplayer/hls/HlsChunkSource.java | 42 ++- .../exoplayer/hls/HlsExtractorWrapper.java | 252 ------------------ .../android/exoplayer/hls/HlsOutput.java | 125 +++++++++ .../exoplayer/hls/HlsSampleSource.java | 175 ++++-------- .../google/android/exoplayer/hls/TsChunk.java | 39 ++- 7 files changed, 288 insertions(+), 641 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java delete mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java create mode 100644 library/src/main/java/com/google/android/exoplayer/hls/HlsOutput.java diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java deleted file mode 100644 index eefccccf38..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.extractor; - -import com.google.android.exoplayer.DecoderInputBuffer; -import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.upstream.Allocator; -import com.google.android.exoplayer.util.ParsableByteArray; - -import java.io.IOException; - -/** - * A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from - * that queue. - */ -public final class DefaultTrackOutput implements TrackOutput { - - private final RollingSampleBuffer rollingBuffer; - private final DecoderInputBuffer sampleBuffer; - - // Accessed only by the consuming thread. - private boolean needKeyframe; - private long lastReadTimeUs; - private long spliceOutTimeUs; - - /** - * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. - */ - public DefaultTrackOutput(Allocator allocator) { - rollingBuffer = new RollingSampleBuffer(allocator); - sampleBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); - needKeyframe = true; - lastReadTimeUs = Long.MIN_VALUE; - spliceOutTimeUs = Long.MIN_VALUE; - } - - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Clears the queue, returning all allocations to the allocator. - */ - public void clear() { - rollingBuffer.clear(); - needKeyframe = true; - lastReadTimeUs = Long.MIN_VALUE; - spliceOutTimeUs = Long.MIN_VALUE; - } - - // Called by the consuming thread. - - /** - * Returns the current upstream {@link Format}. - */ - public Format getUpstreamFormat() { - return rollingBuffer.getUpstreamFormat(); - } - - /** - * The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a - * sample has yet to be received. - */ - public long getLargestParsedTimestampUs() { - return rollingBuffer.getLargestQueuedTimestampUs(); - } - - /** - * True if at least one sample can be read from the queue. False otherwise. - */ - public boolean isEmpty() { - return !advanceToEligibleSample(); - } - - /** - * Removes the next sample from the head of the queue, writing it into the provided buffer. - *

- * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples - * queued prior to the first keyframe are discarded. - * - * @param buffer A {@link DecoderInputBuffer} into which the sample should be read. - * @return True if a sample was read. False otherwise. - */ - public boolean getSample(DecoderInputBuffer buffer) { - boolean foundEligibleSample = advanceToEligibleSample(); - if (!foundEligibleSample) { - return false; - } - // Write the sample into the buffer. - rollingBuffer.readSample(buffer); - needKeyframe = false; - lastReadTimeUs = buffer.timeUs; - return true; - } - - /** - * Skips all currently buffered samples. - */ - public void skipAllSamples() { - rollingBuffer.skipAllSamples(); - } - - /** - * Attempts to configure a splice from this queue to the next. - * - * @param nextQueue The queue being spliced to. - * @return Whether the splice was configured successfully. - */ - public boolean configureSpliceTo(DefaultTrackOutput nextQueue) { - if (spliceOutTimeUs != Long.MIN_VALUE) { - // We've already configured the splice. - return true; - } - long firstPossibleSpliceTime; - if (rollingBuffer.peekSample(sampleBuffer)) { - firstPossibleSpliceTime = sampleBuffer.timeUs; - } else { - firstPossibleSpliceTime = lastReadTimeUs + 1; - } - RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; - while (nextRollingBuffer.peekSample(sampleBuffer) - && (sampleBuffer.timeUs < firstPossibleSpliceTime || !sampleBuffer.isKeyFrame())) { - // Discard samples from the next queue for as long as they are before the earliest possible - // splice time, or not keyframes. - nextRollingBuffer.skipSample(); - } - if (nextRollingBuffer.peekSample(sampleBuffer)) { - // We've found a keyframe in the next queue that can serve as the splice point. Set the - // splice point now. - spliceOutTimeUs = sampleBuffer.timeUs; - return true; - } - return false; - } - - /** - * Advances the underlying buffer to the next sample that is eligible to be returned. - * - * @return True if an eligible sample was found. False otherwise, in which case the underlying - * buffer has been emptied. - */ - private boolean advanceToEligibleSample() { - boolean haveNext = rollingBuffer.peekSample(sampleBuffer); - if (needKeyframe) { - while (haveNext && !sampleBuffer.isKeyFrame()) { - rollingBuffer.skipSample(); - haveNext = rollingBuffer.peekSample(sampleBuffer); - } - } - if (!haveNext) { - return false; - } - if (spliceOutTimeUs != Long.MIN_VALUE && sampleBuffer.timeUs >= spliceOutTimeUs) { - return false; - } - return true; - } - - // Called by the loading thread. - - @Override - public void format(Format format) { - rollingBuffer.format(format); - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - return rollingBuffer.sampleData(input, length, allowEndOfInput); - } - - @Override - public void sampleData(ParsableByteArray buffer, int length) { - rollingBuffer.sampleData(buffer, length); - } - - @Override - public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { - rollingBuffer.sampleMetadata(timeUs, flags, size, offset, encryptionKey); - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java b/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java index 372a9a442f..76bc58c60d 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/RollingSampleBuffer.java @@ -46,11 +46,13 @@ public final class RollingSampleBuffer implements TrackOutput { // Accessed only by the consuming thread. private long totalBytesDropped; - // Accessed only by the loading thread. + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). private long sampleOffsetUs; private long totalBytesWritten; private Allocation lastAllocation; private int lastAllocationOffset; + private boolean needKeyframe; + private boolean pendingSplice; // Accessed by both the loading and consuming threads. private volatile Format upstreamFormat; @@ -66,6 +68,7 @@ public final class RollingSampleBuffer implements TrackOutput { extrasHolder = new BufferExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); lastAllocationOffset = allocationLength; + needKeyframe = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -82,6 +85,15 @@ public final class RollingSampleBuffer implements TrackOutput { totalBytesWritten = 0; lastAllocation = null; lastAllocationOffset = allocationLength; + needKeyframe = true; + } + + /** + * Indicates that samples subsequently queued to the buffer should be spliced into those already + * queued. + */ + public void splice() { + pendingSplice = true; } /** @@ -172,27 +184,6 @@ public final class RollingSampleBuffer implements TrackOutput { return infoQueue.getLargestQueuedTimestampUs(); } - /** - * Fills {@code buffer} with information about the current sample, but does not write its data. - *

- * Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs} and the buffer - * flags. - * - * @param buffer The buffer into which the current sample information should be written. - * @return True if the buffer was filled. False if there is no current sample. - */ - public boolean peekSample(DecoderInputBuffer buffer) { - return infoQueue.peekSample(buffer, extrasHolder); - } - - /** - * Skips the current sample. - */ - public void skipSample() { - long nextOffset = infoQueue.moveToNextSample(); - dropDownstreamTo(nextOffset); - } - /** * Skips all currently buffered samples. */ @@ -227,7 +218,7 @@ public final class RollingSampleBuffer implements TrackOutput { */ public boolean readSample(DecoderInputBuffer buffer) { // Write the sample information into the buffer and extrasHolder. - boolean haveSample = infoQueue.peekSample(buffer, extrasHolder); + boolean haveSample = infoQueue.readSample(buffer, extrasHolder); if (!haveSample) { return false; } @@ -240,8 +231,7 @@ public final class RollingSampleBuffer implements TrackOutput { buffer.ensureSpaceForWrite(buffer.size); readData(extrasHolder.offset, buffer.data, buffer.size); // Advance the read head. - long nextOffset = infoQueue.moveToNextSample(); - dropDownstreamTo(nextOffset); + dropDownstreamTo(extrasHolder.nextOffset); return true; } @@ -396,7 +386,7 @@ public final class RollingSampleBuffer implements TrackOutput { */ public void formatWithOffset(Format format, long sampleOffsetUs) { this.sampleOffsetUs = sampleOffsetUs; - upstreamFormat = getAdjustedSampleFormat(format, sampleOffsetUs); + format(format); } @Override @@ -435,6 +425,22 @@ public final class RollingSampleBuffer implements TrackOutput { @Override public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + if (pendingSplice) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { + return; + } + // TODO - We should be able to actually remove the data from the rolling buffer after a splice + // succeeds, but doing so is a little bit tricky; it requires moving data written after the + // last committed sample. + pendingSplice = false; + } + if (needKeyframe) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // TODO - As above, although this case is probably less worthwhile. + return; + } + needKeyframe = false; + } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey, upstreamFormat); @@ -601,7 +607,8 @@ public final class RollingSampleBuffer implements TrackOutput { /** * Fills {@code buffer} with information about the current sample, but does not write its data. * The absolute position of the sample's data in the rolling buffer is stored in - * {@code extrasHolder}. + * {@code extrasHolder}, along with an encryption id if present, and the absolute position of + * the first byte that may still be required after the current sample has been read. *

* Populates {@link DecoderInputBuffer#size}, {@link DecoderInputBuffer#timeUs}, the buffer * flags and {@code extrasHolder}. @@ -610,7 +617,7 @@ public final class RollingSampleBuffer implements TrackOutput { * @param extrasHolder The holder into which extra sample information should be written. * @return True if the buffer and extras were filled. False if there is no current sample. */ - public synchronized boolean peekSample(DecoderInputBuffer buffer, + public synchronized boolean readSample(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) { if (queueSize == 0) { return false; @@ -620,26 +627,19 @@ public final class RollingSampleBuffer implements TrackOutput { buffer.setFlags(flags[relativeReadIndex]); extrasHolder.offset = offsets[relativeReadIndex]; extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; - return true; - } - /** - * Advances the read index to the next sample. - * - * @return The absolute position of the first byte in the rolling buffer that may still be - * required after advancing the index. Data prior to this position can be dropped. - */ - public synchronized long moveToNextSample() { + largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); queueSize--; - largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, timesUs[relativeReadIndex]); - int lastReadIndex = relativeReadIndex++; + relativeReadIndex++; absoluteReadIndex++; if (relativeReadIndex == capacity) { // Wrap around. relativeReadIndex = 0; } - return queueSize > 0 ? offsets[relativeReadIndex] - : (sizes[lastReadIndex] + offsets[lastReadIndex]); + + extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] + : extrasHolder.offset + buffer.size; + return true; } /** @@ -760,6 +760,26 @@ public final class RollingSampleBuffer implements TrackOutput { } } + /** + * Attempts to discard samples from the tail of the queue to allow samples starting from the + * specified timestamp to be spliced in. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + public synchronized boolean attemptSplice(long timeUs) { + if (largestDequeuedTimestampUs >= timeUs) { + return false; + } + int retainCount = queueSize; + while (retainCount > 0 + && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { + retainCount--; + } + discardUpstreamSamples(absoluteReadIndex + retainCount); + return true; + } + } /** @@ -768,6 +788,7 @@ public final class RollingSampleBuffer implements TrackOutput { private static final class BufferExtrasHolder { public long offset; + public long nextOffset; public byte[] encryptionKeyId; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 27d5b0d1ba..924c21d513 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -399,19 +399,16 @@ public class HlsChunkSource { Format format = variants[variantIndex].format; // Configure the extractor that will read the chunk. - HlsExtractorWrapper extractorWrapper; + Extractor extractor; + boolean extractorNeedsInit = true; String lastPathSegment = chunkUri.getLastPathSegment(); if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 // case below. - Extractor extractor = new AdtsExtractor(startTimeUs); - extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariant); + extractor = new AdtsExtractor(startTimeUs); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - Extractor extractor = new Mp3Extractor(startTimeUs); - extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariant); + extractor = new Mp3Extractor(startTimeUs); } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(false, @@ -422,9 +419,7 @@ public class HlsChunkSource { // a discontinuity sequence greater than the one that this source is trying to start at. return; } - Extractor extractor = new WebvttExtractor(format.language, timestampAdjuster); - extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariant); + extractor = new WebvttExtractor(format.language, timestampAdjuster); } else if (previous == null || previous.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || format != previous.format) { @@ -448,17 +443,16 @@ public class HlsChunkSource { workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; } } - Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); - extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, - switchingVariant); + extractor = new TsExtractor(timestampAdjuster, workaroundFlags); } else { // MPEG-2 TS segments, and we need to continue using the same extractor. - extractorWrapper = previous.extractorWrapper; + extractor = previous.extractor; + extractorNeedsInit = false; } out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, - chunkMediaSequence, segment.discontinuitySequenceNumber, extractorWrapper, encryptionKey, - encryptionIv); + chunkMediaSequence, segment.discontinuitySequenceNumber, extractor, extractorNeedsInit, + switchingVariant, encryptionKey, encryptionIv); } /** @@ -584,15 +578,15 @@ public class HlsChunkSource { private int getNextVariantIndex(TsChunk previous, long playbackPositionUs) { clearStaleBlacklistedVariants(); - long bufferedDurationUs; - if (previous != null) { - // Use start time of the previous chunk rather than its end time because switching format will - // require downloading overlapping segments. - bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs); - } else { - bufferedDurationUs = 0; - } if (enabledVariants.length > 1) { + long bufferedDurationUs; + if (previous != null) { + // Use start time of the previous chunk rather than its end time because switching format + // will require downloading overlapping segments. + bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs); + } else { + bufferedDurationUs = 0; + } adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags, evaluation); } else { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java deleted file mode 100644 index cdcca2771f..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsExtractorWrapper.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.hls; - -import com.google.android.exoplayer.DecoderInputBuffer; -import com.google.android.exoplayer.Format; -import com.google.android.exoplayer.drm.DrmInitData; -import com.google.android.exoplayer.extractor.DefaultTrackOutput; -import com.google.android.exoplayer.extractor.Extractor; -import com.google.android.exoplayer.extractor.ExtractorInput; -import com.google.android.exoplayer.extractor.ExtractorOutput; -import com.google.android.exoplayer.extractor.SeekMap; -import com.google.android.exoplayer.extractor.TrackOutput; -import com.google.android.exoplayer.upstream.Allocator; -import com.google.android.exoplayer.util.Assertions; - -import android.util.SparseArray; - -import java.io.IOException; - -/** - * Wraps a {@link Extractor}, adding functionality to enable reading of the extracted samples. - */ -public final class HlsExtractorWrapper implements ExtractorOutput { - - public final int trigger; - public final Format format; - public final long startTimeUs; - - private final Extractor extractor; - private final SparseArray sampleQueues; - private final boolean shouldSpliceIn; - - private Allocator allocator; - - private volatile boolean tracksBuilt; - - // Accessed only by the consuming thread. - private boolean prepared; - private boolean spliceConfigured; - - public HlsExtractorWrapper(int trigger, Format format, long startTimeUs, Extractor extractor, - boolean shouldSpliceIn) { - this.trigger = trigger; - this.format = format; - this.startTimeUs = startTimeUs; - this.extractor = extractor; - this.shouldSpliceIn = shouldSpliceIn; - sampleQueues = new SparseArray<>(); - } - - /** - * Initializes the wrapper for use. - * - * @param allocator An allocator for obtaining allocations into which extracted data is written. - */ - public void init(Allocator allocator) { - this.allocator = allocator; - extractor.init(this); - } - - /** - * Whether the extractor is prepared. - * - * @return True if the extractor is prepared. False otherwise. - */ - public boolean isPrepared() { - if (!prepared && tracksBuilt) { - for (int i = 0; i < sampleQueues.size(); i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { - return false; - } - } - prepared = true; - } - return prepared; - } - - /** - * Clears queues for all tracks, returning all allocations to the allocator. - */ - public void clear() { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).clear(); - } - } - - /** - * Gets the largest timestamp of any sample parsed by the extractor. - * - * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. - */ - public long getLargestParsedTimestampUs() { - long largestParsedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestParsedTimestampUs = Math.max(largestParsedTimestampUs, - sampleQueues.valueAt(i).getLargestParsedTimestampUs()); - } - return largestParsedTimestampUs; - } - - /** - * Attempts to configure a splice from this extractor to the next. - *

- * The splice is performed such that for each track the samples read from the next extractor - * start with a keyframe, and continue from where the samples read from this extractor finish. - * A successful splice may discard samples from either or both extractors. - *

- * Splice configuration may fail if the next extractor is not yet in a state that allows the - * splice to be performed. Calling this method is a noop if the splice has already been - * configured. Hence this method should be called repeatedly during the window within which a - * splice can be performed. - *

- * This method must only be called after the extractor has been prepared. - * - * @param nextExtractor The extractor being spliced to. - */ - public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) { - Assertions.checkState(isPrepared()); - if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { - // The splice is already configured, or the next extractor doesn't want to be spliced in, or - // the next extractor isn't ready to be spliced in. - return; - } - boolean spliceConfigured = true; - int trackCount = getTrackCount(); - for (int i = 0; i < trackCount; i++) { - DefaultTrackOutput currentSampleQueue = sampleQueues.valueAt(i); - DefaultTrackOutput nextSampleQueue = nextExtractor.sampleQueues.valueAt(i); - spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue); - } - this.spliceConfigured = spliceConfigured; - return; - } - - /** - * Gets the number of available tracks. - *

- * This method must only be called after the extractor has been prepared. - * - * @return The number of available tracks. - */ - public int getTrackCount() { - Assertions.checkState(isPrepared()); - return sampleQueues.size(); - } - - /** - * Gets the {@link Format} of the samples belonging to a specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @param track The track index. - * @return The corresponding sample format. - */ - public Format getSampleFormat(int track) { - Assertions.checkState(isPrepared()); - return sampleQueues.valueAt(track).getUpstreamFormat(); - } - - /** - * Gets the next sample for the specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @param track The track from which to read. - * @param buffer A {@link DecoderInputBuffer} to populate with a sample. - * @return True if a sample was read. False otherwise. - */ - public boolean getSample(int track, DecoderInputBuffer buffer) { - Assertions.checkState(isPrepared()); - return sampleQueues.valueAt(track).getSample(buffer); - } - - /** - * Discards all samples for the specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @param track The track from which samples should be discarded. - */ - public void discardSamplesForTrack(int track) { - Assertions.checkState(isPrepared()); - sampleQueues.valueAt(track).skipAllSamples(); - } - - /** - * Whether samples are available for reading from {@link #getSample(int, DecoderInputBuffer)} for - * the specified track. - *

- * This method must only be called after the extractor has been prepared. - * - * @return True if samples are available for reading from - * {@link #getSample(int, DecoderInputBuffer)} for the specified track. False otherwise. - */ - public boolean hasSamples(int track) { - Assertions.checkState(isPrepared()); - return !sampleQueues.valueAt(track).isEmpty(); - } - - /** - * Reads from the provided {@link ExtractorInput}. - * - * @param input The {@link ExtractorInput} from which to read. - * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. - * @throws IOException If an error occurred reading from the source. - * @throws InterruptedException If the thread was interrupted. - */ - public int read(ExtractorInput input) throws IOException, InterruptedException { - int result = extractor.read(input, null); - Assertions.checkState(result != Extractor.RESULT_SEEK); - return result; - } - - // ExtractorOutput implementation. - - @Override - public TrackOutput track(int id) { - DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); - sampleQueues.put(id, sampleQueue); - return sampleQueue; - } - - @Override - public void endTracks() { - this.tracksBuilt = true; - } - - @Override - public void seekMap(SeekMap seekMap) { - // Do nothing. - } - - @Override - public void drmInitData(DrmInitData drmInit) { - // Do nothing. - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsOutput.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsOutput.java new file mode 100644 index 0000000000..4d7a9919da --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsOutput.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 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.exoplayer.hls; + +import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.extractor.ExtractorOutput; +import com.google.android.exoplayer.extractor.RollingSampleBuffer; +import com.google.android.exoplayer.extractor.SeekMap; +import com.google.android.exoplayer.upstream.Allocator; + +import android.util.SparseArray; + +/** + * An {@link ExtractorOutput} for HLS playbacks. + */ +/* package */ final class HlsOutput implements ExtractorOutput { + + private final Allocator allocator; + private final SparseArray sampleQueues = new SparseArray<>(); + + private boolean prepared; + private RollingSampleBuffer[] trackOutputArray; + private volatile boolean tracksBuilt; + + public HlsOutput(Allocator allocator) { + this.allocator = allocator; + } + + // Called by the consuming thread. + + /** + * Prepares the output, or does nothing if the output is already prepared. + * + * @return True if the output is prepared, false otherwise. + */ + public boolean prepare() { + if (prepared) { + return true; + } else if (!tracksBuilt) { + return false; + } else { + if (trackOutputArray == null) { + trackOutputArray = new RollingSampleBuffer[sampleQueues.size()]; + for (int i = 0; i < trackOutputArray.length; i++) { + trackOutputArray[i] = sampleQueues.valueAt(i); + } + } + for (RollingSampleBuffer sampleQueue : trackOutputArray) { + if (sampleQueue.getUpstreamFormat() == null) { + return false; + } + } + prepared = true; + return true; + } + } + + /** + * Returns the array of track outputs, or null if the output is not yet prepared. + */ + public RollingSampleBuffer[] getTrackOutputs() { + return trackOutputArray; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** + * Clears all track outputs. + */ + public void clear() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); + } + } + + /** + * Indicates to all track outputs that they should splice in subsequently queued samples. + */ + public void splice() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).splice(); + } + } + + // ExtractorOutput implementation. Called by the loading thread. + + @Override + public RollingSampleBuffer track(int id) { + if (sampleQueues.indexOfKey(id) >= 0) { + return sampleQueues.get(id); + } + RollingSampleBuffer trackOutput = new RollingSampleBuffer(allocator); + sampleQueues.put(id, trackOutput); + return trackOutput; + } + + @Override + public void endTracks() { + tracksBuilt = true; + } + + @Override + public void seekMap(SeekMap seekMap) { + // Do nothing. + } + + @Override + public void drmInitData(DrmInitData drmInitData) { + // Do nothing. + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index 43380f749c..bf53335225 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkHolder; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener.EventDispatcher; +import com.google.android.exoplayer.extractor.RollingSampleBuffer; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; @@ -59,7 +60,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private final Loader loader; private final HlsChunkSource chunkSource; - private final LinkedList extractors; + private final LinkedList tsChunks = new LinkedList(); + private final HlsOutput output; private final int bufferSizeContribution; private final ChunkHolder nextChunkHolder; private final EventDispatcher eventDispatcher; @@ -69,6 +71,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { private boolean seenFirstTrackSelection; private int enabledTrackCount; + private RollingSampleBuffer[] trackOutputs; private Format downstreamFormat; // Tracks are complicated in HLS. See documentation of buildTracks for details. @@ -112,7 +115,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { this.pendingResetPositionUs = C.UNSET_TIME_US; loader = new Loader("Loader:HLS", minLoadableRetryCount); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); - extractors = new LinkedList<>(); + output = new HlsOutput(loadControl.getAllocator()); nextChunkHolder = new ChunkHolder(); } @@ -131,20 +134,11 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { prepared = true; return true; } - if (!extractors.isEmpty()) { - while (true) { - // We're not prepared, but we might have loaded what we need. - HlsExtractorWrapper extractor = extractors.getFirst(); - if (extractor.isPrepared()) { - buildTracks(extractor); - prepared = true; - return true; - } else if (extractors.size() > 1) { - extractors.removeFirst().clear(); - } else { - break; - } - } + if (output.prepare()) { + trackOutputs = output.getTrackOutputs(); + buildTracks(); + prepared = true; + return true; } // We're not prepared. maybeThrowError(); @@ -223,9 +217,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { @Override public void continueBuffering(long playbackPositionUs) { downstreamPositionUs = playbackPositionUs; - if (!extractors.isEmpty()) { - discardSamplesForDisabledTracks(getCurrentExtractor()); - } + discardSamplesForDisabledTracks(); if (!loader.isLoading()) { maybeStartLoading(); } @@ -238,20 +230,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { } else if (loadingFinished) { return C.END_OF_SOURCE_US; } else { - long bufferedPositionUs = extractors.getLast().getLargestParsedTimestampUs(); - if (extractors.size() > 1) { - // When adapting from one format to the next, the penultimate extractor may have the largest - // parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet). - bufferedPositionUs = Math.max(bufferedPositionUs, - extractors.get(extractors.size() - 2).getLargestParsedTimestampUs()); - } + long bufferedPositionUs = downstreamPositionUs; if (previousTsLoadable != null) { // Buffered position should be at least as large as the end time of the previously loaded // chunk. bufferedPositionUs = Math.max(previousTsLoadable.endTimeUs, bufferedPositionUs); } - return bufferedPositionUs == Long.MIN_VALUE ? downstreamPositionUs - : bufferedPositionUs; + for (RollingSampleBuffer trackOutput : trackOutputs) { + bufferedPositionUs = Math.max(bufferedPositionUs, + trackOutput.getLargestQueuedTimestampUs()); + } + return bufferedPositionUs; } } @@ -276,19 +265,10 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (loadingFinished) { return true; } - if (isPendingReset() || extractors.isEmpty()) { + if (isPendingReset()) { return false; } - for (int extractorIndex = 0; extractorIndex < extractors.size(); extractorIndex++) { - HlsExtractorWrapper extractor = extractors.get(extractorIndex); - if (!extractor.isPrepared()) { - break; - } - if (extractor.hasSamples(group)) { - return true; - } - } - return false; + return !trackOutputs[group].isEmpty(); } /* package */ void maybeThrowError() throws IOException { @@ -309,53 +289,41 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { return TrackStream.NOTHING_READ; } - HlsExtractorWrapper extractor = getCurrentExtractor(); - if (!extractor.isPrepared()) { + TsChunk currentChunk = tsChunks.getFirst(); + Format currentFormat = currentChunk.format; + if (downstreamFormat == null || !downstreamFormat.equals(currentFormat)) { + eventDispatcher.downstreamFormatChanged(currentFormat, currentChunk.trigger, + currentChunk.startTimeUs); + downstreamFormat = currentFormat; + } + + RollingSampleBuffer sampleQueue = trackOutputs[group]; + if (sampleQueue.isEmpty()) { + if (loadingFinished) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return TrackStream.BUFFER_READ; + } return TrackStream.NOTHING_READ; } - if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) { - // Notify a change in the downstream format. - eventDispatcher.downstreamFormatChanged(extractor.format, extractor.trigger, - extractor.startTimeUs); - downstreamFormat = extractor.format; - } - - if (extractors.size() > 1) { - // If there's more than one extractor, attempt to configure a seamless splice from the - // current one to the next one. - extractor.configureSpliceTo(extractors.get(1)); - } - - int extractorIndex = 0; - while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(group)) { - // We're finished reading from the extractor for this particular track, so advance to the - // next one for the current read. - extractor = extractors.get(++extractorIndex); - if (!extractor.isPrepared()) { - return TrackStream.NOTHING_READ; - } - } - - Format sampleFormat = extractor.getSampleFormat(group); - if (sampleFormat != null && !sampleFormat.equals(downstreamSampleFormats[group])) { + Format sampleFormat = sampleQueue.getDownstreamFormat(); + if (!sampleFormat.equals(downstreamSampleFormats[group])) { formatHolder.format = sampleFormat; downstreamSampleFormats[group] = sampleFormat; return TrackStream.FORMAT_READ; } - if (extractor.getSample(group, buffer)) { - if (buffer.timeUs < lastSeekPositionUs) { + if (sampleQueue.readSample(buffer)) { + long sampleTimeUs = buffer.timeUs; + while (tsChunks.size() > 1 && tsChunks.get(1).startTimeUs <= sampleTimeUs) { + tsChunks.removeFirst(); + } + if (sampleTimeUs < lastSeekPositionUs) { buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } return TrackStream.BUFFER_READ; } - if (loadingFinished) { - buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - return TrackStream.BUFFER_READ; - } - return TrackStream.NOTHING_READ; } @@ -441,17 +409,15 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { * effect of selecting an extractor track, leaving the selected track on the chunk source * unchanged. * - * - * @param extractor The prepared extractor. */ - private void buildTracks(HlsExtractorWrapper extractor) { + private void buildTracks() { // Iterate through the extractor tracks to discover the "primary" track type, and the index // of the single track of this type. int primaryExtractorTrackType = PRIMARY_TYPE_NONE; int primaryExtractorTrackIndex = -1; - int extractorTrackCount = extractor.getTrackCount(); + int extractorTrackCount = trackOutputs.length; for (int i = 0; i < extractorTrackCount; i++) { - String sampleMimeType = extractor.getSampleFormat(i).sampleMimeType; + String sampleMimeType = trackOutputs[i].getUpstreamFormat().sampleMimeType; int trackType; if (MimeTypes.isVideo(sampleMimeType)) { trackType = PRIMARY_TYPE_VIDEO; @@ -484,7 +450,7 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { // Construct the set of exposed track groups. TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; for (int i = 0; i < extractorTrackCount; i++) { - Format sampleFormat = extractor.getSampleFormat(i); + Format sampleFormat = trackOutputs[i].getUpstreamFormat(); if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; for (int j = 0; j < chunkSourceTrackCount; j++) { @@ -550,49 +516,17 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { restartFrom(positionUs); } - /** - * Gets the current extractor from which samples should be read. - *

- * Calling this method discards extractors without any samples from the front of the queue. The - * last extractor is retained even if it doesn't have any samples. - *

- * This method must not be called unless {@link #extractors} is non-empty. - * - * @return The current extractor from which samples should be read. Guaranteed to be non-null. - */ - private HlsExtractorWrapper getCurrentExtractor() { - HlsExtractorWrapper extractor = extractors.getFirst(); - while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { - // We're finished reading from the extractor for all tracks, and so can discard it. - extractors.removeFirst().clear(); - extractor = extractors.getFirst(); - } - return extractor; - } - - private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor) { - if (!extractor.isPrepared()) { + private void discardSamplesForDisabledTracks() { + if (!output.prepare()) { return; } for (int i = 0; i < groupEnabledStates.length; i++) { if (!groupEnabledStates[i]) { - extractor.discardSamplesForTrack(i); + trackOutputs[i].skipAllSamples(); } } } - private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) { - if (!extractor.isPrepared()) { - return false; - } - for (int i = 0; i < groupEnabledStates.length; i++) { - if (groupEnabledStates[i] && extractor.hasSamples(i)) { - return true; - } - } - return false; - } - private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; @@ -605,10 +539,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { } private void clearState() { - for (int i = 0; i < extractors.size(); i++) { - extractors.get(i).clear(); - } - extractors.clear(); + tsChunks.clear(); + output.clear(); clearCurrentLoadable(); previousTsLoadable = null; } @@ -651,11 +583,8 @@ public final class HlsSampleSource implements SampleSource, Loader.Callback { if (isPendingReset()) { pendingResetPositionUs = C.UNSET_TIME_US; } - HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper; - if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) { - extractorWrapper.init(loadControl.getAllocator()); - extractors.addLast(extractorWrapper); - } + tsChunk.init(output); + tsChunks.addLast(tsChunk); eventDispatcher.loadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger, tsChunk.format, tsChunk.startTimeUs, tsChunk.endTimeUs); currentTsLoadable = tsChunk; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java index bfa6101f8e..7784232422 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/TsChunk.java @@ -37,11 +37,13 @@ public final class TsChunk extends MediaChunk { public final int discontinuitySequenceNumber; /** - * The wrapped extractor into which this chunk is being consumed. + * The extractor into which this chunk is being consumed. */ - public final HlsExtractorWrapper extractorWrapper; + public final Extractor extractor; private final boolean isEncrypted; + private final boolean extractorNeedsInit; + private final boolean shouldSpliceIn; private int bytesLoaded; private volatile boolean loadCanceled; @@ -53,23 +55,45 @@ public final class TsChunk extends MediaChunk { * @param format The format of the stream to which this chunk belongs. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. * @param chunkIndex The index of the chunk. - * @param extractorWrapper A wrapped extractor to parse samples from the data. + * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. + * @param extractor The extractor to parse samples from the data. + * @param extractorNeedsInit Whether the extractor needs initializing with the target + * {@link HlsOutput}. + * @param shouldSpliceIn Whether the samples parsed from this chunk should be spliced into any + * samples already queued to the {@link HlsOutput}. * @param encryptionKey For AES encryption chunks, the encryption key. * @param encryptionIv For AES encryption chunks, the encryption initialization vector. */ public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, - HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) { + Extractor extractor, boolean extractorNeedsInit, boolean shouldSpliceIn, + byte[] encryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.extractorWrapper = extractorWrapper; + this.extractor = extractor; + this.extractorNeedsInit = extractorNeedsInit; + this.shouldSpliceIn = shouldSpliceIn; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; } + /** + * Initializes the chunk for loading, setting the {@link HlsOutput} that will receive samples as + * they are loaded. + * + * @param output The output that will receive the loaded samples. + */ + public void init(HlsOutput output) { + if (shouldSpliceIn) { + output.splice(); + } + if (extractorNeedsInit) { + extractor.init(output); + } + } + @Override public long bytesLoaded() { return bytesLoaded; @@ -102,7 +126,6 @@ public final class TsChunk extends MediaChunk { loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); skipLoadedBytes = false; } - try { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); @@ -112,7 +135,7 @@ public final class TsChunk extends MediaChunk { try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);