summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java97
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java37
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSession.java607
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java691
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java425
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java144
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java121
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java146
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java74
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java19
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java342
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java59
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java440
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java195
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java22
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java51
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java46
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java266
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java67
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java66
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java19
22 files changed, 3985 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java
new file mode 100644
index 0000000000..770b8511d9
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ClearKeyUtil.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Utility methods for ClearKey.
+ */
+/* package */ final class ClearKeyUtil {
+
+ private static final String TAG = "ClearKeyUtil";
+
+ private ClearKeyUtil() {}
+
+ /**
+ * Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant.
+ *
+ * @param request The request data.
+ * @return The adjusted request data.
+ */
+ public static byte[] adjustRequestData(byte[] request) {
+ if (Util.SDK_INT >= 27) {
+ return request;
+ }
+ // Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 encoding
+ // rather than Base64Url encoding. See [Internal: b/64388098]. We know the exact request format
+ // from the platform's InitDataParser.cpp. Since there aren't any "+" or "/" symbols elsewhere
+ // in the request, it's safe to fix the encoding by replacement through the whole request.
+ String requestString = Util.fromUtf8Bytes(request);
+ return Util.getUtf8Bytes(base64ToBase64Url(requestString));
+ }
+
+ /**
+ * Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM.
+ *
+ * @param response The response data.
+ * @return The adjusted response data.
+ */
+ public static byte[] adjustResponseData(byte[] response) {
+ if (Util.SDK_INT >= 27) {
+ return response;
+ }
+ // Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for
+ // the "k" and "kid" strings. See [Internal: b/64388098]. We know that the ClearKey CDM only
+ // looks at the k, kid and kty parameters in each key, so can ignore the rest of the response.
+ try {
+ JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response));
+ StringBuilder adjustedResponseBuilder = new StringBuilder("{\"keys\":[");
+ JSONArray keysArray = responseJson.getJSONArray("keys");
+ for (int i = 0; i < keysArray.length(); i++) {
+ if (i != 0) {
+ adjustedResponseBuilder.append(",");
+ }
+ JSONObject key = keysArray.getJSONObject(i);
+ adjustedResponseBuilder.append("{\"k\":\"");
+ adjustedResponseBuilder.append(base64UrlToBase64(key.getString("k")));
+ adjustedResponseBuilder.append("\",\"kid\":\"");
+ adjustedResponseBuilder.append(base64UrlToBase64(key.getString("kid")));
+ adjustedResponseBuilder.append("\",\"kty\":\"");
+ adjustedResponseBuilder.append(key.getString("kty"));
+ adjustedResponseBuilder.append("\"}");
+ }
+ adjustedResponseBuilder.append("]}");
+ return Util.getUtf8Bytes(adjustedResponseBuilder.toString());
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e);
+ return response;
+ }
+ }
+
+ private static String base64ToBase64Url(String base64) {
+ return base64.replace('+', '-').replace('/', '_');
+ }
+
+ private static String base64UrlToBase64(String base64Url) {
+ return base64Url.replace('-', '+').replace('_', '/');
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
new file mode 100644
index 0000000000..989e68befd
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * Thrown when a non-platform component fails to decrypt data.
+ */
+public class DecryptionException extends Exception {
+
+ /**
+ * A component specific error code.
+ */
+ public final int errorCode;
+
+ /**
+ * @param errorCode A component specific error code.
+ * @param message The detail message.
+ */
+ public DecryptionException(int errorCode, String message) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+}
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;
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
new file mode 100644
index 0000000000..35bc7faf28
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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 org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+
+/** Listener of {@link DefaultDrmSessionManager} events. */
+public interface DefaultDrmSessionEventListener {
+
+ /** Called each time a drm session is acquired. */
+ default void onDrmSessionAcquired() {}
+
+ /** Called each time keys are loaded. */
+ default void onDrmKeysLoaded() {}
+
+ /**
+ * Called when a drm error occurs.
+ *
+ * <p>This method being called does not indicate that playback has failed, or that it will fail.
+ * The player may be able to recover from the error and continue. Hence applications should
+ * <em>not</em> implement this method to display a user visible error or initiate an application
+ * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement
+ * such behavior). This method is called to provide the application with an opportunity to log the
+ * error if it wishes to do so.
+ *
+ * @param error The corresponding exception.
+ */
+ default void onDrmSessionManagerError(Exception error) {}
+
+ /** Called each time offline keys are restored. */
+ default void onDrmKeysRestored() {}
+
+ /** Called each time offline keys are removed. */
+ default void onDrmKeysRemoved() {}
+
+ /** Called each time a drm session is released. */
+ default void onDrmSessionReleased() {}
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
new file mode 100644
index 0000000000..683862b99a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -0,0 +1,691 @@
+/*
+ * 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.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import androidx.annotation.IntDef;
+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.DrmSession.DrmSessionException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
+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.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.UUID;
+
+/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
+@TargetApi(18)
+public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
+
+ /**
+ * Builder for {@link DefaultDrmSessionManager} instances.
+ *
+ * <p>See {@link #Builder} for the list of default values.
+ */
+ public static final class Builder {
+
+ private final HashMap<String, String> keyRequestParameters;
+ private UUID uuid;
+ private ExoMediaDrm.Provider<ExoMediaCrypto> exoMediaDrmProvider;
+ private boolean multiSession;
+ private int[] useDrmSessionsForClearContentTrackTypes;
+ private boolean playClearSamplesWithoutKeys;
+ private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+
+ /**
+ * Creates a builder with default values. The default values are:
+ *
+ * <ul>
+ * <li>{@link #setKeyRequestParameters keyRequestParameters}: An empty map.
+ * <li>{@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}.
+ * <li>{@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link
+ * FrameworkMediaDrm#DEFAULT_PROVIDER}.
+ * <li>{@link #setMultiSession multiSession}: {@code false}.
+ * <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.
+ * <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}.
+ * <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link
+ * DefaultLoadErrorHandlingPolicy}.
+ * </ul>
+ */
+ @SuppressWarnings("unchecked")
+ public Builder() {
+ keyRequestParameters = new HashMap<>();
+ uuid = C.WIDEVINE_UUID;
+ exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER;
+ loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
+ useDrmSessionsForClearContentTrackTypes = new int[0];
+ }
+
+ /**
+ * Sets the key request parameters to pass as the last argument to {@link
+ * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}.
+ *
+ * <p>Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}.
+ *
+ * @param keyRequestParameters A map with parameters.
+ * @return This builder.
+ */
+ public Builder setKeyRequestParameters(Map<String, String> keyRequestParameters) {
+ this.keyRequestParameters.clear();
+ this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters));
+ return this;
+ }
+
+ /**
+ * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use.
+ *
+ * @param uuid The UUID of the DRM scheme.
+ * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}.
+ * @return This builder.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public Builder setUuidAndExoMediaDrmProvider(
+ UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
+ this.uuid = Assertions.checkNotNull(uuid);
+ this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);
+ return this;
+ }
+
+ /**
+ * Sets whether this session manager is allowed to acquire multiple simultaneous sessions.
+ *
+ * <p>Users should pass false when a single key request will obtain all keys required to decrypt
+ * the associated content. {@code multiSession} is required when content uses key rotation.
+ *
+ * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous
+ * sessions.
+ * @return This builder.
+ */
+ public Builder setMultiSession(boolean multiSession) {
+ this.multiSession = multiSession;
+ return this;
+ }
+
+ /**
+ * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear
+ * sections of the media content.
+ *
+ * <p>Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders
+ * when transitioning between clear and encrypted sections of content.
+ *
+ * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO}
+ * and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of
+ * whether the content is clear or encrypted.
+ * @return This builder.
+ * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains
+ * track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}.
+ */
+ public Builder setUseDrmSessionsForClearContent(
+ int... useDrmSessionsForClearContentTrackTypes) {
+ for (int trackType : useDrmSessionsForClearContentTrackTypes) {
+ Assertions.checkArgument(
+ trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
+ }
+ this.useDrmSessionsForClearContentTrackTypes =
+ useDrmSessionsForClearContentTrackTypes.clone();
+ return this;
+ }
+
+ /**
+ * Sets whether clear samples within protected content should be played when keys for the
+ * encrypted part of the content have yet to be loaded.
+ *
+ * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be
+ * played when keys for the encrypted part of the content have yet to be loaded.
+ * @return This builder.
+ */
+ public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) {
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ return this;
+ }
+
+ /**
+ * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests.
+ *
+ * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
+ * @return This builder.
+ */
+ public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);
+ return this;
+ }
+
+ /** Builds a {@link DefaultDrmSessionManager} instance. */
+ public DefaultDrmSessionManager<ExoMediaCrypto> build(MediaDrmCallback mediaDrmCallback) {
+ return new DefaultDrmSessionManager<>(
+ uuid,
+ exoMediaDrmProvider,
+ mediaDrmCallback,
+ keyRequestParameters,
+ multiSession,
+ useDrmSessionsForClearContentTrackTypes,
+ playClearSamplesWithoutKeys,
+ loadErrorHandlingPolicy);
+ }
+ }
+
+ /**
+ * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does
+ * not contain scheme data for the required UUID.
+ */
+ public static final class MissingSchemeDataException extends Exception {
+
+ private MissingSchemeDataException(UUID uuid) {
+ super("Media does not support uuid: " + uuid);
+ }
+ }
+
+ /**
+ * A key for specifying PlayReady custom data in the key request parameters passed to {@link
+ * Builder#setKeyRequestParameters(Map)}.
+ */
+ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
+
+ /**
+ * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK},
+ * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
+ public @interface Mode {}
+ /**
+ * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
+ * licenses.
+ */
+ public static final int MODE_PLAYBACK = 0;
+ /** Restores an offline license to allow its status to be queried. */
+ public static final int MODE_QUERY = 1;
+ /** Downloads an offline license or renews an existing one. */
+ public static final int MODE_DOWNLOAD = 2;
+ /** Releases an existing offline license. */
+ 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;
+
+ private static final String TAG = "DefaultDrmSessionMgr";
+
+ private final UUID uuid;
+ private final ExoMediaDrm.Provider<T> exoMediaDrmProvider;
+ private final MediaDrmCallback callback;
+ private final HashMap<String, String> keyRequestParameters;
+ private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;
+ private final boolean multiSession;
+ private final int[] useDrmSessionsForClearContentTrackTypes;
+ private final boolean playClearSamplesWithoutKeys;
+ private final ProvisioningManagerImpl provisioningManagerImpl;
+ private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
+
+ private final List<DefaultDrmSession<T>> sessions;
+ private final List<DefaultDrmSession<T>> provisioningSessions;
+
+ private int prepareCallsCount;
+ @Nullable private ExoMediaDrm<T> exoMediaDrm;
+ @Nullable private DefaultDrmSession<T> placeholderDrmSession;
+ @Nullable private DefaultDrmSession<T> noMultiSessionDrmSession;
+ @Nullable private Looper playbackLooper;
+ private int mode;
+ @Nullable private byte[] offlineLicenseKeySetId;
+
+ /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param keyRequestParameters An optional map of parameters to pass as the last argument to
+ * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
+ * @deprecated Use {@link Builder} instead.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> keyRequestParameters) {
+ this(
+ uuid,
+ exoMediaDrm,
+ callback,
+ keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
+ /* multiSession= */ false,
+ INITIAL_DRM_REQUEST_RETRY_COUNT);
+ }
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param keyRequestParameters An optional map of parameters to pass as the last argument to
+ * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
+ * @param multiSession A boolean that specify whether multiple key session support is enabled.
+ * Default is false.
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> keyRequestParameters,
+ boolean multiSession) {
+ this(
+ uuid,
+ exoMediaDrm,
+ callback,
+ keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
+ multiSession,
+ INITIAL_DRM_REQUEST_RETRY_COUNT);
+ }
+
+ /**
+ * @param uuid The UUID of the drm scheme.
+ * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+ * @param callback Performs key and provisioning requests.
+ * @param keyRequestParameters An optional map of parameters to pass as the last argument to
+ * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
+ * @param multiSession A boolean that specify whether multiple key session support is enabled.
+ * Default is false.
+ * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and
+ * key request before reporting error.
+ * @deprecated Use {@link Builder} instead.
+ */
+ @Deprecated
+ public DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm<T> exoMediaDrm,
+ MediaDrmCallback callback,
+ @Nullable HashMap<String, String> keyRequestParameters,
+ boolean multiSession,
+ int initialDrmRequestRetryCount) {
+ this(
+ uuid,
+ new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm),
+ callback,
+ keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
+ multiSession,
+ /* useDrmSessionsForClearContentTrackTypes= */ new int[0],
+ /* playClearSamplesWithoutKeys= */ false,
+ new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount));
+ }
+
+ // the constructor does not initialize fields: offlineLicenseKeySetId
+ @SuppressWarnings("nullness:initialization.fields.uninitialized")
+ private DefaultDrmSessionManager(
+ UUID uuid,
+ ExoMediaDrm.Provider<T> exoMediaDrmProvider,
+ MediaDrmCallback callback,
+ HashMap<String, String> keyRequestParameters,
+ boolean multiSession,
+ int[] useDrmSessionsForClearContentTrackTypes,
+ boolean playClearSamplesWithoutKeys,
+ LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ Assertions.checkNotNull(uuid);
+ Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
+ this.uuid = uuid;
+ this.exoMediaDrmProvider = exoMediaDrmProvider;
+ this.callback = callback;
+ this.keyRequestParameters = keyRequestParameters;
+ this.eventDispatcher = new EventDispatcher<>();
+ this.multiSession = multiSession;
+ this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
+ provisioningManagerImpl = new ProvisioningManagerImpl();
+ mode = MODE_PLAYBACK;
+ sessions = new ArrayList<>();
+ provisioningSessions = new ArrayList<>();
+ }
+
+ /**
+ * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events.
+ *
+ * @param handler A handler to use when delivering events to {@code eventListener}.
+ * @param eventListener A listener of events.
+ */
+ public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
+ eventDispatcher.addListener(handler, eventListener);
+ }
+
+ /**
+ * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners.
+ *
+ * @param eventListener The listener to remove.
+ */
+ public final void removeListener(DefaultDrmSessionEventListener eventListener) {
+ eventDispatcher.removeListener(eventListener);
+ }
+
+ /**
+ * Sets the mode, which determines the role of sessions acquired from the instance. This must be
+ * called before {@link #acquireSession(Looper, DrmInitData)} or {@link
+ * #acquirePlaceholderSession} is called.
+ *
+ * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
+ * required.
+ *
+ * <p>{@code mode} must be one of these:
+ *
+ * <ul>
+ * <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
+ * requested otherwise the offline license is restored.
+ * <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
+ * is restored.
+ * <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
+ * requested otherwise the offline license is renewed.
+ * <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline
+ * license is released.
+ * </ul>
+ *
+ * @param mode The mode to be set.
+ * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
+ */
+ public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
+ Assertions.checkState(sessions.isEmpty());
+ if (mode == MODE_QUERY || mode == MODE_RELEASE) {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ }
+ this.mode = mode;
+ this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+ }
+
+ // DrmSessionManager implementation.
+
+ @Override
+ public final void prepare() {
+ if (prepareCallsCount++ == 0) {
+ Assertions.checkState(exoMediaDrm == null);
+ exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
+ exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
+ }
+ }
+
+ @Override
+ public final void release() {
+ if (--prepareCallsCount == 0) {
+ Assertions.checkNotNull(exoMediaDrm).release();
+ exoMediaDrm = null;
+ }
+ }
+
+ @Override
+ public boolean canAcquireSession(DrmInitData drmInitData) {
+ if (offlineLicenseKeySetId != null) {
+ // An offline license can be restored so a session can always be acquired.
+ return true;
+ }
+ List<SchemeData> schemeDatas = getSchemeDatas(drmInitData, uuid, true);
+ if (schemeDatas.isEmpty()) {
+ if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) {
+ // Assume scheme specific data will be added before the session is opened.
+ Log.w(
+ TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid);
+ } else {
+ // No data for this manager's scheme.
+ return false;
+ }
+ }
+ String schemeType = drmInitData.schemeType;
+ if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
+ // If there is no scheme information, assume patternless AES-CTR.
+ return true;
+ } else if (C.CENC_TYPE_cbc1.equals(schemeType)
+ || C.CENC_TYPE_cbcs.equals(schemeType)
+ || C.CENC_TYPE_cens.equals(schemeType)) {
+ // API support for AES-CBC and pattern encryption was added in API 24. However, the
+ // implementation was not stable until API 25.
+ return Util.SDK_INT >= 25;
+ }
+ // Unknown schemes, assume one of them is supported.
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {
+ assertExpectedPlaybackLooper(playbackLooper);
+ ExoMediaDrm<T> exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
+ boolean avoidPlaceholderDrmSessions =
+ FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
+ && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
+ // Avoid attaching a session to sparse formats.
+ if (avoidPlaceholderDrmSessions
+ || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
+ || exoMediaDrm.getExoMediaCryptoType() == null) {
+ return null;
+ }
+ maybeCreateMediaDrmHandler(playbackLooper);
+ if (placeholderDrmSession == null) {
+ DefaultDrmSession<T> placeholderDrmSession =
+ createNewDefaultSession(
+ /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);
+ sessions.add(placeholderDrmSession);
+ this.placeholderDrmSession = placeholderDrmSession;
+ }
+ placeholderDrmSession.acquire();
+ return placeholderDrmSession;
+ }
+
+ @Override
+ public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
+ assertExpectedPlaybackLooper(playbackLooper);
+ maybeCreateMediaDrmHandler(playbackLooper);
+
+ @Nullable List<SchemeData> schemeDatas = null;
+ if (offlineLicenseKeySetId == null) {
+ schemeDatas = getSchemeDatas(drmInitData, uuid, false);
+ if (schemeDatas.isEmpty()) {
+ final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
+ eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error));
+ return new ErrorStateDrmSession<>(new DrmSessionException(error));
+ }
+ }
+
+ @Nullable DefaultDrmSession<T> session;
+ if (!multiSession) {
+ session = noMultiSessionDrmSession;
+ } else {
+ // Only use an existing session if it has matching init data.
+ session = null;
+ for (DefaultDrmSession<T> existingSession : sessions) {
+ if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {
+ session = existingSession;
+ break;
+ }
+ }
+ }
+
+ if (session == null) {
+ // Create a new session.
+ session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);
+ if (!multiSession) {
+ noMultiSessionDrmSession = session;
+ }
+ sessions.add(session);
+ }
+ session.acquire();
+ return session;
+ }
+
+ @Override
+ @Nullable
+ public Class<T> getExoMediaCryptoType(DrmInitData drmInitData) {
+ return canAcquireSession(drmInitData)
+ ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType()
+ : null;
+ }
+
+ // Internal methods.
+
+ private void assertExpectedPlaybackLooper(Looper playbackLooper) {
+ Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
+ this.playbackLooper = playbackLooper;
+ }
+
+ private void maybeCreateMediaDrmHandler(Looper playbackLooper) {
+ if (mediaDrmHandler == null) {
+ mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+ }
+ }
+
+ private DefaultDrmSession<T> createNewDefaultSession(
+ @Nullable List<SchemeData> schemeDatas, boolean isPlaceholderSession) {
+ 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),
+ eventDispatcher,
+ loadErrorHandlingPolicy);
+ }
+
+ private void onSessionReleased(DefaultDrmSession<T> drmSession) {
+ sessions.remove(drmSession);
+ if (placeholderDrmSession == drmSession) {
+ placeholderDrmSession = 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);
+ }
+
+ /**
+ * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}.
+ *
+ * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
+ * @param uuid The UUID.
+ * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be
+ * returned.
+ * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is
+ * present.
+ */
+ private static List<SchemeData> getSchemeDatas(
+ DrmInitData drmInitData, UUID uuid, boolean allowMissingData) {
+ // Look for matching scheme data (matching the Common PSSH box for ClearKey).
+ List<SchemeData> matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount);
+ for (int i = 0; i < drmInitData.schemeDataCount; i++) {
+ SchemeData schemeData = drmInitData.get(i);
+ boolean uuidMatches =
+ schemeData.matches(uuid)
+ || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID));
+ if (uuidMatches && (schemeData.data != null || allowMissingData)) {
+ matchingSchemeDatas.add(schemeData);
+ }
+ }
+ return matchingSchemeDatas;
+ }
+
+ @SuppressLint("HandlerLeak")
+ private class MediaDrmHandler extends Handler {
+
+ public MediaDrmHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ byte[] sessionId = (byte[]) msg.obj;
+ if (sessionId == null) {
+ // The event is not associated with any particular session.
+ return;
+ }
+ for (DefaultDrmSession<T> session : sessions) {
+ if (session.hasSessionId(sessionId)) {
+ session.onMediaDrmEvent(msg.what);
+ return;
+ }
+ }
+ }
+ }
+
+ private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager<T> {
+ @Override
+ public void provisionRequired(DefaultDrmSession<T> session) {
+ if (provisioningSessions.contains(session)) {
+ // The session has already requested provisioning.
+ return;
+ }
+ provisioningSessions.add(session);
+ if (provisioningSessions.size() == 1) {
+ // This is the first session requesting provisioning, so have it perform the operation.
+ session.provision();
+ }
+ }
+
+ @Override
+ public void onProvisionCompleted() {
+ for (DefaultDrmSession<T> session : provisioningSessions) {
+ session.onProvisionCompleted();
+ }
+ provisioningSessions.clear();
+ }
+
+ @Override
+ public void onProvisionError(Exception error) {
+ for (DefaultDrmSession<T> session : provisioningSessions) {
+ session.onProvisionError(error);
+ }
+ provisioningSessions.clear();
+ }
+ }
+
+ private class MediaDrmEventListener implements OnEventListener<T> {
+
+ @Override
+ public void onEvent(
+ ExoMediaDrm<? extends T> md,
+ @Nullable byte[] sessionId,
+ int event,
+ int extra,
+ @Nullable byte[] data) {
+ Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
new file mode 100644
index 0000000000..2a25d1deb4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -0,0 +1,425 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+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.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Initialization data for one or more DRM schemes.
+ */
+public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
+
+ /**
+ * Merges {@link DrmInitData} obtained from a media manifest and a media stream.
+ *
+ * <p>The result is generated as follows.
+ *
+ * <ol>
+ * <li>Include all {@link SchemeData}s from {@code manifestData} where {@link
+ * SchemeData#hasData()} is true.
+ * <li>Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()}
+ * is true and for which we did not include an entry from the manifest targeting the same
+ * UUID.
+ * <li>If available, the scheme type from the manifest is used. If not, the scheme type from the
+ * media is used.
+ * </ol>
+ *
+ * @param manifestData DRM session acquisition data obtained from the manifest.
+ * @param mediaData DRM session acquisition data obtained from the media.
+ * @return A {@link DrmInitData} obtained from merging a media manifest and a media stream.
+ */
+ public static @Nullable DrmInitData createSessionCreationData(
+ @Nullable DrmInitData manifestData, @Nullable DrmInitData mediaData) {
+ ArrayList<SchemeData> result = new ArrayList<>();
+ String schemeType = null;
+ if (manifestData != null) {
+ schemeType = manifestData.schemeType;
+ for (SchemeData data : manifestData.schemeDatas) {
+ if (data.hasData()) {
+ result.add(data);
+ }
+ }
+ }
+
+ if (mediaData != null) {
+ if (schemeType == null) {
+ schemeType = mediaData.schemeType;
+ }
+ int manifestDatasCount = result.size();
+ for (SchemeData data : mediaData.schemeDatas) {
+ if (data.hasData() && !containsSchemeDataWithUuid(result, manifestDatasCount, data.uuid)) {
+ result.add(data);
+ }
+ }
+ }
+
+ return result.isEmpty() ? null : new DrmInitData(schemeType, result);
+ }
+
+ private final SchemeData[] schemeDatas;
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /** The protection scheme type, or null if not applicable or unknown. */
+ @Nullable public final String schemeType;
+
+ /**
+ * Number of {@link SchemeData}s.
+ */
+ public final int schemeDataCount;
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(List<SchemeData> schemeDatas) {
+ this(null, false, schemeDatas.toArray(new SchemeData[0]));
+ }
+
+ /**
+ * @param schemeType See {@link #schemeType}.
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(@Nullable String schemeType, List<SchemeData> schemeDatas) {
+ this(schemeType, false, schemeDatas.toArray(new SchemeData[0]));
+ }
+
+ /**
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(SchemeData... schemeDatas) {
+ this(null, schemeDatas);
+ }
+
+ /**
+ * @param schemeType See {@link #schemeType}.
+ * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+ */
+ public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) {
+ this(schemeType, true, schemeDatas);
+ }
+
+ private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas,
+ SchemeData... schemeDatas) {
+ this.schemeType = schemeType;
+ if (cloneSchemeDatas) {
+ schemeDatas = schemeDatas.clone();
+ }
+ this.schemeDatas = schemeDatas;
+ schemeDataCount = schemeDatas.length;
+ // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched
+ // last. It's also required by the equals and hashcode implementations.
+ Arrays.sort(this.schemeDatas, this);
+ }
+
+ /* package */
+ DrmInitData(Parcel in) {
+ schemeType = in.readString();
+ schemeDatas = Util.castNonNull(in.createTypedArray(SchemeData.CREATOR));
+ schemeDataCount = schemeDatas.length;
+ }
+
+ /**
+ * Retrieves data for a given DRM scheme, specified by its UUID.
+ *
+ * @deprecated Use {@link #get(int)} and {@link SchemeData#matches(UUID)} instead.
+ * @param uuid The DRM scheme's UUID.
+ * @return The initialization data for the scheme, or null if the scheme is not supported.
+ */
+ @Deprecated
+ @Nullable
+ public SchemeData get(UUID uuid) {
+ for (SchemeData schemeData : schemeDatas) {
+ if (schemeData.matches(uuid)) {
+ return schemeData;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the {@link SchemeData} at a given index.
+ *
+ * @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}.
+ * @return The {@link SchemeData} at the specified index.
+ */
+ public SchemeData get(int index) {
+ return schemeDatas[index];
+ }
+
+ /**
+ * Returns a copy with the specified protection scheme type.
+ *
+ * @param schemeType A protection scheme type. May be null.
+ * @return A copy with the specified protection scheme type.
+ */
+ public DrmInitData copyWithSchemeType(@Nullable String schemeType) {
+ if (Util.areEqual(this.schemeType, schemeType)) {
+ return this;
+ }
+ return new DrmInitData(schemeType, false, schemeDatas);
+ }
+
+ /**
+ * Returns an instance containing the {@link #schemeDatas} from both this and {@code other}. The
+ * {@link #schemeType} of the instances being merged must either match, or at least one scheme
+ * type must be {@code null}.
+ *
+ * @param drmInitData The instance to merge.
+ * @return The merged result.
+ */
+ public DrmInitData merge(DrmInitData drmInitData) {
+ Assertions.checkState(
+ schemeType == null
+ || drmInitData.schemeType == null
+ || TextUtils.equals(schemeType, drmInitData.schemeType));
+ String mergedSchemeType = schemeType != null ? this.schemeType : drmInitData.schemeType;
+ SchemeData[] mergedSchemeDatas =
+ Util.nullSafeArrayConcatenation(schemeDatas, drmInitData.schemeDatas);
+ return new DrmInitData(mergedSchemeType, mergedSchemeDatas);
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = (schemeType == null ? 0 : schemeType.hashCode());
+ result = 31 * result + Arrays.hashCode(schemeDatas);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ DrmInitData other = (DrmInitData) obj;
+ return Util.areEqual(schemeType, other.schemeType)
+ && Arrays.equals(schemeDatas, other.schemeDatas);
+ }
+
+ @Override
+ public int compare(SchemeData first, SchemeData second) {
+ return C.UUID_NIL.equals(first.uuid) ? (C.UUID_NIL.equals(second.uuid) ? 0 : 1)
+ : first.uuid.compareTo(second.uuid);
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(schemeType);
+ dest.writeTypedArray(schemeDatas, 0);
+ }
+
+ public static final Parcelable.Creator<DrmInitData> CREATOR =
+ new Parcelable.Creator<DrmInitData>() {
+
+ @Override
+ public DrmInitData createFromParcel(Parcel in) {
+ return new DrmInitData(in);
+ }
+
+ @Override
+ public DrmInitData[] newArray(int size) {
+ return new DrmInitData[size];
+ }
+
+ };
+
+ // Internal methods.
+
+ private static boolean containsSchemeDataWithUuid(
+ ArrayList<SchemeData> datas, int limit, UUID uuid) {
+ for (int i = 0; i < limit; i++) {
+ if (datas.get(i).uuid.equals(uuid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Scheme initialization data.
+ */
+ public static final class SchemeData implements Parcelable {
+
+ // Lazily initialized hashcode.
+ private int hashCode;
+
+ /**
+ * The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is universal (i.e.
+ * applies to all schemes).
+ */
+ private final UUID uuid;
+ /** The URL of the server to which license requests should be made. May be null if unknown. */
+ @Nullable public final String licenseServerUrl;
+ /** The mimeType of {@link #data}. */
+ public final String mimeType;
+ /** The initialization data. May be null for scheme support checks only. */
+ @Nullable public final byte[] data;
+
+ /**
+ * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+ * universal (i.e. applies to all schemes).
+ * @param mimeType See {@link #mimeType}.
+ * @param data See {@link #data}.
+ */
+ public SchemeData(UUID uuid, String mimeType, @Nullable byte[] data) {
+ this(uuid, /* licenseServerUrl= */ null, mimeType, data);
+ }
+
+ /**
+ * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+ * universal (i.e. applies to all schemes).
+ * @param licenseServerUrl See {@link #licenseServerUrl}.
+ * @param mimeType See {@link #mimeType}.
+ * @param data See {@link #data}.
+ */
+ public SchemeData(
+ UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) {
+ this.uuid = Assertions.checkNotNull(uuid);
+ this.licenseServerUrl = licenseServerUrl;
+ this.mimeType = Assertions.checkNotNull(mimeType);
+ this.data = data;
+ }
+
+ /* package */ SchemeData(Parcel in) {
+ uuid = new UUID(in.readLong(), in.readLong());
+ licenseServerUrl = in.readString();
+ mimeType = Util.castNonNull(in.readString());
+ data = in.createByteArray();
+ }
+
+ /**
+ * Returns whether this initialization data applies to the specified scheme.
+ *
+ * @param schemeUuid The scheme {@link UUID}.
+ * @return Whether this initialization data applies to the specified scheme.
+ */
+ public boolean matches(UUID schemeUuid) {
+ return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid);
+ }
+
+ /**
+ * Returns whether this {@link SchemeData} can be used to replace {@code other}.
+ *
+ * @param other A {@link SchemeData}.
+ * @return Whether this {@link SchemeData} can be used to replace {@code other}.
+ */
+ public boolean canReplace(SchemeData other) {
+ return hasData() && !other.hasData() && matches(other.uuid);
+ }
+
+ /**
+ * Returns whether {@link #data} is non-null.
+ */
+ public boolean hasData() {
+ return data != null;
+ }
+
+ /**
+ * Returns a copy of this instance with the specified data.
+ *
+ * @param data The data to include in the copy.
+ * @return The new instance.
+ */
+ public SchemeData copyWithData(@Nullable byte[] data) {
+ return new SchemeData(uuid, licenseServerUrl, mimeType, data);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof SchemeData)) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ SchemeData other = (SchemeData) obj;
+ return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
+ && Util.areEqual(mimeType, other.mimeType)
+ && Util.areEqual(uuid, other.uuid)
+ && Arrays.equals(data, other.data);
+ }
+
+ @Override
+ public int hashCode() {
+ if (hashCode == 0) {
+ int result = uuid.hashCode();
+ result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
+ result = 31 * result + mimeType.hashCode();
+ result = 31 * result + Arrays.hashCode(data);
+ hashCode = result;
+ }
+ return hashCode;
+ }
+
+ // Parcelable implementation.
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(uuid.getMostSignificantBits());
+ dest.writeLong(uuid.getLeastSignificantBits());
+ dest.writeString(licenseServerUrl);
+ dest.writeString(mimeType);
+ dest.writeByteArray(data);
+ }
+
+ public static final Parcelable.Creator<SchemeData> CREATOR =
+ new Parcelable.Creator<SchemeData>() {
+
+ @Override
+ public SchemeData createFromParcel(Parcel in) {
+ return new SchemeData(in);
+ }
+
+ @Override
+ public SchemeData[] newArray(int size) {
+ return new SchemeData[size];
+ }
+
+ };
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java
new file mode 100644
index 0000000000..7a9af2684f
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java
@@ -0,0 +1,144 @@
+/*
+ * 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.media.MediaDrm;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * A DRM session.
+ */
+public interface DrmSession<T extends ExoMediaCrypto> {
+
+ /**
+ * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link
+ * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession}
+ * and {@code newSession} are the same session.
+ */
+ static <T extends ExoMediaCrypto> void replaceSession(
+ @Nullable DrmSession<T> previousSession, @Nullable DrmSession<T> newSession) {
+ if (previousSession == newSession) {
+ // Do nothing.
+ return;
+ }
+ if (newSession != null) {
+ newSession.acquire();
+ }
+ if (previousSession != null) {
+ previousSession.release();
+ }
+ }
+
+ /** Wraps the throwable which is the cause of the error state. */
+ class DrmSessionException extends IOException {
+
+ public DrmSessionException(Throwable cause) {
+ super(cause);
+ }
+
+ }
+
+ /**
+ * The state of the DRM session. One of {@link #STATE_RELEASED}, {@link #STATE_ERROR}, {@link
+ * #STATE_OPENING}, {@link #STATE_OPENED} or {@link #STATE_OPENED_WITH_KEYS}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
+ @interface State {}
+ /**
+ * The session has been released.
+ */
+ int STATE_RELEASED = 0;
+ /**
+ * The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
+ */
+ int STATE_ERROR = 1;
+ /**
+ * The session is being opened.
+ */
+ int STATE_OPENING = 2;
+ /** The session is open, but does not have keys required for decryption. */
+ int STATE_OPENED = 3;
+ /** The session is open and has keys required for decryption. */
+ int STATE_OPENED_WITH_KEYS = 4;
+
+ /**
+ * Returns the current state of the session, which is one of {@link #STATE_ERROR},
+ * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and
+ * {@link #STATE_OPENED_WITH_KEYS}.
+ */
+ @State int getState();
+
+ /** Returns whether this session allows playback of clear samples prior to keys being loaded. */
+ default boolean playClearSamplesWithoutKeys() {
+ return false;
+ }
+
+ /**
+ * Returns the cause of the error state, or null if {@link #getState()} is not {@link
+ * #STATE_ERROR}.
+ */
+ @Nullable
+ DrmSessionException getError();
+
+ /**
+ * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has
+ * been opened or after it's been released.
+ */
+ @Nullable
+ T getMediaCrypto();
+
+ /**
+ * Returns a map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
+ *
+ * <p>Since DRM license policies vary by vendor, the specific status field names are determined by
+ * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
+ * for a particular DRM engine plugin.
+ *
+ * @return A map describing the key status for the session, or null if called before the session
+ * has been opened or after it's been released.
+ * @see MediaDrm#queryKeyStatus(byte[])
+ */
+ @Nullable
+ Map<String, String> queryKeyStatus();
+
+ /**
+ * Returns the key set id of the offline license loaded into this session, or null if there isn't
+ * one.
+ */
+ @Nullable
+ byte[] getOfflineLicenseKeySetId();
+
+ /**
+ * Increments the reference count. When the caller no longer needs to use the instance, it must
+ * call {@link #release()} to decrement the reference count.
+ */
+ void acquire();
+
+ /**
+ * Decrements the reference count. If the reference count drops to 0 underlying resources are
+ * released, and the instance cannot be re-used.
+ */
+ void release();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
new file mode 100644
index 0000000000..bf98a0a658
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -0,0 +1,121 @@
+/*
+ * 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.os.Looper;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+
+/**
+ * Manages a DRM session.
+ */
+public interface DrmSessionManager<T extends ExoMediaCrypto> {
+
+ /** Returns {@link #DUMMY}. */
+ @SuppressWarnings("unchecked")
+ static <T extends ExoMediaCrypto> DrmSessionManager<T> getDummyDrmSessionManager() {
+ return (DrmSessionManager<T>) DUMMY;
+ }
+
+ /** {@link DrmSessionManager} that supports no DRM schemes. */
+ DrmSessionManager<ExoMediaCrypto> DUMMY =
+ new DrmSessionManager<ExoMediaCrypto>() {
+
+ @Override
+ public boolean canAcquireSession(DrmInitData drmInitData) {
+ return false;
+ }
+
+ @Override
+ public DrmSession<ExoMediaCrypto> acquireSession(
+ Looper playbackLooper, DrmInitData drmInitData) {
+ return new ErrorStateDrmSession<>(
+ new DrmSession.DrmSessionException(
+ new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)));
+ }
+
+ @Override
+ @Nullable
+ public Class<ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData) {
+ return null;
+ }
+ };
+
+ /**
+ * Acquires any required resources.
+ *
+ * <p>{@link #release()} must be called to ensure the acquired resources are released. After
+ * releasing, an instance may be re-prepared.
+ */
+ default void prepare() {
+ // Do nothing.
+ }
+
+ /** Releases any acquired resources. */
+ default void release() {
+ // Do nothing.
+ }
+
+ /**
+ * Returns whether the manager is capable of acquiring a session for the given
+ * {@link DrmInitData}.
+ *
+ * @param drmInitData DRM initialization data.
+ * @return Whether the manager is capable of acquiring a session for the given
+ * {@link DrmInitData}.
+ */
+ boolean canAcquireSession(DrmInitData drmInitData);
+
+ /**
+ * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference
+ * count. When the caller no longer needs to use the instance, it must call {@link
+ * DrmSession#release()} to decrement the reference count.
+ *
+ * <p>Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for
+ * playback of clear content periods. This can reduce the cost of transitioning between clear and
+ * encrypted content periods.
+ *
+ * @param playbackLooper The looper associated with the media playback thread.
+ * @param trackType The type of the track to acquire a placeholder session for. Must be one of the
+ * {@link C}{@code .TRACK_TYPE_*} constants.
+ * @return The placeholder DRM session, or null if this DRM session manager does not support
+ * placeholder sessions.
+ */
+ @Nullable
+ default DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {
+ return null;
+ }
+
+ /**
+ * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented
+ * reference count. When the caller no longer needs to use the instance, it must call {@link
+ * DrmSession#release()} to decrement the reference count.
+ *
+ * @param playbackLooper The looper associated with the media playback thread.
+ * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain
+ * non-null {@link SchemeData#data}.
+ * @return The DRM session.
+ */
+ DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData);
+
+ /**
+ * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link
+ * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}.
+ */
+ @Nullable
+ Class<? extends ExoMediaCrypto> getExoMediaCryptoType(DrmInitData drmInitData);
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java
new file mode 100644
index 0000000000..b6a66ceac0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 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.media.MediaDrmException;
+import android.os.PersistableBundle;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** An {@link ExoMediaDrm} that does not support any protection schemes. */
+@RequiresApi(18)
+public final class DummyExoMediaDrm<T extends ExoMediaCrypto> implements ExoMediaDrm<T> {
+
+ /** Returns a new instance. */
+ @SuppressWarnings("unchecked")
+ public static <T extends ExoMediaCrypto> DummyExoMediaDrm<T> getInstance() {
+ return (DummyExoMediaDrm<T>) new DummyExoMediaDrm<>();
+ }
+
+ @Override
+ public void setOnEventListener(OnEventListener<? super T> listener) {
+ // Do nothing.
+ }
+
+ @Override
+ public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> listener) {
+ // Do nothing.
+ }
+
+ @Override
+ public byte[] openSession() throws MediaDrmException {
+ throw new MediaDrmException("Attempting to open a session using a dummy ExoMediaDrm.");
+ }
+
+ @Override
+ public void closeSession(byte[] sessionId) {
+ // Do nothing.
+ }
+
+ @Override
+ public KeyRequest getKeyRequest(
+ byte[] scope,
+ @Nullable List<DrmInitData.SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> optionalParameters) {
+ // Should not be invoked. No session should exist.
+ throw new IllegalStateException();
+ }
+
+ @Nullable
+ @Override
+ public byte[] provideKeyResponse(byte[] scope, byte[] response) {
+ // Should not be invoked. No session should exist.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public ProvisionRequest getProvisionRequest() {
+ // Should not be invoked. No provision should be required.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void provideProvisionResponse(byte[] response) {
+ // Should not be invoked. No provision should be required.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus(byte[] sessionId) {
+ // Should not be invoked. No session should exist.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public void acquire() {
+ // Do nothing.
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+
+ @Override
+ public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+ // Should not be invoked. No session should exist.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ @Nullable
+ public PersistableBundle getMetrics() {
+ return null;
+ }
+
+ @Override
+ public String getPropertyString(String propertyName) {
+ return "";
+ }
+
+ @Override
+ public byte[] getPropertyByteArray(String propertyName) {
+ return Util.EMPTY_BYTE_ARRAY;
+ }
+
+ @Override
+ public void setPropertyString(String propertyName, String value) {
+ // Do nothing.
+ }
+
+ @Override
+ public void setPropertyByteArray(String propertyName, byte[] value) {
+ // Do nothing.
+ }
+
+ @Override
+ public T createMediaCrypto(byte[] sessionId) {
+ // Should not be invoked. No session should exist.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ @Nullable
+ public Class<T> getExoMediaCryptoType() {
+ // No ExoMediaCrypto type is supported.
+ return null;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
new file mode 100644
index 0000000000..97d0ecaaa4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 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 androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Map;
+
+/** A {@link DrmSession} that's in a terminal error state. */
+public final class ErrorStateDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {
+
+ private final DrmSessionException error;
+
+ public ErrorStateDrmSession(DrmSessionException error) {
+ this.error = Assertions.checkNotNull(error);
+ }
+
+ @Override
+ public int getState() {
+ return STATE_ERROR;
+ }
+
+ @Override
+ public boolean playClearSamplesWithoutKeys() {
+ return false;
+ }
+
+ @Override
+ @Nullable
+ public DrmSessionException getError() {
+ return error;
+ }
+
+ @Override
+ @Nullable
+ public T getMediaCrypto() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Map<String, String> queryKeyStatus() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public byte[] getOfflineLicenseKeySetId() {
+ return null;
+ }
+
+ @Override
+ public void acquire() {
+ // Do nothing.
+ }
+
+ @Override
+ public void release() {
+ // Do nothing.
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
new file mode 100644
index 0000000000..a12b212799
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+/** An opaque {@link android.media.MediaCrypto} equivalent. */
+public interface ExoMediaCrypto {}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java
new file mode 100644
index 0000000000..1e851a7c0b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -0,0 +1,342 @@
+/*
+ * 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.media.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}.
+ *
+ * <h3>Reference counting</h3>
+ *
+ * <p>Access to an instance is managed by reference counting, where {@link #acquire()} increments
+ * the reference count and {@link #release()} decrements it. When the reference count drops to 0
+ * underlying resources are released, and the instance cannot be re-used.
+ *
+ * <p>Each new instance has an initial reference count of 1. Hence application code that creates a
+ * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()}
+ * when the instance is no longer required.
+ */
+public interface ExoMediaDrm<T extends ExoMediaCrypto> {
+
+ /** {@link ExoMediaDrm} instances provider. */
+ interface Provider<T extends ExoMediaCrypto> {
+
+ /**
+ * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller
+ * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement
+ * the reference count.
+ */
+ ExoMediaDrm<T> acquireExoMediaDrm(UUID uuid);
+ }
+
+ /**
+ * Provides an {@link ExoMediaDrm} instance owned by the app.
+ *
+ * <p>Note that when using this provider the app will have instantiated the {@link ExoMediaDrm}
+ * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance
+ * when it's no longer being used.
+ */
+ final class AppManagedProvider<T extends ExoMediaCrypto> implements Provider<T> {
+
+ private final ExoMediaDrm<T> exoMediaDrm;
+
+ /** Creates an instance that provides the given {@link ExoMediaDrm}. */
+ public AppManagedProvider(ExoMediaDrm<T> exoMediaDrm) {
+ this.exoMediaDrm = exoMediaDrm;
+ }
+
+ @Override
+ public ExoMediaDrm<T> acquireExoMediaDrm(UUID uuid) {
+ exoMediaDrm.acquire();
+ return exoMediaDrm;
+ }
+ }
+
+ /** @see MediaDrm#EVENT_KEY_REQUIRED */
+ @SuppressWarnings("InlinedApi")
+ int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED;
+ /**
+ * @see MediaDrm#EVENT_KEY_EXPIRED
+ */
+ @SuppressWarnings("InlinedApi")
+ int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED;
+ /**
+ * @see MediaDrm#EVENT_PROVISION_REQUIRED
+ */
+ @SuppressWarnings("InlinedApi")
+ int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED;
+
+ /**
+ * @see MediaDrm#KEY_TYPE_STREAMING
+ */
+ @SuppressWarnings("InlinedApi")
+ int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING;
+ /**
+ * @see MediaDrm#KEY_TYPE_OFFLINE
+ */
+ @SuppressWarnings("InlinedApi")
+ int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE;
+ /**
+ * @see MediaDrm#KEY_TYPE_RELEASE
+ */
+ @SuppressWarnings("InlinedApi")
+ int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE;
+
+ /**
+ * @see android.media.MediaDrm.OnEventListener
+ */
+ interface OnEventListener<T extends ExoMediaCrypto> {
+ /**
+ * Called when an event occurs that requires the app to be notified
+ *
+ * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.
+ * @param sessionId The DRM session ID on which the event occurred.
+ * @param event Indicates the event type.
+ * @param extra A secondary error code.
+ * @param data Optional byte array of data that may be associated with the event.
+ */
+ void onEvent(
+ ExoMediaDrm<? extends T> mediaDrm,
+ @Nullable byte[] sessionId,
+ int event,
+ int extra,
+ @Nullable byte[] data);
+ }
+
+ /**
+ * @see android.media.MediaDrm.OnKeyStatusChangeListener
+ */
+ interface OnKeyStatusChangeListener<T extends ExoMediaCrypto> {
+ /**
+ * Called when the keys in a session change status, such as when the license is renewed or
+ * expires.
+ *
+ * @param mediaDrm The {@link ExoMediaDrm} object on which the event occurred.
+ * @param sessionId The DRM session ID on which the event occurred.
+ * @param exoKeyInformation A list of {@link KeyStatus} that contains key ID and status.
+ * @param hasNewUsableKey Whether a new key became usable.
+ */
+ void onKeyStatusChange(
+ ExoMediaDrm<? extends T> mediaDrm,
+ byte[] sessionId,
+ List<KeyStatus> exoKeyInformation,
+ boolean hasNewUsableKey);
+ }
+
+ /** @see android.media.MediaDrm.KeyStatus */
+ final class KeyStatus {
+
+ private final int statusCode;
+ private final byte[] keyId;
+
+ public KeyStatus(int statusCode, byte[] keyId) {
+ this.statusCode = statusCode;
+ this.keyId = keyId;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public byte[] getKeyId() {
+ return keyId;
+ }
+
+ }
+
+ /** @see android.media.MediaDrm.KeyRequest */
+ final class KeyRequest {
+
+ private final byte[] data;
+ private final String licenseServerUrl;
+
+ public KeyRequest(byte[] data, String licenseServerUrl) {
+ this.data = data;
+ this.licenseServerUrl = licenseServerUrl;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public String getLicenseServerUrl() {
+ return licenseServerUrl;
+ }
+
+ }
+
+ /** @see android.media.MediaDrm.ProvisionRequest */
+ final class ProvisionRequest {
+
+ private final byte[] data;
+ private final String defaultUrl;
+
+ public ProvisionRequest(byte[] data, String defaultUrl) {
+ this.data = data;
+ this.defaultUrl = defaultUrl;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public String getDefaultUrl() {
+ return defaultUrl;
+ }
+
+ }
+
+ /**
+ * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener)
+ */
+ void setOnEventListener(OnEventListener<? super T> listener);
+
+ /**
+ * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler)
+ */
+ void setOnKeyStatusChangeListener(OnKeyStatusChangeListener<? super T> listener);
+
+ /**
+ * @see MediaDrm#openSession()
+ */
+ byte[] openSession() throws MediaDrmException;
+
+ /**
+ * @see MediaDrm#closeSession(byte[])
+ */
+ void closeSession(byte[] sessionId);
+
+ /**
+ * Generates a key request.
+ *
+ * @param scope If {@code keyType} is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE},
+ * the session id that the keys will be provided to. If {@code keyType} is {@link
+ * #KEY_TYPE_RELEASE}, the keySetId of the keys to release.
+ * @param schemeDatas If key type is {@link #KEY_TYPE_STREAMING} or {@link #KEY_TYPE_OFFLINE}, a
+ * list of {@link SchemeData} instances extracted from the media. Null otherwise.
+ * @param keyType The type of the request. Either {@link #KEY_TYPE_STREAMING} to acquire keys for
+ * streaming, {@link #KEY_TYPE_OFFLINE} to acquire keys for offline usage, or {@link
+ * #KEY_TYPE_RELEASE} to release acquired keys. Releasing keys invalidates them for all
+ * sessions.
+ * @param optionalParameters Are included in the key request message to allow a client application
+ * to provide additional message parameters to the server. This may be {@code null} if no
+ * additional parameters are to be sent.
+ * @return The generated key request.
+ * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)
+ */
+ KeyRequest getKeyRequest(
+ byte[] scope,
+ @Nullable List<SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> optionalParameters)
+ throws NotProvisionedException;
+
+ /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */
+ @Nullable
+ byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException;
+
+ /**
+ * @see MediaDrm#getProvisionRequest()
+ */
+ ProvisionRequest getProvisionRequest();
+
+ /**
+ * @see MediaDrm#provideProvisionResponse(byte[])
+ */
+ void provideProvisionResponse(byte[] response) throws DeniedByServerException;
+
+ /**
+ * @see MediaDrm#queryKeyStatus(byte[])
+ */
+ Map<String, String> queryKeyStatus(byte[] sessionId);
+
+ /**
+ * Increments the reference count. When the caller no longer needs to use the instance, it must
+ * call {@link #release()} to decrement the reference count.
+ *
+ * <p>A new instance will have an initial reference count of 1, and therefore it is not normally
+ * necessary for application code to call this method.
+ */
+ void acquire();
+
+ /**
+ * Decrements the reference count. If the reference count drops to 0 underlying resources are
+ * released, and the instance cannot be re-used.
+ */
+ void release();
+
+ /**
+ * @see MediaDrm#restoreKeys(byte[], byte[])
+ */
+ void restoreKeys(byte[] sessionId, byte[] keySetId);
+
+ /**
+ * Returns drm metrics. May be null if unavailable.
+ *
+ * @see MediaDrm#getMetrics()
+ */
+ @Nullable
+ PersistableBundle getMetrics();
+
+ /**
+ * @see MediaDrm#getPropertyString(String)
+ */
+ String getPropertyString(String propertyName);
+
+ /**
+ * @see MediaDrm#getPropertyByteArray(String)
+ */
+ byte[] getPropertyByteArray(String propertyName);
+
+ /**
+ * @see MediaDrm#setPropertyString(String, String)
+ */
+ void setPropertyString(String propertyName, String value);
+
+ /**
+ * @see MediaDrm#setPropertyByteArray(String, byte[])
+ */
+ void setPropertyByteArray(String propertyName, byte[] value);
+
+ /**
+ * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[])
+ * @param sessionId The DRM session ID.
+ * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
+ * @throws MediaCryptoException If the instance can't be created.
+ */
+ T createMediaCrypto(byte[] sessionId) throws MediaCryptoException;
+
+ /**
+ * Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}, or null
+ * if this instance cannot create any {@link ExoMediaCrypto} instances.
+ */
+ @Nullable
+ Class<T> getExoMediaCryptoType();
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
new file mode 100644
index 0000000000..bb3a9b272b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
@@ -0,0 +1,59 @@
+/*
+ * 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.media.MediaCrypto;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.UUID;
+
+/**
+ * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or
+ * update a framework {@link MediaCrypto}.
+ */
+public final class FrameworkMediaCrypto implements ExoMediaCrypto {
+
+ /**
+ * Whether the device needs keys to have been loaded into the {@link DrmSession} before codec
+ * configuration.
+ */
+ public static final boolean WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC =
+ "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
+ || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
+
+ /** The DRM scheme UUID. */
+ public final UUID uuid;
+ /** The DRM session id. */
+ public final byte[] sessionId;
+ /**
+ * Whether to allow use of insecure decoder components even if the underlying platform says
+ * otherwise.
+ */
+ public final boolean forceAllowInsecureDecoderComponents;
+
+ /**
+ * @param uuid The DRM scheme UUID.
+ * @param sessionId The DRM session id.
+ * @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components
+ * even if the underlying platform says otherwise.
+ */
+ public FrameworkMediaCrypto(
+ UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) {
+ this.uuid = uuid;
+ this.sessionId = sessionId;
+ this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
new file mode 100644
index 0000000000..10ca857448
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -0,0 +1,440 @@
+/*
+ * 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.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.PersistableBundle;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+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.extractor.mp4.PsshAtomUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */
+@TargetApi(23)
+@RequiresApi(18)
+public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
+
+ private static final String TAG = "FrameworkMediaDrm";
+
+ /**
+ * {@link ExoMediaDrm.Provider} that returns a new {@link FrameworkMediaDrm} for the requested
+ * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID
+ * is not supported by the device.
+ */
+ public static final Provider<FrameworkMediaCrypto> DEFAULT_PROVIDER =
+ uuid -> {
+ try {
+ return newInstance(uuid);
+ } catch (UnsupportedDrmException e) {
+ Log.e(TAG, "Failed to instantiate a FrameworkMediaDrm for uuid: " + uuid + ".");
+ return new DummyExoMediaDrm<>();
+ }
+ };
+
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+ private static final String MOCK_LA_URL_VALUE = "https://x";
+ private static final String MOCK_LA_URL = "<LA_URL>" + MOCK_LA_URL_VALUE + "</LA_URL>";
+ private static final int UTF_16_BYTES_PER_CHARACTER = 2;
+
+ private final UUID uuid;
+ private final MediaDrm mediaDrm;
+ private int referenceCount;
+
+ /**
+ * Creates an instance with an initial reference count of 1. {@link #release()} must be called on
+ * the instance when it's no longer required.
+ *
+ * @param uuid The scheme uuid.
+ * @return The created instance.
+ * @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.
+ */
+ public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {
+ try {
+ return new FrameworkMediaDrm(uuid);
+ } catch (UnsupportedSchemeException e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
+ } catch (Exception e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
+ }
+ }
+
+ private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
+ Assertions.checkNotNull(uuid);
+ Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
+ this.uuid = uuid;
+ this.mediaDrm = new MediaDrm(adjustUuid(uuid));
+ // Creators of an instance automatically acquire ownership of the created instance.
+ referenceCount = 1;
+ if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {
+ forceWidevineL3(mediaDrm);
+ }
+ }
+
+ @Override
+ public void setOnEventListener(
+ final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
+ mediaDrm.setOnEventListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, event, extra, data) ->
+ listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data));
+ }
+
+ @Override
+ public void setOnKeyStatusChangeListener(
+ final ExoMediaDrm.OnKeyStatusChangeListener<? super FrameworkMediaCrypto> listener) {
+ if (Util.SDK_INT < 23) {
+ throw new UnsupportedOperationException();
+ }
+
+ mediaDrm.setOnKeyStatusChangeListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> {
+ List<KeyStatus> exoKeyInfo = new ArrayList<>();
+ for (MediaDrm.KeyStatus keyStatus : keyInfo) {
+ exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId()));
+ }
+ listener.onKeyStatusChange(
+ FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey);
+ },
+ null);
+ }
+
+ @Override
+ public byte[] openSession() throws MediaDrmException {
+ return mediaDrm.openSession();
+ }
+
+ @Override
+ public void closeSession(byte[] sessionId) {
+ mediaDrm.closeSession(sessionId);
+ }
+
+ @Override
+ public KeyRequest getKeyRequest(
+ byte[] scope,
+ @Nullable List<DrmInitData.SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> optionalParameters)
+ throws NotProvisionedException {
+ SchemeData schemeData = null;
+ byte[] initData = null;
+ String mimeType = null;
+ if (schemeDatas != null) {
+ schemeData = getSchemeData(uuid, schemeDatas);
+ initData = adjustRequestInitData(uuid, Assertions.checkNotNull(schemeData.data));
+ mimeType = adjustRequestMimeType(uuid, schemeData.mimeType);
+ }
+ MediaDrm.KeyRequest request =
+ mediaDrm.getKeyRequest(scope, initData, mimeType, keyType, optionalParameters);
+
+ byte[] requestData = adjustRequestData(uuid, request.getData());
+
+ String licenseServerUrl = request.getDefaultUrl();
+ if (MOCK_LA_URL_VALUE.equals(licenseServerUrl)) {
+ licenseServerUrl = "";
+ }
+ if (TextUtils.isEmpty(licenseServerUrl)
+ && schemeData != null
+ && !TextUtils.isEmpty(schemeData.licenseServerUrl)) {
+ licenseServerUrl = schemeData.licenseServerUrl;
+ }
+
+ return new KeyRequest(requestData, licenseServerUrl);
+ }
+
+ @Nullable
+ @Override
+ public byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException {
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ response = ClearKeyUtil.adjustResponseData(response);
+ }
+
+ return mediaDrm.provideKeyResponse(scope, response);
+ }
+
+ @Override
+ public ProvisionRequest getProvisionRequest() {
+ final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest();
+ return new ProvisionRequest(request.getData(), request.getDefaultUrl());
+ }
+
+ @Override
+ public void provideProvisionResponse(byte[] response) throws DeniedByServerException {
+ mediaDrm.provideProvisionResponse(response);
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus(byte[] sessionId) {
+ return mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public synchronized void acquire() {
+ Assertions.checkState(referenceCount > 0);
+ referenceCount++;
+ }
+
+ @Override
+ public synchronized void release() {
+ if (--referenceCount == 0) {
+ mediaDrm.release();
+ }
+ }
+
+ @Override
+ public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+ mediaDrm.restoreKeys(sessionId, keySetId);
+ }
+
+ @Override
+ @Nullable
+ @TargetApi(28)
+ public PersistableBundle getMetrics() {
+ if (Util.SDK_INT < 28) {
+ return null;
+ }
+ return mediaDrm.getMetrics();
+ }
+
+ @Override
+ public String getPropertyString(String propertyName) {
+ return mediaDrm.getPropertyString(propertyName);
+ }
+
+ @Override
+ public byte[] getPropertyByteArray(String propertyName) {
+ return mediaDrm.getPropertyByteArray(propertyName);
+ }
+
+ @Override
+ public void setPropertyString(String propertyName, String value) {
+ mediaDrm.setPropertyString(propertyName, value);
+ }
+
+ @Override
+ public void setPropertyByteArray(String propertyName, byte[] value) {
+ mediaDrm.setPropertyByteArray(propertyName, value);
+ }
+
+ @Override
+ public FrameworkMediaCrypto createMediaCrypto(byte[] initData) throws MediaCryptoException {
+ // Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still
+ // indicate that it required secure video decoders [Internal ref: b/11428937].
+ boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
+ && C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
+ return new FrameworkMediaCrypto(
+ adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
+ }
+
+ @Override
+ public Class<FrameworkMediaCrypto> getExoMediaCryptoType() {
+ return FrameworkMediaCrypto.class;
+ }
+
+ private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
+ if (!C.WIDEVINE_UUID.equals(uuid)) {
+ // For non-Widevine CDMs always use the first scheme data.
+ return schemeDatas.get(0);
+ }
+
+ if (Util.SDK_INT >= 28 && schemeDatas.size() > 1) {
+ // For API level 28 and above, concatenate multiple PSSH scheme datas if possible.
+ SchemeData firstSchemeData = schemeDatas.get(0);
+ int concatenatedDataLength = 0;
+ boolean canConcatenateData = true;
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ byte[] schemeDataData = Util.castNonNull(schemeData.data);
+ if (Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType)
+ && Util.areEqual(schemeData.licenseServerUrl, firstSchemeData.licenseServerUrl)
+ && PsshAtomUtil.isPsshAtom(schemeDataData)) {
+ concatenatedDataLength += schemeDataData.length;
+ } else {
+ canConcatenateData = false;
+ break;
+ }
+ }
+ if (canConcatenateData) {
+ byte[] concatenatedData = new byte[concatenatedDataLength];
+ int concatenatedDataPosition = 0;
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ byte[] schemeDataData = Util.castNonNull(schemeData.data);
+ int schemeDataLength = schemeDataData.length;
+ System.arraycopy(
+ schemeDataData, 0, concatenatedData, concatenatedDataPosition, schemeDataLength);
+ concatenatedDataPosition += schemeDataLength;
+ }
+ return firstSchemeData.copyWithData(concatenatedData);
+ }
+ }
+
+ // For API levels 23 - 27, prefer the first V1 PSSH box. For API levels 22 and earlier, prefer
+ // the first V0 box.
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ int version = PsshAtomUtil.parseVersion(Util.castNonNull(schemeData.data));
+ if (Util.SDK_INT < 23 && version == 0) {
+ return schemeData;
+ } else if (Util.SDK_INT >= 23 && version == 1) {
+ return schemeData;
+ }
+ }
+
+ // If all else fails, use the first scheme data.
+ return schemeDatas.get(0);
+ }
+
+ private static UUID adjustUuid(UUID uuid) {
+ // ClearKey had to be accessed using the Common PSSH UUID prior to API level 27.
+ return Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;
+ }
+
+ private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) {
+ // TODO: Add API level check once [Internal ref: b/112142048] is fixed.
+ if (C.PLAYREADY_UUID.equals(uuid)) {
+ byte[] schemeSpecificData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);
+ if (schemeSpecificData == null) {
+ // The init data is not contained in a pssh box.
+ schemeSpecificData = initData;
+ }
+ initData =
+ PsshAtomUtil.buildPsshAtom(
+ C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData));
+ }
+
+ // Prior to API level 21, the Widevine CDM required scheme specific data to be extracted from
+ // the PSSH atom. We also extract the data on API levels 21 and 22 because these API levels
+ // don't handle V1 PSSH atoms, but do handle scheme specific data regardless of whether it's
+ // extracted from a V0 or a V1 PSSH atom. Hence extracting the data allows us to support content
+ // that only provides V1 PSSH atoms. API levels 23 and above understand V0 and V1 PSSH atoms,
+ // and so we do not extract the data.
+ // Some Amazon devices also require data to be extracted from the PSSH atom for PlayReady.
+ if ((Util.SDK_INT < 23 && C.WIDEVINE_UUID.equals(uuid))
+ || (C.PLAYREADY_UUID.equals(uuid)
+ && "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
+ || "AFTS".equals(Util.MODEL) // Fire TV Gen 2
+ || "AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
+ || "AFTT".equals(Util.MODEL)))) { // Fire TV Stick Gen 2
+ byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);
+ if (psshData != null) {
+ // Extraction succeeded, so return the extracted data.
+ return psshData;
+ }
+ }
+ return initData;
+ }
+
+ private static String adjustRequestMimeType(UUID uuid, String mimeType) {
+ // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
+ if (Util.SDK_INT < 26
+ && C.CLEARKEY_UUID.equals(uuid)
+ && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {
+ return CENC_SCHEME_MIME_TYPE;
+ }
+ return mimeType;
+ }
+
+ private static byte[] adjustRequestData(UUID uuid, byte[] requestData) {
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ return ClearKeyUtil.adjustRequestData(requestData);
+ }
+ return requestData;
+ }
+
+ @SuppressLint("WrongConstant") // Suppress spurious lint error [Internal ref: b/32137960]
+ private static void forceWidevineL3(MediaDrm mediaDrm) {
+ mediaDrm.setPropertyString("securityLevel", "L3");
+ }
+
+ /**
+ * Returns whether the device codec is known to fail if security level L1 is used.
+ *
+ * <p>See <a href="https://github.com/google/ExoPlayer/issues/4413">GitHub issue #4413</a>.
+ */
+ private static boolean needsForceWidevineL3Workaround() {
+ return "ASUS_Z00AD".equals(Util.MODEL);
+ }
+
+ /**
+ * If the LA_URL tag is missing, injects a mock LA_URL value to avoid causing the CDM to throw
+ * when creating the key request. The LA_URL attribute is optional but some Android PlayReady
+ * implementations are known to require it. Does nothing it the provided {@code data} already
+ * contains an LA_URL value.
+ */
+ private static byte[] addLaUrlAttributeIfMissing(byte[] data) {
+ ParsableByteArray byteArray = new ParsableByteArray(data);
+ // See https://docs.microsoft.com/en-us/playready/specifications/specifications for more
+ // information about the init data format.
+ int length = byteArray.readLittleEndianInt();
+ int objectRecordCount = byteArray.readLittleEndianShort();
+ int recordType = byteArray.readLittleEndianShort();
+ if (objectRecordCount != 1 || recordType != 1) {
+ Log.i(TAG, "Unexpected record count or type. Skipping LA_URL workaround.");
+ return data;
+ }
+ int recordLength = byteArray.readLittleEndianShort();
+ String xml = byteArray.readString(recordLength, Charset.forName(C.UTF16LE_NAME));
+ if (xml.contains("<LA_URL>")) {
+ // LA_URL already present. Do nothing.
+ return data;
+ }
+ // This PlayReady object record does not include an LA_URL. We add a mock value for it.
+ int endOfDataTagIndex = xml.indexOf("</DATA>");
+ if (endOfDataTagIndex == -1) {
+ Log.w(TAG, "Could not find the </DATA> tag. Skipping LA_URL workaround.");
+ }
+ String xmlWithMockLaUrl =
+ xml.substring(/* beginIndex= */ 0, /* endIndex= */ endOfDataTagIndex)
+ + MOCK_LA_URL
+ + xml.substring(/* beginIndex= */ endOfDataTagIndex);
+ int extraBytes = MOCK_LA_URL.length() * UTF_16_BYTES_PER_CHARACTER;
+ ByteBuffer newData = ByteBuffer.allocate(length + extraBytes);
+ newData.order(ByteOrder.LITTLE_ENDIAN);
+ newData.putInt(length + extraBytes);
+ newData.putShort((short) objectRecordCount);
+ newData.putShort((short) recordType);
+ newData.putShort((short) (xmlWithMockLaUrl.length() * UTF_16_BYTES_PER_CHARACTER));
+ newData.put(xmlWithMockLaUrl.getBytes(Charset.forName(C.UTF16LE_NAME)));
+ return newData.array();
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
new file mode 100644
index 0000000000..baa5bf0916
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
@@ -0,0 +1,195 @@
+/*
+ * 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.TargetApi;
+import android.net.Uri;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+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.DataSourceInputStream;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link MediaDrmCallback} that makes requests using {@link HttpDataSource} instances.
+ */
+@TargetApi(18)
+public final class HttpMediaDrmCallback implements MediaDrmCallback {
+
+ private static final int MAX_MANUAL_REDIRECTS = 5;
+
+ private final HttpDataSource.Factory dataSourceFactory;
+ private final String defaultLicenseUrl;
+ private final boolean forceDefaultLicenseUrl;
+ private final Map<String, String> keyRequestProperties;
+
+ /**
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ */
+ public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
+ this(defaultLicenseUrl, false, dataSourceFactory);
+ }
+
+ /**
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is
+ * set to true.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
+ * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ */
+ public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
+ HttpDataSource.Factory dataSourceFactory) {
+ this.dataSourceFactory = dataSourceFactory;
+ this.defaultLicenseUrl = defaultLicenseUrl;
+ this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
+ this.keyRequestProperties = new HashMap<>();
+ }
+
+ /**
+ * Sets a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ * @param value The value of the field.
+ */
+ public void setKeyRequestProperty(String name, String value) {
+ Assertions.checkNotNull(name);
+ Assertions.checkNotNull(value);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.put(name, value);
+ }
+ }
+
+ /**
+ * Clears a header for key requests made by the callback.
+ *
+ * @param name The name of the header field.
+ */
+ public void clearKeyRequestProperty(String name) {
+ Assertions.checkNotNull(name);
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.remove(name);
+ }
+ }
+
+ /**
+ * Clears all headers for key requests made by the callback.
+ */
+ public void clearAllKeyRequestProperties() {
+ synchronized (keyRequestProperties) {
+ keyRequestProperties.clear();
+ }
+ }
+
+ @Override
+ public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
+ String url =
+ request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData());
+ return executePost(dataSourceFactory, url, /* httpBody= */ null, /* requestProperties= */ null);
+ }
+
+ @Override
+ public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+ String url = request.getLicenseServerUrl();
+ if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
+ url = defaultLicenseUrl;
+ }
+ Map<String, String> requestProperties = new HashMap<>();
+ // Add standard request properties for supported schemes.
+ String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml"
+ : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream");
+ requestProperties.put("Content-Type", contentType);
+ if (C.PLAYREADY_UUID.equals(uuid)) {
+ requestProperties.put("SOAPAction",
+ "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense");
+ }
+ // Add additional request properties.
+ synchronized (keyRequestProperties) {
+ requestProperties.putAll(keyRequestProperties);
+ }
+ return executePost(dataSourceFactory, url, request.getData(), requestProperties);
+ }
+
+ private static byte[] executePost(
+ HttpDataSource.Factory dataSourceFactory,
+ String url,
+ @Nullable byte[] httpBody,
+ @Nullable Map<String, String> requestProperties)
+ throws IOException {
+ HttpDataSource dataSource = dataSourceFactory.createDataSource();
+ if (requestProperties != null) {
+ for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
+ dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue());
+ }
+ }
+
+ int manualRedirectCount = 0;
+ while (true) {
+ DataSpec dataSpec =
+ new DataSpec(
+ Uri.parse(url),
+ DataSpec.HTTP_METHOD_POST,
+ httpBody,
+ /* absoluteStreamPosition= */ 0,
+ /* position= */ 0,
+ /* length= */ C.LENGTH_UNSET,
+ /* key= */ null,
+ DataSpec.FLAG_ALLOW_GZIP);
+ DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+ try {
+ return Util.toByteArray(inputStream);
+ } catch (InvalidResponseCodeException e) {
+ // For POST requests, the underlying network stack will not normally follow 307 or 308
+ // redirects automatically. Do so manually here.
+ boolean manuallyRedirect =
+ (e.responseCode == 307 || e.responseCode == 308)
+ && manualRedirectCount++ < MAX_MANUAL_REDIRECTS;
+ String redirectUrl = manuallyRedirect ? getRedirectUrl(e) : null;
+ if (redirectUrl == null) {
+ throw e;
+ }
+ url = redirectUrl;
+ } finally {
+ Util.closeQuietly(inputStream);
+ }
+ }
+ }
+
+ private static @Nullable String getRedirectUrl(InvalidResponseCodeException exception) {
+ Map<String, List<String>> headerFields = exception.headerFields;
+ if (headerFields != null) {
+ List<String> locationHeaders = headerFields.get("Location");
+ if (locationHeaders != null && !locationHeaders.isEmpty()) {
+ return locationHeaders.get(0);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java
new file mode 100644
index 0000000000..79208489c4
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+/**
+ * Thrown when the drm keys loaded into an open session expire.
+ */
+public final class KeysExpiredException extends Exception {
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
new file mode 100644
index 0000000000..23e1859ca8
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 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 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.util.Assertions;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * A {@link MediaDrmCallback} that provides a fixed response to key requests. Provisioning is not
+ * supported. This implementation is primarily useful for providing locally stored keys to decrypt
+ * ClearKey protected content. It is not suitable for use with Widevine or PlayReady protected
+ * content.
+ */
+public final class LocalMediaDrmCallback implements MediaDrmCallback {
+
+ private final byte[] keyResponse;
+
+ /**
+ * @param keyResponse The fixed response for all key requests.
+ */
+ public LocalMediaDrmCallback(byte[] keyResponse) {
+ this.keyResponse = Assertions.checkNotNull(keyResponse);
+ }
+
+ @Override
+ public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+ return keyResponse;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java
new file mode 100644
index 0000000000..2bc41f6bec
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import java.util.UUID;
+
+/**
+ * Performs {@link ExoMediaDrm} key and provisioning requests.
+ */
+public interface MediaDrmCallback {
+
+ /**
+ * Executes a provisioning request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws Exception;
+
+ /**
+ * Executes a key request.
+ *
+ * @param uuid The UUID of the content protection scheme.
+ * @param request The request.
+ * @return The response data.
+ * @throws Exception If an error occurred executing the request.
+ */
+ byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception;
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
new file mode 100644
index 0000000000..3ce3879a76
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -0,0 +1,266 @@
+/*
+ * 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.TargetApi;
+import android.media.MediaDrm;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+/** Helper class to download, renew and release offline licenses. */
+@TargetApi(18)
+@RequiresApi(18)
+public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
+
+ private static final DrmInitData DUMMY_DRM_INIT_DATA = new DrmInitData();
+
+ private final ConditionVariable conditionVariable;
+ private final DefaultDrmSessionManager<T> drmSessionManager;
+ private final HandlerThread handlerThread;
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ String defaultLicenseUrl, Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null);
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
+ * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory)
+ throws UnsupportedDrmException {
+ return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory,
+ null);
+ }
+
+ /**
+ * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance
+ * is no longer required.
+ *
+ * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
+ * their own license URL.
+ * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
+ * include their own license URL.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest}. May be null.
+ * @return A new instance which uses Widevine CDM.
+ * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+ * instantiated.
+ * @see DefaultDrmSessionManager.Builder
+ */
+ public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+ String defaultLicenseUrl,
+ boolean forceDefaultLicenseUrl,
+ Factory httpDataSourceFactory,
+ @Nullable Map<String, String> optionalKeyRequestParameters)
+ throws UnsupportedDrmException {
+ return new OfflineLicenseHelper<>(
+ C.WIDEVINE_UUID,
+ FrameworkMediaDrm.DEFAULT_PROVIDER,
+ new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory),
+ optionalKeyRequestParameters);
+ }
+
+ /**
+ * Constructs an instance. Call {@link #release()} when the instance is no longer required.
+ *
+ * @param uuid The UUID of the drm scheme.
+ * @param mediaDrmProvider A {@link ExoMediaDrm.Provider}.
+ * @param callback Performs key and provisioning requests.
+ * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+ * to {@link MediaDrm#getKeyRequest}. May be null.
+ * @see DefaultDrmSessionManager.Builder
+ */
+ @SuppressWarnings("unchecked")
+ public OfflineLicenseHelper(
+ UUID uuid,
+ ExoMediaDrm.Provider<T> mediaDrmProvider,
+ MediaDrmCallback callback,
+ @Nullable Map<String, String> optionalKeyRequestParameters) {
+ handlerThread = new HandlerThread("OfflineLicenseHelper");
+ handlerThread.start();
+ conditionVariable = new ConditionVariable();
+ DefaultDrmSessionEventListener eventListener =
+ new DefaultDrmSessionEventListener() {
+ @Override
+ public void onDrmKeysLoaded() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmSessionManagerError(Exception e) {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRestored() {
+ conditionVariable.open();
+ }
+
+ @Override
+ public void onDrmKeysRemoved() {
+ conditionVariable.open();
+ }
+ };
+ if (optionalKeyRequestParameters == null) {
+ optionalKeyRequestParameters = Collections.emptyMap();
+ }
+ drmSessionManager =
+ (DefaultDrmSessionManager<T>)
+ new DefaultDrmSessionManager.Builder()
+ .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider)
+ .setKeyRequestParameters(optionalKeyRequestParameters)
+ .build(callback);
+ drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener);
+ }
+
+ /**
+ * Downloads an offline license.
+ *
+ * @param drmInitData The {@link DrmInitData} for the content whose license is to be downloaded.
+ * @return The key set id for the downloaded license.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] downloadLicense(DrmInitData drmInitData) throws DrmSessionException {
+ Assertions.checkArgument(drmInitData != null);
+ return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
+ }
+
+ /**
+ * Renews an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be renewed.
+ * @return The renewed offline license key set id.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ return blockingKeyRequest(
+ DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ }
+
+ /**
+ * Releases an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license to be released.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized void releaseLicense(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ blockingKeyRequest(
+ DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ }
+
+ /**
+ * Returns the remaining license and playback durations in seconds, for an offline license.
+ *
+ * @param offlineLicenseKeySetId The key set id of the license.
+ * @return The remaining license and playback durations, in seconds.
+ * @throws DrmSessionException Thrown when a DRM session error occurs.
+ */
+ public synchronized Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
+ throws DrmSessionException {
+ Assertions.checkNotNull(offlineLicenseKeySetId);
+ drmSessionManager.prepare();
+ DrmSession<T> drmSession =
+ openBlockingKeyRequest(
+ DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA);
+ DrmSessionException error = drmSession.getError();
+ Pair<Long, Long> licenseDurationRemainingSec =
+ WidevineUtil.getLicenseDurationRemainingSec(drmSession);
+ drmSession.release();
+ drmSessionManager.release();
+ if (error != null) {
+ if (error.getCause() instanceof KeysExpiredException) {
+ return Pair.create(0L, 0L);
+ }
+ throw error;
+ }
+ return Assertions.checkNotNull(licenseDurationRemainingSec);
+ }
+
+ /**
+ * Releases the helper. Should be called when the helper is no longer required.
+ */
+ public void release() {
+ handlerThread.quit();
+ }
+
+ private byte[] blockingKeyRequest(
+ @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData)
+ throws DrmSessionException {
+ drmSessionManager.prepare();
+ DrmSession<T> drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
+ drmInitData);
+ DrmSessionException error = drmSession.getError();
+ byte[] keySetId = drmSession.getOfflineLicenseKeySetId();
+ drmSession.release();
+ drmSessionManager.release();
+ if (error != null) {
+ throw error;
+ }
+ return Assertions.checkNotNull(keySetId);
+ }
+
+ private DrmSession<T> openBlockingKeyRequest(
+ @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) {
+ drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
+ conditionVariable.close();
+ DrmSession<T> drmSession = drmSessionManager.acquireSession(handlerThread.getLooper(),
+ drmInitData);
+ // Block current thread until key loading is finished
+ conditionVariable.block();
+ return drmSession;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
new file mode 100644
index 0000000000..4dc9f2b0b2
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
@@ -0,0 +1,67 @@
+/*
+ * 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 androidx.annotation.IntDef;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when the requested DRM scheme is not supported.
+ */
+public final class UnsupportedDrmException extends Exception {
+
+ /**
+ * The reason for the exception. One of {@link #REASON_UNSUPPORTED_SCHEME} or {@link
+ * #REASON_INSTANTIATION_ERROR}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_UNSUPPORTED_SCHEME, REASON_INSTANTIATION_ERROR})
+ public @interface Reason {}
+ /**
+ * The requested DRM scheme is unsupported by the device.
+ */
+ public static final int REASON_UNSUPPORTED_SCHEME = 1;
+ /**
+ * There device advertises support for the requested DRM scheme, but there was an error
+ * instantiating it. The cause can be retrieved using {@link #getCause()}.
+ */
+ public static final int REASON_INSTANTIATION_ERROR = 2;
+
+ /**
+ * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ @Reason public final int reason;
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ */
+ public UnsupportedDrmException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ /**
+ * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
+ * @param cause The cause of this exception.
+ */
+ public UnsupportedDrmException(@Reason int reason, Exception cause) {
+ super(cause);
+ this.reason = reason;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java
new file mode 100644
index 0000000000..67539bef39
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 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.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import java.util.Map;
+
+/**
+ * Utility methods for Widevine.
+ */
+public final class WidevineUtil {
+
+ /** Widevine specific key status field name for the remaining license duration, in seconds. */
+ public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
+ /** Widevine specific key status field name for the remaining playback duration, in seconds. */
+ public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
+
+ private WidevineUtil() {}
+
+ /**
+ * Returns license and playback durations remaining in seconds.
+ *
+ * @param drmSession The drm session to query.
+ * @return A {@link Pair} consisting of the remaining license and playback durations in seconds,
+ * or null if called before the session has been opened or after it's been released.
+ */
+ public static @Nullable Pair<Long, Long> getLicenseDurationRemainingSec(
+ DrmSession<?> drmSession) {
+ Map<String, String> keyStatus = drmSession.queryKeyStatus();
+ if (keyStatus == null) {
+ return null;
+ }
+ return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
+ getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
+ }
+
+ private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
+ if (keyStatus != null) {
+ try {
+ String value = keyStatus.get(property);
+ if (value != null) {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException e) {
+ // do nothing.
+ }
+ }
+ return C.TIME_UNSET;
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java
new file mode 100644
index 0000000000..ec885e2ad7
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.drm;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;