Commit 316f8a88 by ibaker Committed by Oliver Woodman

Keep DRM sessions alive for a while before fully releasing them

Issue: #7011
Issue: #6725
Issue: #7066

This also mitigates (but doesn't fix) Issue: #4133 because it
prevents a second key load after a short clear section.

PiperOrigin-RevId: 319184325
parent e4e743a3
......@@ -144,6 +144,10 @@
`OfflineLicenseHelper`
([#7078](https://github.com/google/ExoPlayer/issues/7078)).
* Remove generics from DRM components.
* Keep DRM sessions alive for a short time before fully releasing them
([#7011](https://github.com/google/ExoPlayer/issues/7011),
[#6725](https://github.com/google/ExoPlayer/issues/6725),
[#7066](https://github.com/google/ExoPlayer/issues/7066)).
* Downloads and caching:
* Support passing an `Executor` to `DefaultDownloaderFactory` on which
data downloads are performed.
......
......@@ -85,15 +85,26 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
void onProvisionCompleted();
}
/** Callback to be notified when the session is released. */
public interface ReleaseCallback {
/** Callback to be notified when the reference count of this session changes. */
public interface ReferenceCountListener {
/**
* Called immediately after releasing session resources.
* Called when the internal reference count of this session is incremented.
*
* @param session The session.
* @param session This session.
* @param newReferenceCount The reference count after being incremented.
*/
void onReferenceCountIncremented(DefaultDrmSession session, int newReferenceCount);
/**
* Called when the internal reference count of this session is decremented.
*
* <p>{@code newReferenceCount == 0} indicates this session is in {@link #STATE_RELEASED}.
*
* @param session This session.
* @param newReferenceCount The reference count after being decremented.
*/
void onSessionReleased(DefaultDrmSession session);
void onReferenceCountDecremented(DefaultDrmSession session, int newReferenceCount);
}
private static final String TAG = "DefaultDrmSession";
......@@ -107,7 +118,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ExoMediaDrm mediaDrm;
private final ProvisioningManager provisioningManager;
private final ReleaseCallback releaseCallback;
private final ReferenceCountListener referenceCountListener;
private final @DefaultDrmSessionManager.Mode int mode;
private final boolean playClearSamplesWithoutKeys;
private final boolean isPlaceholderSession;
......@@ -137,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param uuid The UUID of the drm scheme.
* @param mediaDrm The media DRM.
* @param provisioningManager The manager for provisioning.
* @param releaseCallback The {@link ReleaseCallback}.
* @param referenceCountListener The {@link ReferenceCountListener}.
* @param schemeDatas DRM scheme datas for this session, or null if an {@code
* offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true.
* @param mode The DRM mode. Ignored if {@code isPlaceholderSession} is true.
......@@ -154,7 +165,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
UUID uuid,
ExoMediaDrm mediaDrm,
ProvisioningManager provisioningManager,
ReleaseCallback releaseCallback,
ReferenceCountListener referenceCountListener,
@Nullable List<SchemeData> schemeDatas,
@DefaultDrmSessionManager.Mode int mode,
boolean playClearSamplesWithoutKeys,
......@@ -170,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
this.uuid = uuid;
this.provisioningManager = provisioningManager;
this.releaseCallback = releaseCallback;
this.referenceCountListener = referenceCountListener;
this.mediaDrm = mediaDrm;
this.mode = mode;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
......@@ -280,6 +291,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
eventDispatcher.dispatch(
DrmSessionEventListener::onDrmSessionAcquired, DrmSessionEventListener.class);
}
referenceCountListener.onReferenceCountIncremented(this, referenceCount);
}
@Override
......@@ -300,7 +312,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
mediaDrm.closeSession(sessionId);
sessionId = null;
}
releaseCallback.onSessionReleased(this);
dispatchEvent(DrmSessionEventListener::onDrmSessionReleased);
}
if (eventDispatcher != null) {
......@@ -312,6 +323,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
eventDispatchers.remove(eventDispatcher);
}
referenceCountListener.onReferenceCountDecremented(this, referenceCount);
}
// Internal methods.
......
......@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Assertions;
......@@ -47,7 +49,7 @@ public class DefaultDrmSessionManagerTest {
private static final DrmInitData DRM_INIT_DATA = new DrmInitData(DRM_SCHEME_DATAS);
@Test(timeout = 10_000)
public void acquireSessionTriggersKeyLoadAndSessionIsOpened() throws Exception {
public void acquireSession_triggersKeyLoadAndSessionIsOpened() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
......@@ -68,6 +70,151 @@ public class DefaultDrmSessionManagerTest {
.containsExactly(FakeExoMediaDrm.KEY_STATUS_KEY, FakeExoMediaDrm.KEY_STATUS_AVAILABLE);
}
@Test(timeout = 10_000)
public void keepaliveEnabled_sessionsKeptForRequestedTime() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setSessionKeepaliveMs(10_000)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
waitForOpenedWithKeys(drmSession);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
drmSession.release(/* eventDispatcher= */ null);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
ShadowLooper.idleMainLooper(10, SECONDS);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
}
@Test(timeout = 10_000)
public void keepaliveDisabled_sessionsReleasedImmediately() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setSessionKeepaliveMs(C.TIME_UNSET)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
}
@Test(timeout = 10_000)
public void managerRelease_allKeepaliveSessionsImmediatelyReleased() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setSessionKeepaliveMs(10_000)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
waitForOpenedWithKeys(drmSession);
drmSession.release(/* eventDispatcher= */ null);
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
drmSessionManager.release();
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
}
@Test(timeout = 10_000)
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS, secondSchemeDatas);
DrmInitData secondInitData = new DrmInitData(secondSchemeDatas);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm(/* maxConcurrentSessions= */ 1))
.setSessionKeepaliveMs(10_000)
.setMultiSession(true)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession firstDrmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
waitForOpenedWithKeys(firstDrmSession);
firstDrmSession.release(/* eventDispatcher= */ null);
// All external references to firstDrmSession have been released, it's being kept alive by
// drmSessionManager's internal reference.
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
DrmSession secondDrmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
secondInitData);
// The drmSessionManager had to release firstDrmSession in order to acquire secondDrmSession.
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
waitForOpenedWithKeys(secondDrmSession);
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
}
@Test(timeout = 10_000)
public void sessionReacquired_keepaliveTimeOutCancelled() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.setSessionKeepaliveMs(10_000)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession firstDrmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
waitForOpenedWithKeys(firstDrmSession);
firstDrmSession.release(/* eventDispatcher= */ null);
ShadowLooper.idleMainLooper(5, SECONDS);
// Acquire a session for the same init data 5s in to the 10s timeout (so expect the same
// instance).
DrmSession secondDrmSession =
drmSessionManager.acquireSession(
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
DRM_INIT_DATA);
assertThat(secondDrmSession).isSameInstanceAs(firstDrmSession);
// Let the timeout definitely expire, and check the session didn't get released.
ShadowLooper.idleMainLooper(10, SECONDS);
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
}
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();
......
......@@ -20,6 +20,7 @@ import android.media.DeniedByServerException;
import android.media.MediaCryptoException;
import android.media.MediaDrmException;
import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
......@@ -57,7 +58,7 @@ import java.util.concurrent.atomic.AtomicInteger;
// TODO: Consider replacing this with a Robolectric ShadowMediaDrm so we can use a real
// FrameworkMediaDrm.
@RequiresApi(29)
public class FakeExoMediaDrm implements ExoMediaDrm {
public final class FakeExoMediaDrm implements ExoMediaDrm {
public static final ProvisionRequest DUMMY_PROVISION_REQUEST =
new ProvisionRequest(TestUtil.createByteArray(7, 8, 9), "bar.test");
......@@ -72,6 +73,7 @@ public class FakeExoMediaDrm implements ExoMediaDrm {
private static final ImmutableList<Byte> VALID_KEY_RESPONSE = TestUtil.createByteList(1, 2, 3);
private static final ImmutableList<Byte> KEY_DENIED_RESPONSE = TestUtil.createByteList(9, 8, 7);
private final int maxConcurrentSessions;
private final Map<String, byte[]> byteProperties;
private final Map<String, String> stringProperties;
private final Set<List<Byte>> openSessionIds;
......@@ -82,9 +84,20 @@ public class FakeExoMediaDrm implements ExoMediaDrm {
/**
* Constructs an instance that returns random and unique {@code sessionIds} for subsequent calls
* to {@link #openSession()}.
* to {@link #openSession()} with no limit on the number of concurrent open sessions.
*/
public FakeExoMediaDrm() {
this(/* maxConcurrentSessions= */ Integer.MAX_VALUE);
}
/**
* Constructs an instance that returns random and unique {@code sessionIds} for subsequent calls
* to {@link #openSession()} with a limit on the number of concurrent open sessions.
*
* @param maxConcurrentSessions The max number of sessions allowed to be open simultaneously.
*/
public FakeExoMediaDrm(int maxConcurrentSessions) {
this.maxConcurrentSessions = maxConcurrentSessions;
byteProperties = new HashMap<>();
stringProperties = new HashMap<>();
openSessionIds = new HashSet<>();
......@@ -114,6 +127,9 @@ public class FakeExoMediaDrm implements ExoMediaDrm {
@Override
public byte[] openSession() throws MediaDrmException {
Assertions.checkState(referenceCount > 0);
if (openSessionIds.size() >= maxConcurrentSessions) {
throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions);
}
byte[] sessionId =
TestUtil.buildTestData(/* length= */ 10, sessionIdGenerator.incrementAndGet());
if (!openSessionIds.add(toByteList(sessionId))) {
......
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