diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 4fe20d018d..62ccb5e2b7 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -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.
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 ea7994868b..5a51638c17 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
@@ -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 onSessionReleased(DefaultDrmSession session);
+ void onReferenceCountIncremented(DefaultDrmSession session, int newReferenceCount);
+
+ /**
+ * Called when the internal reference count of this session is decremented.
+ *
+ *
{@code newReferenceCount == 0} indicates this session is in {@link #STATE_RELEASED}.
+ *
+ * @param session This session.
+ * @param newReferenceCount The reference count after being decremented.
+ */
+ 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 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.
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 8335831ce0..2912af4c36 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
@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
+import android.media.ResourceBusyException;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -32,15 +34,18 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
@RequiresApi(18)
@@ -60,6 +65,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private int[] useDrmSessionsForClearContentTrackTypes;
private boolean playClearSamplesWithoutKeys;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private long sessionKeepaliveMs;
/**
* Creates a builder with default values. The default values are:
@@ -82,6 +88,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER;
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
useDrmSessionsForClearContentTrackTypes = new int[0];
+ sessionKeepaliveMs = DEFAULT_SESSION_KEEPALIVE_MS;
}
/**
@@ -180,6 +187,27 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return this;
}
+ /**
+ * Sets the time to keep {@link DrmSession DrmSessions} alive when they're not in use.
+ *
+ * It can be useful to keep sessions alive during playback of short clear sections of media
+ * (e.g. ad breaks) to avoid opening new DRM sessions (and re-requesting keys) at the transition
+ * back into secure content. This assumes the secure sections before and after the clear section
+ * are encrypted with the same keys.
+ *
+ *
Defaults to {@link #DEFAULT_SESSION_KEEPALIVE_MS}. Pass {@link C#TIME_UNSET} to disable
+ * keep-alive.
+ *
+ * @param sessionKeepaliveMs The time to keep {@link DrmSession}s alive before fully releasing,
+ * in milliseconds. Must be > 0 or {@link C#TIME_UNSET} to disable keep-alive.
+ * @return This builder.
+ */
+ public Builder setSessionKeepaliveMs(long sessionKeepaliveMs) {
+ Assertions.checkArgument(sessionKeepaliveMs > 0 || sessionKeepaliveMs == C.TIME_UNSET);
+ this.sessionKeepaliveMs = sessionKeepaliveMs;
+ return this;
+ }
+
/** Builds a {@link DefaultDrmSessionManager} instance. */
public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) {
return new DefaultDrmSessionManager(
@@ -190,7 +218,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
multiSession,
useDrmSessionsForClearContentTrackTypes,
playClearSamplesWithoutKeys,
- loadErrorHandlingPolicy);
+ loadErrorHandlingPolicy,
+ sessionKeepaliveMs);
}
}
@@ -232,6 +261,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
public static final int MODE_RELEASE = 3;
/** Number of times to retry for initial provisioning and key request for reporting error. */
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
+ /** Default value for {@link Builder#setSessionKeepaliveMs(long)}. */
+ public static final long DEFAULT_SESSION_KEEPALIVE_MS = 5 * 60 * C.MILLIS_PER_SECOND;
private static final String TAG = "DefaultDrmSessionMgr";
@@ -244,15 +275,19 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private final boolean playClearSamplesWithoutKeys;
private final ProvisioningManagerImpl provisioningManagerImpl;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+ private final ReferenceCountListenerImpl referenceCountListener;
+ private final long sessionKeepaliveMs;
private final List sessions;
private final List provisioningSessions;
+ private final Set keepaliveSessions;
private int prepareCallsCount;
@Nullable private ExoMediaDrm exoMediaDrm;
@Nullable private DefaultDrmSession placeholderDrmSession;
@Nullable private DefaultDrmSession noMultiSessionDrmSession;
@Nullable private Looper playbackLooper;
+ private @MonotonicNonNull Handler sessionReleasingHandler;
private int mode;
@Nullable private byte[] offlineLicenseKeySetId;
@@ -336,7 +371,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
multiSession,
/* useDrmSessionsForClearContentTrackTypes= */ new int[0],
/* playClearSamplesWithoutKeys= */ false,
- new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount));
+ new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount),
+ DEFAULT_SESSION_KEEPALIVE_MS);
}
private DefaultDrmSessionManager(
@@ -347,7 +383,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
boolean multiSession,
int[] useDrmSessionsForClearContentTrackTypes,
boolean playClearSamplesWithoutKeys,
- LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy,
+ long sessionKeepaliveMs) {
Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid;
@@ -359,9 +396,12 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
provisioningManagerImpl = new ProvisioningManagerImpl();
+ referenceCountListener = new ReferenceCountListenerImpl();
mode = MODE_PLAYBACK;
sessions = new ArrayList<>();
provisioningSessions = new ArrayList<>();
+ keepaliveSessions = Sets.newIdentityHashSet();
+ this.sessionKeepaliveMs = sessionKeepaliveMs;
}
/**
@@ -411,6 +451,13 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override
public final void release() {
if (--prepareCallsCount == 0) {
+ // Make a local copy, because sessions are removed from this.sessions during release (via
+ // callback).
+ List sessions = new ArrayList<>(this.sessions);
+ for (int i = 0; i < sessions.size(); i++) {
+ // Release all the keepalive acquisitions.
+ sessions.get(i).release(/* eventDispatcher= */ null);
+ }
Assertions.checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
}
@@ -451,7 +498,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Override
@Nullable
public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) {
- assertExpectedPlaybackLooper(playbackLooper);
+ initPlaybackLooper(playbackLooper);
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
@@ -465,12 +512,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
maybeCreateMediaDrmHandler(playbackLooper);
if (placeholderDrmSession == null) {
DefaultDrmSession placeholderDrmSession =
- createNewDefaultSession(
- /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);
+ createAndAcquireSessionWithRetry(
+ /* schemeDatas= */ ImmutableList.of(),
+ /* isPlaceholderSession= */ true,
+ /* eventDispatcher= */ null);
sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession;
+ } else {
+ placeholderDrmSession.acquire(/* eventDispatcher= */ null);
}
- placeholderDrmSession.acquire(/* eventDispatcher= */ null);
return placeholderDrmSession;
}
@@ -479,7 +529,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Looper playbackLooper,
@Nullable MediaSourceEventDispatcher eventDispatcher,
DrmInitData drmInitData) {
- assertExpectedPlaybackLooper(playbackLooper);
+ initPlaybackLooper(playbackLooper);
maybeCreateMediaDrmHandler(playbackLooper);
@Nullable List schemeDatas = null;
@@ -513,13 +563,17 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (session == null) {
// Create a new session.
- session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);
+ session =
+ createAndAcquireSessionWithRetry(
+ schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher);
if (!multiSession) {
noMultiSessionDrmSession = session;
}
sessions.add(session);
+ } else {
+ session.acquire(eventDispatcher);
}
- session.acquire(eventDispatcher);
+
return session;
}
@@ -533,9 +587,13 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Internal methods.
- private void assertExpectedPlaybackLooper(Looper playbackLooper) {
- Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
- this.playbackLooper = playbackLooper;
+ private void initPlaybackLooper(Looper playbackLooper) {
+ if (this.playbackLooper == null) {
+ this.playbackLooper = playbackLooper;
+ this.sessionReleasingHandler = new Handler(playbackLooper);
+ } else {
+ Assertions.checkState(this.playbackLooper == playbackLooper);
+ }
}
private void maybeCreateMediaDrmHandler(Looper playbackLooper) {
@@ -544,41 +602,77 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
}
- private DefaultDrmSession createNewDefaultSession(
- @Nullable List schemeDatas, boolean isPlaceholderSession) {
+ private DefaultDrmSession createAndAcquireSessionWithRetry(
+ @Nullable List schemeDatas,
+ boolean isPlaceholderSession,
+ @Nullable MediaSourceEventDispatcher eventDispatcher) {
+ DefaultDrmSession session =
+ createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
+ if (session.getState() == DrmSession.STATE_ERROR
+ && (Util.SDK_INT < 19
+ || Assertions.checkNotNull(session.getError()).getCause()
+ instanceof ResourceBusyException)) {
+ // We're short on DRM session resources, so eagerly release all our keepalive sessions.
+ // ResourceBusyException is only available at API 19, so on earlier versions we always
+ // eagerly release regardless of the underlying error.
+ if (!keepaliveSessions.isEmpty()) {
+ // Make a local copy, because sessions are removed from this.timingOutSessions during
+ // release (via callback).
+ ImmutableList timingOutSessions =
+ ImmutableList.copyOf(this.keepaliveSessions);
+ for (DrmSession timingOutSession : timingOutSessions) {
+ timingOutSession.release(/* eventDispatcher= */ null);
+ }
+ // Undo the acquisitions from createAndAcquireSession().
+ session.release(eventDispatcher);
+ if (sessionKeepaliveMs != C.TIME_UNSET) {
+ session.release(/* eventDispatcher= */ null);
+ }
+ session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
+ }
+ }
+ return session;
+ }
+
+ /**
+ * Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
+ * {@code eventDispatcher}).
+ *
+ * If {@link #sessionKeepaliveMs} != {@link C#TIME_UNSET} then acquires it again to allow the
+ * manager to keep it alive (passing in {@code eventDispatcher=null}.
+ */
+ private DefaultDrmSession createAndAcquireSession(
+ @Nullable List schemeDatas,
+ boolean isPlaceholderSession,
+ @Nullable MediaSourceEventDispatcher eventDispatcher) {
Assertions.checkNotNull(exoMediaDrm);
// Placeholder sessions should always play clear samples without keys.
boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
- return new DefaultDrmSession(
- uuid,
- exoMediaDrm,
- /* provisioningManager= */ provisioningManagerImpl,
- /* releaseCallback= */ this::onSessionReleased,
- schemeDatas,
- mode,
- playClearSamplesWithoutKeys,
- isPlaceholderSession,
- offlineLicenseKeySetId,
- keyRequestParameters,
- callback,
- Assertions.checkNotNull(playbackLooper),
- loadErrorHandlingPolicy);
- }
-
- private void onSessionReleased(DefaultDrmSession drmSession) {
- sessions.remove(drmSession);
- if (placeholderDrmSession == drmSession) {
- placeholderDrmSession = null;
+ DefaultDrmSession session =
+ new DefaultDrmSession(
+ uuid,
+ exoMediaDrm,
+ /* provisioningManager= */ provisioningManagerImpl,
+ referenceCountListener,
+ schemeDatas,
+ mode,
+ playClearSamplesWithoutKeys,
+ isPlaceholderSession,
+ offlineLicenseKeySetId,
+ keyRequestParameters,
+ callback,
+ Assertions.checkNotNull(playbackLooper),
+ loadErrorHandlingPolicy);
+ // Acquire the session once on behalf of the caller to DrmSessionManager - this is the
+ // reference 'assigned' to the caller which they're responsible for releasing. Do this first,
+ // to ensure that eventDispatcher receives all events related to the initial
+ // acquisition/opening.
+ session.acquire(eventDispatcher);
+ if (sessionKeepaliveMs != C.TIME_UNSET) {
+ // Acquire the session once more so the Manager can keep it alive.
+ session.acquire(/* eventDispatcher= */ null);
}
- if (noMultiSessionDrmSession == drmSession) {
- noMultiSessionDrmSession = null;
- }
- if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) {
- // 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(drmSession);
+ return session;
}
/**
@@ -661,6 +755,52 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
}
+ private class ReferenceCountListenerImpl implements DefaultDrmSession.ReferenceCountListener {
+
+ @Override
+ public void onReferenceCountIncremented(DefaultDrmSession session, int newReferenceCount) {
+ if (sessionKeepaliveMs != C.TIME_UNSET) {
+ // The session has been acquired elsewhere so we want to cancel our timeout.
+ keepaliveSessions.remove(session);
+ Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
+ }
+ }
+
+ @Override
+ public void onReferenceCountDecremented(DefaultDrmSession session, int newReferenceCount) {
+ if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) {
+ // Only the internal keep-alive reference remains, so we can start the timeout.
+ keepaliveSessions.add(session);
+ Assertions.checkNotNull(sessionReleasingHandler)
+ .postAtTime(
+ () -> {
+ session.release(/* eventDispatcher= */ null);
+ },
+ session,
+ /* uptimeMillis= */ SystemClock.uptimeMillis() + sessionKeepaliveMs);
+ } else if (newReferenceCount == 0) {
+ // This session is fully released.
+ sessions.remove(session);
+ if (placeholderDrmSession == session) {
+ placeholderDrmSession = null;
+ }
+ 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);
+ if (sessionKeepaliveMs != C.TIME_UNSET) {
+ Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
+ keepaliveSessions.remove(session);
+ }
+ }
+ }
+ }
+
private class MediaDrmEventListener implements OnEventListener {
@Override
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 73f68d1202..3c203f1457 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
@@ -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 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();
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 cf422ffab3..c698b2e8b3 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
@@ -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 VALID_KEY_RESPONSE = TestUtil.createByteList(1, 2, 3);
private static final ImmutableList KEY_DENIED_RESPONSE = TestUtil.createByteList(9, 8, 7);
+ private final int maxConcurrentSessions;
private final Map byteProperties;
private final Map stringProperties;
private final Set> 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))) {