diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java new file mode 100644 index 0000000000..68761cef19 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2019 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; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; +import com.google.android.exoplayer2.upstream.Allocation; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** A queue of media sample data. */ +/* package */ class SampleDataQueue { + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + private final ParsableByteArray scratch; + + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; + private AllocationNode writeAllocationNode; + + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private long totalBytesWritten; + + public SampleDataQueue(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** Clears all sample data. */ + public void reset() { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); + } + + /** + * Discards sample data bytes from the write side of the queue. + * + * @param totalBytesWritten The reduced total number of bytes written after the samples have been + * discarded, or 0 if the queue is now empty. + */ + public void discardUpstreamSampleBytes(long totalBytesWritten) { + this.totalBytesWritten = totalBytesWritten; + if (this.totalBytesWritten == 0 + || this.totalBytesWritten == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } else { + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (this.totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = + this.totalBytesWritten == lastNodeToKeep.endPosition + ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; + } + } + } + + // Called by the consuming thread. + + /** Rewinds the read position to the first sample in the queue. */ + public void rewind() { + readAllocationNode = firstAllocationNode; + } + + /** + * Reads data from the rolling buffer to populate a decoder input buffer. + * + * @param buffer The buffer to populate. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Read sample data, extracting supplemental data into a separate buffer if needed. + if (buffer.hasSupplementalData()) { + // If there is supplemental data, the sample data is prefixed by its size. + scratch.reset(4); + readData(extrasHolder.offset, scratch.data, 4); + int sampleSize = scratch.readUnsignedIntToInt(); + extrasHolder.offset += 4; + extrasHolder.size -= 4; + + // Write the sample data. + buffer.ensureSpaceForWrite(sampleSize); + readData(extrasHolder.offset, buffer.data, sampleSize); + extrasHolder.offset += sampleSize; + extrasHolder.size -= sampleSize; + + // Write the remaining data as supplemental data. + buffer.resetSupplementalData(extrasHolder.size); + readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); + } else { + // Write the sample data. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in + * which case calling this method is a no-op. + */ + public void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } + while (absolutePosition >= firstAllocationNode.endPosition) { + // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are + // advanced past, and return their underlying allocations to the allocator. + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); + } + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + // We discarded the node referenced by readAllocationNode. We need to advance it to the first + // remaining node. + readAllocationNode = firstAllocationNode; + } + } + + // Called by the loading thread. + + public long getTotalBytesWritten() { + return totalBytesWritten; + } + + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + 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(); + } + postAppend(bytesAppended); + return bytesAppended; + } + + public void sampleData(ParsableByteArray buffer, int length) { + while (length > 0) { + int bytesAppended = preAppend(length); + buffer.readBytes( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); + } + } + + // Private methods. + + /** + * Reads encryption data for the current sample. + * + *
The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link + * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same + * value is added to {@link SampleExtrasHolder#offset}. + * + * @param buffer The buffer into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + scratch.reset(1); + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + if (buffer.cryptoInfo.iv == null) { + buffer.cryptoInfo.iv = new byte[16]; + } + readData(offset, buffer.cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + scratch.reset(2); + readData(offset, scratch.data, 2); + offset += 2; + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + scratch.reset(subsampleDataLength); + readData(offset, scratch.data, subsampleDataLength); + offset += subsampleDataLength; + scratch.setPosition(0); + for (int i = 0; i < subsampleCount; i++) { + clearDataSizes[i] = scratch.readUnsignedShort(); + encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); + } + } else { + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + CryptoData cryptoData = extrasHolder.cryptoData; + buffer.cryptoInfo.set( + subsampleCount, + clearDataSizes, + encryptedDataSizes, + cryptoData.encryptionKey, + buffer.cryptoInfo.iv, + cryptoData.cryptoMode, + cryptoData.encryptedBlocks, + cryptoData.clearBlocks); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + extrasHolder.size -= bytesRead; + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The array into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy( + allocation.data, + readAllocationNode.translateOffset(absolutePosition), + target, + length - remaining, + toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = + (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) + / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); + } + + /** + * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link + * #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. + */ + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { + writeAllocationNode.initialize( + allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + } + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } + } + + /** A node in a linked list of {@link Allocation}s held by the output. */ + private static final class AllocationNode { + + /** The absolute position of the start of the data (inclusive). */ + public final long startPosition; + /** The absolute position of the end of the data (exclusive). */ + public final long endPosition; + /** Whether the node has been initialized. Remains true after {@link #clear()}. */ + public boolean wasInitialized; + /** The {@link Allocation}, or {@code null} if the node is not initialized. */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + + /** + * Clears {@link #allocation} and {@link #next}. + * + * @return The cleared next {@link AllocationNode}. + */ + public AllocationNode clear() { + allocation = null; + AllocationNode temp = next; + next = null; + return temp; + } + } +} 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 deleted file mode 100644 index bb578ddec7..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ /dev/null @@ -1,721 +0,0 @@ -/* - * 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; - -import android.os.Looper; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmSession; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; - -/** - * A queue of metadata describing the contents of a media buffer. - */ -/* package */ final class SampleMetadataQueue { - - /** - * A holder for sample metadata not held by {@link DecoderInputBuffer}. - */ - public static final class SampleExtrasHolder { - - public int size; - public long offset; - public CryptoData cryptoData; - - } - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private final DrmSessionManager> drmSessionManager; - - @Nullable private Format downstreamFormat; - @Nullable private DrmSession> currentDrmSession; - - private int capacity; - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private CryptoData[] cryptoDatas; - private Format[] formats; - - private int length; - private int absoluteFirstIndex; - private int relativeFirstIndex; - private int readPosition; - - private long largestDiscardedTimestampUs; - private long largestQueuedTimestampUs; - private boolean isLastSampleQueued; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private Format upstreamCommittedFormat; - private int upstreamSourceId; - - public SampleMetadataQueue(DrmSessionManager> drmSessionManager) { - this.drmSessionManager = drmSessionManager; - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - cryptoDatas = new CryptoData[capacity]; - formats = new Format[capacity]; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Clears all sample metadata from the queue. - * - * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, - * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) - * are assumed to have the current upstream format. If set to true, {@link #format(Format)} - * must be called after the reset before any more samples can be queued. - */ - public void reset(boolean resetUpstreamFormat) { - length = 0; - absoluteFirstIndex = 0; - relativeFirstIndex = 0; - readPosition = 0; - upstreamKeyframeRequired = true; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - isLastSampleQueued = false; - upstreamCommittedFormat = null; - if (resetUpstreamFormat) { - upstreamFormat = null; - upstreamFormatRequired = true; - } - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteFirstIndex + length; - } - - /** - * Discards samples from the write side of the queue. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written after the samples have been discarded, or 0 - * if the queue is now empty. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); - length -= discardCount; - largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); - isLastSampleQueued = discardCount == 0 && isLastSampleQueued; - if (length == 0) { - return 0; - } else { - int relativeLastWriteIndex = getRelativeIndex(length - 1); - return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; - } - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Throws an error that's preventing data from being read. Does nothing if no such error exists. - * - * @throws IOException The underlying error. - */ - public void maybeThrowError() throws IOException { - // TODO: Avoid throwing if the DRM error is not preventing a read operation. - if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { - throw Assertions.checkNotNull(currentDrmSession.getError()); - } - } - - /** Releases any owned {@link DrmSession} references. */ - public void releaseDrmSessionReferences() { - if (currentDrmSession != null) { - currentDrmSession.release(); - currentDrmSession = null; - // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData - // != null implies currentSession != null - downstreamFormat = null; - } - } - - /** Returns the current absolute start index. */ - public int getFirstIndex() { - return absoluteFirstIndex; - } - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteFirstIndex + readPosition; - } - - /** - * Peeks the source id of the next sample to be read, or the current upstream source id if the - * queue is empty or if the read position is at the end of the queue. - * - * @return The source id. - */ - public synchronized int peekSourceId() { - int relativeReadIndex = getRelativeIndex(readPosition); - return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last call to - * {@link #reset(boolean)}. - *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return largestQueuedTimestampUs; - } - - /** - * Returns whether the last sample of the stream has knowingly been queued. A return value of - * {@code false} means that the last sample had not been queued or that it's unknown whether the - * last sample has been queued. - * - *
Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - */ - public synchronized boolean isLastSampleQueued() { - return isLastSampleQueued; - } - - /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public synchronized long getFirstTimestampUs() { - return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; - } - - /** - * Rewinds the read position to the first sample retained in the queue. - */ - public synchronized void rewind() { - readPosition = 0; - } - - /** - * Returns whether there is data available for reading. - * - *
Note: If the stream has ended then a buffer with the end of stream flag can always be read - * from {@link #read}. Hence an ended stream is always ready. - * - * @param loadingFinished Whether no more samples will be written to the sample queue. When true, - * this method returns true if the sample queue is empty, because an empty sample queue means - * the end of stream has been reached. When false, this method returns false if the sample - * queue is empty. - */ - public boolean isReady(boolean loadingFinished) { - if (!hasNextSample()) { - return loadingFinished - || isLastSampleQueued - || (upstreamFormat != null && upstreamFormat != downstreamFormat); - } - int relativeReadIndex = getRelativeIndex(readPosition); - if (formats[relativeReadIndex] != downstreamFormat) { - // A format can be read. - return true; - } - return mayReadSample(relativeReadIndex); - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information about - * the sample, but not its data. The size and absolute position of the data in the rolling - * buffer is stored in {@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. If a {@link DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only - * the buffer flags may be populated by this method and the read position of the queue will - * not change. May be null if the caller requires that the format of the stream be read even - * if it's not changing. - * @param formatRequired Whether the caller requires that the format of the stream be read even if - * it's not changing. A sample will never be read if set to true, however it is still possible - * for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int read( - FormatHolder formatHolder, - DecoderInputBuffer buffer, - boolean formatRequired, - boolean loadingFinished, - SampleExtrasHolder extrasHolder) { - if (!hasNextSample()) { - if (loadingFinished || isLastSampleQueued) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { - onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - onFormatResult(formats[relativeReadIndex], formatHolder); - return C.RESULT_FORMAT_READ; - } - - if (!mayReadSample(relativeReadIndex)) { - return C.RESULT_NOTHING_READ; - } - - buffer.setFlags(flags[relativeReadIndex]); - buffer.timeUs = timesUs[relativeReadIndex]; - if (buffer.isFlagsOnly()) { - return C.RESULT_BUFFER_READ; - } - - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; - - readPosition++; - return C.RESULT_BUFFER_READ; - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe) in the - * queue. - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A - * successful advance is one in which the read position was unchanged or advanced, and is now - * at a sample meeting the specified criteria. - */ - public synchronized int advanceTo(long timeUs, boolean toKeyframe, - boolean allowTimeBeyondBuffer) { - int relativeReadIndex = getRelativeIndex(readPosition); - if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] - || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { - return SampleQueue.ADVANCE_FAILED; - } - int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); - if (offset == -1) { - return SampleQueue.ADVANCE_FAILED; - } - readPosition += offset; - return offset; - } - - /** - * Advances the read position to the end of the queue. - * - * @return The number of samples that were skipped. - */ - public synchronized int advanceToEnd() { - int skipCount = length - readPosition; - readPosition = length; - return skipCount; - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public synchronized boolean setReadPosition(int sampleIndex) { - if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { - readPosition = sampleIndex - absoluteFirstIndex; - return true; - } - return false; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. - * - * @param timeUs The time to discard up to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than just any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the read - * position. If false then samples at and beyond the read position may be discarded, in which - * case the read position is advanced to the first remaining sample. - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { - return C.POSITION_UNSET; - } - int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; - int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); - if (discardCount == -1) { - return C.POSITION_UNSET; - } - return discardSamples(discardCount); - } - - /** - * Discards samples up to but not including the read position. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToRead() { - if (readPosition == 0) { - return C.POSITION_UNSET; - } - return discardSamples(readPosition); - } - - /** - * Discards all samples in the queue. The read position is also advanced. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToEnd() { - if (length == 0) { - return C.POSITION_UNSET; - } - return discardSamples(length); - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - upstreamFormatRequired = false; - if (Util.areEqual(format, upstreamFormat)) { - // The format is unchanged. If format and upstreamFormat are different objects, we keep the - // current upstreamFormat so we can detect format changes in read() using cheap referential - // equality. - return false; - } else if (Util.areEqual(format, upstreamCommittedFormat)) { - // The format has changed back to the format of the last committed sample. If they are - // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat so - // we can detect format changes in read() using cheap referential equality. - upstreamFormat = upstreamCommittedFormat; - return true; - } else { - upstreamFormat = format; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, CryptoData cryptoData) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - - isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - - int relativeEndIndex = getRelativeIndex(length); - timesUs[relativeEndIndex] = timeUs; - offsets[relativeEndIndex] = offset; - sizes[relativeEndIndex] = size; - flags[relativeEndIndex] = sampleFlags; - cryptoDatas[relativeEndIndex] = cryptoData; - formats[relativeEndIndex] = upstreamFormat; - sourceIds[relativeEndIndex] = upstreamSourceId; - upstreamCommittedFormat = upstreamFormat; - - length++; - if (length == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeFirstIndex; - System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeFirstIndex; - System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); - System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); - System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); - System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - cryptoDatas = newCryptoDatas; - formats = newFormats; - sourceIds = newSourceIds; - relativeFirstIndex = 0; - length = capacity; - capacity = newCapacity; - } - } - - /** - * Attempts to discard samples from the end of the queue to allow samples starting from the - * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (length == 0) { - return timeUs > largestDiscardedTimestampUs; - } - long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(readPosition)); - if (largestReadTimestampUs >= timeUs) { - return false; - } - int retainCount = length; - int relativeSampleIndex = getRelativeIndex(length - 1); - while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { - retainCount--; - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - discardUpstreamSamples(absoluteFirstIndex + retainCount); - return true; - } - - // Internal methods. - - private boolean hasNextSample() { - return readPosition != length; - } - - /** - * Sets the downstream format, performs DRM resource management, and populates the {@code - * outputFormatHolder}. - * - * @param newFormat The new downstream format. - * @param outputFormatHolder The output {@link FormatHolder}. - */ - private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { - outputFormatHolder.format = newFormat; - boolean isFirstFormat = downstreamFormat == null; - DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; - downstreamFormat = newFormat; - if (drmSessionManager == DrmSessionManager.DUMMY) { - // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that - // the media source creation has not yet been migrated and the renderer can acquire the - // session for the read DRM init data. - // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. - return; - } - DrmInitData newDrmInitData = newFormat.drmInitData; - outputFormatHolder.includesDrmSession = true; - outputFormatHolder.drmSession = currentDrmSession; - if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { - // Nothing to do. - return; - } - // Ensure we acquire the new session before releasing the previous one in case the same session - // is being used for both DrmInitData. - DrmSession> previousSession = currentDrmSession; - Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); - currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); - outputFormatHolder.drmSession = currentDrmSession; - - if (previousSession != null) { - previousSession.release(); - } - } - - /** - * Returns whether it's possible to read the next sample. - * - * @param relativeReadIndex The relative read index of the next sample. - * @return Whether it's possible to read the next sample. - */ - private boolean mayReadSample(int relativeReadIndex) { - if (drmSessionManager == DrmSessionManager.DUMMY) { - // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. - // For protected content it's likely that the DrmSessionManager is still being injected into - // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. - return true; - } - return currentDrmSession == null - || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS - || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 - && currentDrmSession.playClearSamplesWithoutKeys()); - } - - /** - * Finds the sample in the specified range that's before or at the specified time. If {@code - * keyframe} is {@code true} then the sample is additionally required to be a keyframe. - * - * @param relativeStartIndex The relative index from which to start searching. - * @param length The length of the range being searched. - * @param timeUs The specified time. - * @param keyframe Whether only keyframes should be considered. - * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching - * sample was found. - */ - private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { - // This could be optimized to use a binary search, however in practice callers to this method - // normally pass times near to the start of the search region. Hence it's unclear whether - // switching to a binary search would yield any real benefit. - int sampleCountToTarget = -1; - int searchIndex = relativeStartIndex; - for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { - if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a suitable sample. - sampleCountToTarget = i; - } - searchIndex++; - if (searchIndex == capacity) { - searchIndex = 0; - } - } - return sampleCountToTarget; - } - - /** - * Discards the specified number of samples. - * - * @param discardCount The number of samples to discard. - * @return The corresponding offset up to which data should be discarded. - */ - private long discardSamples(int discardCount) { - largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(discardCount)); - length -= discardCount; - absoluteFirstIndex += discardCount; - relativeFirstIndex += discardCount; - if (relativeFirstIndex >= capacity) { - relativeFirstIndex -= capacity; - } - readPosition -= discardCount; - if (readPosition < 0) { - readPosition = 0; - } - if (length == 0) { - int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; - return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; - } else { - return offsets[relativeFirstIndex]; - } - } - - /** - * Finds the largest timestamp of any sample from the start of the queue up to the specified - * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of - * the keyframe itself, and of subsequent frames. - * - * @param length The length of the range being searched. - * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. - */ - private long getLargestTimestamp(int length) { - if (length == 0) { - return Long.MIN_VALUE; - } - long largestTimestampUs = Long.MIN_VALUE; - int relativeSampleIndex = getRelativeIndex(length - 1); - for (int i = 0; i < length; i++) { - largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); - if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - return largestTimestampUs; - } - - /** - * Returns the relative index for a given offset from the start of the queue. - * - * @param offset The offset, which must be in the range [0, length]. - */ - private int getRelativeIndex(int offset) { - int relativeIndex = relativeFirstIndex + offset; - return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; - } - -} 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 1230b45fe4..8eb3bfcb0a 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2019 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. @@ -15,29 +15,28 @@ */ package com.google.android.exoplayer2.source; +import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder; -import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.EOFException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.nio.ByteBuffer; /** A queue of media samples. */ public class SampleQueue implements TrackOutput { - /** - * A listener for changes to the upstream format. - */ + /** A listener for changes to the upstream format. */ public interface UpstreamFormatChangedListener { /** @@ -46,31 +45,47 @@ public class SampleQueue implements TrackOutput { * @param format The new upstream format. */ void onUpstreamFormatChanged(Format format); - } public static final int ADVANCE_FAILED = -1; - private static final int INITIAL_SCRATCH_SIZE = 32; + private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - private final Allocator allocator; - private final int allocationLength; - private final SampleMetadataQueue metadataQueue; + private final SampleDataQueue sampleDataQueue; private final SampleExtrasHolder extrasHolder; - private final ParsableByteArray scratch; + private final DrmSessionManager> drmSessionManager; + private UpstreamFormatChangedListener upstreamFormatChangeListener; - // References into the linked list of allocations. - private AllocationNode firstAllocationNode; - private AllocationNode readAllocationNode; - private AllocationNode writeAllocationNode; + @Nullable private Format downstreamFormat; + @Nullable private DrmSession> currentDrmSession; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private CryptoData[] cryptoDatas; + private Format[] formats; + + private int length; + private int absoluteFirstIndex; + private int relativeFirstIndex; + private int readPosition; + + private long largestDiscardedTimestampUs; + private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private Format upstreamFormat; + private Format upstreamCommittedFormat; + private int upstreamSourceId; - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). private boolean pendingFormatAdjustment; private Format lastUnadjustedFormat; private long sampleOffsetUs; - private long totalBytesWritten; private boolean pendingSplice; - private UpstreamFormatChangedListener upstreamFormatChangeListener; /** * Creates a sample queue. @@ -80,27 +95,40 @@ public class SampleQueue implements TrackOutput { * from. The created instance does not take ownership of this {@link DrmSessionManager}. */ public SampleQueue(Allocator allocator, DrmSessionManager> drmSessionManager) { - this.allocator = allocator; - allocationLength = allocator.getIndividualAllocationLength(); - metadataQueue = new SampleMetadataQueue(drmSessionManager); + sampleDataQueue = new SampleDataQueue(allocator); + this.drmSessionManager = drmSessionManager; extrasHolder = new SampleExtrasHolder(); - scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } - // Called by the consuming thread, but only when there is no loading thread. + // Called by the consuming thread when there is no loading thread. /** - * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}. + * Calls {@link #reset(boolean) reset(true)} and releases any owned {@link DrmSession} references. */ + public void release() { + reset(/* resetUpstreamFormat= */ true); + releaseDrmSessionReferences(); + } + + /** Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}. */ public void reset() { - reset(false); + reset(/* resetUpstreamFormat= */ false); } /** - * Resets the output. + * Clears all samples from the queue. * * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) @@ -108,13 +136,20 @@ public class SampleQueue implements TrackOutput { * must be called after the reset before any more samples can be queued. */ public void reset(boolean resetUpstreamFormat) { - metadataQueue.reset(resetUpstreamFormat); - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - totalBytesWritten = 0; - allocator.trim(); + sampleDataQueue.reset(); + length = 0; + absoluteFirstIndex = 0; + relativeFirstIndex = 0; + readPosition = 0; + upstreamKeyframeRequired = true; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; + upstreamCommittedFormat = null; + if (resetUpstreamFormat) { + upstreamFormat = null; + upstreamFormatRequired = true; + } } /** @@ -123,21 +158,17 @@ public class SampleQueue implements TrackOutput { * @param sourceId The source identifier. */ public void sourceId(int sourceId) { - metadataQueue.sourceId(sourceId); + upstreamSourceId = sourceId; } - /** - * Indicates samples that are subsequently queued should be spliced into those already queued. - */ + /** Indicates samples that are subsequently queued should be spliced into those already queued. */ public void splice() { pendingSplice = true; } - /** - * Returns the current absolute write index. - */ + /** Returns the current absolute write index. */ public int getWriteIndex() { - return metadataQueue.getWriteIndex(); + return absoluteFirstIndex + length; } /** @@ -147,55 +178,37 @@ public class SampleQueue implements TrackOutput { * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. */ public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); - if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) { - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - } else { - // Find the last node containing at least 1 byte of data that we need to keep. - AllocationNode lastNodeToKeep = firstAllocationNode; - while (totalBytesWritten > lastNodeToKeep.endPosition) { - lastNodeToKeep = lastNodeToKeep.next; - } - // Discard all subsequent nodes. - AllocationNode firstNodeToDiscard = lastNodeToKeep.next; - clearAllocationNodes(firstNodeToDiscard); - // Reset the successor of the last node to be an uninitialized node. - lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); - // Update writeAllocationNode and readAllocationNode as necessary. - writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next - : lastNodeToKeep; - if (readAllocationNode == firstNodeToDiscard) { - readAllocationNode = lastNodeToKeep.next; - } - } + sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); } // Called by the consuming thread. + /** Calls {@link #discardToEnd()} and releases any owned {@link DrmSession} references. */ + public void preRelease() { + discardToEnd(); + releaseDrmSessionReferences(); + } + /** * Throws an error that's preventing data from being read. Does nothing if no such error exists. * * @throws IOException The underlying error. */ public void maybeThrowError() throws IOException { - metadataQueue.maybeThrowError(); + // TODO: Avoid throwing if the DRM error is not preventing a read operation. + if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentDrmSession.getError()); + } } - /** - * Returns the absolute index of the first sample. - */ + /** Returns the current absolute start index. */ public int getFirstIndex() { - return metadataQueue.getFirstIndex(); + return absoluteFirstIndex; } - /** - * Returns the current absolute read index. - */ + /** Returns the current absolute read index. */ public int getReadIndex() { - return metadataQueue.getReadIndex(); + return absoluteFirstIndex + readPosition; } /** @@ -204,129 +217,71 @@ public class SampleQueue implements TrackOutput { * * @return The source id. */ - public int peekSourceId() { - return metadataQueue.peekSourceId(); + public synchronized int peekSourceId() { + int relativeReadIndex = getRelativeIndex(readPosition); + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public Format getUpstreamFormat() { - return metadataQueue.getUpstreamFormat(); + /** Returns the upstream {@link Format} in which samples are being queued. */ + public synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; } /** * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *
- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * + *
Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not * considered as having been queued. Samples that were dequeued from the front of the queue are * considered as having been queued. * * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no * samples have been queued. */ - public long getLargestQueuedTimestampUs() { - return metadataQueue.getLargestQueuedTimestampUs(); + public synchronized long getLargestQueuedTimestampUs() { + return largestQueuedTimestampUs; } /** * Returns whether the last sample of the stream has knowingly been queued. A return value of * {@code false} means that the last sample had not been queued or that it's unknown whether the * last sample has been queued. + * + *
Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. */ - public boolean isLastSampleQueued() { - return metadataQueue.isLastSampleQueued(); + public synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; } /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public long getFirstTimestampUs() { - return metadataQueue.getFirstTimestampUs(); + public synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; } /** - * Rewinds the read position to the first sample in the queue. - */ - public void rewind() { - metadataQueue.rewind(); - readAllocationNode = firstAllocationNode; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. + * Returns whether there is data available for reading. * - * @param timeUs The time to discard to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the - * read position. If false then samples at and beyond the read position may be discarded, in - * which case the read position is advanced to the first remaining sample. - */ - public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition)); - } - - /** - * Discards up to but not including the read position. - */ - public void discardToRead() { - discardDownstreamTo(metadataQueue.discardToRead()); - } - - /** Calls {@link #discardToEnd()} and releases any owned {@link DrmSession} references. */ - public void preRelease() { - discardToEnd(); - metadataQueue.releaseDrmSessionReferences(); - } - - /** Calls {@link #reset()} and releases any owned {@link DrmSession} references. */ - public void release() { - reset(); - metadataQueue.releaseDrmSessionReferences(); - } - - /** - * Discards to the end of the queue. The read position is also advanced. - */ - public void discardToEnd() { - discardDownstreamTo(metadataQueue.discardToEnd()); - } - - /** - * Advances the read position to the end of the queue. + *
Note: If the stream has ended then a buffer with the end of stream flag can always be read + * from {@link #read}. Hence an ended stream is always ready. * - * @return The number of samples that were skipped. + * @param loadingFinished Whether no more samples will be written to the sample queue. When true, + * this method returns true if the sample queue is empty, because an empty sample queue means + * the end of stream has been reached. When false, this method returns false if the sample + * queue is empty. */ - public int advanceToEnd() { - return metadataQueue.advanceToEnd(); - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe). - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful - * advance is one in which the read position was unchanged or advanced, and is now at a sample - * meeting the specified criteria. - */ - public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { - return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public boolean setReadPosition(int sampleIndex) { - return metadataQueue.setReadPosition(sampleIndex); + public synchronized boolean isReady(boolean loadingFinished) { + if (!hasNextSample()) { + return loadingFinished + || isLastSampleQueued + || (upstreamFormat != null && upstreamFormat != downstreamFormat); + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + // A format can be read. + return true; + } + return mayReadSample(relativeReadIndex); } /** @@ -364,238 +319,106 @@ public class SampleQueue implements TrackOutput { boolean loadingFinished, long decodeOnlyUntilUs) { int result = - metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, extrasHolder); - if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - if (!buffer.isFlagsOnly()) { - readToBuffer(buffer, extrasHolder); - } + readSampleMetadata( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { + sampleDataQueue.readToBuffer(buffer, extrasHolder); } return result; } - /** - * Returns whether there is data available for reading. - * - *
Note: If the stream has ended then a buffer with the end of stream flag can always be read - * from {@link #read}. Hence an ended stream is always ready. - * - * @param loadingFinished Whether no more samples will be written to the sample queue. When true, - * this method returns true if the sample queue is empty, because an empty sample queue means - * the end of stream has been reached. When false, this method returns false if the sample - * queue is empty. - */ - public boolean isReady(boolean loadingFinished) { - return metadataQueue.isReady(loadingFinished); + /** Rewinds the read position to the first sample in the queue. */ + public void rewind() { + rewindMetadata(); + sampleDataQueue.rewind(); } /** - * Reads data from the rolling buffer to populate a decoder input buffer. + * Attempts to advance the read position to the sample before or at the specified time. * - * @param buffer The buffer to populate. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param timeUs The time to advance to. + * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified + * time, rather than to any sample before or at that time. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by advancing the read position to the last sample (or keyframe). + * @return The number of samples that were skipped if the operation was successful, which may be + * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful + * advance is one in which the read position was unchanged or advanced, and is now at a sample + * meeting the specified criteria. */ - private void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); + public synchronized int advanceTo( + long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() + || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { + return SampleQueue.ADVANCE_FAILED; } - // Read sample data, extracting supplemental data into a separate buffer if needed. - if (buffer.hasSupplementalData()) { - // If there is supplemental data, the sample data is prefixed by its size. - scratch.reset(4); - readData(extrasHolder.offset, scratch.data, 4); - int sampleSize = scratch.readUnsignedIntToInt(); - extrasHolder.offset += 4; - extrasHolder.size -= 4; - - // Write the sample data. - buffer.ensureSpaceForWrite(sampleSize); - readData(extrasHolder.offset, buffer.data, sampleSize); - extrasHolder.offset += sampleSize; - extrasHolder.size -= sampleSize; - - // Write the remaining data as supplemental data. - buffer.resetSupplementalData(extrasHolder.size); - readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); - } else { - // Write the sample data. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); + int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); + if (offset == -1) { + return SampleQueue.ADVANCE_FAILED; } + readPosition += offset; + return offset; } /** - * Reads encryption data for the current sample. + * Advances the read position to the end of the queue. * - *
The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link - * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same - * value is added to {@link SampleExtrasHolder#offset}. - * - * @param buffer The buffer into which the encryption data should be written. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @return The number of samples that were skipped. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { - long offset = extrasHolder.offset; - - // Read the signal byte. - scratch.reset(1); - readData(offset, scratch.data, 1); - offset++; - byte signalByte = scratch.data[0]; - boolean subsampleEncryption = (signalByte & 0x80) != 0; - int ivSize = signalByte & 0x7F; - - // Read the initialization vector. - if (buffer.cryptoInfo.iv == null) { - buffer.cryptoInfo.iv = new byte[16]; - } - readData(offset, buffer.cryptoInfo.iv, ivSize); - offset += ivSize; - - // Read the subsample count, if present. - int subsampleCount; - if (subsampleEncryption) { - scratch.reset(2); - readData(offset, scratch.data, 2); - offset += 2; - subsampleCount = scratch.readUnsignedShort(); - } else { - subsampleCount = 1; - } - - // Write the clear and encrypted subsample sizes. - int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; - if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { - clearDataSizes = new int[subsampleCount]; - } - int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; - if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { - encryptedDataSizes = new int[subsampleCount]; - } - if (subsampleEncryption) { - int subsampleDataLength = 6 * subsampleCount; - scratch.reset(subsampleDataLength); - readData(offset, scratch.data, subsampleDataLength); - offset += subsampleDataLength; - scratch.setPosition(0); - for (int i = 0; i < subsampleCount; i++) { - clearDataSizes[i] = scratch.readUnsignedShort(); - encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); - } - } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); - } - - // Populate the cryptoInfo. - CryptoData cryptoData = extrasHolder.cryptoData; - buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode, - cryptoData.encryptedBlocks, cryptoData.clearBlocks); - - // Adjust the offset and size to take into account the bytes read. - int bytesRead = (int) (offset - extrasHolder.offset); - extrasHolder.offset += bytesRead; - extrasHolder.size -= bytesRead; + public synchronized int advanceToEnd() { + int skipCount = length - readPosition; + readPosition = length; + return skipCount; } /** - * Reads data from the front of the rolling buffer. + * Attempts to set the read position to the specified sample index. * - * @param absolutePosition The absolute position from which data should be read. - * @param target The buffer into which data should be written. - * @param length The number of bytes to read. + * @param sampleIndex The sample index. + * @return Whether the read position was set successfully. False is returned if the specified + * index is smaller than the index of the first sample in the queue, or larger than the index + * of the next sample that will be written. */ - private void readData(long absolutePosition, ByteBuffer target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + public synchronized boolean setReadPosition(int sampleIndex) { + if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { + readPosition = sampleIndex - absoluteFirstIndex; + return true; } + return false; } /** - * Reads data from the front of the rolling buffer. + * Discards up to but not including the sample immediately before or at the specified time. * - * @param absolutePosition The absolute position from which data should be read. - * @param target The array into which data should be written. - * @param length The number of bytes to read. + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. */ - private void readData(long absolutePosition, byte[] target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition), - target, length - remaining, toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } - } + public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + sampleDataQueue.discardDownstreamTo( + discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition)); } - /** - * Advances {@link #readAllocationNode} to the specified absolute position. - * - * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. - */ - private void advanceReadTo(long absolutePosition) { - while (absolutePosition >= readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + /** Discards up to but not including the read position. */ + public void discardToRead() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead()); } - /** - * Advances {@link #firstAllocationNode} to the specified absolute position. - * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind - * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their - * underlying allocations are returned to the allocator. - * - * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. - * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op. - */ - private void discardDownstreamTo(long absolutePosition) { - if (absolutePosition == C.POSITION_UNSET) { - return; - } - while (absolutePosition >= firstAllocationNode.endPosition) { - allocator.release(firstAllocationNode.allocation); - firstAllocationNode = firstAllocationNode.clear(); - } - // If we discarded the node referenced by readAllocationNode then we need to advance it to the - // first remaining node. - if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { - readAllocationNode = firstAllocationNode; - } + /** Discards all samples in the queue and advances the read position. */ + public void discardToEnd() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd()); } // Called by the loading thread. /** - * Sets a listener to be notified of changes to the upstream format. - * - * @param listener The listener. - */ - public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { - upstreamFormatChangeListener = listener; - } - - /** - * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * that are subsequently queued. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently queued. * * @param sampleOffsetUs The timestamp offset in microseconds. */ @@ -606,11 +429,22 @@ public class SampleQueue implements TrackOutput { } } + /** + * Sets a listener to be notified of changes to the upstream format. + * + * @param listener The listener. + */ + public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + upstreamFormatChangeListener = listener; + } + + // TrackOutput implementation. Called by the loading thread. + @Override - public void format(Format format) { - Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = metadataQueue.format(adjustedFormat); - lastUnadjustedFormat = format; + public void format(Format unadjustedFormat) { + Format adjustedFormat = getAdjustedSampleFormat(unadjustedFormat, sampleOffsetUs); + boolean formatChanged = setUpstreamFormat(adjustedFormat); + lastUnadjustedFormat = unadjustedFormat; pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); @@ -620,28 +454,12 @@ public class SampleQueue implements TrackOutput { @Override public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - 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(); - } - postAppend(bytesAppended); - return bytesAppended; + return sampleDataQueue.sampleData(input, length, allowEndOfInput); } @Override public void sampleData(ParsableByteArray buffer, int length) { - while (length > 0) { - int bytesAppended = preAppend(length); - buffer.readBytes(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended); - length -= bytesAppended; - postAppend(bytesAppended); - } + sampleDataQueue.sampleData(buffer, length); } @Override @@ -656,66 +474,388 @@ public class SampleQueue implements TrackOutput { } timeUs += sampleOffsetUs; if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !attemptSplice(timeUs)) { return; } pendingSplice = false; } - long absoluteOffset = totalBytesWritten - size - offset; - metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset; + commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } - // Private methods. + // Internal methods. + + private synchronized void rewindMetadata() { + readPosition = 0; + } + + private synchronized int readSampleMetadata( + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs, + SampleExtrasHolder extrasHolder) { + if (!hasNextSample()) { + if (loadingFinished || isLastSampleQueued) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { + onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + int relativeReadIndex = getRelativeIndex(readPosition); + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + onFormatResult(formats[relativeReadIndex], formatHolder); + return C.RESULT_FORMAT_READ; + } + + if (!mayReadSample(relativeReadIndex)) { + return C.RESULT_NOTHING_READ; + } + + buffer.setFlags(flags[relativeReadIndex]); + buffer.timeUs = timesUs[relativeReadIndex]; + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + readPosition++; + return C.RESULT_BUFFER_READ; + } + + private synchronized boolean setUpstreamFormat(Format format) { + if (format == null) { + upstreamFormatRequired = true; + return false; + } + upstreamFormatRequired = false; + if (Util.areEqual(format, upstreamFormat)) { + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes in read() using cheap referential + // equality. + return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat + // so we can detect format changes in read() using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; + } else { + upstreamFormat = format; + return true; + } + } + + private synchronized long discardSampleMetadataTo( + long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + public synchronized long discardSampleMetadataToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + return discardSamples(readPosition); + } + + private synchronized long discardSampleMetadataToEnd() { + if (length == 0) { + return C.POSITION_UNSET; + } + return discardSamples(length); + } + + private void releaseDrmSessionReferences() { + if (currentDrmSession != null) { + currentDrmSession.release(); + currentDrmSession = null; + // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData + // != null implies currentSession != null + downstreamFormat = null; + } + } + + private synchronized void commitSample( + long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + + int relativeEndIndex = getRelativeIndex(length); + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; + + length++; + if (length == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeFirstIndex; + System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeFirstIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeFirstIndex = 0; + length = capacity; + capacity = newCapacity; + } + } /** - * Clears allocation nodes starting from {@code fromNode}. + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. * - * @param fromNode The node from which to clear. + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. */ - private void clearAllocationNodes(AllocationNode fromNode) { - if (!fromNode.wasInitialized) { + private synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } + long largestReadTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); + if (largestReadTimestampUs >= timeUs) { + return false; + } + int retainCount = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); + return true; + } + + private long discardUpstreamSampleMetadata(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + length -= discardCount; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; + if (length != 0) { + int relativeLastWriteIndex = getRelativeIndex(length - 1); + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } + return 0; + } + + private boolean hasNextSample() { + return readPosition != length; + } + + /** + * Sets the downstream format, performs DRM resource management, and populates the {@code + * outputFormatHolder}. + * + * @param newFormat The new downstream format. + * @param outputFormatHolder The output {@link FormatHolder}. + */ + private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { + outputFormatHolder.format = newFormat; + boolean isFirstFormat = downstreamFormat == null; + DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; + downstreamFormat = newFormat; + if (drmSessionManager == DrmSessionManager.DUMMY) { + // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that + // the media source creation has not yet been migrated and the renderer can acquire the + // session for the read DRM init data. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. return; } - // Bulk release allocations for performance (it's significantly faster when using - // DefaultAllocator because the allocator's lock only needs to be acquired and released once) - // [Internal: See b/29542039]. - int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0) - + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength); - Allocation[] allocationsToRelease = new Allocation[allocationCount]; - AllocationNode currentNode = fromNode; - for (int i = 0; i < allocationsToRelease.length; i++) { - allocationsToRelease[i] = currentNode.allocation; - currentNode = currentNode.clear(); + DrmInitData newDrmInitData = newFormat.drmInitData; + outputFormatHolder.includesDrmSession = true; + outputFormatHolder.drmSession = currentDrmSession; + if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { + // Nothing to do. + return; + } + // Ensure we acquire the new session before releasing the previous one in case the same session + // is being used for both DrmInitData. + DrmSession> previousSession = currentDrmSession; + Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); + currentDrmSession = + newDrmInitData != null + ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) + : drmSessionManager.acquirePlaceholderSession( + playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + outputFormatHolder.drmSession = currentDrmSession; + + if (previousSession != null) { + previousSession.release(); } - allocator.release(allocationsToRelease); } /** - * Called before writing sample data to {@link #writeAllocationNode}. May cause - * {@link #writeAllocationNode} to be initialized. + * Returns whether it's possible to read the next sample. * - * @param length The number of bytes that the caller wishes to write. - * @return The number of bytes that the caller is permitted to write, which may be less than - * {@code length}. + * @param relativeReadIndex The relative read index of the next sample. + * @return Whether it's possible to read the next sample. */ - private int preAppend(int length) { - if (!writeAllocationNode.wasInitialized) { - writeAllocationNode.initialize(allocator.allocate(), - new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + private boolean mayReadSample(int relativeReadIndex) { + if (drmSessionManager == DrmSessionManager.DUMMY) { + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + // For protected content it's likely that the DrmSessionManager is still being injected into + // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. + return true; } - return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + return currentDrmSession == null + || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS + || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && currentDrmSession.playClearSamplesWithoutKeys()); } /** - * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * Finds the sample in the specified range that's before or at the specified time. If {@code + * keyframe} is {@code true} then the sample is additionally required to be a keyframe. * - * @param length The number of bytes that were written. + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching + * sample was found. */ - private void postAppend(int length) { - totalBytesWritten += length; - if (totalBytesWritten == writeAllocationNode.endPosition) { - writeAllocationNode = writeAllocationNode.next; + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded. + */ + private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); + length -= discardCount; + absoluteFirstIndex += discardCount; + relativeFirstIndex += discardCount; + if (relativeFirstIndex >= capacity) { + relativeFirstIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; + } else { + return offsets[relativeFirstIndex]; + } + } + + /** + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. + * + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. + */ + private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } + long largestTimestampUs = Long.MIN_VALUE; + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return largestTimestampUs; + } + + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeFirstIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; } /** @@ -735,76 +875,11 @@ public class SampleQueue implements TrackOutput { return format; } - /** A node in a linked list of {@link Allocation}s held by the output. */ - private static final class AllocationNode { - - /** - * The absolute position of the start of the data (inclusive). - */ - public final long startPosition; - /** - * The absolute position of the end of the data (exclusive). - */ - public final long endPosition; - /** - * Whether the node has been initialized. Remains true after {@link #clear()}. - */ - public boolean wasInitialized; - /** - * The {@link Allocation}, or {@code null} if the node is not initialized. - */ - @Nullable public Allocation allocation; - /** - * The next {@link AllocationNode} in the list, or {@code null} if the node has not been - * initialized. Remains set after {@link #clear()}. - */ - @Nullable public AllocationNode next; - - /** - * @param startPosition See {@link #startPosition}. - * @param allocationLength The length of the {@link Allocation} with which this node will be - * initialized. - */ - public AllocationNode(long startPosition, int allocationLength) { - this.startPosition = startPosition; - this.endPosition = startPosition + allocationLength; - } - - /** - * Initializes the node. - * - * @param allocation The node's {@link Allocation}. - * @param next The next {@link AllocationNode}. - */ - public void initialize(Allocation allocation, AllocationNode next) { - this.allocation = allocation; - this.next = next; - wasInitialized = true; - } - - /** - * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to - * the specified absolute position. - * - * @param absolutePosition The absolute position. - * @return The corresponding offset into the allocation's data. - */ - public int translateOffset(long absolutePosition) { - return (int) (absolutePosition - startPosition) + allocation.offset; - } - - /** - * Clears {@link #allocation} and {@link #next}. - * - * @return The cleared next {@link AllocationNode}. - */ - public AllocationNode clear() { - allocation = null; - AllocationNode temp = next; - next = null; - return temp; - } + /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */ + /* package */ static final class SampleExtrasHolder { + public int size; + public long offset; + public CryptoData cryptoData; } - }