From b68a698806a771968bbdaf5a911665bf9011ffd7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 28 Oct 2019 11:32:41 +0000 Subject: [PATCH] Make SampleQueue populate FormatHolder.drmSession Also add unit tests for SampleQueue read for samples with DRM requirements. PiperOrigin-RevId: 277037826 --- .../source/DecryptableSampleQueueReader.java | 208 ------------- .../source/ProgressiveMediaPeriod.java | 26 +- .../exoplayer2/source/SampleQueue.java | 195 ++++++++++-- .../source/chunk/ChunkSampleStream.java | 25 +- .../exoplayer2/source/SampleQueueTest.java | 281 ++++++++++++++---- .../source/dash/PlayerEmsgHandler.java | 5 +- .../source/hls/HlsSampleStreamWrapper.java | 32 +- 7 files changed, 417 insertions(+), 355 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java deleted file mode 100644 index b6f730cea3..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DecryptableSampleQueueReader.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 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.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * Reads from a {@link SampleQueue} and attaches {@link DrmSession} references to the {@link Format - * Formats} of encrypted regions. - */ -public final class DecryptableSampleQueueReader { - - private final SampleQueue upstream; - private final DrmSessionManager sessionManager; - private final FormatHolder formatHolder; - private final boolean playClearSamplesWithoutKeys; - private @MonotonicNonNull Format currentFormat; - @Nullable private DrmSession currentSession; - - /** - * Creates a sample queue reader. - * - * @param upstream The {@link SampleQueue} from which the created reader will read samples. - * @param sessionManager The {@link DrmSessionManager} that will provide {@link DrmSession - * DrmSessions} for the encrypted regions. - */ - public DecryptableSampleQueueReader(SampleQueue upstream, DrmSessionManager sessionManager) { - this.upstream = upstream; - this.sessionManager = sessionManager; - formatHolder = new FormatHolder(); - playClearSamplesWithoutKeys = - (sessionManager.getFlags() & DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS) != 0; - } - - /** Releases any resources acquired by this reader. */ - public void release() { - if (currentSession != null) { - currentSession.releaseReference(); - currentSession = null; - } - } - - /** - * 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 (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) { - throw Assertions.checkNotNull(currentSession.getError()); - } - } - - /** - * Reads from the upstream {@link SampleQueue}, populating {@link FormatHolder#drmSession} if the - * current {@link Format#drmInitData} is not null. - * - *

This reader guarantees that any read results are usable by clients. An encrypted sample will - * only be returned along with a {@link FormatHolder#drmSession} that has available keys. - * - * @param outputFormatHolder A {@link FormatHolder} to populate in the case of reading a format. - * {@link FormatHolder#drmSession} will be populated if the read format's {@link - * Format#drmInitData} is not null. - * @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 - * 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. - * @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 decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will - * be set if the buffer's timestamp is less than this value. - * @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 int read( - FormatHolder outputFormatHolder, - DecoderInputBuffer buffer, - boolean formatRequired, - boolean loadingFinished, - long decodeOnlyUntilUs) { - - boolean readFlagFormatRequired = false; - boolean readFlagAllowOnlyClearBuffers = false; - boolean onlyPropagateFormatChanges = false; - - if (currentFormat == null || formatRequired) { - readFlagFormatRequired = true; - } else if (sessionManager != DrmSessionManager.DUMMY - && currentFormat.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 = - upstream.read( - formatHolder, - buffer, - readFlagFormatRequired, - readFlagAllowOnlyClearBuffers, - loadingFinished, - decodeOnlyUntilUs); - if (result == C.RESULT_FORMAT_READ) { - if (onlyPropagateFormatChanges && currentFormat == formatHolder.format) { - return C.RESULT_NOTHING_READ; - } - onFormat(Assertions.checkNotNull(formatHolder.format), outputFormatHolder); - } - return result; - } - - /** - * 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 = currentFormat == null; - DrmInitData oldDrmInitData = currentFormat != null ? currentFormat.drmInitData : null; - currentFormat = format; - if (sessionManager == 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 = currentFormat.drmInitData; - Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); - currentSession = - drmInitData != null - ? sessionManager.acquireSession(playbackLooper, drmInitData) - : sessionManager.acquirePlaceholderSession(playbackLooper); - outputFormatHolder.drmSession = currentSession; - - if (previousSession != null) { - previousSession.releaseReference(); - } - } - - /** Returns whether there is data available for reading. */ - public boolean isReady(boolean loadingFinished) { - @SampleQueue.PeekResult int nextInQueue = upstream.peekNext(); - if (nextInQueue == SampleQueue.PEEK_RESULT_NOTHING) { - return loadingFinished; - } else if (nextInQueue == SampleQueue.PEEK_RESULT_FORMAT) { - return true; - } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_CLEAR) { - return currentSession == null || playClearSamplesWithoutKeys; - } else if (nextInQueue == SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED) { - return sessionManager == DrmSessionManager.DUMMY - || Assertions.checkNotNull(currentSession).getState() - == DrmSession.STATE_OPENED_WITH_KEYS; - } else { - throw new IllegalStateException(); - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 687472df75..30c46006b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -116,7 +116,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private SeekMap seekMap; @Nullable private IcyHeaders icyHeaders; private SampleQueue[] sampleQueues; - private DecryptableSampleQueueReader[] sampleQueueReaders; private TrackId[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; @@ -193,7 +192,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; handler = new Handler(); sampleQueueTrackIds = new TrackId[0]; sampleQueues = new SampleQueue[0]; - sampleQueueReaders = new DecryptableSampleQueueReader[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; @@ -206,10 +204,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); - } - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + sampleQueue.preRelease(); } } loader.release(/* callback= */ this); @@ -222,10 +217,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onLoaderReleased() { for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); - } - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + sampleQueue.release(); } extractorHolder.release(); } @@ -461,11 +453,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // SampleStream methods. /* package */ boolean isReady(int track) { - return !suppressRead() && sampleQueueReaders[track].isReady(loadingFinished); + return !suppressRead() && sampleQueues[track].isReady(loadingFinished); } /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException { - sampleQueueReaders[sampleQueueIndex].maybeThrowError(); + sampleQueues[sampleQueueIndex].maybeThrowError(); maybeThrowError(); } @@ -483,7 +475,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } maybeNotifyDownstreamFormat(sampleQueueIndex); int result = - sampleQueueReaders[sampleQueueIndex].read( + sampleQueues[sampleQueueIndex].read( formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { maybeStartDeferredRetry(sampleQueueIndex); @@ -690,7 +682,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return sampleQueues[i]; } } - SampleQueue trackOutput = new SampleQueue(allocator); + SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager); trackOutput.setUpstreamFormatChangeListener(this); @NullableType TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1); @@ -699,12 +691,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @NullableType SampleQueue[] sampleQueues = Arrays.copyOf(this.sampleQueues, trackCount + 1); sampleQueues[trackCount] = trackOutput; this.sampleQueues = Util.castNonNullTypeArray(sampleQueues); - @NullableType - DecryptableSampleQueueReader[] sampleQueueReaders = - Arrays.copyOf(this.sampleQueueReaders, trackCount + 1); - sampleQueueReaders[trackCount] = - new DecryptableSampleQueueReader(this.sampleQueues[trackCount], drmSessionManager); - this.sampleQueueReaders = Util.castNonNullTypeArray(sampleQueueReaders); return trackOutput; } 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 fa4a26aa3c..83dc13a6f9 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,18 +15,24 @@ */ package com.google.android.exoplayer2.source; +import android.os.Looper; import androidx.annotation.IntDef; 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.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; @@ -61,26 +67,29 @@ public class SampleQueue implements TrackOutput { PEEK_RESULT_BUFFER_CLEAR, PEEK_RESULT_BUFFER_ENCRYPTED }) - @interface PeekResult {} + /* package */ @interface PeekResult {} /** Nothing is available for reading. */ - public static final int PEEK_RESULT_NOTHING = 0; + /* package */ static final int PEEK_RESULT_NOTHING = 0; /** A format change is available for reading */ - public static final int PEEK_RESULT_FORMAT = 1; + /* package */ static final int PEEK_RESULT_FORMAT = 1; /** A clear buffer is available for reading. */ - public static final int PEEK_RESULT_BUFFER_CLEAR = 2; + /* package */ static final int PEEK_RESULT_BUFFER_CLEAR = 2; /** An encrypted buffer is available for reading. */ - public static final int PEEK_RESULT_BUFFER_ENCRYPTED = 3; + /* package */ static final int PEEK_RESULT_BUFFER_ENCRYPTED = 3; public static final int ADVANCE_FAILED = -1; 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; @@ -89,6 +98,7 @@ public class SampleQueue implements TrackOutput { // 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; @@ -99,14 +109,23 @@ public class SampleQueue implements TrackOutput { private UpstreamFormatChangedListener upstreamFormatChangeListener; /** + * Creates a sample queue. + * * @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. */ - public SampleQueue(Allocator allocator) { + 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(); extrasHolder = new SampleExtrasHolder(); scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + scratchFormatHolder = new FormatHolder(); firstAllocationNode = new AllocationNode(0, allocationLength); readAllocationNode = firstAllocationNode; writeAllocationNode = firstAllocationNode; @@ -122,7 +141,7 @@ public class SampleQueue implements TrackOutput { } /** - * Resets the output. + * Resets the output and releases any held DRM resources. * * @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)}) @@ -198,8 +217,18 @@ public class SampleQueue implements TrackOutput { // Called by the consuming thread. /** - * Returns whether a sample is available to be read. + * 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 (currentSession != null && currentSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentSession.getError()); + } + } + + /** Returns whether a sample is available to be read. */ public boolean hasNextSample() { return metadataQueue.hasNextSample(); } @@ -292,6 +321,18 @@ public class SampleQueue implements TrackOutput { discardDownstreamTo(metadataQueue.discardToRead()); } + /** Calls {@link #discardToEnd()} and releases any held DRM resources. */ + public void preRelease() { + discardToEnd(); + releaseDrmResources(); + } + + /** Calls {@link #reset()} and releases any held DRM resources. */ + public void release() { + reset(); + releaseDrmResources(); + } + /** * Discards to the end of the queue. The read position is also advanced. */ @@ -337,20 +378,19 @@ public class SampleQueue implements TrackOutput { return metadataQueue.setReadPosition(sampleIndex); } - /** - * 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. - */ - @PeekResult - public int peekNext() { - return metadataQueue.peekNext(downstreamFormat); - } - /** * Attempts to read from the queue. * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. + *

{@link Format Formats} read from the this method may be associated to a {@link DrmSession} + * through {@link FormatHolder#drmSession}, which is populated in two scenarios: + * + *

+ * + * @param outputFormatHolder 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 @@ -359,33 +399,57 @@ public class SampleQueue implements TrackOutput { * @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 decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * be set if the buffer's timestamp is less than this value. * @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 int read( - FormatHolder formatHolder, + FormatHolder outputFormatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean allowOnlyClearBuffers, 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, + scratchFormatHolder, buffer, - formatRequired, - allowOnlyClearBuffers, + readFlagFormatRequired, + readFlagAllowOnlyClearBuffers, loadingFinished, downstreamFormat, extrasHolder); switch (result) { case C.RESULT_FORMAT_READ: - downstreamFormat = formatHolder.format; + 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()) { @@ -404,6 +468,35 @@ public class SampleQueue implements TrackOutput { } } + /** + * 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) { + @SampleQueue.PeekResult int nextInQueue = metadataQueue.peekNext(downstreamFormat); + switch (nextInQueue) { + case SampleQueue.PEEK_RESULT_NOTHING: + return loadingFinished; + case SampleQueue.PEEK_RESULT_FORMAT: + return true; + case SampleQueue.PEEK_RESULT_BUFFER_CLEAR: + return currentSession == null || playClearSamplesWithoutKeys; + case SampleQueue.PEEK_RESULT_BUFFER_ENCRYPTED: + return drmSessionManager == DrmSessionManager.DUMMY + || Assertions.checkNotNull(currentSession).getState() + == DrmSession.STATE_OPENED_WITH_KEYS; + default: + throw new IllegalStateException(); + } + } + /** * Reads data from the rolling buffer to populate a decoder input buffer. * @@ -748,9 +841,55 @@ public class SampleQueue implements TrackOutput { return format; } + /** Releases any held DRM resources. */ + private void releaseDrmResources() { + if (currentSession != null) { + currentSession.releaseReference(); + currentSession = null; + } + } + /** - * A node in a linked list of {@link Allocation}s held by the output. + * 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 { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 61e2868725..efdf927091 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; @@ -74,7 +73,6 @@ public class ChunkSampleStream implements SampleStream, S private final ArrayList mediaChunks; private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; - private final DecryptableSampleQueueReader primarySampleQueueReader; private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; @@ -132,14 +130,13 @@ public class ChunkSampleStream implements SampleStream, S int[] trackTypes = new int[1 + embeddedTrackCount]; SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new SampleQueue(allocator); - primarySampleQueueReader = - new DecryptableSampleQueueReader(primarySampleQueue, drmSessionManager); + primarySampleQueue = new SampleQueue(allocator, drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { - SampleQueue sampleQueue = new SampleQueue(allocator); + SampleQueue sampleQueue = + new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager()); embeddedSampleQueues[i] = sampleQueue; sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; @@ -337,19 +334,18 @@ public class ChunkSampleStream implements SampleStream, S public void release(@Nullable ReleaseCallback callback) { this.releaseCallback = callback; // Discard as much as we can synchronously. - primarySampleQueue.discardToEnd(); - primarySampleQueueReader.release(); + primarySampleQueue.preRelease(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.discardToEnd(); + embeddedSampleQueue.preRelease(); } loader.release(this); } @Override public void onLoaderReleased() { - primarySampleQueue.reset(); + primarySampleQueue.release(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(); + embeddedSampleQueue.release(); } if (releaseCallback != null) { releaseCallback.onSampleStreamReleased(this); @@ -360,13 +356,13 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return !isPendingReset() && primarySampleQueueReader.isReady(loadingFinished); + return !isPendingReset() && primarySampleQueue.isReady(loadingFinished); } @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); - primarySampleQueueReader.maybeThrowError(); + primarySampleQueue.maybeThrowError(); if (!loader.isLoading()) { chunkSource.maybeThrowError(); } @@ -380,7 +376,7 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyPrimaryTrackFormatChanged(); - return primarySampleQueueReader.read( + return primarySampleQueue.read( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @@ -781,7 +777,6 @@ public class ChunkSampleStream implements SampleStream, S formatHolder, buffer, formatRequired, - /* allowOnlyClearBuffers= */ false, loadingFinished, decodeOnlyUntilPositionUs); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 6812e08ef7..b6b5b69e14 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -29,16 +29,24 @@ 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.drm.ExoMediaCrypto; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; import java.util.Arrays; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; /** Test for {@link SampleQueue}. */ @RunWith(AndroidJUnit4.class) @@ -50,6 +58,13 @@ public final class SampleQueueTest { private static final Format FORMAT_2 = Format.createSampleFormat("2", "mimeType", 0); private static final Format FORMAT_1_COPY = Format.createSampleFormat("1", "mimeType", 0); private static final Format FORMAT_SPLICED = Format.createSampleFormat("spliced", "mimeType", 0); + private static final Format FORMAT_ENCRYPTED = + Format.createSampleFormat( + /* id= */ "encrypted", + "mimeType", + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + new DrmInitData()); private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10); /* @@ -91,18 +106,45 @@ public final class SampleQueueTest { private static final Format[] SAMPLE_FORMATS = new Format[] {FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_1, FORMAT_2, FORMAT_2, FORMAT_2, FORMAT_2}; private static final int DATA_SECOND_KEYFRAME_INDEX = 4; + + private static final int[] ENCRYPTED_SAMPLES_FLAGS = + new int[] { + C.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_ENCRYPTED, 0, C.BUFFER_FLAG_ENCRYPTED, + }; + private static final long[] ENCRYPTED_SAMPLE_TIMESTAMPS = new long[] {0, 1000, 2000, 3000}; + private static final Format[] ENCRYPTED_SAMPLES_FORMATS = + new Format[] {FORMAT_ENCRYPTED, FORMAT_ENCRYPTED, FORMAT_1, FORMAT_ENCRYPTED}; + /** Encrypted samples require the encryption preamble. */ + private static final int[] ENCRYPTED_SAMPLES_SIZES = new int[] {1, 3, 1, 3}; + + private static final int[] ENCRYPTED_SAMPLES_OFFSETS = new int[] {7, 4, 3, 0}; + private static final byte[] ENCRYPTED_SAMPLES_DATA = new byte[8]; + + static { + Arrays.fill(ENCRYPTED_SAMPLES_DATA, (byte) 1); + } + private static final TrackOutput.CryptoData DUMMY_CRYPTO_DATA = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; + private DrmSessionManager mockDrmSessionManager; + private DrmSession mockDrmSession; private SampleQueue sampleQueue; private FormatHolder formatHolder; private DecoderInputBuffer inputBuffer; @Before + @SuppressWarnings("unchecked") public void setUp() throws Exception { allocator = new DefaultAllocator(false, ALLOCATION_SIZE); - sampleQueue = new SampleQueue(allocator); + mockDrmSessionManager = + (DrmSessionManager) Mockito.mock(DrmSessionManager.class); + mockDrmSession = (DrmSession) Mockito.mock(DrmSession.class); + Mockito.when( + mockDrmSessionManager.acquireSession(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(mockDrmSession); + sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @@ -153,9 +195,9 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); assertReadFormat(false, FORMAT_1); - assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE); + assertReadSample(0, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); // Assert the second sample is read without a format change. - assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE); + assertReadSample(1000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); // The same applies if the queue is empty when the formats are written. sampleQueue.format(FORMAT_2); @@ -164,7 +206,7 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(2000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // Assert the third sample is read without a format change. - assertReadSample(2000, true, DATA, 0, ALLOCATION_SIZE); + assertReadSample(2000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); } @Test @@ -187,7 +229,7 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Otherwise should read the sample. - assertReadSample(1000, true, DATA, 0, ALLOCATION_SIZE); + assertReadSample(1000, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -204,7 +246,7 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Read the sample. - assertReadSample(2000, false, DATA, 0, ALLOCATION_SIZE - 1); + assertReadSample(2000, false, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE - 1); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -218,7 +260,7 @@ public final class SampleQueueTest { // If formatRequired, should read the format rather than the sample. assertReadFormat(true, FORMAT_1); // Read the sample. - assertReadSample(3000, false, DATA, ALLOCATION_SIZE - 1, 1); + assertReadSample(3000, false, /* isEncrypted= */ false, DATA, ALLOCATION_SIZE - 1, 1); // Allocation should still be held. assertAllocationCount(1); sampleQueue.discardToRead(); @@ -265,6 +307,137 @@ public final class SampleQueueTest { assertReadTestData(); } + @Test + public void testReadEncryptedSectionsWaitsForKeys() { + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); + writeEncryptedTestData(); + + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadNothing(/* formatRequired= */ false); + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + assertReadEncryptedSample(/* sampleIndex= */ 0); + } + + @Test + public void testReadEncryptedSectionsPopulatesDrmSession() { + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + writeEncryptedTestData(); + + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); + assertReadEncryptedSample(/* sampleIndex= */ 0); + assertReadEncryptedSample(/* sampleIndex= */ 1); + formatHolder.clear(); + assertThat(formatHolder.drmSession).isNull(); + result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isNull(); + assertReadEncryptedSample(/* sampleIndex= */ 2); + result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); + } + + @Test + @SuppressWarnings("unchecked") + public void testAllowPlaceholderSessionPopulatesDrmSession() { + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + DrmSession mockPlaceholderDrmSession = + (DrmSession) Mockito.mock(DrmSession.class); + Mockito.when(mockPlaceholderDrmSession.getState()) + .thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + Mockito.when(mockDrmSessionManager.acquirePlaceholderSession(ArgumentMatchers.any())) + .thenReturn(mockPlaceholderDrmSession); + writeEncryptedTestData(); + + int result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); + assertReadEncryptedSample(/* sampleIndex= */ 0); + assertReadEncryptedSample(/* sampleIndex= */ 1); + formatHolder.clear(); + assertThat(formatHolder.drmSession).isNull(); + result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isSameInstanceAs(mockPlaceholderDrmSession); + assertReadEncryptedSample(/* sampleIndex= */ 2); + result = + sampleQueue.read( + formatHolder, + inputBuffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); + assertThat(formatHolder.drmSession).isSameInstanceAs(mockDrmSession); + } + + @Test + public void testReadWithErrorSessionReadsNothingAndThrows() throws IOException { + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); + writeEncryptedTestData(); + + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadNothing(/* formatRequired= */ false); + sampleQueue.maybeThrowError(); + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_ERROR); + Mockito.when(mockDrmSession.getError()) + .thenReturn(new DrmSession.DrmSessionException(new Exception())); + assertReadNothing(/* formatRequired= */ false); + try { + sampleQueue.maybeThrowError(); + Assert.fail(); + } catch (IOException e) { + // Expected. + } + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); + assertReadEncryptedSample(/* sampleIndex= */ 0); + } + + @Test + public void testAllowPlayClearSamplesWithoutKeysReadsClearSamples() { + Mockito.when(mockDrmSessionManager.getFlags()) + .thenReturn(DrmSessionManager.FLAG_PLAY_CLEAR_SAMPLES_WITHOUT_KEYS); + // We recreate the queue to ensure the mock DRM session manager flags are taken into account. + sampleQueue = new SampleQueue(allocator, mockDrmSessionManager); + Mockito.when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); + writeEncryptedTestData(); + + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadEncryptedSample(/* sampleIndex= */ 0); + } + @Test public void testRewindAfterDiscard() { writeTestData(); @@ -313,7 +486,7 @@ public final class SampleQueueTest { sampleQueue.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); // Once the metadata has been written, check the sample can be read as expected. - assertReadSample(0, true, DATA, 0, ALLOCATION_SIZE); + assertReadSample(0, true, /* isEncrypted= */ false, DATA, 0, ALLOCATION_SIZE); assertNoSamplesToRead(FORMAT_1); assertAllocationCount(1); sampleQueue.discardToRead(); @@ -541,50 +714,7 @@ public final class SampleQueueTest { // Discarding everything from upstream without reading should unset the largest timestamp. assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } - - @Test - public void testAllowOnlyClearBuffers() { - int[] flags = - new int[] { - C.BUFFER_FLAG_KEY_FRAME, - C.BUFFER_FLAG_ENCRYPTED, - 0, - 0, - 0, - C.BUFFER_FLAG_KEY_FRAME | C.BUFFER_FLAG_ENCRYPTED, - 0, - 0 - }; - int[] sampleSizes = new int[flags.length]; - Arrays.fill(sampleSizes, /* val= */ 1); - - // Two encryption preamble bytes per encrypted sample in the sample queue. - byte[] sampleData = new byte[flags.length + 2 + 2]; - Arrays.fill(sampleData, /* val= */ (byte) 1); - - writeTestData( - sampleData, sampleSizes, new int[flags.length], SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, flags); - assertReadFormat(/* formatRequired= */ false, FORMAT_1); - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); - - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); - - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_FORMAT_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); - - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ false); - - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_BUFFER_READ, /* allowOnlyClearBuffers= */ true); - assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ true); - - assertResult(RESULT_NOTHING_READ, /* allowOnlyClearBuffers= */ false); - } - + @Test public void testLargestQueuedTimestampWithRead() { writeTestData(); @@ -612,7 +742,7 @@ public final class SampleQueueTest { writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); assertReadTestData(null, 0, 4); assertReadFormat(false, FORMAT_SPLICED); - assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length); + assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); assertReadEndOfStream(false); } @@ -634,7 +764,7 @@ public final class SampleQueueTest { spliceSampleTimeUs = SAMPLE_TIMESTAMPS[3] + 1; writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); assertReadFormat(false, FORMAT_SPLICED); - assertReadSample(spliceSampleTimeUs, true, DATA, 0, DATA.length); + assertReadSample(spliceSampleTimeUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); assertReadEndOfStream(false); } @@ -649,7 +779,8 @@ public final class SampleQueueTest { writeSample(DATA, spliceSampleTimeUs, FORMAT_SPLICED, C.BUFFER_FLAG_KEY_FRAME); assertReadTestData(null, 0, 4, sampleOffsetUs); assertReadFormat(false, FORMAT_SPLICED.copyWithSubsampleOffsetUs(sampleOffsetUs)); - assertReadSample(spliceSampleTimeUs + sampleOffsetUs, true, DATA, 0, DATA.length); + assertReadSample( + spliceSampleTimeUs + sampleOffsetUs, true, /* isEncrypted= */ false, DATA, 0, DATA.length); assertReadEndOfStream(false); } @@ -663,6 +794,16 @@ public final class SampleQueueTest { DATA, SAMPLE_SIZES, SAMPLE_OFFSETS, SAMPLE_TIMESTAMPS, SAMPLE_FORMATS, SAMPLE_FLAGS); } + private void writeEncryptedTestData() { + writeTestData( + ENCRYPTED_SAMPLES_DATA, + ENCRYPTED_SAMPLES_SIZES, + ENCRYPTED_SAMPLES_OFFSETS, + ENCRYPTED_SAMPLE_TIMESTAMPS, + ENCRYPTED_SAMPLES_FORMATS, + ENCRYPTED_SAMPLES_FLAGS); + } + /** * Writes the specified test data to {@code sampleQueue}. */ @@ -755,6 +896,7 @@ public final class SampleQueueTest { assertReadSample( SAMPLE_TIMESTAMPS[i] + sampleOffsetUs, (SAMPLE_FLAGS[i] & C.BUFFER_FLAG_KEY_FRAME) != 0, + /* isEncrypted= */ false, DATA, DATA.length - SAMPLE_OFFSETS[i] - SAMPLE_SIZES[i], SAMPLE_SIZES[i]); @@ -801,7 +943,6 @@ public final class SampleQueueTest { formatHolder, inputBuffer, formatRequired, - /* allowOnlyClearBuffers= */ false, /* loadingFinished= */ false, /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_NOTHING_READ); @@ -825,7 +966,6 @@ public final class SampleQueueTest { formatHolder, inputBuffer, formatRequired, - /* allowOnlyClearBuffers= */ false, /* loadingFinished= */ true, /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); @@ -852,7 +992,6 @@ public final class SampleQueueTest { formatHolder, inputBuffer, formatRequired, - /* allowOnlyClearBuffers= */ false, /* loadingFinished= */ false, /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_FORMAT_READ); @@ -863,25 +1002,44 @@ public final class SampleQueueTest { assertInputBufferHasNoDefaultFlagsSet(); } + private void assertReadEncryptedSample(int sampleIndex) { + byte[] sampleData = new byte[ENCRYPTED_SAMPLES_SIZES[sampleIndex]]; + Arrays.fill(sampleData, (byte) 1); + boolean isKeyFrame = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0; + boolean isEncrypted = (ENCRYPTED_SAMPLES_FLAGS[sampleIndex] & C.BUFFER_FLAG_ENCRYPTED) != 0; + assertReadSample( + ENCRYPTED_SAMPLE_TIMESTAMPS[sampleIndex], + isKeyFrame, + isEncrypted, + sampleData, + /* offset= */ 0, + ENCRYPTED_SAMPLES_SIZES[sampleIndex] - (isEncrypted ? 2 : 0)); + } + /** * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is * filled with the specified sample data. * * @param timeUs The expected buffer timestamp. * @param isKeyframe The expected keyframe flag. + * @param isEncrypted The expected encrypted flag. * @param sampleData An array containing the expected sample data. * @param offset The offset in {@code sampleData} of the expected sample data. * @param length The length of the expected sample data. */ private void assertReadSample( - long timeUs, boolean isKeyframe, byte[] sampleData, int offset, int length) { + long timeUs, + boolean isKeyframe, + boolean isEncrypted, + byte[] sampleData, + int offset, + int length) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read( formatHolder, inputBuffer, /* formatRequired= */ false, - /* allowOnlyClearBuffers= */ false, /* loadingFinished= */ false, /* decodeOnlyUntilUs= */ 0); assertThat(result).isEqualTo(RESULT_BUFFER_READ); @@ -891,7 +1049,7 @@ public final class SampleQueueTest { assertThat(inputBuffer.timeUs).isEqualTo(timeUs); assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyframe); assertThat(inputBuffer.isDecodeOnly()).isFalse(); - assertThat(inputBuffer.isEncrypted()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted); inputBuffer.flip(); assertThat(inputBuffer.data.limit()).isEqualTo(length); byte[] readData = new byte[length]; @@ -905,7 +1063,6 @@ public final class SampleQueueTest { sampleQueue.read( formatHolder, inputBuffer, - /* formatRequired= */ false, allowOnlyClearBuffers, /* loadingFinished= */ false, /* decodeOnlyUntilUs= */ 0); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index af4bf3ad70..f7879a18cc 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; +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.metadata.Metadata; @@ -195,7 +196,8 @@ public final class PlayerEmsgHandler implements Handler.Callback { /** Returns a {@link TrackOutput} that emsg messages could be written to. */ public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { - return new PlayerTrackEmsgHandler(new SampleQueue(allocator)); + return new PlayerTrackEmsgHandler( + new SampleQueue(allocator, DrmSessionManager.getDummyDrmSessionManager())); } /** Release this emsg handler. It should not be reused after this call. */ @@ -376,7 +378,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { formatHolder, buffer, /* formatRequired= */ false, - /* allowOnlyClearBuffers= */ false, /* loadingFinished= */ false, /* decodeOnlyUntilUs= */ 0); if (result == C.RESULT_BUFFER_READ) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 41646096b1..e0d7437a21 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -36,7 +36,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; -import com.google.android.exoplayer2.source.DecryptableSampleQueueReader; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -129,7 +128,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final Map overridingDrmInitData; private SampleQueue[] sampleQueues; - private DecryptableSampleQueueReader[] sampleQueueReaders; private int[] sampleQueueTrackIds; private Set sampleQueueMappingDoneByType; private SparseIntArray sampleQueueIndicesByType; @@ -209,7 +207,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); sampleQueues = new SampleQueue[0]; - sampleQueueReaders = new DecryptableSampleQueueReader[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); @@ -490,10 +487,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); - } - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + sampleQueue.preRelease(); } } loader.release(this); @@ -504,9 +498,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void onLoaderReleased() { - resetSampleQueues(); - for (DecryptableSampleQueueReader reader : sampleQueueReaders) { - reader.release(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.release(); } } @@ -521,12 +514,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // SampleStream implementation. public boolean isReady(int sampleQueueIndex) { - return !isPendingReset() && sampleQueueReaders[sampleQueueIndex].isReady(loadingFinished); + return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished); } public void maybeThrowError(int sampleQueueIndex) throws IOException { maybeThrowError(); - sampleQueueReaders[sampleQueueIndex].maybeThrowError(); + sampleQueues[sampleQueueIndex].maybeThrowError(); } public void maybeThrowError() throws IOException { @@ -559,7 +552,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } int result = - sampleQueueReaders[sampleQueueIndex].read( + sampleQueues[sampleQueueIndex].read( formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { Format format = Assertions.checkNotNull(formatHolder.format); @@ -917,17 +910,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private SampleQueue createSampleQueue(int id, int type) { int trackCount = sampleQueues.length; - SampleQueue trackOutput = new FormatAdjustingSampleQueue(allocator, overridingDrmInitData); + SampleQueue trackOutput = + new FormatAdjustingSampleQueue(allocator, drmSessionManager, overridingDrmInitData); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput); - sampleQueueReaders = - Util.nullSafeArrayAppend( - sampleQueueReaders, - new DecryptableSampleQueueReader(sampleQueues[trackCount], drmSessionManager)); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; @@ -1295,8 +1285,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final Map overridingDrmInitData; public FormatAdjustingSampleQueue( - Allocator allocator, Map overridingDrmInitData) { - super(allocator); + Allocator allocator, + DrmSessionManager drmSessionManager, + Map overridingDrmInitData) { + super(allocator, drmSessionManager); this.overridingDrmInitData = overridingDrmInitData; }