From 1bf5a273ff99261b39a03f4c4eed5d91a6e4b8af Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 27 May 2021 18:11:03 +0100 Subject: [PATCH] Ensure DefaultDrmSessions keep working if their manager is released This change introduces a third 'state' for `DefaultDrmSessionManager`: It's been fully released (prepareCount == 0) but at least one of its sessions is still active. In this state new acquisitions are rejected (`(pre)acquireSession()` calls will fail) but the machinery to support the existing sessions (ExoMediaDrm and MediaDrmHandler) is kept until they're all released. This change will allow us to remove the TODO in MediaCodecRenderer that resolves Issue: #8842. PiperOrigin-RevId: 376193952 --- RELEASENOTES.md | 2 + .../drm/DefaultDrmSessionManager.java | 27 +++- .../drm/DefaultDrmSessionManagerTest.java | 124 ++++++++++++++++++ .../exoplayer2/testutil/FakeExoMediaDrm.java | 4 + 4 files changed, 152 insertions(+), 5 deletions(-) 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},