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
This commit is contained in:
ibaker 2021-05-27 18:11:03 +01:00 committed by Oliver Woodman
parent 4cca8b6d4a
commit 1bf5a273ff
4 changed files with 152 additions and 5 deletions

View File

@ -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

View File

@ -457,9 +457,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (prepareCallsCount++ != 0) {
return;
}
checkState(exoMediaDrm == null);
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 class DefaultDrmSessionManager implements DrmSessionManager {
}
releaseAllPreacquiredSessions();
checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
maybeReleaseMediaDrm();
}
@Override
@ -776,6 +781,17 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
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 class DefaultDrmSessionManager implements DrmSessionManager {
keepaliveSessions.remove(session);
}
}
maybeReleaseMediaDrm();
}
}

View File

@ -181,6 +181,49 @@ public class DefaultDrmSessionManagerTest {
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<DrmInitData.SchemeData> secondSchemeDatas =
@ -452,6 +495,49 @@ public class DefaultDrmSessionManagerTest {
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 class DefaultDrmSessionManagerTest {
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();

View File

@ -283,6 +283,10 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
// 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},