From 41e6577dca908ba543957c9e75d242010e50e91b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 18 Aug 2020 15:00:45 +0100 Subject: [PATCH] Remove DrmSessionManager.acquirePlaceholderSession In order to acquire a placeholder session, clients can call acquireSession with a format with a null drmInitData. PiperOrigin-RevId: 327220249 --- .../drm/DefaultDrmSessionManager.java | 61 ++++++++--------- .../exoplayer2/drm/DrmSessionManager.java | 65 +++++++++---------- .../exoplayer2/drm/OfflineLicenseHelper.java | 6 +- .../exoplayer2/source/SampleQueue.java | 13 +--- .../exoplayer2/source/SampleQueueTest.java | 56 +++++++++++----- .../exoplayer2/testutil/FakeSampleStream.java | 6 +- 6 files changed, 108 insertions(+), 99 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 3f75500a2b..04756c57ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -409,7 +409,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be * called before {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} - * or {@link #acquirePlaceholderSession} is called. + * is called. * *

By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. @@ -469,34 +469,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override @Nullable - public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { - initPlaybackLooper(playbackLooper); - ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); - boolean avoidPlaceholderDrmSessions = - FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) - && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; - // Avoid attaching a session to sparse formats. - if (avoidPlaceholderDrmSessions - || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET - || exoMediaDrm.getExoMediaCryptoType() == null) { - return null; - } - maybeCreateMediaDrmHandler(playbackLooper); - if (placeholderDrmSession == null) { - DefaultDrmSession placeholderDrmSession = - createAndAcquireSessionWithRetry( - /* schemeDatas= */ ImmutableList.of(), - /* isPlaceholderSession= */ true, - /* eventDispatcher= */ null); - sessions.add(placeholderDrmSession); - this.placeholderDrmSession = placeholderDrmSession; - } else { - placeholderDrmSession.acquire(/* eventDispatcher= */ null); - } - return placeholderDrmSession; - } - - @Override public DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @@ -504,6 +476,11 @@ public class DefaultDrmSessionManager implements DrmSessionManager { initPlaybackLooper(playbackLooper); maybeCreateMediaDrmHandler(playbackLooper); + if (format.drmInitData == null) { + // Content is not encrypted. + return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType)); + } + @Nullable List schemeDatas = null; if (offlineLicenseKeySetId == null) { schemeDatas = getSchemeDatas(Assertions.checkNotNull(format.drmInitData), uuid, false); @@ -565,6 +542,32 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Internal methods. + @Nullable + private DrmSession maybeAcquirePlaceholderSession(int trackType) { + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + boolean avoidPlaceholderDrmSessions = + FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) + && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + // Avoid attaching a session to sparse formats. + if (avoidPlaceholderDrmSessions + || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET + || exoMediaDrm.getExoMediaCryptoType() == null) { + return null; + } + if (placeholderDrmSession == null) { + DefaultDrmSession placeholderDrmSession = + createAndAcquireSessionWithRetry( + /* schemeDatas= */ ImmutableList.of(), + /* isPlaceholderSession= */ true, + /* eventDispatcher= */ null); + sessions.add(placeholderDrmSession); + this.placeholderDrmSession = placeholderDrmSession; + } else { + placeholderDrmSession.acquire(/* eventDispatcher= */ null); + } + return placeholderDrmSession; + } + private boolean canAcquireSession(DrmInitData drmInitData) { if (offlineLicenseKeySetId != null) { // An offline license can be restored so a session can always be acquired. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 7c26142216..1168884d76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; /** Manages a DRM session. */ @@ -33,13 +32,19 @@ public interface DrmSessionManager { new DrmSessionManager() { @Override + @Nullable public DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { - return new ErrorStateDrmSession( - new DrmSession.DrmSessionException( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + if (format.drmInitData == null) { + return null; + } else { + return new ErrorStateDrmSession( + new DrmSession.DrmSessionException( + new UnsupportedDrmException( + UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + } } @Override @@ -64,39 +69,27 @@ public interface DrmSessionManager { // Do nothing. } - /** - * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference - * count. When the caller no longer needs to use the instance, it must call {@link - * DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count. - * - *

Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for - * playback of clear content periods. This can reduce the cost of transitioning between clear and - * encrypted content periods. - * - * @param playbackLooper The looper associated with the media playback thread. - * @param trackType The type of the track to acquire a placeholder session for. Must be one of the - * {@link C}{@code .TRACK_TYPE_*} constants. - * @return The placeholder DRM session, or null if this DRM session manager does not support - * placeholder sessions. - */ - @Nullable - default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { - return null; - } - /** * Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference - * count. When the caller no longer needs to use the instance, it must call {@link + * count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is + * not configured to attach a {@link DrmSession} to clear content. When the caller no longer needs + * to use a returned {@link DrmSession}, it must call {@link * DrmSession#release(DrmSessionEventListener.EventDispatcher)} to decrement the reference count. * + *

If the provided {@link Format} contains a null {@link Format#drmInitData}, the returned + * {@link DrmSession} (if not null) will be a placeholder session which does not execute key + * requests, and cannot be used to handle encrypted content. However, a placeholder session may be + * used to configure secure decoders for playback of clear content periods, which can reduce the + * cost of transitioning between clear and encrypted content. + * * @param playbackLooper The looper associated with the media playback thread. * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute * events, and passed on to {@link * DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}. - * @param format The {@link Format} for which to acquire a {@link DrmSession}. Must contain a - * non-null {@link Format#drmInitData}. - * @return The DRM session. + * @param format The {@link Format} for which to acquire a {@link DrmSession}. + * @return The DRM session. May be null if the given {@link Format#drmInitData} is null. */ + @Nullable DrmSession acquireSession( Looper playbackLooper, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @@ -105,16 +98,16 @@ public interface DrmSessionManager { /** * Returns the {@link ExoMediaCrypto} type associated to sessions acquired for the given {@link * Format}. Returns the {@link UnsupportedMediaCrypto} type if this DRM session manager does not - * support any of the DRM schemes defined in the given {@link Format}. If the {@link Format} - * describes unencrypted content, returns an {@link ExoMediaCrypto} type if this DRM session - * manager would associate a {@link #acquirePlaceholderSession placeholder session} to the given - * {@link Format}, or null otherwise. + * support any of the DRM schemes defined in the given {@link Format}. Returns null if {@link + * Format#drmInitData} is null and {@link #acquireSession} would return null for the given {@link + * Format}. * * @param format The {@link Format} for which to return the {@link ExoMediaCrypto} type. - * @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given - * parameters, or the {@link UnsupportedMediaCrypto} type if the provided {@code drmInitData} - * is not supported, or {@code null} if {@code drmInitData} is null and no DRM session will be - * associated to the given {@code trackType}. + * @return The {@link ExoMediaCrypto} type associated to sessions acquired using the given {@link + * Format}, or {@link UnsupportedMediaCrypto} if this DRM session manager does not support any + * of the DRM schemes defined in the given {@link Format}. May be null if {@link + * Format#drmInitData} is null and {@link #acquireSession} would return null for the given + * {@link Format}. */ @Nullable Class getExoMediaCryptoType(Format format); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 71091c878d..b218d0cadb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -184,7 +184,8 @@ public final class OfflineLicenseHelper { /** * Downloads an offline license. * - * @param format The {@link Format} of the content whose license is to be downloaded. + * @param format The {@link Format} of the content whose license is to be downloaded. Must contain + * a non-null {@link Format#drmInitData}. * @return The key set id for the downloaded license. * @throws DrmSessionException Thrown when a DRM session error occurs. */ @@ -278,13 +279,14 @@ public final class OfflineLicenseHelper { private DrmSession openBlockingKeyRequest( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) { + Assertions.checkNotNull(format.drmInitData); drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); conditionVariable.close(); DrmSession drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(), eventDispatcher, format); // Block current thread until key loading is finished conditionVariable.block(); - return drmSession; + return Assertions.checkNotNull(drmSession); } } 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 515f805845..28d25feb03 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 @@ -327,13 +327,7 @@ public class SampleQueue implements TrackOutput { * Attempts to read from the queue. * *

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

+ * through {@link FormatHolder#drmSession}. * * @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 @@ -842,10 +836,7 @@ public class SampleQueue implements TrackOutput { // is being used for both DrmInitData. @Nullable DrmSession previousSession = currentDrmSession; currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat); outputFormatHolder.drmSession = currentDrmSession; if (previousSession != null) { 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 4583c542b3..54e2dd902d 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 @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; 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; @@ -54,7 +55,6 @@ 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}. */ @@ -69,6 +69,8 @@ public final class SampleQueueTest { private static final Format FORMAT_SPLICED = buildFormat(/* id= */ "spliced"); private static final Format FORMAT_ENCRYPTED = new Format.Builder().setId(/* id= */ "encrypted").setDrmInitData(new DrmInitData()).build(); + private static final Format FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE = + FORMAT_ENCRYPTED.copyWithExoMediaCryptoType(MockExoMediaCrypto.class); private static final byte[] DATA = TestUtil.buildTestData(ALLOCATION_SIZE * 10); /* @@ -128,7 +130,7 @@ public final class SampleQueueTest { new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, new byte[16], 0, 0); private Allocator allocator; - private DrmSessionManager mockDrmSessionManager; + private MockDrmSessionManager mockDrmSessionManager; private DrmSession mockDrmSession; private DrmSessionEventListener.EventDispatcher eventDispatcher; private SampleQueue sampleQueue; @@ -138,11 +140,8 @@ public final class SampleQueueTest { @Before public void setUp() { allocator = new DefaultAllocator(false, ALLOCATION_SIZE); - mockDrmSessionManager = Mockito.mock(DrmSessionManager.class); mockDrmSession = Mockito.mock(DrmSession.class); - when(mockDrmSessionManager.acquireSession( - ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) - .thenReturn(mockDrmSession); + mockDrmSessionManager = new MockDrmSessionManager(mockDrmSession); eventDispatcher = new DrmSessionEventListener.EventDispatcher(); sampleQueue = new SampleQueue( @@ -399,7 +398,7 @@ public final class SampleQueueTest { @Test public void isReadyReturnsTrueForValidDrmSession() { writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isFalse(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue(); @@ -424,7 +423,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadNothing(/* formatRequired= */ false); assertThat(inputBuffer.waitingForKeys).isTrue(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); @@ -464,9 +463,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - when(mockDrmSessionManager.acquirePlaceholderSession( - ArgumentMatchers.any(), ArgumentMatchers.anyInt())) - .thenReturn(mockPlaceholderDrmSession); + mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession; writeTestDataWithEncryptedSections(); int result = @@ -497,9 +494,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); DrmSession mockPlaceholderDrmSession = Mockito.mock(DrmSession.class); when(mockPlaceholderDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); - when(mockDrmSessionManager.acquirePlaceholderSession( - ArgumentMatchers.any(), ArgumentMatchers.anyInt())) - .thenReturn(mockPlaceholderDrmSession); + mockDrmSessionManager.mockPlaceholderDrmSession = mockPlaceholderDrmSession; writeFormat(ENCRYPTED_SAMPLE_FORMATS[0]); byte[] sampleData = new byte[] {0, 1, 2}; @@ -540,7 +535,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadNothing(/* formatRequired= */ false); sampleQueue.maybeThrowError(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_ERROR); @@ -569,7 +564,7 @@ public final class SampleQueueTest { when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED); writeTestDataWithEncryptedSections(); - assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); + assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED_WITH_EXO_MEDIA_CRYPTO_TYPE); assertReadEncryptedSample(/* sampleIndex= */ 0); } @@ -1497,4 +1492,33 @@ public final class SampleQueueTest { private static Format copyWithLabel(Format format, String label) { return format.buildUpon().setLabel(label).build(); } + + private static final class MockExoMediaCrypto implements ExoMediaCrypto {} + + private static final class MockDrmSessionManager implements DrmSessionManager { + + private final DrmSession mockDrmSession; + @Nullable private DrmSession mockPlaceholderDrmSession; + + private MockDrmSessionManager(DrmSession mockDrmSession) { + this.mockDrmSession = mockDrmSession; + } + + @Nullable + @Override + public DrmSession acquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + return format.drmInitData != null ? mockDrmSession : mockPlaceholderDrmSession; + } + + @Nullable + @Override + public Class getExoMediaCryptoType(Format format) { + return mockPlaceholderDrmSession != null || format.drmInitData != null + ? MockExoMediaCrypto.class + : null; + } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 824d3c02e3..7d63e129db 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.Iterables; import java.io.IOException; @@ -274,10 +273,7 @@ public class FakeSampleStream implements SampleStream { @Nullable DrmSession previousSession = currentDrmSession; Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper()); currentDrmSession = - newDrmInitData != null - ? drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat) - : drmSessionManager.acquirePlaceholderSession( - playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + drmSessionManager.acquireSession(playbackLooper, drmEventDispatcher, newFormat); outputFormatHolder.drmSession = currentDrmSession; if (previousSession != null) {