summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java607
1 files changed, 607 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java
new file mode 100644
index 0000000000..ad7ed80580
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.NotProvisionedException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */
+@TargetApi(18)
+/* package */ class DefaultDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {
+
+ /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */
+ public static final class UnexpectedDrmSessionException extends IOException {
+
+ public UnexpectedDrmSessionException(Throwable cause) {
+ super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
+ }
+ }
+
+ /** Manages provisioning requests. */
+ public interface ProvisioningManager<T extends ExoMediaCrypto> {
+
+ /**
+ * Called when a session requires provisioning. The manager <em>may</em> call {@link
+ * #provision()} to have this session perform the provisioning operation. The manager
+ * <em>will</em> call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has
+ * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails.
+ *
+ * @param session The session.
+ */
+ void provisionRequired(DefaultDrmSession<T> session);
+
+ /**
+ * Called by a session when it fails to perform a provisioning operation.
+ *
+ * @param error The error that occurred.
+ */
+ void onProvisionError(Exception error);
+
+ /** Called by a session when it successfully completes a provisioning operation. */
+ void onProvisionCompleted();
+ }
+
+ /** Callback to be notified when the session is released. */
+ public interface ReleaseCallback<T extends ExoMediaCrypto> {
+
+ /**
+ * Called immediately after releasing session resources.
+ *
+ * @param session The session.
+ */
+ void onSessionReleased(DefaultDrmSession<T> session);
+ }
+
+ private static final String TAG = "DefaultDrmSession";
+
+ private static final int MSG_PROVISION = 0;
+ private static final int MSG_KEYS = 1;
+ private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60;
+
+ /** The DRM scheme datas, or null if this session uses offline keys. */
+ @Nullable public final List<SchemeData> schemeDatas;
+
+ private final ExoMediaDrm<T> mediaDrm;
+ private final ProvisioningManager<T> provisioningManager;
+ private final ReleaseCallback<T> releaseCallback;
+ private final @DefaultDrmSessionManager.Mode int mode;
+ private final boolean playClearSamplesWithoutKeys;
+ private final boolean isPlaceholderSession;
+ private final HashMap<String, String> keyRequestParameters;
+ private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+
+ /* package */ final MediaDrmCallback callback;
+ /* package */ final UUID uuid;
+ /* package */ final ResponseHandler responseHandler;
+
+ private @DrmSession.State int state;
+ private int referenceCount;
+ @Nullable private HandlerThread requestHandlerThread;
+ @Nullable private RequestHandler requestHandler;
+ @Nullable private T mediaCrypto;
+ @Nullable private DrmSessionException lastException;
+ @Nullable private byte[] sessionId;
+ @MonotonicNonNull private byte[] offlineLicenseKeySetId;
+
+ @Nullable private KeyRequest currentKeyRequest;
+ @Nullable private ProvisionRequest currentProvisionRequest;
+
+ /**
+ * Instantiates a new DRM session.
+ *
+ * @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 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.
+ * @param isPlaceholderSession Whether this session is not expected to acquire any keys.
+ * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using
+ * offline keys.
+ * @param keyRequestParameters Key request parameters.
+ * @param callback The media DRM callback.
+ * @param playbackLooper The playback looper.
+ * @param eventDispatcher The dispatcher for DRM session manager events.
+ * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning
+ * requests.
+ */
+ // the constructor does not initialize fields: sessionId
+ @SuppressWarnings("nullness:initialization.fields.uninitialized")
+ public DefaultDrmSession(
+ UUID uuid,
+ ExoMediaDrm<T> mediaDrm,
+ ProvisioningManager<T> provisioningManager,
+ ReleaseCallback<T> releaseCallback,
+ @Nullable List<SchemeData> schemeDatas,
+ @DefaultDrmSessionManager.Mode int mode,
+ boolean playClearSamplesWithoutKeys,
+ boolean isPlaceholderSession,
+ @Nullable byte[] offlineLicenseKeySetId,
+ HashMap<String, String> keyRequestParameters,
+ MediaDrmCallback callback,
+ Looper playbackLooper,
+ EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ if (mode == DefaultDrmSessionManager.MODE_QUERY
+ || mode == DefaultDrmSessionManager.MODE_RELEASE) {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ }
+ this.uuid = uuid;
+ this.provisioningManager = provisioningManager;
+ this.releaseCallback = releaseCallback;
+ this.mediaDrm = mediaDrm;
+ this.mode = mode;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ this.isPlaceholderSession = isPlaceholderSession;
+ if (offlineLicenseKeySetId != null) {
+ this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ this.schemeDatas = null;
+ } else {
+ this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas));
+ }
+ this.keyRequestParameters = keyRequestParameters;
+ this.callback = callback;
+ this.eventDispatcher = eventDispatcher;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ state = STATE_OPENING;
+ responseHandler = new ResponseHandler(playbackLooper);
+ }
+
+ public boolean hasSessionId(byte[] sessionId) {
+ return Arrays.equals(this.sessionId, sessionId);
+ }
+
+ public void onMediaDrmEvent(int what) {
+ switch (what) {
+ case ExoMediaDrm.EVENT_KEY_REQUIRED:
+ onKeysRequired();
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Provisioning implementation.
+
+ public void provision() {
+ currentProvisionRequest = mediaDrm.getProvisionRequest();
+ Util.castNonNull(requestHandler)
+ .post(
+ MSG_PROVISION,
+ Assertions.checkNotNull(currentProvisionRequest),
+ /* allowRetry= */ true);
+ }
+
+ public void onProvisionCompleted() {
+ if (openInternal(false)) {
+ doLicense(true);
+ }
+ }
+
+ public void onProvisionError(Exception error) {
+ onError(error);
+ }
+
+ // DrmSession implementation.
+
+ @Override
+ @DrmSession.State
+ public final int getState() {
+ return state;
+ }
+
+ @Override
+ public boolean playClearSamplesWithoutKeys() {
+ return playClearSamplesWithoutKeys;
+ }
+
+ @Override
+ public final @Nullable DrmSessionException getError() {
+ return state == STATE_ERROR ? lastException : null;
+ }
+
+ @Override
+ public final @Nullable T getMediaCrypto() {
+ return mediaCrypto;
+ }
+
+ @Override
+ @Nullable
+ public Map<String, String> queryKeyStatus() {
+ return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ @Nullable
+ public byte[] getOfflineLicenseKeySetId() {
+ return offlineLicenseKeySetId;
+ }
+
+ @Override
+ public void acquire() {
+ Assertions.checkState(referenceCount >= 0);
+ if (++referenceCount == 1) {
+ Assertions.checkState(state == STATE_OPENING);
+ requestHandlerThread = new HandlerThread("DrmRequestHandler");
+ requestHandlerThread.start();
+ requestHandler = new RequestHandler(requestHandlerThread.getLooper());
+ if (openInternal(true)) {
+ doLicense(true);
+ }
+ }
+ }
+
+ @Override
+ public void release() {
+ if (--referenceCount == 0) {
+ // Assigning null to various non-null variables for clean-up.
+ state = STATE_RELEASED;
+ Util.castNonNull(responseHandler).removeCallbacksAndMessages(null);
+ Util.castNonNull(requestHandler).removeCallbacksAndMessages(null);
+ requestHandler = null;
+ Util.castNonNull(requestHandlerThread).quit();
+ requestHandlerThread = null;
+ mediaCrypto = null;
+ lastException = null;
+ currentKeyRequest = null;
+ currentProvisionRequest = null;
+ if (sessionId != null) {
+ mediaDrm.closeSession(sessionId);
+ sessionId = null;
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased);
+ }
+ releaseCallback.onSessionReleased(this);
+ }
+ }
+
+ // Internal methods.
+
+ /**
+ * Try to open a session, do provisioning if necessary.
+ *
+ * @param allowProvisioning if provisioning is allowed, set this to false when calling from
+ * processing provision response.
+ * @return true on success, false otherwise.
+ */
+ @EnsuresNonNullIf(result = true, expression = "sessionId")
+ private boolean openInternal(boolean allowProvisioning) {
+ if (isOpen()) {
+ // Already opened
+ return true;
+ }
+
+ try {
+ sessionId = mediaDrm.openSession();
+ mediaCrypto = mediaDrm.createMediaCrypto(sessionId);
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired);
+ state = STATE_OPENED;
+ Assertions.checkNotNull(sessionId);
+ return true;
+ } catch (NotProvisionedException e) {
+ if (allowProvisioning) {
+ provisioningManager.provisionRequired(this);
+ } else {
+ onError(e);
+ }
+ } catch (Exception e) {
+ onError(e);
+ }
+
+ return false;
+ }
+
+ private void onProvisionResponse(Object request, Object response) {
+ if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) {
+ // This event is stale.
+ return;
+ }
+ currentProvisionRequest = null;
+
+ if (response instanceof Exception) {
+ provisioningManager.onProvisionError((Exception) response);
+ return;
+ }
+
+ try {
+ mediaDrm.provideProvisionResponse((byte[]) response);
+ } catch (Exception e) {
+ provisioningManager.onProvisionError(e);
+ return;
+ }
+
+ provisioningManager.onProvisionCompleted();
+ }
+
+ @RequiresNonNull("sessionId")
+ private void doLicense(boolean allowRetry) {
+ if (isPlaceholderSession) {
+ return;
+ }
+ byte[] sessionId = Util.castNonNull(this.sessionId);
+ switch (mode) {
+ case DefaultDrmSessionManager.MODE_PLAYBACK:
+ case DefaultDrmSessionManager.MODE_QUERY:
+ if (offlineLicenseKeySetId == null) {
+ postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry);
+ } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) {
+ long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
+ if (mode == DefaultDrmSessionManager.MODE_PLAYBACK
+ && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) {
+ Log.d(
+ TAG,
+ "Offline license has expired or will expire soon. "
+ + "Remaining seconds: "
+ + licenseDurationRemainingSec);
+ postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);
+ } else if (licenseDurationRemainingSec <= 0) {
+ onError(new KeysExpiredException());
+ } else {
+ state = STATE_OPENED_WITH_KEYS;
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);
+ }
+ }
+ break;
+ case DefaultDrmSessionManager.MODE_DOWNLOAD:
+ if (offlineLicenseKeySetId == null || restoreKeys()) {
+ postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry);
+ }
+ break;
+ case DefaultDrmSessionManager.MODE_RELEASE:
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ Assertions.checkNotNull(this.sessionId);
+ // It's not necessary to restore the key (and open a session to do that) before releasing it
+ // but this serves as a good sanity/fast-failure check.
+ if (restoreKeys()) {
+ postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ @RequiresNonNull({"sessionId", "offlineLicenseKeySetId"})
+ private boolean restoreKeys() {
+ try {
+ mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Error trying to restore keys.", e);
+ onError(e);
+ }
+ return false;
+ }
+
+ private long getLicenseDurationRemainingSec() {
+ if (!C.WIDEVINE_UUID.equals(uuid)) {
+ return Long.MAX_VALUE;
+ }
+ Pair<Long, Long> pair =
+ Assertions.checkNotNull(WidevineUtil.getLicenseDurationRemainingSec(this));
+ return Math.min(pair.first, pair.second);
+ }
+
+ private void postKeyRequest(byte[] scope, int type, boolean allowRetry) {
+ try {
+ currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters);
+ Util.castNonNull(requestHandler)
+ .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry);
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeyResponse(Object request, Object response) {
+ if (request != currentKeyRequest || !isOpen()) {
+ // This event is stale.
+ return;
+ }
+ currentKeyRequest = null;
+
+ if (response instanceof Exception) {
+ onKeysError((Exception) response);
+ return;
+ }
+
+ try {
+ byte[] responseData = (byte[]) response;
+ if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
+ mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData);
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysRestored);
+ } else {
+ byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
+ if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
+ || (mode == DefaultDrmSessionManager.MODE_PLAYBACK
+ && offlineLicenseKeySetId != null))
+ && keySetId != null
+ && keySetId.length != 0) {
+ offlineLicenseKeySetId = keySetId;
+ }
+ state = STATE_OPENED_WITH_KEYS;
+ eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmKeysLoaded);
+ }
+ } catch (Exception e) {
+ onKeysError(e);
+ }
+ }
+
+ private void onKeysRequired() {
+ if (mode == DefaultDrmSessionManager.MODE_PLAYBACK && state == STATE_OPENED_WITH_KEYS) {
+ Util.castNonNull(sessionId);
+ doLicense(/* allowRetry= */ false);
+ }
+ }
+
+ private void onKeysError(Exception e) {
+ if (e instanceof NotProvisionedException) {
+ provisioningManager.provisionRequired(this);
+ } else {
+ onError(e);
+ }
+ }
+
+ private void onError(final Exception e) {
+ lastException = new DrmSessionException(e);
+ eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(e));
+ if (state != STATE_OPENED_WITH_KEYS) {
+ state = STATE_ERROR;
+ }
+ }
+
+ @EnsuresNonNullIf(result = true, expression = "sessionId")
+ @SuppressWarnings("contracts.conditional.postcondition.not.satisfied")
+ private boolean isOpen() {
+ return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
+ }
+
+ // Internal classes.
+
+ @SuppressLint("HandlerLeak")
+ private class ResponseHandler extends Handler {
+
+ public ResponseHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void handleMessage(Message msg) {
+ Pair<Object, Object> requestAndResponse = (Pair<Object, Object>) msg.obj;
+ Object request = requestAndResponse.first;
+ Object response = requestAndResponse.second;
+ switch (msg.what) {
+ case MSG_PROVISION:
+ onProvisionResponse(request, response);
+ break;
+ case MSG_KEYS:
+ onKeyResponse(request, response);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class RequestHandler extends Handler {
+
+ public RequestHandler(Looper backgroundLooper) {
+ super(backgroundLooper);
+ }
+
+ void post(int what, Object request, boolean allowRetry) {
+ RequestTask requestTask =
+ new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request);
+ obtainMessage(what, requestTask).sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ RequestTask requestTask = (RequestTask) msg.obj;
+ Object response;
+ try {
+ switch (msg.what) {
+ case MSG_PROVISION:
+ response =
+ callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request);
+ break;
+ case MSG_KEYS:
+ response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request);
+ break;
+ default:
+ throw new RuntimeException();
+ }
+ } catch (Exception e) {
+ if (maybeRetryRequest(msg, e)) {
+ return;
+ }
+ response = e;
+ }
+ responseHandler
+ .obtainMessage(msg.what, Pair.create(requestTask.request, response))
+ .sendToTarget();
+ }
+
+ private boolean maybeRetryRequest(Message originalMsg, Exception e) {
+ RequestTask requestTask = (RequestTask) originalMsg.obj;
+ if (!requestTask.allowRetry) {
+ return false;
+ }
+ requestTask.errorCount++;
+ if (requestTask.errorCount
+ > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) {
+ return false;
+ }
+ IOException ioException =
+ e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e);
+ long retryDelayMs =
+ loadErrorHandlingPolicy.getRetryDelayMsFor(
+ C.DATA_TYPE_DRM,
+ /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs,
+ ioException,
+ requestTask.errorCount);
+ if (retryDelayMs == C.TIME_UNSET) {
+ // The error is fatal.
+ return false;
+ }
+ sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs);
+ return true;
+ }
+ }
+
+ private static final class RequestTask {
+
+ public final boolean allowRetry;
+ public final long startTimeMs;
+ public final Object request;
+ public int errorCount;
+
+ public RequestTask(boolean allowRetry, long startTimeMs, Object request) {
+ this.allowRetry = allowRetry;
+ this.startTimeMs = startTimeMs;
+ this.request = request;
+ }
+ }
+}