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
This commit is contained in:
ibaker 2021-06-18 15:23:28 +01:00 committed by Oliver Woodman
parent 775507088c
commit 607fa8bf74
5 changed files with 129 additions and 34 deletions

View file

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

View file

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

View file

@ -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<DefaultDrmSession> sessions;
private final List<DefaultDrmSession> provisioningSessions;
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
private final Set<DefaultDrmSession> 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<DefaultDrmSession> 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<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();
}
provisioningSessions.clear();
}
@Override
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);
}
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);

View file

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

View file

@ -78,8 +78,8 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
* to be provisioned.
*
* <p>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[])}.
*
* <p>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<Byte> 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<Byte> toByteList(byte[] byteArray) {
return ImmutableList.copyOf(Bytes.asList(byteArray));
}