Skip to content

Adds load info to DrmSessionEventListeneronDrmKeysLoaded() #1134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.KeyRequestInfo;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MaskingMediaPeriod;
import androidx.media3.exoplayer.source.MaskingMediaSource;
Expand Down Expand Up @@ -686,13 +687,17 @@ public void onDrmSessionAcquired(

@Override
public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
@Nullable KeyRequestInfo keyRequestInfo) {
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysLoaded(eventParameters.first, eventParameters.second));
() ->
eventListener.onDrmKeysLoaded(
eventParameters.first, eventParameters.second, keyRequestInfo));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.KeyRequestInfo;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
Expand Down Expand Up @@ -1346,13 +1347,24 @@ default void onDrmSessionAcquired(EventTime eventTime) {}
@UnstableApi
default void onDrmSessionAcquired(EventTime eventTime, @DrmSession.State int state) {}

/**
* @deprecated Implement {@link #onDrmKeysLoaded(EventTime, KeyRequestInfo)} instead.
*/
@UnstableApi
@Deprecated
default void onDrmKeysLoaded(EventTime eventTime) {}

/**
* Called each time drm keys are loaded.
*
* <p>Includes a {@link KeyRequestInfo} with details on the loaded key and any network request[s]
* required to complete the load
*
* @param eventTime The event time.
* @param keyRequestInfo information for any required load operation, null if none
*/
@UnstableApi
default void onDrmKeysLoaded(EventTime eventTime) {}
default void onDrmKeysLoaded(EventTime eventTime, @Nullable KeyRequestInfo keyRequestInfo) {}

/**
* Called when a drm error occurs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.KeyRequestInfo;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
Expand Down Expand Up @@ -838,12 +839,19 @@ public final void onDrmSessionAcquired(
}

@Override
public final void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
@SuppressWarnings("deprecation") // Calls deprecated listener method.
public void onDrmKeysLoaded(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
@Nullable KeyRequestInfo keyRequestInfo) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
sendEvent(
eventTime,
AnalyticsListener.EVENT_DRM_KEYS_LOADED,
listener -> listener.onDrmKeysLoaded(eventTime));
listener -> {
listener.onDrmKeysLoaded(eventTime);
listener.onDrmKeysLoaded(eventTime, keyRequestInfo);
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest;
import androidx.media3.exoplayer.drm.ExoMediaDrm.ProvisionRequest;
import androidx.media3.exoplayer.drm.KeyRequestInfo.Builder;
import androidx.media3.exoplayer.drm.MediaDrmCallback.KeyResponse;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
Expand Down Expand Up @@ -149,6 +151,7 @@ public interface ReferenceCountListener {
private byte @MonotonicNonNull [] offlineLicenseKeySetId;

@Nullable private KeyRequest currentKeyRequest;
@Nullable private KeyRequestInfo.Builder currentKeyRequestInfo;
@Nullable private ProvisionRequest currentProvisionRequest;

/**
Expand Down Expand Up @@ -494,6 +497,7 @@ private long getLicenseDurationRemainingSec() {

private void postKeyRequest(byte[] scope, int type, boolean allowRetry) {
try {
currentKeyRequestInfo = new Builder(schemeDatas);
currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters);
Util.castNonNull(requestHandler)
.post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry);
Expand All @@ -516,8 +520,11 @@ private void onKeyResponse(Object request, Object response) {

try {
byte[] responseData = (byte[]) response;
KeyRequestInfo keyRequestInfo = currentKeyRequestInfo.build();
currentKeyRequestInfo = null;
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData);
// TODO plumb the KeyLoadInfo up into drmKeysRemoved
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmKeysRemoved);
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
Expand All @@ -529,7 +536,7 @@ private void onKeyResponse(Object request, Object response) {
offlineLicenseKeySetId = keySetId;
}
state = STATE_OPENED_WITH_KEYS;
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmKeysLoaded);
dispatchEvent(eventDispatcher -> eventDispatcher.drmKeysLoaded(keyRequestInfo));
}
} catch (Exception | NoSuchMethodError e) {
onKeysError(e, /* thrownByExoMediaDrm= */ true);
Expand Down Expand Up @@ -662,10 +669,18 @@ public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
response =
callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request);
callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request)
.responseData;
break;
case MSG_KEYS:
response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);
KeyResponse keyResponse =
callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);
response = keyResponse.responseData;
if (currentKeyRequestInfo != null) {
LoadEventInfo loadEventInfo =
keyResponse.loadEventInfo.copyWithTaskId(requestTask.taskId);
currentKeyRequestInfo.setMainLoadRequest(loadEventInfo);
}
break;
default:
throw new RuntimeException();
Expand Down Expand Up @@ -721,6 +736,9 @@ private boolean maybeRetryRequest(Message originalMsg, MediaDrmCallbackException
// The error is fatal.
return false;
}
if (currentKeyRequestInfo != null) {
currentKeyRequestInfo.addRetryLoadRequest(loadEventInfo);
}
synchronized (this) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be useful if you want the trails of LoadEventInfo's including redirects in the path. The main LoadEventInfo should include all the time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will report all (most? any?) redirects - because at least some types of redirect will be 'silently' handled inside the HttpDataSource implementation underneath HttpMediaDrmCallback, e.g.

So I think we need to be quite careful we can usefully describe what this field holds, because i think it will only show retries that happened at this DefaultDrmSession layer.

Copy link
Contributor Author

@stevemayhew stevemayhew Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is certainly true for DefaultHttpDataSource, not sure for Cronet. something we'll look into, as our servers are using this as a method to hold off clients when they are loaded. Also, the POST data is retained in the HttpMediaDrmCallback so not sure if the POST redirect in the stack will do this correctly.

We'll have a look and let you know, definitely preference would be to do it in the HttpMediaDrmCallback as the default redirect following code in lower layers usually is much higher than 5 tries.

FWIW this is the same as how other Loader / Loadable implementations handle this.

Definitely something to look into.

if (!isReleased) {
sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,23 @@ public interface DrmSessionEventListener {
default void onDrmSessionAcquired(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, @DrmSession.State int state) {}

/**
* @deprecated Implement {@link #onDrmKeysLoaded(int, MediaPeriodId, KeyRequestInfo)} instead
*/
@Deprecated
default void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}

/**
* Called each time keys are loaded.
*
* @param windowIndex The window index in the timeline this media period belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} associated with the drm session.
* @param keyRequestInfo The {@link KeyRequestInfo} with load info for the drm server request[s]
*/
default void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
default void onDrmKeysLoaded(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
@Nullable KeyRequestInfo keyRequestInfo) {}

/**
* Called when a drm error occurs.
Expand Down Expand Up @@ -166,12 +176,19 @@ public void drmSessionAcquired(@DrmSession.State int state) {
}
}

/** Dispatches {@link #onDrmKeysLoaded(int, MediaPeriodId)}. */
public void drmKeysLoaded() {
/**
* Dispatches {@link #onDrmKeysLoaded(int, MediaPeriodId)}. and {@link #onDrmKeysLoaded(int,
* MediaPeriodId, KeyRequestInfo)}
*/
public void drmKeysLoaded(@Nullable KeyRequestInfo keyRequestInfo) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
DrmSessionEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler, () -> listener.onDrmKeysLoaded(windowIndex, mediaPeriodId));
listenerAndHandler.handler,
() -> {
listener.onDrmKeysLoaded(windowIndex, mediaPeriodId);
listener.onDrmKeysLoaded(windowIndex, mediaPeriodId, keyRequestInfo);
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import android.media.MediaDrmResetException;
import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
Expand All @@ -38,6 +39,8 @@
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.StatsDataSource;
import androidx.media3.exoplayer.drm.MediaDrmCallback.KeyResponse;
import androidx.media3.exoplayer.source.LoadEventInfo;
import com.google.common.io.ByteStreams;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
Expand Down Expand Up @@ -150,10 +153,10 @@ public static boolean isFailureToConstructResourceBusyException(@Nullable Throwa
* @param url The requested URL.
* @param httpBody The HTTP request payload.
* @param requestProperties A keyed map of HTTP header request properties.
* @return A byte array that holds the response payload.
* @return A {@link KeyResponse} that holds the response payload, and LoadEventInfo
* @throws MediaDrmCallbackException if an exception was encountered during the download.
*/
public static byte[] executePost(
public static KeyResponse executePost(
DataSource dataSource,
String url,
@Nullable byte[] httpBody,
Expand All @@ -170,11 +173,22 @@ public static byte[] executePost(
.setFlags(DataSpec.FLAG_ALLOW_GZIP)
.build();
DataSpec originalDataSpec = dataSpec;
long startTimeMs = SystemClock.elapsedRealtime();
try {
while (true) {
DataSourceInputStream inputStream = new DataSourceInputStream(statsDataSource, dataSpec);
try {
return ByteStreams.toByteArray(inputStream);
byte[] response = ByteStreams.toByteArray(inputStream);
LoadEventInfo loadEventInfo =
new LoadEventInfo(
-1, // note this is replaced with the actual taskId from the request
originalDataSpec,
statsDataSource.getLastOpenedUri(),
statsDataSource.getLastResponseHeaders(),
SystemClock.elapsedRealtime(),
/* loadDurationMs= */ SystemClock.elapsedRealtime() - startTimeMs,
((byte[]) response).length);
return new KeyResponse(response, loadEventInfo);
} catch (HttpDataSource.InvalidResponseCodeException e) {
@Nullable String redirectUrl = getRedirectUrl(e, manualRedirectCount);
if (redirectUrl == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void clearAllKeyRequestProperties() {
}

@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request)
public KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request)
throws MediaDrmCallbackException {
String url =
request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData());
Expand All @@ -125,7 +125,8 @@ public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request)
}

@Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws MediaDrmCallbackException {
public KeyResponse executeKeyRequest(UUID uuid, KeyRequest request)
throws MediaDrmCallbackException {
String url = request.getLicenseServerUrl();
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package androidx.media3.exoplayer.drm;

import androidx.annotation.Nullable;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.util.Assertions;
import androidx.media3.exoplayer.source.LoadEventInfo;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/**
* Encapsulates info for the sequence of load requests ({@link LoadEventInfo}, which were required
* to complete loading a DRM key
*/
public class KeyRequestInfo {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor renamed, in prep for other license requests beside Key load.


public static class Builder {
@MonotonicNonNull private LoadEventInfo loadEventInfo;
private final List<LoadEventInfo> retriedLoadRequests;
@Nullable private final List<SchemeData> schemeDatas;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up the lint, schemeDatas is nullable for offline keys (we may want to add an additional identifying member for this case).

loadEventInfo is non-null after the main request is indemnified (which must be before build()

public Builder(@Nullable List<SchemeData> schemeDatas) {
this.schemeDatas = schemeDatas;
retriedLoadRequests = new ArrayList<>();
loadEventInfo = null;
}

public Builder setMainLoadRequest(LoadEventInfo loadEventInfo) {
this.loadEventInfo = loadEventInfo;
return this;
}

public Builder addRetryLoadRequest(LoadEventInfo loadEventInfo) {
retriedLoadRequests.add(loadEventInfo);
return this;
}

public KeyRequestInfo build() {
Assertions.checkNotNull(loadEventInfo, "build() called before setMainLoadRequest()");
return new KeyRequestInfo(this);
}
}

/**
* The {@link LoadEventInfo} for the initial request to laod the key, or null if no load required
*/
public final LoadEventInfo loadEventInfo;

/** If the load required multiple retries, the {@link LoadEventInfo} for each retry */
public final ImmutableList<LoadEventInfo> retriedLoadRequests;

/**
* The DRM {@link SchemeData} that identifies the loaded key, or null if this session uses offline
* keys. // TODO add sessionId to the KeyLoadInfo maybe?
*/
@Nullable public final ImmutableList<SchemeData> schemeDatas;

private KeyRequestInfo(Builder builder) {
retriedLoadRequests =
new ImmutableList.Builder<LoadEventInfo>().addAll(builder.retriedLoadRequests).build();
loadEventInfo = builder.loadEventInfo;
schemeDatas = builder.schemeDatas == null ? null : ImmutableList.copyOf(builder.schemeDatas);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest;
import androidx.media3.exoplayer.drm.ExoMediaDrm.ProvisionRequest;
import androidx.media3.exoplayer.source.LoadEventInfo;
import java.util.UUID;

/**
Expand All @@ -40,12 +42,12 @@ public LocalMediaDrmCallback(byte[] keyResponse) {
}

@Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) {
public KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request) {
throw new UnsupportedOperationException();
}

@Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) {
return keyResponse;
public KeyResponse executeKeyRequest(UUID uuid, KeyRequest request) {
return new KeyResponse(keyResponse, new LoadEventInfo(-1, new DataSpec.Builder().build(), 0));
}
}
Loading