From b4aa8688dfe565c81c99a59fed383ae304cc85be Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 14 Sep 2021 17:26:48 +0100 Subject: [PATCH] Fix how preacquired DRM sessions are released under resource contention Previously the released preacquired sessions would start their keepalive timeout, and so no additional resources would be freed in time for the manager to retry the session acquisition. This change adds an additional purge of keepalive sessions *after* the preacquired sessions are released, which fixes the problem. PiperOrigin-RevId: 396613352 --- RELEASENOTES.md | 3 + .../drm/DefaultDrmSessionManager.java | 22 +++++-- .../drm/DefaultDrmSessionManagerTest.java | 65 ++++++++++++++++++- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d2779ced0c..2ebd6e27dd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -30,6 +30,9 @@ thrown from `Requirements.isInternetConnectivityValidated` on devices running Android 11 ([#9002](https://github.com/google/ExoPlayer/issues/9002)). +* DRM: + * Fix `DefaultDrmSessionManager` to correctly eagerly release preacquired + DRM sessions when there's a shortage of DRM resources on the device. * DASH: * Use identical cache keys for downloading and playing DASH segments ([#9370](https://github.com/google/ExoPlayer/issues/9370)). 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 774e91eee8..f0c4c30d62 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 @@ -687,13 +687,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // If we're short on DRM session resources, first try eagerly releasing all our keepalive // sessions and then retry the acquisition. if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) { - // Make a local copy, because sessions are removed from this.keepaliveSessions during - // release (via callback). - ImmutableSet keepaliveSessions = - ImmutableSet.copyOf(this.keepaliveSessions); - for (DrmSession keepaliveSession : keepaliveSessions) { - keepaliveSession.release(/* eventDispatcher= */ null); - } + releaseAllKeepaliveSessions(); undoAcquisition(session, eventDispatcher); session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); } @@ -705,6 +699,11 @@ public class DefaultDrmSessionManager implements DrmSessionManager { && shouldReleasePreacquiredSessionsBeforeRetrying && !preacquiredSessionReferences.isEmpty()) { releaseAllPreacquiredSessions(); + if (!keepaliveSessions.isEmpty()) { + // Some preacquired sessions released above are now in their keepalive timeout phase. We + // release the keepalive references immediately. + releaseAllKeepaliveSessions(); + } undoAcquisition(session, eventDispatcher); session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); } @@ -731,6 +730,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } } + private void releaseAllKeepaliveSessions() { + // Make a local copy, because sessions are removed from this.keepaliveSessions during + // release (via callback). + ImmutableSet keepaliveSessions = ImmutableSet.copyOf(this.keepaliveSessions); + for (DrmSession keepaliveSession : keepaliveSessions) { + keepaliveSession.release(/* eventDispatcher= */ null); + } + } + private void releaseAllPreacquiredSessions() { // Make a local copy, because sessions are removed from this.preacquiredSessionReferences // during release (via callback). diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java index 4dac826ce2..0af11b2a2d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmSessionManager.DrmSessionReference; import com.google.android.exoplayer2.drm.ExoMediaDrm.AppManagedProvider; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; @@ -302,6 +303,64 @@ public class DefaultDrmSessionManagerTest { assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); } + @Test(timeout = 10_000) + public void maxConcurrentSessionsExceeded_allPreacquiredAndKeepaliveSessionsEagerlyReleased() + throws Exception { + ImmutableList secondSchemeDatas = + ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6))); + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS, secondSchemeDatas); + Format secondFormatWithDrmInitData = + new Format.Builder().setDrmInitData(new DrmInitData(secondSchemeDatas)).build(); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider( + DRM_SCHEME_UUID, + uuid -> new FakeExoMediaDrm.Builder().setMaxConcurrentSessions(1).build()) + .setSessionKeepaliveMs(10_000) + .setMultiSession(true) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + DrmSessionReference firstDrmSessionReference = + checkNotNull( + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + DrmSession firstDrmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + waitForOpenedWithKeys(firstDrmSession); + firstDrmSession.release(/* eventDispatcher= */ null); + + // The direct reference to firstDrmSession has been released, it's being kept alive by both + // firstDrmSessionReference and drmSessionManager's internal reference. + assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + DrmSession secondDrmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + secondFormatWithDrmInitData)); + // The drmSessionManager had to release both it's internal keep-alive reference and the + // reference represented by firstDrmSessionReference in order to acquire secondDrmSession. + assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + + waitForOpenedWithKeys(secondDrmSession); + assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + + // Not needed (because the manager has already released this reference) but we call it anyway + // for completeness. + firstDrmSessionReference.release(); + // Clean-up + secondDrmSession.release(/* eventDispatcher= */ null); + drmSessionManager.release(); + } + @Test(timeout = 10_000) public void sessionReacquired_keepaliveTimeOutCancelled() throws Exception { FakeExoMediaDrm.LicenseServer licenseServer = @@ -364,7 +423,7 @@ public class DefaultDrmSessionManagerTest { drmSessionManager.prepare(); - DrmSessionManager.DrmSessionReference sessionReference = + DrmSessionReference sessionReference = drmSessionManager.preacquireSession( /* playbackLooper= */ checkNotNull(Looper.myLooper()), eventDispatcher, @@ -413,7 +472,7 @@ public class DefaultDrmSessionManagerTest { drmSessionManager.prepare(); - DrmSessionManager.DrmSessionReference sessionReference = + DrmSessionReference sessionReference = drmSessionManager.preacquireSession( /* playbackLooper= */ checkNotNull(Looper.myLooper()), /* eventDispatcher= */ null, @@ -453,7 +512,7 @@ public class DefaultDrmSessionManagerTest { drmSessionManager.prepare(); - DrmSessionManager.DrmSessionReference sessionReference = + DrmSessionReference sessionReference = drmSessionManager.preacquireSession( /* playbackLooper= */ checkNotNull(Looper.myLooper()), /* eventDispatcher= */ null,