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:
parent
ecb109dad8
commit
74ad0949fd
@ -47,9 +47,16 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
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)
|
@RequiresApi(18)
|
||||||
public class DefaultDrmSessionManager implements DrmSessionManager {
|
public class DefaultDrmSessionManager implements DrmSessionManager {
|
||||||
|
|
||||||
@ -282,14 +289,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
|
|
||||||
private final List<DefaultDrmSession> sessions;
|
private final List<DefaultDrmSession> sessions;
|
||||||
private final List<DefaultDrmSession> provisioningSessions;
|
private final List<DefaultDrmSession> provisioningSessions;
|
||||||
|
private final Set<PreacquiredSessionReference> preacquiredSessionReferences;
|
||||||
private final Set<DefaultDrmSession> keepaliveSessions;
|
private final Set<DefaultDrmSession> keepaliveSessions;
|
||||||
|
|
||||||
private int prepareCallsCount;
|
private int prepareCallsCount;
|
||||||
@Nullable private ExoMediaDrm exoMediaDrm;
|
@Nullable private ExoMediaDrm exoMediaDrm;
|
||||||
@Nullable private DefaultDrmSession placeholderDrmSession;
|
@Nullable private DefaultDrmSession placeholderDrmSession;
|
||||||
@Nullable private DefaultDrmSession noMultiSessionDrmSession;
|
@Nullable private DefaultDrmSession noMultiSessionDrmSession;
|
||||||
@Nullable private Looper playbackLooper;
|
private @MonotonicNonNull Looper playbackLooper;
|
||||||
private @MonotonicNonNull Handler sessionReleasingHandler;
|
private @MonotonicNonNull Handler playbackHandler;
|
||||||
private int mode;
|
private int mode;
|
||||||
@Nullable private byte[] offlineLicenseKeySetId;
|
@Nullable private byte[] offlineLicenseKeySetId;
|
||||||
|
|
||||||
@ -403,6 +411,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
mode = MODE_PLAYBACK;
|
mode = MODE_PLAYBACK;
|
||||||
sessions = new ArrayList<>();
|
sessions = new ArrayList<>();
|
||||||
provisioningSessions = new ArrayList<>();
|
provisioningSessions = new ArrayList<>();
|
||||||
|
preacquiredSessionReferences = Sets.newIdentityHashSet();
|
||||||
keepaliveSessions = Sets.newIdentityHashSet();
|
keepaliveSessions = Sets.newIdentityHashSet();
|
||||||
this.sessionKeepaliveMs = sessionKeepaliveMs;
|
this.sessionKeepaliveMs = sessionKeepaliveMs;
|
||||||
}
|
}
|
||||||
@ -466,10 +475,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
sessions.get(i).release(/* eventDispatcher= */ null);
|
sessions.get(i).release(/* eventDispatcher= */ null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
releaseAllPreacquiredSessions();
|
||||||
|
|
||||||
Assertions.checkNotNull(exoMediaDrm).release();
|
Assertions.checkNotNull(exoMediaDrm).release();
|
||||||
exoMediaDrm = null;
|
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
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public DrmSession acquireSession(
|
public DrmSession acquireSession(
|
||||||
@ -477,11 +500,27 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||||
Format format) {
|
Format format) {
|
||||||
initPlaybackLooper(playbackLooper);
|
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);
|
maybeCreateMediaDrmHandler(playbackLooper);
|
||||||
|
|
||||||
if (format.drmInitData == null) {
|
if (format.drmInitData == null) {
|
||||||
// Content is not encrypted.
|
// Content is not encrypted.
|
||||||
return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType));
|
return maybeAcquirePlaceholderSession(
|
||||||
|
MimeTypes.getTrackType(format.sampleMimeType),
|
||||||
|
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable List<SchemeData> schemeDatas = null;
|
@Nullable List<SchemeData> schemeDatas = null;
|
||||||
@ -515,7 +554,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
// Create a new session.
|
// Create a new session.
|
||||||
session =
|
session =
|
||||||
createAndAcquireSessionWithRetry(
|
createAndAcquireSessionWithRetry(
|
||||||
schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher);
|
schemeDatas,
|
||||||
|
/* isPlaceholderSession= */ false,
|
||||||
|
eventDispatcher,
|
||||||
|
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||||
if (!multiSession) {
|
if (!multiSession) {
|
||||||
noMultiSessionDrmSession = session;
|
noMultiSessionDrmSession = session;
|
||||||
}
|
}
|
||||||
@ -547,7 +589,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private DrmSession maybeAcquirePlaceholderSession(int trackType) {
|
private DrmSession maybeAcquirePlaceholderSession(
|
||||||
|
int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
|
||||||
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
|
ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
|
||||||
boolean avoidPlaceholderDrmSessions =
|
boolean avoidPlaceholderDrmSessions =
|
||||||
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
|
FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
|
||||||
@ -563,7 +606,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
createAndAcquireSessionWithRetry(
|
createAndAcquireSessionWithRetry(
|
||||||
/* schemeDatas= */ ImmutableList.of(),
|
/* schemeDatas= */ ImmutableList.of(),
|
||||||
/* isPlaceholderSession= */ true,
|
/* isPlaceholderSession= */ true,
|
||||||
/* eventDispatcher= */ null);
|
/* eventDispatcher= */ null,
|
||||||
|
shouldReleasePreacquiredSessionsBeforeRetrying);
|
||||||
sessions.add(placeholderDrmSession);
|
sessions.add(placeholderDrmSession);
|
||||||
this.placeholderDrmSession = placeholderDrmSession;
|
this.placeholderDrmSession = placeholderDrmSession;
|
||||||
} else {
|
} else {
|
||||||
@ -607,12 +651,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPlaybackLooper(Looper playbackLooper) {
|
@EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"})
|
||||||
|
private synchronized void initPlaybackLooper(Looper playbackLooper) {
|
||||||
if (this.playbackLooper == null) {
|
if (this.playbackLooper == null) {
|
||||||
this.playbackLooper = playbackLooper;
|
this.playbackLooper = playbackLooper;
|
||||||
this.sessionReleasingHandler = new Handler(playbackLooper);
|
this.playbackHandler = new Handler(playbackLooper);
|
||||||
} else {
|
} else {
|
||||||
Assertions.checkState(this.playbackLooper == playbackLooper);
|
Assertions.checkState(this.playbackLooper == playbackLooper);
|
||||||
|
Assertions.checkNotNull(playbackHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -625,35 +671,68 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
private DefaultDrmSession createAndAcquireSessionWithRetry(
|
private DefaultDrmSession createAndAcquireSessionWithRetry(
|
||||||
@Nullable List<SchemeData> schemeDatas,
|
@Nullable List<SchemeData> schemeDatas,
|
||||||
boolean isPlaceholderSession,
|
boolean isPlaceholderSession,
|
||||||
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) {
|
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
|
||||||
|
boolean shouldReleasePreacquiredSessionsBeforeRetrying) {
|
||||||
DefaultDrmSession session =
|
DefaultDrmSession session =
|
||||||
createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher);
|
||||||
if (session.getState() == DrmSession.STATE_ERROR
|
// If we're short on DRM session resources, first try eagerly releasing all our keepalive
|
||||||
&& (Util.SDK_INT < 19
|
// sessions and then retry the acquisition.
|
||||||
|| Assertions.checkNotNull(session.getError()).getCause()
|
if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) {
|
||||||
instanceof ResourceBusyException)) {
|
// Make a local copy, because sessions are removed from this.keepaliveSessions during
|
||||||
// We're short on DRM session resources, so eagerly release all our keepalive sessions.
|
// release (via callback).
|
||||||
// ResourceBusyException is only available at API 19, so on earlier versions we always
|
ImmutableSet<DefaultDrmSession> keepaliveSessions =
|
||||||
// eagerly release regardless of the underlying error.
|
ImmutableSet.copyOf(this.keepaliveSessions);
|
||||||
if (!keepaliveSessions.isEmpty()) {
|
for (DrmSession keepaliveSession : keepaliveSessions) {
|
||||||
// Make a local copy, because sessions are removed from this.keepaliveSessions during
|
keepaliveSession.release(/* eventDispatcher= */ null);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
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;
|
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
|
* Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in
|
||||||
* {@code eventDispatcher}).
|
* {@code eventDispatcher}).
|
||||||
@ -782,7 +861,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||||
// The session has been acquired elsewhere so we want to cancel our timeout.
|
// The session has been acquired elsewhere so we want to cancel our timeout.
|
||||||
keepaliveSessions.remove(session);
|
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) {
|
if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) {
|
||||||
// Only the internal keep-alive reference remains, so we can start the timeout.
|
// Only the internal keep-alive reference remains, so we can start the timeout.
|
||||||
keepaliveSessions.add(session);
|
keepaliveSessions.add(session);
|
||||||
Assertions.checkNotNull(sessionReleasingHandler)
|
Assertions.checkNotNull(playbackHandler)
|
||||||
.postAtTime(
|
.postAtTime(
|
||||||
() -> session.release(/* eventDispatcher= */ null),
|
() -> session.release(/* eventDispatcher= */ null),
|
||||||
session,
|
session,
|
||||||
@ -812,7 +891,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
}
|
}
|
||||||
provisioningSessions.remove(session);
|
provisioningSessions.remove(session);
|
||||||
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
if (sessionKeepaliveMs != C.TIME_UNSET) {
|
||||||
Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session);
|
Assertions.checkNotNull(playbackHandler).removeCallbacksAndMessages(session);
|
||||||
keepaliveSessions.remove(session);
|
keepaliveSessions.remove(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -827,4 +906,75 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
|
|||||||
Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format;
|
|||||||
/** Manages a DRM session. */
|
/** Manages a DRM session. */
|
||||||
public interface DrmSessionManager {
|
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. */
|
/** An instance that supports no DRM schemes. */
|
||||||
DrmSessionManager DRM_UNSUPPORTED =
|
DrmSessionManager DRM_UNSUPPORTED =
|
||||||
new DrmSessionManager() {
|
new DrmSessionManager() {
|
||||||
@ -81,6 +98,51 @@ public interface DrmSessionManager {
|
|||||||
// Do nothing.
|
// 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
|
* 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
|
* count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is
|
||||||
|
@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
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.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
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.FakeExoMediaDrm;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.shadows.ShadowLooper;
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper;
|
|||||||
// - Multiple acquisitions & releases for same keys -> multiple requests.
|
// - Multiple acquisitions & releases for same keys -> multiple requests.
|
||||||
// - Provisioning.
|
// - Provisioning.
|
||||||
// - Key denial.
|
// - Key denial.
|
||||||
|
// - Handling of ResourceBusyException (indicating session scarcity).
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class DefaultDrmSessionManagerTest {
|
public class DefaultDrmSessionManagerTest {
|
||||||
|
|
||||||
@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
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) {
|
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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user