mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
Keep DRM sessions alive for a while before fully releasing them
Issue: #7011 Issue: #6725 Issue: #7066 This also mitigates (but doesn't fix) Issue: #4133 because it prevents a second key load after a short clear section. PiperOrigin-RevId: 319184325
This commit is contained in:
parent
e4e743a35f
commit
316f8a88cd
5 changed files with 377 additions and 58 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>{@code newReferenceCount == 0} indicates this session is in {@link #STATE_RELEASED}.
|
||||
*
|
||||
* @param session This session.
|
||||
* @param newReferenceCount The reference count after being decremented.
|
||||
*/
|
||||
void 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<SchemeData> 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<DefaultDrmSession> sessions;
|
||||
private final List<DefaultDrmSession> provisioningSessions;
|
||||
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 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<DefaultDrmSession> 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<SchemeData> 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<SchemeData> schemeDatas, boolean isPlaceholderSession) {
|
||||
private DefaultDrmSession createAndAcquireSessionWithRetry(
|
||||
@Nullable List<SchemeData> 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<DefaultDrmSession> 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}).
|
||||
*
|
||||
* <p>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<SchemeData> 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
|
||||
|
|
|
|||
|
|
@ -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<DrmInitData.SchemeData> secondSchemeDatas =
|
||||
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS, secondSchemeDatas);
|
||||
DrmInitData secondInitData = new DrmInitData(secondSchemeDatas);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(
|
||||
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm(/* maxConcurrentSessions= */ 1))
|
||||
.setSessionKeepaliveMs(10_000)
|
||||
.setMultiSession(true)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
DrmSession firstDrmSession =
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
DRM_INIT_DATA);
|
||||
waitForOpenedWithKeys(firstDrmSession);
|
||||
firstDrmSession.release(/* eventDispatcher= */ null);
|
||||
|
||||
// All external references to firstDrmSession have been released, it's being kept alive by
|
||||
// drmSessionManager's internal reference.
|
||||
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
DrmSession secondDrmSession =
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
secondInitData);
|
||||
// The drmSessionManager had to release firstDrmSession in order to acquire secondDrmSession.
|
||||
assertThat(firstDrmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED);
|
||||
|
||||
waitForOpenedWithKeys(secondDrmSession);
|
||||
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void sessionReacquired_keepaliveTimeOutCancelled() throws Exception {
|
||||
FakeExoMediaDrm.LicenseServer licenseServer =
|
||||
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
|
||||
DrmSessionManager drmSessionManager =
|
||||
new DefaultDrmSessionManager.Builder()
|
||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||
.setSessionKeepaliveMs(10_000)
|
||||
.build(/* mediaDrmCallback= */ licenseServer);
|
||||
|
||||
drmSessionManager.prepare();
|
||||
DrmSession firstDrmSession =
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
DRM_INIT_DATA);
|
||||
waitForOpenedWithKeys(firstDrmSession);
|
||||
firstDrmSession.release(/* eventDispatcher= */ null);
|
||||
|
||||
ShadowLooper.idleMainLooper(5, SECONDS);
|
||||
|
||||
// Acquire a session for the same init data 5s in to the 10s timeout (so expect the same
|
||||
// instance).
|
||||
DrmSession secondDrmSession =
|
||||
drmSessionManager.acquireSession(
|
||||
/* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
|
||||
/* eventDispatcher= */ null,
|
||||
DRM_INIT_DATA);
|
||||
assertThat(secondDrmSession).isSameInstanceAs(firstDrmSession);
|
||||
|
||||
// Let the timeout definitely expire, and check the session didn't get released.
|
||||
ShadowLooper.idleMainLooper(10, SECONDS);
|
||||
assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
|
||||
}
|
||||
|
||||
private static void waitForOpenedWithKeys(DrmSession drmSession) {
|
||||
// Check the error first, so we get a meaningful failure if there's been an error.
|
||||
assertThat(drmSession.getError()).isNull();
|
||||
|
|
|
|||
|
|
@ -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<Byte> VALID_KEY_RESPONSE = TestUtil.createByteList(1, 2, 3);
|
||||
private static final ImmutableList<Byte> KEY_DENIED_RESPONSE = TestUtil.createByteList(9, 8, 7);
|
||||
|
||||
private final int maxConcurrentSessions;
|
||||
private final Map<String, byte[]> byteProperties;
|
||||
private final Map<String, String> stringProperties;
|
||||
private final Set<List<Byte>> 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))) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue