Commit 607fa8bf by ibaker Committed by Oliver Woodman

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
parent 77550708
...@@ -67,6 +67,8 @@ ...@@ -67,6 +67,8 @@
* Support changing ad break positions in the player logic * Support changing ad break positions in the player logic
([#5067](https://github.com/google/ExoPlayer/issues/5067). ([#5067](https://github.com/google/ExoPlayer/issues/5067).
* Support resuming content with an offset after an ad group. * Support resuming content with an offset after an ad group.
* DRM:
* Allow repeated provisioning in `DefaultDrmSession(Manager)`.
* PlayerNotificationManager: * PlayerNotificationManager:
* Add `PendingIntent.FLAG_IMMUTABLE` flag to BroadcastReceiver to support * Add `PendingIntent.FLAG_IMMUTABLE` flag to BroadcastReceiver to support
Android 12. Android 12.
......
...@@ -230,7 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -230,7 +230,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
public void onProvisionCompleted() { public void onProvisionCompleted() {
if (openInternal(false)) { if (openInternal()) {
doLicense(true); doLicense(true);
} }
} }
...@@ -290,7 +290,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -290,7 +290,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
requestHandlerThread = new HandlerThread("ExoPlayer:DrmRequestHandler"); requestHandlerThread = new HandlerThread("ExoPlayer:DrmRequestHandler");
requestHandlerThread.start(); requestHandlerThread.start();
requestHandler = new RequestHandler(requestHandlerThread.getLooper()); requestHandler = new RequestHandler(requestHandlerThread.getLooper());
if (openInternal(true)) { if (openInternal()) {
doLicense(true); doLicense(true);
} }
} else if (eventDispatcher != null } else if (eventDispatcher != null
...@@ -338,12 +338,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -338,12 +338,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Try to open a session, do provisioning if necessary. * 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. * @return true on success, false otherwise.
*/ */
@EnsuresNonNullIf(result = true, expression = "sessionId") @EnsuresNonNullIf(result = true, expression = "sessionId")
private boolean openInternal(boolean allowProvisioning) { private boolean openInternal() {
if (isOpen()) { if (isOpen()) {
// Already opened // Already opened
return true; return true;
...@@ -359,11 +357,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -359,11 +357,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
Assertions.checkNotNull(sessionId); Assertions.checkNotNull(sessionId);
return true; return true;
} catch (NotProvisionedException e) { } catch (NotProvisionedException e) {
if (allowProvisioning) { provisioningManager.provisionRequired(this);
provisioningManager.provisionRequired(this);
} else {
onError(e);
}
} catch (Exception e) { } catch (Exception e) {
onError(e); onError(e);
} }
......
...@@ -46,6 +46,7 @@ import java.lang.annotation.Retention; ...@@ -46,6 +46,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
...@@ -289,7 +290,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -289,7 +290,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private final long sessionKeepaliveMs; private final long sessionKeepaliveMs;
private final List<DefaultDrmSession> sessions; private final List<DefaultDrmSession> sessions;
private final List<DefaultDrmSession> provisioningSessions;
private final Set<PreacquiredSessionReference> preacquiredSessionReferences; private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
private final Set<DefaultDrmSession> keepaliveSessions; private final Set<DefaultDrmSession> keepaliveSessions;
...@@ -411,7 +411,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -411,7 +411,6 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
referenceCountListener = new ReferenceCountListenerImpl(); referenceCountListener = new ReferenceCountListenerImpl();
mode = MODE_PLAYBACK; mode = MODE_PLAYBACK;
sessions = new ArrayList<>(); sessions = new ArrayList<>();
provisioningSessions = new ArrayList<>();
preacquiredSessionReferences = Sets.newIdentityHashSet(); preacquiredSessionReferences = Sets.newIdentityHashSet();
keepaliveSessions = Sets.newIdentityHashSet(); keepaliveSessions = Sets.newIdentityHashSet();
this.sessionKeepaliveMs = sessionKeepaliveMs; this.sessionKeepaliveMs = sessionKeepaliveMs;
...@@ -842,33 +841,60 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -842,33 +841,60 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
} }
private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager {
private final Set<DefaultDrmSession> sessionsAwaitingProvisioning;
@Nullable private DefaultDrmSession provisioningSession;
public ProvisioningManagerImpl() {
sessionsAwaitingProvisioning = new HashSet<>();
}
@Override @Override
public void provisionRequired(DefaultDrmSession session) { public void provisionRequired(DefaultDrmSession session) {
if (provisioningSessions.contains(session)) { sessionsAwaitingProvisioning.add(session);
// The session has already requested provisioning. if (provisioningSession != null) {
// Provisioning is already in-flight.
return; return;
} }
provisioningSessions.add(session); provisioningSession = session;
if (provisioningSessions.size() == 1) { session.provision();
// This is the first session requesting provisioning, so have it perform the operation.
session.provision();
}
} }
@Override @Override
public void onProvisionCompleted() { public void onProvisionCompleted() {
for (DefaultDrmSession session : provisioningSessions) { provisioningSession = null;
ImmutableList<DefaultDrmSession> sessionsToNotify =
ImmutableList.copyOf(sessionsAwaitingProvisioning);
// Clear the list before calling onProvisionComplete in case provisioning is re-requested.
sessionsAwaitingProvisioning.clear();
for (DefaultDrmSession session : sessionsToNotify) {
session.onProvisionCompleted(); session.onProvisionCompleted();
} }
provisioningSessions.clear();
} }
@Override @Override
public void onProvisionError(Exception error) { public void onProvisionError(Exception error) {
for (DefaultDrmSession session : provisioningSessions) { provisioningSession = null;
ImmutableList<DefaultDrmSession> 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); 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 { ...@@ -902,12 +928,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (noMultiSessionDrmSession == session) { if (noMultiSessionDrmSession == session) {
noMultiSessionDrmSession = null; noMultiSessionDrmSession = null;
} }
if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == session) { provisioningManagerImpl.onSessionFullyReleased(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);
if (sessionKeepaliveMs != C.TIME_UNSET) { if (sessionKeepaliveMs != C.TIME_UNSET) {
checkNotNull(playbackHandler).removeCallbacksAndMessages(session); checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
keepaliveSessions.remove(session); keepaliveSessions.remove(session);
......
...@@ -564,6 +564,69 @@ public class DefaultDrmSessionManagerTest { ...@@ -564,6 +564,69 @@ public class DefaultDrmSessionManagerTest {
} }
@Test @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 { public void managerNotPrepared_acquireSessionAndPreacquireSessionFail() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
......
...@@ -78,8 +78,8 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -78,8 +78,8 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
* to be provisioned. * to be provisioned.
* *
* <p>An unprovisioned {@link FakeExoMediaDrm} will throw {@link NotProvisionedException} from * <p>An unprovisioned {@link FakeExoMediaDrm} will throw {@link NotProvisionedException} from
* {@link FakeExoMediaDrm#openSession()} until enough valid provisioning responses are passed to * methods that declare it until enough valid provisioning responses are passed to {@link
* {@link FakeExoMediaDrm#provideProvisionResponse(byte[])}. * FakeExoMediaDrm#provideProvisionResponse(byte[])}.
* *
* <p>Defaults to 0 (i.e. device is already provisioned). * <p>Defaults to 0 (i.e. device is already provisioned).
*/ */
...@@ -182,9 +182,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -182,9 +182,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
@Override @Override
public byte[] openSession() throws MediaDrmException { public byte[] openSession() throws MediaDrmException {
Assertions.checkState(referenceCount > 0); Assertions.checkState(referenceCount > 0);
if (provisionsReceived < provisionsRequired) { assertProvisioned();
throw new NotProvisionedException("Not provisioned.");
}
if (openSessionIds.size() >= maxConcurrentSessions) { if (openSessionIds.size() >= maxConcurrentSessions) {
throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions); throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions);
} }
...@@ -218,6 +216,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -218,6 +216,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
throw new UnsupportedOperationException("Offline key requests are not supported."); throw new UnsupportedOperationException("Offline key requests are not supported.");
} }
Assertions.checkArgument(keyType == KEY_TYPE_STREAMING, "Unrecognised keyType: " + keyType); Assertions.checkArgument(keyType == KEY_TYPE_STREAMING, "Unrecognised keyType: " + keyType);
assertProvisioned();
Assertions.checkState(openSessionIds.contains(toByteList(scope))); Assertions.checkState(openSessionIds.contains(toByteList(scope)));
Assertions.checkNotNull(schemeDatas); Assertions.checkNotNull(schemeDatas);
KeyRequestData requestData = KeyRequestData requestData =
...@@ -238,6 +237,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -238,6 +237,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
public byte[] provideKeyResponse(byte[] scope, byte[] response) public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException { throws NotProvisionedException, DeniedByServerException {
Assertions.checkState(referenceCount > 0); Assertions.checkState(referenceCount > 0);
assertProvisioned();
List<Byte> responseAsList = Bytes.asList(response); List<Byte> responseAsList = Bytes.asList(response);
if (responseAsList.equals(VALID_KEY_RESPONSE)) { if (responseAsList.equals(VALID_KEY_RESPONSE)) {
sessionIdsWithValidKeys.add(Bytes.asList(scope)); sessionIdsWithValidKeys.add(Bytes.asList(scope));
...@@ -365,6 +365,21 @@ public final class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -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<Byte> toByteList(byte[] byteArray) { private static ImmutableList<Byte> toByteList(byte[] byteArray) {
return ImmutableList.copyOf(Bytes.asList(byteArray)); return ImmutableList.copyOf(Bytes.asList(byteArray));
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment