diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm')
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; |