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 @@ ...@@ -144,6 +144,10 @@
`OfflineLicenseHelper` `OfflineLicenseHelper`
([#7078](https://github.com/google/ExoPlayer/issues/7078)). ([#7078](https://github.com/google/ExoPlayer/issues/7078)).
* Remove generics from DRM components. * 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: * Downloads and caching:
* Support passing an `Executor` to `DefaultDownloaderFactory` on which * Support passing an `Executor` to `DefaultDownloaderFactory` on which
data downloads are performed. data downloads are performed.
......
...@@ -85,15 +85,26 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -85,15 +85,26 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
void onProvisionCompleted(); void onProvisionCompleted();
} }
/** Callback to be notified when the session is released. */ /** Callback to be notified when the reference count of this session changes. */
public interface ReleaseCallback { 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"; private static final String TAG = "DefaultDrmSession";
...@@ -107,7 +118,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -107,7 +118,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
private final ExoMediaDrm mediaDrm; private final ExoMediaDrm mediaDrm;
private final ProvisioningManager provisioningManager; private final ProvisioningManager provisioningManager;
private final ReleaseCallback releaseCallback; private final ReferenceCountListener referenceCountListener;
private final @DefaultDrmSessionManager.Mode int mode; private final @DefaultDrmSessionManager.Mode int mode;
private final boolean playClearSamplesWithoutKeys; private final boolean playClearSamplesWithoutKeys;
private final boolean isPlaceholderSession; private final boolean isPlaceholderSession;
...@@ -137,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -137,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param uuid The UUID of the drm scheme. * @param uuid The UUID of the drm scheme.
* @param mediaDrm The media DRM. * @param mediaDrm The media DRM.
* @param provisioningManager The manager for provisioning. * @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 * @param schemeDatas DRM scheme datas for this session, or null if an {@code
* offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true. * offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true.
* @param mode The DRM mode. Ignored 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; ...@@ -154,7 +165,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
UUID uuid, UUID uuid,
ExoMediaDrm mediaDrm, ExoMediaDrm mediaDrm,
ProvisioningManager provisioningManager, ProvisioningManager provisioningManager,
ReleaseCallback releaseCallback, ReferenceCountListener referenceCountListener,
@Nullable List<SchemeData> schemeDatas, @Nullable List<SchemeData> schemeDatas,
@DefaultDrmSessionManager.Mode int mode, @DefaultDrmSessionManager.Mode int mode,
boolean playClearSamplesWithoutKeys, boolean playClearSamplesWithoutKeys,
...@@ -170,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -170,7 +181,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
this.uuid = uuid; this.uuid = uuid;
this.provisioningManager = provisioningManager; this.provisioningManager = provisioningManager;
this.releaseCallback = releaseCallback; this.referenceCountListener = referenceCountListener;
this.mediaDrm = mediaDrm; this.mediaDrm = mediaDrm;
this.mode = mode; this.mode = mode;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
...@@ -280,6 +291,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -280,6 +291,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
eventDispatcher.dispatch( eventDispatcher.dispatch(
DrmSessionEventListener::onDrmSessionAcquired, DrmSessionEventListener.class); DrmSessionEventListener::onDrmSessionAcquired, DrmSessionEventListener.class);
} }
referenceCountListener.onReferenceCountIncremented(this, referenceCount);
} }
@Override @Override
...@@ -300,7 +312,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -300,7 +312,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
mediaDrm.closeSession(sessionId); mediaDrm.closeSession(sessionId);
sessionId = null; sessionId = null;
} }
releaseCallback.onSessionReleased(this);
dispatchEvent(DrmSessionEventListener::onDrmSessionReleased); dispatchEvent(DrmSessionEventListener::onDrmSessionReleased);
} }
if (eventDispatcher != null) { if (eventDispatcher != null) {
...@@ -312,6 +323,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -312,6 +323,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
eventDispatchers.remove(eventDispatcher); eventDispatchers.remove(eventDispatcher);
} }
referenceCountListener.onReferenceCountDecremented(this, referenceCount);
} }
// Internal methods. // Internal methods.
......
...@@ -16,9 +16,11 @@ ...@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.os.Looper; import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -47,7 +49,7 @@ public class DefaultDrmSessionManagerTest { ...@@ -47,7 +49,7 @@ public class DefaultDrmSessionManagerTest {
private static final DrmInitData DRM_INIT_DATA = new DrmInitData(DRM_SCHEME_DATAS); private static final DrmInitData DRM_INIT_DATA = new DrmInitData(DRM_SCHEME_DATAS);
@Test(timeout = 10_000) @Test(timeout = 10_000)
public void acquireSessionTriggersKeyLoadAndSessionIsOpened() throws Exception { public void acquireSession_triggersKeyLoadAndSessionIsOpened() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
...@@ -68,6 +70,151 @@ public class DefaultDrmSessionManagerTest { ...@@ -68,6 +70,151 @@ public class DefaultDrmSessionManagerTest {
.containsExactly(FakeExoMediaDrm.KEY_STATUS_KEY, FakeExoMediaDrm.KEY_STATUS_AVAILABLE); .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) { private static void waitForOpenedWithKeys(DrmSession drmSession) {
// Check the error first, so we get a meaningful failure if there's been an error. // Check the error first, so we get a meaningful failure if there's been an error.
assertThat(drmSession.getError()).isNull(); assertThat(drmSession.getError()).isNull();
......
...@@ -20,6 +20,7 @@ import android.media.DeniedByServerException; ...@@ -20,6 +20,7 @@ import android.media.DeniedByServerException;
import android.media.MediaCryptoException; import android.media.MediaCryptoException;
import android.media.MediaDrmException; import android.media.MediaDrmException;
import android.media.NotProvisionedException; import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.os.PersistableBundle; import android.os.PersistableBundle;
...@@ -57,7 +58,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -57,7 +58,7 @@ import java.util.concurrent.atomic.AtomicInteger;
// TODO: Consider replacing this with a Robolectric ShadowMediaDrm so we can use a real // TODO: Consider replacing this with a Robolectric ShadowMediaDrm so we can use a real
// FrameworkMediaDrm. // FrameworkMediaDrm.
@RequiresApi(29) @RequiresApi(29)
public class FakeExoMediaDrm implements ExoMediaDrm { public final class FakeExoMediaDrm implements ExoMediaDrm {
public static final ProvisionRequest DUMMY_PROVISION_REQUEST = public static final ProvisionRequest DUMMY_PROVISION_REQUEST =
new ProvisionRequest(TestUtil.createByteArray(7, 8, 9), "bar.test"); new ProvisionRequest(TestUtil.createByteArray(7, 8, 9), "bar.test");
...@@ -72,6 +73,7 @@ public class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -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> VALID_KEY_RESPONSE = TestUtil.createByteList(1, 2, 3);
private static final ImmutableList<Byte> KEY_DENIED_RESPONSE = TestUtil.createByteList(9, 8, 7); 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, byte[]> byteProperties;
private final Map<String, String> stringProperties; private final Map<String, String> stringProperties;
private final Set<List<Byte>> openSessionIds; private final Set<List<Byte>> openSessionIds;
...@@ -82,9 +84,20 @@ public class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -82,9 +84,20 @@ public class FakeExoMediaDrm implements ExoMediaDrm {
/** /**
* Constructs an instance that returns random and unique {@code sessionIds} for subsequent calls * 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() { 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<>(); byteProperties = new HashMap<>();
stringProperties = new HashMap<>(); stringProperties = new HashMap<>();
openSessionIds = new HashSet<>(); openSessionIds = new HashSet<>();
...@@ -114,6 +127,9 @@ public class FakeExoMediaDrm implements ExoMediaDrm { ...@@ -114,6 +127,9 @@ public 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 (openSessionIds.size() >= maxConcurrentSessions) {
throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions);
}
byte[] sessionId = byte[] sessionId =
TestUtil.buildTestData(/* length= */ 10, sessionIdGenerator.incrementAndGet()); TestUtil.buildTestData(/* length= */ 10, sessionIdGenerator.incrementAndGet());
if (!openSessionIds.add(toByteList(sessionId))) { 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