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 10d6accc51..e0eb8a26aa 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 @@ -47,9 +47,16 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; -/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ +/** + * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. + * + *
This implementation supports pre-acquisition of sessions using {@link
+ * #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
+ */
@RequiresApi(18)
public class DefaultDrmSessionManager implements DrmSessionManager {
@@ -282,14 +289,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private final List A new instance is needed for each reference (compared to maintaining exactly one instance
+ * for each {@link DrmSession}) because each associated {@link
+ * DrmSessionEventListener.EventDispatcher} might be different. The {@link
+ * DrmSessionEventListener.EventDispatcher} is required to implement the zero-arg {@link
+ * DrmSessionReference#release()} method.
+ */
+ private class PreacquiredSessionReference implements DrmSessionReference {
+
+ @Nullable private final DrmSessionEventListener.EventDispatcher eventDispatcher;
+
+ @Nullable private DrmSession session;
+ private boolean isReleased;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link
+ * #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}.
+ */
+ public PreacquiredSessionReference(
+ @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
+ this.eventDispatcher = eventDispatcher;
+ }
+
+ /**
+ * Acquires the underlying session.
+ *
+ * Must be called at most once. Can be called from any thread.
+ */
+ @RequiresNonNull("playbackHandler")
+ public void acquire(Format format) {
+ playbackHandler.post(
+ () -> {
+ if (prepareCallsCount == 0 || isReleased) {
+ // The manager has been fully released or this reference has already been released.
+ // Abort the acquisition attempt.
+ return;
+ }
+ this.session =
+ acquireSession(
+ Assertions.checkNotNull(playbackLooper),
+ eventDispatcher,
+ format,
+ /* shouldReleasePreacquiredSessionsBeforeRetrying= */ false);
+ preacquiredSessionReferences.add(this);
+ });
+ }
+
+ @Override
+ public void release() {
+ // Ensure the underlying session is released immediately if we're already on the playback
+ // thread, to allow a failed session opening to be immediately retried.
+ Util.postOrRun(
+ Assertions.checkNotNull(playbackHandler),
+ () -> {
+ if (isReleased) {
+ return;
+ }
+ if (session != null) {
+ session.release(eventDispatcher);
+ }
+ preacquiredSessionReferences.remove(this);
+ isReleased = true;
+ });
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
index 70dc4fa7f5..4b3ee553d8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format;
/** Manages a DRM session. */
public interface DrmSessionManager {
+ /**
+ * Represents a single reference count of a {@link DrmSession}, while deliberately not giving
+ * access to the underlying session.
+ */
+ interface DrmSessionReference {
+ /** A reference that is never populated with an underlying {@link DrmSession}. */
+ DrmSessionReference EMPTY = () -> {};
+
+ /**
+ * Releases the underlying session at most once.
+ *
+ * Can be called from any thread. Calling this method more than once will only release the
+ * underlying session once.
+ */
+ void release();
+ }
+
/** An instance that supports no DRM schemes. */
DrmSessionManager DRM_UNSUPPORTED =
new DrmSessionManager() {
@@ -81,6 +98,51 @@ public interface DrmSessionManager {
// Do nothing.
}
+ /**
+ * Pre-acquires a DRM session for the specified {@link Format}.
+ *
+ * This notifies the manager that a subsequent call to {@link #acquireSession(Looper,
+ * DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely,
+ * allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready
+ * in the background.
+ *
+ * The caller must call {@link DrmSessionReference#release()} on the returned instance when
+ * they no longer require the pre-acquisition (i.e. they know they won't be making a matching call
+ * to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near
+ * future).
+ *
+ * This manager may silently release the underlying session in order to allow another operation
+ * to complete. This will result in a subsequent call to {@link #acquireSession(Looper,
+ * DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including
+ * repeating key loads and other async initialization steps.
+ *
+ * The caller must separately call {@link #acquireSession(Looper,
+ * DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for
+ * playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances
+ * are distinct. The caller must release both, and can release the {@link DrmSessionReference}
+ * before the {@link DrmSession} without affecting playback.
+ *
+ * This can be called from any thread.
+ *
+ * Implementations that do not support pre-acquisition always return an empty {@link
+ * DrmSessionReference} instance.
+ *
+ * @param playbackLooper The looper associated with the media playback thread.
+ * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute
+ * events, and passed on to {@link
+ * DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}.
+ * @param format The {@link Format} for which to pre-acquire a {@link DrmSession}.
+ * @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching
+ * {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would
+ * return null.
+ */
+ default DrmSessionReference preacquireSession(
+ Looper playbackLooper,
+ @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
+ Format format) {
+ return DrmSessionReference.EMPTY;
+ }
+
/**
* Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
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 c0b83e7a65..07398e0bf1 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
@@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.os.Looper;
+import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowLooper;
@@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper;
// - Multiple acquisitions & releases for same keys -> multiple requests.
// - Provisioning.
// - Key denial.
+// - Handling of ResourceBusyException (indicating session scarcity).
@RunWith(AndroidJUnit4.class)
public class DefaultDrmSessionManagerTest {
@@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest {
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
}
+ @Test(timeout = 10_000)
+ public void preacquireSession_loadsKeysBeforeFullAcquisition() throws Exception {
+ AtomicInteger keyLoadCount = new AtomicInteger(0);
+ DrmSessionEventListener.EventDispatcher eventDispatcher =
+ new DrmSessionEventListener.EventDispatcher();
+ eventDispatcher.addEventListener(
+ Util.createHandlerForCurrentLooper(),
+ new DrmSessionEventListener() {
+ @Override
+ public void onDrmKeysLoaded(
+ int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
+ keyLoadCount.incrementAndGet();
+ }
+ });
+ FakeExoMediaDrm.LicenseServer licenseServer =
+ FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
+ DrmSessionManager drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
+ // Disable keepalive
+ .setSessionKeepaliveMs(C.TIME_UNSET)
+ .build(/* mediaDrmCallback= */ licenseServer);
+
+ drmSessionManager.prepare();
+
+ DrmSessionManager.DrmSessionReference sessionReference =
+ drmSessionManager.preacquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ eventDispatcher,
+ FORMAT_WITH_DRM_INIT_DATA);
+
+ // Wait for the key load event to propagate, indicating the pre-acquired session is in
+ // STATE_OPENED_WITH_KEYS.
+ while (keyLoadCount.get() == 0) {
+ // Allow the key response to be handled.
+ ShadowLooper.idleMainLooper();
+ }
+
+ DrmSession drmSession =
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
+
+ // Without idling the main/playback looper, we assert the session is already in OPENED_WITH_KEYS
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
+ assertThat(keyLoadCount.get()).isEqualTo(1);
+
+ // After releasing our concrete session reference, the session is held open by the pre-acquired
+ // reference.
+ drmSession.release(/* eventDispatcher= */ null);
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
+
+ // Releasing the pre-acquired reference allows the session to be fully released.
+ sessionReference.release();
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
+ }
+
+ @Test(timeout = 10_000)
+ public void
+ preacquireSession_releaseBeforeUnderlyingAcquisitionCompletesReleasesSessionOnceAcquired()
+ throws Exception {
+ FakeExoMediaDrm.LicenseServer licenseServer =
+ FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
+ DrmSessionManager drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
+ // Disable keepalive
+ .setSessionKeepaliveMs(C.TIME_UNSET)
+ .build(/* mediaDrmCallback= */ licenseServer);
+
+ drmSessionManager.prepare();
+
+ DrmSessionManager.DrmSessionReference sessionReference =
+ drmSessionManager.preacquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA);
+
+ // Release the pre-acquired reference before the underlying session has had a chance to be
+ // constructed.
+ sessionReference.release();
+
+ // Acquiring the same session triggers a second key load (because the pre-acquired session was
+ // fully released).
+ DrmSession drmSession =
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
+
+ waitForOpenedWithKeys(drmSession);
+
+ drmSession.release(/* eventDispatcher= */ null);
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
+ }
+
+ @Test(timeout = 10_000)
+ public void preacquireSession_releaseManagerBeforeAcquisition_acquisitionDoesntHappen()
+ throws Exception {
+ FakeExoMediaDrm.LicenseServer licenseServer =
+ FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
+ DrmSessionManager drmSessionManager =
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
+ // Disable keepalive
+ .setSessionKeepaliveMs(C.TIME_UNSET)
+ .build(/* mediaDrmCallback= */ licenseServer);
+
+ drmSessionManager.prepare();
+
+ DrmSessionManager.DrmSessionReference sessionReference =
+ drmSessionManager.preacquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA);
+
+ // Release the manager before the underlying session has had a chance to be constructed. This
+ // will release all pre-acquired sessions.
+ drmSessionManager.release();
+
+ // Allow the acquisition event to be handled on the main/playback thread.
+ ShadowLooper.idleMainLooper();
+
+ // Re-prepare the manager so we can fully acquire the same session, and check the previous
+ // pre-acquisition didn't do anything.
+ drmSessionManager.prepare();
+ DrmSession drmSession =
+ checkNotNull(
+ drmSessionManager.acquireSession(
+ /* playbackLooper= */ checkNotNull(Looper.myLooper()),
+ /* eventDispatcher= */ null,
+ FORMAT_WITH_DRM_INIT_DATA));
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED);
+ waitForOpenedWithKeys(drmSession);
+
+ drmSession.release(/* eventDispatcher= */ null);
+ // If the (still unreleased) pre-acquired session above was linked to the same underlying
+ // session then the state would still be OPENED_WITH_KEYS.
+ assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
+
+ // Release the pre-acquired session from above (this is a no-op, but we do it anyway for
+ // correctness).
+ sessionReference.release();
+ drmSessionManager.release();
+ }
+
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();