From 4a0031889924ab4468295e906f9fba1dcc0d251a Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Fri, 4 Oct 2024 13:03:41 -0700 Subject: [PATCH] Provides thread-safe usage of `LoadEventInfo` in `HttpMediaDrmCallback` 1. Restore creation of `LoadEventInfo` to the code that was migrated to `DrmUtil.executePost()` that was removed in the rebase. 2. Refactor change signature of `MediaDrmCallback` to return both the byte[] response and an assoicated `LoadEventInfo` --- .../exoplayer/drm/DefaultDrmSession.java | 17 +++++--- .../media3/exoplayer/drm/DrmUtil.java | 20 +++++++-- .../exoplayer/drm/HttpMediaDrmCallback.java | 5 ++- .../exoplayer/drm/LocalMediaDrmCallback.java | 8 ++-- .../exoplayer/drm/MediaDrmCallback.java | 41 ++++++++++--------- .../DefaultAnalyticsCollectorTest.java | 30 ++++++++++---- .../drm/OfflineLicenseHelperTest.java | 4 ++ .../media3/test/utils/FakeExoMediaDrm.java | 24 ++++++++--- 8 files changed, 102 insertions(+), 47 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java index 4b01898154a..41270023048 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java @@ -41,6 +41,7 @@ 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; @@ -668,14 +669,16 @@ 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 = callback.getLastLoadEventInfo(); - loadEventInfo = - loadEventInfo != null ? loadEventInfo.copyWithTaskId(requestTask.taskId) : null; + LoadEventInfo loadEventInfo = + keyResponse.loadEventInfo.copyWithTaskId(requestTask.taskId); currentKeyRequestInfo.setMainLoadRequest(loadEventInfo); } break; @@ -733,7 +736,9 @@ private boolean maybeRetryRequest(Message originalMsg, MediaDrmCallbackException // The error is fatal. return false; } - currentKeyRequestInfo.addRetryLoadRequest(loadEventInfo); + if (currentKeyRequestInfo != null) { + currentKeyRequestInfo.addRetryLoadRequest(loadEventInfo); + } synchronized (this) { if (!isReleased) { sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java index 2d3a9f9c754..9b176fc8c22 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DrmUtil.java @@ -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; @@ -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; @@ -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, @@ -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) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java index 37f76cd242f..420f36b877a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/HttpMediaDrmCallback.java @@ -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()); @@ -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; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/LocalMediaDrmCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/LocalMediaDrmCallback.java index ca6ce6adab5..5e98f222ea6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/LocalMediaDrmCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/LocalMediaDrmCallback.java @@ -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; /** @@ -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)); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/MediaDrmCallback.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/MediaDrmCallback.java index 000404557d5..c340ca78585 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/MediaDrmCallback.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/MediaDrmCallback.java @@ -15,7 +15,6 @@ */ package androidx.media3.exoplayer.drm; -import androidx.annotation.Nullable; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest; import androidx.media3.exoplayer.drm.ExoMediaDrm.ProvisionRequest; @@ -26,15 +25,32 @@ @UnstableApi public interface MediaDrmCallback { + /** + * Response data from the {@link MediaDrmCallback} requests. + * + *

Encapsulates the license server response data {@link #responseData} along with information + * ({@link #loadEventInfo} about the network transfer (if any) that was issued to gather the + * response. + */ + public class KeyResponse { + public final byte[] responseData; + public final LoadEventInfo loadEventInfo; + + public KeyResponse(byte[] responseData, LoadEventInfo loadEventInfo) { + this.responseData = responseData; + this.loadEventInfo = loadEventInfo; + } + } + /** * Executes a provisioning request. * * @param uuid The UUID of the content protection scheme. * @param request The request. - * @return The response data. + * @return A {@link KeyResponse} that holds the response payload, and LoadEventInfo * @throws MediaDrmCallbackException If an error occurred executing the request. */ - byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) + KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request) throws MediaDrmCallbackException; /** @@ -42,23 +58,8 @@ byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) * * @param uuid The UUID of the content protection scheme. * @param request The request. - * @return The response data. + * @return A {@link KeyResponse} that holds the response payload, and LoadEventInfo * @throws MediaDrmCallbackException If an error occurred executing the request. */ - byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws MediaDrmCallbackException; - - /** - * Get the {@link LoadEventInfo} for the last executed request. - * - *

Valid after a call to {@link #executeKeyRequest(UUID, KeyRequest)} or {@link - * #executeProvisionRequest(UUID, ProvisionRequest)}, either contains the load event info for that - * request or null if no load was performed or this implementation does not support reporting load - * info - * - * @return the {@link LoadEventInfo} or null if no load was performed - */ - @Nullable - default LoadEventInfo getLastLoadEventInfo() { - return null; - } + KeyResponse executeKeyRequest(UUID uuid, KeyRequest request) throws MediaDrmCallbackException; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index d5a65b1c0f2..60d025e60e9 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -74,6 +74,7 @@ import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask; import android.graphics.SurfaceTexture; +import android.net.Uri; import android.os.Looper; import android.util.SparseArray; import android.view.Surface; @@ -96,6 +97,7 @@ import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DecoderReuseEvaluation; import androidx.media3.exoplayer.ExoPlaybackException; @@ -106,6 +108,8 @@ import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.drm.ExoMediaDrm; +import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest; +import androidx.media3.exoplayer.drm.ExoMediaDrm.ProvisionRequest; import androidx.media3.exoplayer.drm.MediaDrmCallback; import androidx.media3.exoplayer.drm.MediaDrmCallbackException; import androidx.media3.exoplayer.source.LoadEventInfo; @@ -131,6 +135,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.UUID; @@ -190,6 +195,15 @@ public final class DefaultAnalyticsCollectorTest { private EventWindowAndPeriodId period1Seq2; private EventWindowAndPeriodId window0Period1Seq0; private EventWindowAndPeriodId window1Period0Seq1; + private static final LoadEventInfo FAKE_LOAD_EVENT_INFO = + new LoadEventInfo( + 1, + new DataSpec.Builder().setUri(Uri.EMPTY).build(), + Uri.EMPTY, + Collections.emptyMap(), + 1000, + 2000, + 8192); /** * Verify that {@link DefaultAnalyticsCollector} explicitly overrides all {@link Player.Listener} @@ -2402,13 +2416,13 @@ public String toString() { */ private static final class EmptyDrmCallback implements MediaDrmCallback { @Override - public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) { - return new byte[0]; + public KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request) { + return new KeyResponse(new byte[0], FAKE_LOAD_EVENT_INFO); } @Override - public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) { - return new byte[0]; + public KeyResponse executeKeyRequest(UUID uuid, KeyRequest request) { + return new KeyResponse(new byte[0], FAKE_LOAD_EVENT_INFO); } } @@ -2442,26 +2456,26 @@ public static BlockingDrmCallback alwaysFailing() { } @Override - public byte[] executeProvisionRequest(UUID uuid, ExoMediaDrm.ProvisionRequest request) + public KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request) throws MediaDrmCallbackException { provisionCondition.blockUninterruptible(); provisionCondition.close(); if (alwaysFail) { throw new RuntimeException("executeProvisionRequest failed"); } else { - return new byte[0]; + return new KeyResponse(new byte[0], FAKE_LOAD_EVENT_INFO); } } @Override - public byte[] executeKeyRequest(UUID uuid, ExoMediaDrm.KeyRequest request) + public KeyResponse executeKeyRequest(UUID uuid, KeyRequest request) throws MediaDrmCallbackException { keyCondition.blockUninterruptible(); keyCondition.close(); if (alwaysFail) { throw new RuntimeException("executeKeyRequest failed"); } else { - return new byte[0]; + return new KeyResponse(new byte[0], FAKE_LOAD_EVENT_INFO); } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/OfflineLicenseHelperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/OfflineLicenseHelperTest.java index 29f0e1fdfcb..110660af7f4 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/OfflineLicenseHelperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/OfflineLicenseHelperTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.drm; +import static androidx.media3.test.utils.FakeExoMediaDrm.FAKE_LOAD_EVENT_INFO; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -27,6 +28,7 @@ import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest; +import androidx.media3.exoplayer.drm.MediaDrmCallback.KeyResponse; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.HashMap; import org.junit.After; @@ -61,6 +63,8 @@ public void setUp() throws Exception { C.WIDEVINE_UUID, new ExoMediaDrm.AppManagedProvider(mediaDrm)) .build(mediaDrmCallback), new DrmSessionEventListener.EventDispatcher()); + when(mediaDrmCallback.executeKeyRequest(any(), any())) + .thenReturn(new KeyResponse(new byte[0], FAKE_LOAD_EVENT_INFO)); } @After diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java index 0a303170a02..e54fcdae483 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeExoMediaDrm.java @@ -25,6 +25,7 @@ import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.ResourceBusyException; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; @@ -34,10 +35,12 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSpec; import androidx.media3.decoder.CryptoConfig; import androidx.media3.exoplayer.drm.ExoMediaDrm; import androidx.media3.exoplayer.drm.MediaDrmCallback; import androidx.media3.exoplayer.drm.MediaDrmCallbackException; +import androidx.media3.exoplayer.source.LoadEventInfo; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -46,6 +49,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -177,6 +181,16 @@ public FakeExoMediaDrm build() { public static final ImmutableList VALID_PROVISION_RESPONSE = TestUtil.createByteList(4, 5, 6); + public static final LoadEventInfo FAKE_LOAD_EVENT_INFO = + new LoadEventInfo( + 1, + new DataSpec.Builder().setUri(Uri.EMPTY).build(), + Uri.EMPTY, + Collections.emptyMap(), + 1000, + 2000, + 8192); + /** Key for use with the Map returned from {@link FakeExoMediaDrm#queryKeyStatus(byte[])}. */ public static final String KEY_STATUS_KEY = "KEY_STATUS"; @@ -547,18 +561,18 @@ public ImmutableList> getReceivedSchemeDat } @Override - public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) + public KeyResponse executeProvisionRequest(UUID uuid, ProvisionRequest request) throws MediaDrmCallbackException { receivedProvisionRequests.add(ImmutableList.copyOf(Bytes.asList(request.getData()))); if (Arrays.equals(request.getData(), FAKE_PROVISION_REQUEST.getData())) { - return Bytes.toArray(VALID_PROVISION_RESPONSE); + return new KeyResponse(Bytes.toArray(VALID_PROVISION_RESPONSE), FAKE_LOAD_EVENT_INFO); } else { - return Util.EMPTY_BYTE_ARRAY; + return new KeyResponse(Util.EMPTY_BYTE_ARRAY, FAKE_LOAD_EVENT_INFO); } } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) + public KeyResponse executeKeyRequest(UUID uuid, KeyRequest request) throws MediaDrmCallbackException { ImmutableList schemeDatas = KeyRequestData.fromByteArray(request.getData()).schemeDatas; @@ -573,7 +587,7 @@ public byte[] executeKeyRequest(UUID uuid, KeyRequest request) } else { response = KEY_DENIED_RESPONSE; } - return Bytes.toArray(response); + return new KeyResponse(Bytes.toArray(response), FAKE_LOAD_EVENT_INFO); } }