diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 994216172fd..e085209e2d6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -68,6 +68,8 @@ * DRM: * Don't restore offline keys before releasing them. In OEMCrypto v16+ keys must be released without restoring them first. + * Ensure `DefaultDrmSession` instances keep working even after their + `DefaultDrmSessionManager` instance is released. * UI: * Keep subtitle language features embedded (e.g. rubies & tate-chu-yoko) in `Cue.text` even when `SubtitleView#setApplyEmbeddedStyles()` is 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 559f333d48c..ffdfb779e55 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 @@ -457,9 +457,15 @@ public final void prepare() { if (prepareCallsCount++ != 0) { return; } - checkState(exoMediaDrm == null); - exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); - exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); + if (exoMediaDrm == null) { + exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); + exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); + } else if (sessionKeepaliveMs != C.TIME_UNSET) { + // Re-acquire the keepalive references for any sessions that are still active. + for (int i = 0; i < sessions.size(); i++) { + sessions.get(i).acquire(/* eventDispatcher= */ null); + } + } } @Override @@ -478,8 +484,7 @@ public final void release() { } releaseAllPreacquiredSessions(); - checkNotNull(exoMediaDrm).release(); - exoMediaDrm = null; + maybeReleaseMediaDrm(); } @Override @@ -776,6 +781,17 @@ private DefaultDrmSession createAndAcquireSession( return session; } + private void maybeReleaseMediaDrm() { + if (exoMediaDrm != null + && prepareCallsCount == 0 + && sessions.isEmpty() + && preacquiredSessionReferences.isEmpty()) { + // This manager and all its sessions are fully released so we can release exoMediaDrm. + checkNotNull(exoMediaDrm).release(); + exoMediaDrm = null; + } + } + /** * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. * @@ -897,6 +913,7 @@ public void onReferenceCountDecremented(DefaultDrmSession session, int newRefere keepaliveSessions.remove(session); } } + maybeReleaseMediaDrm(); } } 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 86e6c55d6b1..d58a55e4443 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 @@ -181,6 +181,49 @@ public void managerRelease_keepaliveDisabled_doesntReleaseAnySessions() throws E assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); } + @Test(timeout = 10_000) + public void managerRelease_mediaDrmNotReleasedUntilLastSessionReleased() throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + FakeExoMediaDrm exoMediaDrm = new FakeExoMediaDrm(); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm)) + .setSessionKeepaliveMs(10_000) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + drmSessionManager.release(); + + // The manager is now in a 'releasing' state because the session is still active - so the + // ExoMediaDrm instance should still be active (with 1 reference held by this test, and 1 held + // by the manager). + assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2); + + // And re-preparing the session shouldn't acquire another reference. + drmSessionManager.prepare(); + assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2); + drmSessionManager.release(); + + drmSession.release(/* eventDispatcher= */ null); + + // The final session has been released, so now the ExoMediaDrm should be released too. + assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(1); + + // Re-preparing the fully released manager should now acquire another ExoMediaDrm reference. + drmSessionManager.prepare(); + assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2); + drmSessionManager.release(); + + exoMediaDrm.release(); + } + @Test(timeout = 10_000) public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception { ImmutableList secondSchemeDatas = @@ -452,6 +495,49 @@ public void keyRefreshEvent_triggersKeyRefresh() throws Exception { exoMediaDrm.release(); } + @Test(timeout = 10_000) + public void keyRefreshEvent_whileManagerIsReleasing_triggersKeyRefresh() throws Exception { + FakeExoMediaDrm exoMediaDrm = new FakeExoMediaDrm(); + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm)) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DefaultDrmSession drmSession = + (DefaultDrmSession) + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + waitForOpenedWithKeys(drmSession); + + assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(1); + + drmSessionManager.release(); + + exoMediaDrm.triggerEvent( + drmSession::hasSessionId, + ExoMediaDrm.EVENT_KEY_REQUIRED, + /* extra= */ 0, + /* data= */ Util.EMPTY_BYTE_ARRAY); + + while (licenseServer.getReceivedSchemeDatas().size() == 1) { + // Allow the key refresh event to be handled. + ShadowLooper.idleMainLooper(); + } + + assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(2); + assertThat(ImmutableSet.copyOf(licenseServer.getReceivedSchemeDatas())).hasSize(1); + + drmSession.release(/* eventDispatcher= */ null); + exoMediaDrm.release(); + } + @Test public void managerNotPrepared_acquireSessionAndPreacquireSessionFail() throws Exception { FakeExoMediaDrm.LicenseServer licenseServer = @@ -477,6 +563,44 @@ public void managerNotPrepared_acquireSessionAndPreacquireSessionFail() throws E FORMAT_WITH_DRM_INIT_DATA)); } + @Test + public void managerReleasing_acquireSessionAndPreacquireSessionFail() throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DefaultDrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + drmSessionManager.release(); + + // The manager's prepareCount is now zero, but the drmSession is keeping it in a 'releasing' + // state. acquireSession and preacquireSession should still fail. + assertThrows( + Exception.class, + () -> + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + assertThrows( + Exception.class, + () -> + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + + drmSession.release(/* eventDispatcher= */ null); + } + private static void waitForOpenedWithKeys(DrmSession drmSession) { // Check the error first, so we get a meaningful failure if there's been an error. assertThat(drmSession.getError()).isNull(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java index 35bb52ccec8..14663985ab8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExoMediaDrm.java @@ -283,6 +283,10 @@ public Class getExoMediaCryptoType() { // Methods to facilitate testing + public int getReferenceCount() { + return referenceCount; + } + /** * Calls {@link OnEventListener#onEvent(ExoMediaDrm, byte[], int, int, byte[])} on the attached * listener (if present) once for each open session ID which passes {@code sessionIdPredicate},