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)); }