diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fd02527b4d..2237810a19 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ * Request smaller decoder input buffers for Dolby Vision. This fixes an issue that could cause UHD Dolby Vision playbacks to fail on some devices, including Amazon Fire TV 4K. +* DRM: + * Fix `DefaultDrmSessionManager` to correctly eagerly release preacquired + DRM sessions when there's a shortage of DRM resources on the device. * UI * `SubtitleView` no longer implements `TextOutput`. `SubtitleView` implements `Player.Listener`, so can be registered to a player with 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 ad5ba1427b..cd40555205 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 @@ -684,13 +684,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); } @@ -702,6 +696,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); } @@ -728,6 +727,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 e5375012f7..ea8e91628b 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,