From 607fa8bf7422b5245dabc621cb8ff600fcbcdc19 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 18 Jun 2021 15:23:28 +0100 Subject: [PATCH] Allow repeated DRM provisioning in DefaultDrmSessionManager Also change to explicitly track the provisioning session, which makes the code easier to reason about than always using the zero'th element of the list. PiperOrigin-RevId: 380181453 --- RELEASENOTES.md | 2 + .../exoplayer2/drm/DefaultDrmSession.java | 14 ++--- .../drm/DefaultDrmSessionManager.java | 59 +++++++++++------ .../drm/DefaultDrmSessionManagerTest.java | 63 +++++++++++++++++++ .../exoplayer2/testutil/FakeExoMediaDrm.java | 25 ++++++-- 5 files changed, 129 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4c841fd1ac..fd18c3deb3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -67,6 +67,8 @@ * Support changing ad break positions in the player logic ([#5067](https://github.com/google/ExoPlayer/issues/5067). * Support resuming content with an offset after an ad group. +* DRM: + * Allow repeated provisioning in `DefaultDrmSession(Manager)`. * PlayerNotificationManager: * Add `PendingIntent.FLAG_IMMUTABLE` flag to BroadcastReceiver to support Android 12. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 5e40bbf03e..aa29dc7504 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -230,7 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } public void onProvisionCompleted() { - if (openInternal(false)) { + if (openInternal()) { doLicense(true); } } @@ -290,7 +290,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; requestHandlerThread = new HandlerThread("ExoPlayer:DrmRequestHandler"); requestHandlerThread.start(); requestHandler = new RequestHandler(requestHandlerThread.getLooper()); - if (openInternal(true)) { + if (openInternal()) { doLicense(true); } } else if (eventDispatcher != null @@ -338,12 +338,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Try to open a session, do provisioning if necessary. * - * @param allowProvisioning if provisioning is allowed, set this to false when calling from - * processing provision response. * @return true on success, false otherwise. */ @EnsuresNonNullIf(result = true, expression = "sessionId") - private boolean openInternal(boolean allowProvisioning) { + private boolean openInternal() { if (isOpen()) { // Already opened return true; @@ -359,11 +357,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; Assertions.checkNotNull(sessionId); return true; } catch (NotProvisionedException e) { - if (allowProvisioning) { - provisioningManager.provisionRequired(this); - } else { - onError(e); - } + provisioningManager.provisionRequired(this); } catch (Exception e) { onError(e); } 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 ffdfb779e5..093257e870 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 @@ -46,6 +46,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -289,7 +290,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { private final long sessionKeepaliveMs; private final List sessions; - private final List provisioningSessions; private final Set preacquiredSessionReferences; private final Set keepaliveSessions; @@ -411,7 +411,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { referenceCountListener = new ReferenceCountListenerImpl(); mode = MODE_PLAYBACK; sessions = new ArrayList<>(); - provisioningSessions = new ArrayList<>(); preacquiredSessionReferences = Sets.newIdentityHashSet(); keepaliveSessions = Sets.newIdentityHashSet(); this.sessionKeepaliveMs = sessionKeepaliveMs; @@ -842,33 +841,60 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { + + private final Set sessionsAwaitingProvisioning; + @Nullable private DefaultDrmSession provisioningSession; + + public ProvisioningManagerImpl() { + sessionsAwaitingProvisioning = new HashSet<>(); + } + @Override public void provisionRequired(DefaultDrmSession session) { - if (provisioningSessions.contains(session)) { - // The session has already requested provisioning. + sessionsAwaitingProvisioning.add(session); + if (provisioningSession != null) { + // Provisioning is already in-flight. return; } - provisioningSessions.add(session); - if (provisioningSessions.size() == 1) { - // This is the first session requesting provisioning, so have it perform the operation. - session.provision(); - } + provisioningSession = session; + session.provision(); } @Override public void onProvisionCompleted() { - for (DefaultDrmSession session : provisioningSessions) { + provisioningSession = null; + ImmutableList sessionsToNotify = + ImmutableList.copyOf(sessionsAwaitingProvisioning); + // Clear the list before calling onProvisionComplete in case provisioning is re-requested. + sessionsAwaitingProvisioning.clear(); + for (DefaultDrmSession session : sessionsToNotify) { session.onProvisionCompleted(); } - provisioningSessions.clear(); } @Override public void onProvisionError(Exception error) { - for (DefaultDrmSession session : provisioningSessions) { + provisioningSession = null; + ImmutableList sessionsToNotify = + ImmutableList.copyOf(sessionsAwaitingProvisioning); + // Clear the list before calling onProvisionError in case provisioning is re-requested. + sessionsAwaitingProvisioning.clear(); + for (DefaultDrmSession session : sessionsToNotify) { session.onProvisionError(error); } - provisioningSessions.clear(); + } + + public void onSessionFullyReleased(DefaultDrmSession session) { + sessionsAwaitingProvisioning.remove(session); + if (provisioningSession == session) { + provisioningSession = null; + if (!sessionsAwaitingProvisioning.isEmpty()) { + // Other sessions were waiting for the released session to complete a provision operation. + // We need to have one of those sessions perform the provision operation instead. + provisioningSession = sessionsAwaitingProvisioning.iterator().next(); + provisioningSession.provision(); + } + } } } @@ -902,12 +928,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (noMultiSessionDrmSession == session) { noMultiSessionDrmSession = null; } - if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == session) { - // Other sessions were waiting for the released session to complete a provision operation. - // We need to have one of those sessions perform the provision operation instead. - provisioningSessions.get(1).provision(); - } - provisioningSessions.remove(session); + provisioningManagerImpl.onSessionFullyReleased(session); if (sessionKeepaliveMs != C.TIME_UNSET) { checkNotNull(playbackHandler).removeCallbacksAndMessages(session); keepaliveSessions.remove(session); 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 cc3c5f0bd7..0b9f560546 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 @@ -563,6 +563,69 @@ public class DefaultDrmSessionManagerTest { .containsExactly(FakeExoMediaDrm.KEY_STATUS_KEY, FakeExoMediaDrm.KEY_STATUS_AVAILABLE); } + @Test + public void deviceNotProvisioned_doubleProvisioningHandledAndOpenSessionRetried() { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + + DefaultDrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider( + DRM_SCHEME_UUID, + uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build()) + .build(/* mediaDrmCallback= */ licenseServer); + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + // Confirm the device isn't provisioned (otherwise state would be OPENED) + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENING); + waitForOpenedWithKeys(drmSession); + + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + assertThat(drmSession.queryKeyStatus()) + .containsExactly(FakeExoMediaDrm.KEY_STATUS_KEY, FakeExoMediaDrm.KEY_STATUS_AVAILABLE); + } + + @Test + public void provisioningUndoneWhileManagerIsActive_deviceReprovisioned() { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + + FakeExoMediaDrm mediaDrm = new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build(); + DefaultDrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm)) + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + // Confirm the device isn't provisioned (otherwise state would be OPENED) + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENING); + waitForOpenedWithKeys(drmSession); + drmSession.release(/* eventDispatcher= */ null); + + mediaDrm.resetProvisioning(); + + drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + // Confirm the device isn't provisioned (otherwise state would be OPENED) + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENING); + waitForOpenedWithKeys(drmSession); + } + @Test public void managerNotPrepared_acquireSessionAndPreacquireSessionFail() throws Exception { FakeExoMediaDrm.LicenseServer licenseServer = 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 608347c569..e48307d7ee 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 @@ -78,8 +78,8 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { * to be provisioned. * *

An unprovisioned {@link FakeExoMediaDrm} will throw {@link NotProvisionedException} from - * {@link FakeExoMediaDrm#openSession()} until enough valid provisioning responses are passed to - * {@link FakeExoMediaDrm#provideProvisionResponse(byte[])}. + * methods that declare it until enough valid provisioning responses are passed to {@link + * FakeExoMediaDrm#provideProvisionResponse(byte[])}. * *

Defaults to 0 (i.e. device is already provisioned). */ @@ -182,9 +182,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { @Override public byte[] openSession() throws MediaDrmException { Assertions.checkState(referenceCount > 0); - if (provisionsReceived < provisionsRequired) { - throw new NotProvisionedException("Not provisioned."); - } + assertProvisioned(); if (openSessionIds.size() >= maxConcurrentSessions) { throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions); } @@ -218,6 +216,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { throw new UnsupportedOperationException("Offline key requests are not supported."); } Assertions.checkArgument(keyType == KEY_TYPE_STREAMING, "Unrecognised keyType: " + keyType); + assertProvisioned(); Assertions.checkState(openSessionIds.contains(toByteList(scope))); Assertions.checkNotNull(schemeDatas); KeyRequestData requestData = @@ -238,6 +237,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { public byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException { Assertions.checkState(referenceCount > 0); + assertProvisioned(); List responseAsList = Bytes.asList(response); if (responseAsList.equals(VALID_KEY_RESPONSE)) { sessionIdsWithValidKeys.add(Bytes.asList(scope)); @@ -365,6 +365,21 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { } } + /** + * Resets the provisioning state of this instance, so it requires {@link + * Builder#setProvisionsRequired(int) provisionsRequired} (possibly zero) provision operations + * before it's operational again. + */ + public void resetProvisioning() { + provisionsReceived = 0; + } + + private void assertProvisioned() throws NotProvisionedException { + if (provisionsReceived < provisionsRequired) { + throw new NotProvisionedException("Not provisioned."); + } + } + private static ImmutableList toByteList(byte[] byteArray) { return ImmutableList.copyOf(Bytes.asList(byteArray)); }