Skip to content
Merged
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
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@
information. This is a source breaking change, but breakages can be
easily resolved by wrapping the previous `byte[]` return value with `new
Response` before returning.
* Add key request info like URL and latency to
`AnalyticsListener.onDrmKeysLoaded`
([#1001](https://github.com/androidx/media/issues/1001)).
* Effect:
* Muxers:
* Add `MediaMuxerCompat`, a drop-in replacement for framework
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 @@ -692,13 +693,17 @@ public void onDrmSessionAcquired(

@Override
public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
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 @@ -1363,13 +1364,21 @@ 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.
*
* @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, KeyRequestInfo keyRequestInfo) {}

/**
* Called when a drm error occurs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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 @@ -852,12 +853,17 @@ 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, 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 @@ -137,6 +137,7 @@ public interface ReferenceCountListener {
private final UUID uuid;
private final Looper playbackLooper;
private final ResponseHandler responseHandler;
private final Object keyRequestInfoLock;

private @DrmSession.State int state;
private int referenceCount;
Expand All @@ -148,6 +149,11 @@ public interface ReferenceCountListener {
private byte @MonotonicNonNull [] offlineLicenseKeySetId;

@Nullable private KeyRequest currentKeyRequest;

@GuardedBy("keyRequestInfoLock")
@Nullable
private KeyRequestInfo.Builder currentKeyRequestInfo;

@Nullable private ProvisionRequest currentProvisionRequest;

/**
Expand Down Expand Up @@ -209,6 +215,7 @@ public DefaultDrmSession(
state = STATE_OPENING;
this.playbackLooper = playbackLooper;
responseHandler = new ResponseHandler(playbackLooper);
keyRequestInfoLock = new Object();
}

public boolean hasSessionId(byte[] sessionId) {
Expand Down Expand Up @@ -348,6 +355,9 @@ public void release(@Nullable DrmSessionEventListener.EventDispatcher eventDispa
cryptoConfig = null;
lastException = null;
currentKeyRequest = null;
synchronized (keyRequestInfoLock) {
currentKeyRequestInfo = null;
}
currentProvisionRequest = null;
if (sessionId != null) {
mediaDrm.closeSession(sessionId);
Expand Down Expand Up @@ -489,6 +499,12 @@ private long getLicenseDurationRemainingSec() {

private void postKeyRequest(byte[] scope, int type, boolean allowRetry) {
try {
synchronized (keyRequestInfoLock) {
currentKeyRequestInfo = new KeyRequestInfo.Builder();
if (schemeDatas != null) {
currentKeyRequestInfo.setSchemeDatas(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.

This should probably just check for mode != DefaultDrmSessionManager.MODE_RELEASE. That way we cover download requests too.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually I think we should probably include release calls too, since they can theoretically also take time right?

In any case, we should indicate what type of request is being reported in KeyLoadInfo. But not sure exactly how to do that. @Mode is on DefaultDrmSessionManager so we can't really reference it from a 'generic' DRM context. Maybe @ExoMediaDrm.KeyType would work instead (though it feels kinda low-level).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that sounds like a good idea. We can start a list for what to add to KeyLoadInfo here.

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 is done in the latest commit, except for adding the KeyLoadInfo to the drmKeysRemoved() method. I'm fine with doing this too, however then we should rename KeyLoadInfo to KeyRequestInfo. I'm all for the YAGNI thinking too ;-)

But, seems most of the work will be rebasing all the files open for this change so it is probably now or never.

I'll follow your lead on it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm fine with doing this too, however then we should rename KeyLoadInfo to KeyRequestInfo

Yeah, I think we should do this rename and include key releasing - I agree it's going to be easier to do this now than later.

currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters);
Util.castNonNull(requestHandler).post(MSG_KEYS, checkNotNull(currentKeyRequest), allowRetry);
} catch (Exception | NoSuchMethodError e) {
Expand All @@ -502,6 +518,13 @@ private void onKeyResponse(Object request, Object response) {
return;
}
currentKeyRequest = null;
KeyRequestInfo keyRequestInfo;
synchronized (keyRequestInfoLock) {
// currentKeyRequest and currentKeyRequestInfo are assigned together, and nulled-out together,
// so it must be non-null here.
keyRequestInfo = checkNotNull(currentKeyRequestInfo).build();
currentKeyRequestInfo = null;
}

if (response instanceof Exception || response instanceof NoSuchMethodError) {
onKeysError((Throwable) response, /* thrownByExoMediaDrm= */ false);
Expand All @@ -510,8 +533,11 @@ private void onKeyResponse(Object request, Object response) {

try {
byte[] responseData = ((MediaDrmCallback.Response) response).data;

if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData);
// TODO: http://github.com/androidx/media/issues/1001 - Plumb the KeyLoadInfo up into
// drmKeysRemoved.
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmKeysRemoved);
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
Expand All @@ -523,7 +549,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 @@ -659,7 +685,17 @@ public void handleMessage(Message msg) {
callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request);
break;
case MSG_KEYS:
response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);
MediaDrmCallback.Response keyResponse =
callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);
response = keyResponse;
synchronized (keyRequestInfoLock) {
if (currentKeyRequestInfo != null && keyResponse.loadEventInfo != null) {
currentKeyRequestInfo.addLoadInfo(
keyResponse.loadEventInfo.copyWithTaskIdAndDurationMs(
requestTask.taskId,
SystemClock.elapsedRealtime() - requestTask.startTimeMs));
}
}
break;
default:
throw new RuntimeException();
Expand Down Expand Up @@ -715,6 +751,11 @@ private boolean maybeRetryRequest(Message originalMsg, MediaDrmCallbackException
// The error is fatal.
return false;
}
synchronized (keyRequestInfoLock) {
if (currentKeyRequestInfo != null) {
currentKeyRequestInfo.addLoadInfo(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,21 @@ 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 requests
*/
default void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {}
default void onDrmKeysLoaded(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, KeyRequestInfo keyRequestInfo) {}

/**
* Called when a drm error occurs.
Expand Down Expand Up @@ -166,12 +174,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(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 @@ -27,6 +27,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.media3.common.PlaybackException;
Expand All @@ -37,6 +38,7 @@
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.StatsDataSource;
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 @@ -145,11 +147,21 @@ public static boolean isFailureToConstructResourceBusyException(@Nullable Throwa
*
* <p>Note that this method is executing the request synchronously and blocks until finished.
*
* <p>The {@link LoadEventInfo} returned inside the {@link MediaDrmCallback.Response} will have
* the following fields unset, and they must be updated by caller before the {@link LoadEventInfo}
* is used elsewhere:
*
* <ul>
* <li>{@link LoadEventInfo#loadTaskId}
* <li>{@link LoadEventInfo#loadDurationMs}
* </ul>
*
* @param dataSource A {@link DataSource}.
* @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 MediaDrmCallback.Response} that holds the response payload, and {@link
* LoadEventInfo}.
* @throws MediaDrmCallbackException if an exception was encountered during the download.
*/
public static MediaDrmCallback.Response executePost(
Expand All @@ -173,7 +185,19 @@ public static MediaDrmCallback.Response executePost(
while (true) {
DataSourceInputStream inputStream = new DataSourceInputStream(statsDataSource, dataSpec);
try {
return new MediaDrmCallback.Response(ByteStreams.toByteArray(inputStream));
byte[] response = ByteStreams.toByteArray(inputStream);
LoadEventInfo loadEventInfo =
new LoadEventInfo(
-1, // This will be replaced with the actual taskId from the request.
originalDataSpec,
statsDataSource.getLastOpenedUri(),
statsDataSource.getLastResponseHeaders(),
SystemClock.elapsedRealtime(),
/* loadDurationMs= */ 0,
response.length);
return new MediaDrmCallback.Response.Builder(response)
.setLoadEventInfo(loadEventInfo)
.build();
} catch (HttpDataSource.InvalidResponseCodeException e) {
@Nullable String redirectUrl = getRedirectUrl(e, manualRedirectCount);
if (redirectUrl == null) {
Expand Down
Loading