From 2d530478eea5b60b34a396586ad4bae2b2caf395 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 31 Oct 2019 10:54:52 +0000 Subject: [PATCH] Make FormatHolder be entirely populated by SampleMetadataQueue This should not introduce any functional changes. PiperOrigin-RevId: 277691550 --- .../source/SampleMetadataQueue.java | 190 ++++++++++++------ .../exoplayer2/source/SampleQueue.java | 164 ++------------- 2 files changed, 152 insertions(+), 202 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 5ca9750d96..28aa5d86e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -15,17 +15,19 @@ */ package com.google.android.exoplayer2.source; -import androidx.annotation.IntDef; +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.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.io.IOException; /** * A queue of metadata describing the contents of a media buffer. @@ -43,29 +45,14 @@ import java.lang.annotation.RetentionPolicy; } - /** Values returned by {@link #peekNext} ()}. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef( - value = { - PEEK_RESULT_NOTHING, - PEEK_RESULT_FORMAT, - PEEK_RESULT_BUFFER_CLEAR, - PEEK_RESULT_BUFFER_ENCRYPTED - }) - public @interface PeekResult {} - - /** Nothing is available for reading. */ - public static final int PEEK_RESULT_NOTHING = 0; - /** A format change is available for reading */ - public static final int PEEK_RESULT_FORMAT = 1; - /** A clear buffer is available for reading. */ - public static final int PEEK_RESULT_BUFFER_CLEAR = 2; - /** An encrypted buffer is available for reading. */ - public static final int PEEK_RESULT_BUFFER_ENCRYPTED = 3; - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; + private final DrmSessionManager drmSessionManager; + private final boolean playClearSamplesWithoutKeys; + + @Nullable private Format downstreamFormat; + @Nullable private DrmSession currentDrmSession; + private int capacity; private int[] sourceIds; private long[] offsets; @@ -89,7 +76,11 @@ import java.lang.annotation.RetentionPolicy; private Format upstreamCommittedFormat; private int upstreamSourceId; - public SampleMetadataQueue() { + public SampleMetadataQueue(DrmSessionManager drmSessionManager) { + this.drmSessionManager = drmSessionManager; + playClearSamplesWithoutKeys = + (drmSessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) + != 0; capacity = SAMPLE_CAPACITY_INCREMENT; sourceIds = new int[capacity]; offsets = new long[capacity]; @@ -104,6 +95,8 @@ import java.lang.annotation.RetentionPolicy; upstreamKeyframeRequired = true; } + // Called by the consuming thread, but only when there is no loading thread. + /** * Clears all sample metadata from the queue. * @@ -163,8 +156,29 @@ import java.lang.annotation.RetentionPolicy; // Called by the consuming thread. /** - * Returns the current absolute start index. + * 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.releaseReference(); + 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; } @@ -182,18 +196,11 @@ import java.lang.annotation.RetentionPolicy; * * @return The source id. */ - public int peekSourceId() { + public synchronized int peekSourceId() { int relativeReadIndex = getRelativeIndex(readPosition); return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } - /** - * Returns whether a sample is available to be read. - */ - public synchronized boolean hasNextSample() { - return readPosition != length; - } - /** * Returns the upstream {@link Format} in which samples are being queued. */ @@ -242,23 +249,39 @@ import java.lang.annotation.RetentionPolicy; } /** - * Returns a {@link PeekResult} depending on what a following call to {@link #read - * read(formatHolder, decoderInputBuffer, formatRequired= false, allowOnlyClearBuffers= false, - * loadingFinished= false, decodeOnlyUntilUs= 0)} would result in. + * 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. */ - @SuppressWarnings("ReferenceEquality") - @PeekResult - public synchronized int peekNext(Format downstreamFormat) { - if (readPosition == length) { - return PEEK_RESULT_NOTHING; + public boolean isReady(boolean loadingFinished) { + if (!hasNextSample()) { + return loadingFinished + || isLastSampleQueued + || (upstreamFormat != null && upstreamFormat != downstreamFormat); } int relativeReadIndex = getRelativeIndex(readPosition); if (formats[relativeReadIndex] != downstreamFormat) { - return PEEK_RESULT_FORMAT; + // A format can be read. + return true; + } else if (Assertions.checkNotNull(downstreamFormat).drmInitData == null) { + // A sample from a clear section can be read. + return true; + } else if (drmSessionManager == DrmSessionManager.DUMMY + || Assertions.checkNotNull(currentDrmSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS) { + // TODO: Remove DUMMY DrmSessionManager check once renderers are migrated [Internal ref: + // b/122519809]. + return true; } else { - return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0 - ? PEEK_RESULT_BUFFER_ENCRYPTED - : PEEK_RESULT_BUFFER_CLEAR; + // A clear sample in an encrypted section may be read if playClearSamplesWithoutKeys is true. + return (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && playClearSamplesWithoutKeys; } } @@ -278,11 +301,7 @@ import java.lang.annotation.RetentionPolicy; * @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 allowOnlyClearBuffers If set to true, this method will not return encrypted buffers, - * returning {@link C#RESULT_NOTHING_READ} (without advancing the read position) instead. * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param downstreamFormat The current downstream {@link Format}. If the format of the next sample - * is different to the current downstream format then a format will be read. * @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}. @@ -292,16 +311,14 @@ import java.lang.annotation.RetentionPolicy; FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean allowOnlyClearBuffers, boolean loadingFinished, - Format downstreamFormat, 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)) { - formatHolder.format = upstreamFormat; + onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); return C.RESULT_FORMAT_READ; } else { return C.RESULT_NOTHING_READ; @@ -310,11 +327,23 @@ import java.lang.annotation.RetentionPolicy; int relativeReadIndex = getRelativeIndex(readPosition); if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; + onFormatResult(formats[relativeReadIndex], formatHolder); return C.RESULT_FORMAT_READ; } - if (allowOnlyClearBuffers && (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0) { + // It's likely that the media source creation has not yet been migrated and the renderer can + // acquire the session for the sample. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + boolean skipDrmChecks = drmSessionManager == DrmSessionManager.DUMMY; + boolean isNextSampleEncrypted = (flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0; + + boolean mayReadSample = + skipDrmChecks + || Util.castNonNull(downstreamFormat).drmInitData == null + || (playClearSamplesWithoutKeys && !isNextSampleEncrypted) + || Assertions.checkNotNull(currentDrmSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; + if (!mayReadSample) { return C.RESULT_NOTHING_READ; } @@ -557,9 +586,54 @@ import java.lang.annotation.RetentionPolicy; // Internal methods. + private boolean hasNextSample() { + return readPosition != length; + } + /** - * 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. + * 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); + outputFormatHolder.drmSession = currentDrmSession; + + if (previousSession != null) { + previousSession.releaseReference(); + } + } + + /** + * 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. 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 6977cf3b12..d92dd48d4e 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 @@ -15,13 +15,11 @@ */ 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; @@ -29,9 +27,7 @@ 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.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; @@ -58,23 +54,16 @@ public class SampleQueue implements TrackOutput { private static final int INITIAL_SCRATCH_SIZE = 32; private final Allocator allocator; - private final DrmSessionManager drmSessionManager; - private final boolean playClearSamplesWithoutKeys; private final int allocationLength; private final SampleMetadataQueue metadataQueue; private final SampleExtrasHolder extrasHolder; private final ParsableByteArray scratch; - private final FormatHolder scratchFormatHolder; // References into the linked list of allocations. private AllocationNode firstAllocationNode; private AllocationNode readAllocationNode; private AllocationNode writeAllocationNode; - // Accessed only by the consuming thread. - private Format downstreamFormat; - @Nullable private DrmSession currentSession; - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). private boolean pendingFormatAdjustment; private Format lastUnadjustedFormat; @@ -88,19 +77,14 @@ public class SampleQueue implements TrackOutput { * * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} - * from. + * from. The created instance does not take ownership of this {@link DrmSessionManager}. */ public SampleQueue(Allocator allocator, DrmSessionManager drmSessionManager) { this.allocator = allocator; - this.drmSessionManager = drmSessionManager; - playClearSamplesWithoutKeys = - (drmSessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) - != 0; allocationLength = allocator.getIndividualAllocationLength(); - metadataQueue = new SampleMetadataQueue(); + metadataQueue = new SampleMetadataQueue(drmSessionManager); extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - scratchFormatHolder = new FormatHolder(); firstAllocationNode = new AllocationNode(0, allocationLength); readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; @@ -116,7 +100,7 @@ public class SampleQueue implements TrackOutput { } /** - * Resets the output and releases any held DRM resources. + * Resets the output. * * @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)}) @@ -197,10 +181,7 @@ public class SampleQueue implements TrackOutput { * @throws IOException The underlying error. */ public void maybeThrowError() throws IOException { - // TODO: Avoid throwing if the DRM error is not preventing a read operation. - if (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) { - throw Assertions.checkNotNull(currentSession.getError()); - } + metadataQueue.maybeThrowError(); } /** @@ -291,16 +272,16 @@ public class SampleQueue implements TrackOutput { discardDownstreamTo(metadataQueue.discardToRead()); } - /** Calls {@link #discardToEnd()} and releases any held DRM resources. */ + /** Calls {@link #discardToEnd()} and releases any owned {@link DrmSession} references. */ public void preRelease() { discardToEnd(); - releaseDrmResources(); + metadataQueue.releaseDrmSessionReferences(); } - /** Calls {@link #reset()} and releases any held DRM resources. */ + /** Calls {@link #reset()} and releases any owned {@link DrmSession} references. */ public void release() { reset(); - releaseDrmResources(); + metadataQueue.releaseDrmSessionReferences(); } /** @@ -360,7 +341,7 @@ public class SampleQueue implements TrackOutput { * {@link DrmSessionManager#FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS}. * * - * @param outputFormatHolder A {@link FormatHolder} to populate in the case of reading a format. + * @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 the end of the stream has been reached, the {@link * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. If a {@link @@ -377,65 +358,22 @@ public class SampleQueue implements TrackOutput { */ @SuppressWarnings("ReferenceEquality") public int read( - FormatHolder outputFormatHolder, + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, long decodeOnlyUntilUs) { - - boolean readFlagFormatRequired = false; - boolean readFlagAllowOnlyClearBuffers = false; - boolean onlyPropagateFormatChanges = false; - - if (downstreamFormat == null || formatRequired) { - readFlagFormatRequired = true; - } else if (drmSessionManager != DrmSessionManager.DUMMY - && downstreamFormat.drmInitData != null - && Assertions.checkNotNull(currentSession).getState() - != DrmSession.STATE_OPENED_WITH_KEYS) { - if (playClearSamplesWithoutKeys) { - // Content is encrypted and keys are not available, but clear samples are ok for reading. - readFlagAllowOnlyClearBuffers = true; - } else { - // We must not read any samples, but we may still read a format or the end of stream. - // However, because the formatRequired argument is false, we should not propagate a read - // format unless it is different than the current format. - onlyPropagateFormatChanges = true; - readFlagFormatRequired = true; + 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); } } - - int result = - metadataQueue.read( - scratchFormatHolder, - buffer, - readFlagFormatRequired, - readFlagAllowOnlyClearBuffers, - loadingFinished, - downstreamFormat, - extrasHolder); - switch (result) { - case C.RESULT_FORMAT_READ: - if (onlyPropagateFormatChanges && downstreamFormat == scratchFormatHolder.format) { - return C.RESULT_NOTHING_READ; - } - onFormat(Assertions.checkNotNull(scratchFormatHolder.format), outputFormatHolder); - return C.RESULT_FORMAT_READ; - case C.RESULT_BUFFER_READ: - if (!buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - if (!buffer.isFlagsOnly()) { - readToBuffer(buffer, extrasHolder); - } - } - return C.RESULT_BUFFER_READ; - case C.RESULT_NOTHING_READ: - return C.RESULT_NOTHING_READ; - default: - throw new IllegalStateException(); - } + return result; } /** @@ -450,21 +388,7 @@ public class SampleQueue implements TrackOutput { * queue is empty. */ public boolean isReady(boolean loadingFinished) { - @SampleMetadataQueue.PeekResult int nextInQueue = metadataQueue.peekNext(downstreamFormat); - switch (nextInQueue) { - case SampleMetadataQueue.PEEK_RESULT_NOTHING: - return loadingFinished; - case SampleMetadataQueue.PEEK_RESULT_FORMAT: - return true; - case SampleMetadataQueue.PEEK_RESULT_BUFFER_CLEAR: - return currentSession == null || playClearSamplesWithoutKeys; - case SampleMetadataQueue.PEEK_RESULT_BUFFER_ENCRYPTED: - return drmSessionManager == DrmSessionManager.DUMMY - || Assertions.checkNotNull(currentSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS; - default: - throw new IllegalStateException(); - } + return metadataQueue.isReady(loadingFinished); } /** @@ -811,54 +735,6 @@ public class SampleQueue implements TrackOutput { return format; } - /** Releases any held DRM resources. */ - private void releaseDrmResources() { - if (currentSession != null) { - currentSession.releaseReference(); - currentSession = null; - } - } - - /** - * Updates the current format and manages any necessary DRM resources. - * - * @param format The format read from upstream. - * @param outputFormatHolder The output {@link FormatHolder}. - */ - private void onFormat(Format format, FormatHolder outputFormatHolder) { - outputFormatHolder.format = format; - boolean isFirstFormat = downstreamFormat == null; - DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; - downstreamFormat = format; - 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; - } - outputFormatHolder.includesDrmSession = true; - outputFormatHolder.drmSession = currentSession; - if (!isFirstFormat && Util.areEqual(oldDrmInitData, format.drmInitData)) { - // Nothing to do. - return; - } - // Ensure we acquire the new session before releasing the previous one in case the same session - // can be used for both DrmInitData. - DrmSession previousSession = currentSession; - DrmInitData drmInitData = downstreamFormat.drmInitData; - Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); - currentSession = - drmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, drmInitData) - : drmSessionManager.acquirePlaceholderSession(playbackLooper); - outputFormatHolder.drmSession = currentSession; - - if (previousSession != null) { - previousSession.releaseReference(); - } - } - /** A node in a linked list of {@link Allocation}s held by the output. */ private static final class AllocationNode {