Add support for preparing DRM sessions before they're needed

This adds an optional DrmSessionManager#preacquireSession() method
and implements it on DefaultDrmSessionManager.

The manager doesn't promise to keep the preacquired sessions alive, and
will proactively release them if a ResourceBusyException suggests the
device is running out of available sessions in the underlying framework.

In a future change, SampleQueue will preacquire sessions on the loading
thread and keep track of preacquired 'references', releasing them
when seeking or clearing the queue.

Issue: #4133
PiperOrigin-RevId: 358381616
This commit is contained in:
ibaker 2021-02-19 12:52:46 +00:00 committed by bachinger
parent ecb109dad8
commit 74ad0949fd
3 changed files with 401 additions and 34 deletions

View File

@ -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}.
*
* <p>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<DefaultDrmSession> sessions;
private final List<DefaultDrmSession> provisioningSessions;
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
private final Set<DefaultDrmSession> 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 @MonotonicNonNull Looper playbackLooper;
private @MonotonicNonNull Handler playbackHandler;
private int mode;
@Nullable private byte[] offlineLicenseKeySetId;
@ -403,6 +411,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
mode = MODE_PLAYBACK;
sessions = new ArrayList<>();
provisioningSessions = new ArrayList<>();
preacquiredSessionReferences = Sets.newIdentityHashSet();
keepaliveSessions = Sets.newIdentityHashSet();
this.sessionKeepaliveMs = sessionKeepaliveMs;
}
@ -466,10 +475,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
sessions.get(i).release(/* eventDispatcher= */ null);
}
}
releaseAllPreacquiredSessions();
Assertions.checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
}
@Override
public DrmSessionReference preacquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
initPlaybackLooper(playbackLooper);
PreacquiredSessionReference preacquiredSessionReference =
new PreacquiredSessionReference(eventDispatcher);
preacquiredSessionReference.acquire(format);
return preacquiredSessionReference;
}
@Override
@Nullable
public DrmSession acquireSession(
@ -477,11 +500,27 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
initPlaybackLooper(playbackLooper);
return acquireSession(
playbackLooper,
eventDispatcher,
format,
/* shouldReleasePreacquiredSessionsBeforeRetrying= */ true);
}
// Must be called on the playback thread.
@Nullable
private DrmSession acquireSession(
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format,
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
maybeCreateMediaDrmHandler(playbackLooper);
if (format.drmInitData == null) {
// Content is not encrypted.
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
return maybeAcquirePlaceholderSession(
MimeTypes.getTrackType(format.sampleMimeType),
shouldReleasePreacquiredSessionsBeforeRetrying);
}
@Nullable List<SchemeData> schemeDatas = null;
@ -515,7 +554,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Create a new session.
session =
createAndAcquireSessionWithRetry(
schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher);
schemeDatas,
/* isPlaceholderSession= */ false,
eventDispatcher,
shouldReleasePreacquiredSessionsBeforeRetrying);
if (!multiSession) {
noMultiSessionDrmSession = session;
}
@ -547,7 +589,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
// Internal methods.
@Nullable
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
private DrmSession maybeAcquirePlaceholderSession(
int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
boolean avoidPlaceholderDrmSessions =
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
@ -563,7 +606,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
createAndAcquireSessionWithRetry(
/* schemeDatas= */ ImmutableList.of(),
/* isPlaceholderSession= */ true,
/* eventDispatcher= */ null);
/* eventDispatcher= */ null,
shouldReleasePreacquiredSessionsBeforeRetrying);
sessions.add(placeholderDrmSession);
this.placeholderDrmSession = placeholderDrmSession;
} else {
@ -607,12 +651,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return true;
}
private void initPlaybackLooper(Looper playbackLooper) {
@EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"})
private synchronized void initPlaybackLooper(Looper playbackLooper) {
if (this.playbackLooper == null) {
this.playbackLooper = playbackLooper;
this.sessionReleasingHandler = new Handler(playbackLooper);
this.playbackHandler = new Handler(playbackLooper);
} else {
Assertions.checkState(this.playbackLooper == playbackLooper);
Assertions.checkNotNull(playbackHandler);
}
}
@ -625,35 +671,68 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
private DefaultDrmSession createAndAcquireSessionWithRetry(
@Nullable List<SchemeData> schemeDatas,
boolean isPlaceholderSession,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
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.keepaliveSessions during
// release (via callback).
ImmutableSet<DefaultDrmSession> keepaliveSessions =
ImmutableSet.copyOf(this.keepaliveSessions);
for (DrmSession keepaliveSession : keepaliveSessions) {
keepaliveSession.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);
// If we're short on DRM session resources, first try eagerly releasing all our keepalive
// sessions and then retry the acquisition.
if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) {
// Make a local copy, because sessions are removed from this.keepaliveSessions during
// release (via callback).
ImmutableSet<DefaultDrmSession> keepaliveSessions =
ImmutableSet.copyOf(this.keepaliveSessions);
for (DrmSession keepaliveSession : keepaliveSessions) {
keepaliveSession.release(/* eventDispatcher= */ null);
}
undoAcquisition(session, eventDispatcher);
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
}
// If the acquisition failed again due to continued resource shortage, and
// shouldReleasePreacquiredSessionsBeforeRetrying is true, try releasing all pre-acquired
// sessions and then retry the acquisition.
if (acquisitionFailedIndicatingResourceShortage(session)
&& shouldReleasePreacquiredSessionsBeforeRetrying
&& !preacquiredSessionReferences.isEmpty()) {
releaseAllPreacquiredSessions();
undoAcquisition(session, eventDispatcher);
session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
}
return session;
}
private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) {
// ResourceBusyException is only available at API 19, so on earlier versions we
// assume any error indicates resource shortage (ensuring we retry).
return session.getState() == DrmSession.STATE_ERROR
&& (Util.SDK_INT < 19
|| Assertions.checkNotNull(session.getError()).getCause()
instanceof ResourceBusyException);
}
/**
* Undoes the acquisitions from {@link #createAndAcquireSession(List, boolean,
* DrmSessionEventListener.EventDispatcher)}.
*/
private void undoAcquisition(
DrmSession session, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
session.release(eventDispatcher);
if (sessionKeepaliveMs != C.TIME_UNSET) {
session.release(/* eventDispatcher= */ null);
}
}
private void releaseAllPreacquiredSessions() {
// Make a local copy, because sessions are removed from this.preacquiredSessionReferences
// during release (via callback).
ImmutableSet<PreacquiredSessionReference> preacquiredSessionReferences =
ImmutableSet.copyOf(this.preacquiredSessionReferences);
for (PreacquiredSessionReference preacquiredSessionReference : preacquiredSessionReferences) {
preacquiredSessionReference.release();
}
}
/**
* Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
* {@code eventDispatcher}).
@ -782,7 +861,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
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);
Assertions.checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
}
}
@ -791,7 +870,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
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)
Assertions.checkNotNull(playbackHandler)
.postAtTime(
() -> session.release(/* eventDispatcher= */ null),
session,
@ -812,7 +891,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
provisioningSessions.remove(session);
if (sessionKeepaliveMs != C.TIME_UNSET) {
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
Assertions.checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
keepaliveSessions.remove(session);
}
}
@ -827,4 +906,75 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
}
}
/**
* An implementation of {@link DrmSessionReference} that lazily acquires the underlying {@link
* DrmSession}.
*
* <p>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.
*
* <p>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;
});
}
}
}

View File

@ -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.
*
* <p>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}.
*
* <p>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.
*
* <p>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).
*
* <p>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.
*
* <p>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.
*
* <p>This can be called from any thread.
*
* <p>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

View File

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