| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // Modifications Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // 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 dev.cobalt.media; |
| |
| import static dev.cobalt.media.Log.TAG; |
| |
| import android.media.DeniedByServerException; |
| import android.media.MediaCrypto; |
| import android.media.MediaCryptoException; |
| import android.media.MediaDrm; |
| import android.media.MediaDrm.OnEventListener; |
| import android.media.MediaDrmException; |
| import android.media.NotProvisionedException; |
| import android.media.UnsupportedSchemeException; |
| import android.os.Build; |
| import android.util.Base64; |
| import androidx.annotation.RequiresApi; |
| import dev.cobalt.coat.CobaltHttpHelper; |
| import dev.cobalt.util.Log; |
| import dev.cobalt.util.UsedByNative; |
| import java.nio.ByteBuffer; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.UUID; |
| |
| /** A wrapper of the android MediaDrm class. */ |
| @UsedByNative |
| public class MediaDrmBridge { |
| // Implementation Notes: |
| // - A media crypto session (mMediaCryptoSession) is opened after MediaDrm |
| // is created. This session will NOT be added to mSessionIds and will only |
| // be used to create the MediaCrypto object. |
| // - Each createSession() call creates a new session. All created sessions |
| // are managed in mSessionIds. |
| // - Whenever NotProvisionedException is thrown, we will clean up the |
| // current state and start the provisioning process. |
| // - When provisioning is finished, we will try to resume suspended |
| // operations: |
| // a) Create the media crypto session if it's not created. |
| // b) Finish createSession() if previous createSession() was interrupted |
| // by a NotProvisionedException. |
| // - Whenever an unexpected error occurred, we'll call release() to release |
| // all resources immediately, clear all states and fail all pending |
| // operations. After that all calls to this object will fail (e.g. return |
| // null or reject the promise). All public APIs and callbacks should check |
| // mMediaBridge to make sure release() hasn't been called. |
| |
| private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray(); |
| private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0; |
| |
| // The value of this must stay in sync with kSbDrmTicketInvalid in "starboard/drm.h" |
| private static final int SB_DRM_TICKET_INVALID = Integer.MIN_VALUE; |
| |
| // Scheme UUID for Widevine. See http://dashif.org/identifiers/protection/ |
| private static final UUID WIDEVINE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); |
| |
| // Deprecated in API 26, but we still log it on earlier devices. |
| // We do handle STATUS_EXPIRED in nativeOnKeyStatusChange() for API 23+ devices. |
| @SuppressWarnings("deprecation") |
| private static final int MEDIA_DRM_EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED; |
| |
| private MediaDrm mMediaDrm; |
| private long mNativeMediaDrmBridge; |
| private UUID mSchemeUUID; |
| |
| // A session only for the purpose of creating a MediaCrypto object. Created |
| // after construction, or after the provisioning process is successfully |
| // completed. No getKeyRequest() should be called on |mMediaCryptoSession|. |
| private byte[] mMediaCryptoSession; |
| |
| // The map of all opened sessions (excluding mMediaCryptoSession) to their |
| // mime types. |
| private HashMap<ByteBuffer, String> mSessionIds = new HashMap<>(); |
| |
| private MediaCrypto mMediaCrypto; |
| |
| // Return value type for calls to updateSession(), which contains whether or not the call |
| // succeeded, and optionally an error message (that is empty on success). |
| @UsedByNative |
| private static class UpdateSessionResult { |
| public enum Status { |
| SUCCESS, |
| FAILURE |
| } |
| |
| // Whether or not the update session attempt succeeded or failed. |
| private boolean mIsSuccess; |
| |
| // Descriptive error message or details, in the scenario where the update session call failed. |
| private String mErrorMessage; |
| |
| public UpdateSessionResult(Status status, String errorMessage) { |
| this.mIsSuccess = status == Status.SUCCESS; |
| this.mErrorMessage = errorMessage; |
| } |
| |
| @UsedByNative |
| public boolean isSuccess() { |
| return mIsSuccess; |
| } |
| |
| @UsedByNative |
| public String getErrorMessage() { |
| return mErrorMessage; |
| } |
| } |
| |
| /** |
| * Create a new MediaDrmBridge with the Widevine crypto scheme. |
| * |
| * @param nativeMediaDrmBridge The native owner of this class. |
| */ |
| @UsedByNative |
| static MediaDrmBridge create(long nativeMediaDrmBridge) { |
| UUID cryptoScheme = WIDEVINE_UUID; |
| if (!MediaDrm.isCryptoSchemeSupported(cryptoScheme)) { |
| return null; |
| } |
| |
| MediaDrmBridge mediaDrmBridge = null; |
| try { |
| mediaDrmBridge = new MediaDrmBridge(cryptoScheme, nativeMediaDrmBridge); |
| Log.d(TAG, "MediaDrmBridge successfully created."); |
| } catch (UnsupportedSchemeException e) { |
| Log.e(TAG, "Unsupported DRM scheme", e); |
| return null; |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, "Failed to create MediaDrmBridge", e); |
| return null; |
| } catch (IllegalStateException e) { |
| Log.e(TAG, "Failed to create MediaDrmBridge", e); |
| return null; |
| } |
| |
| if (!mediaDrmBridge.createMediaCrypto()) { |
| return null; |
| } |
| |
| return mediaDrmBridge; |
| } |
| |
| /** |
| * Check whether the Widevine crypto scheme is supported. |
| * |
| * @return true if the container and the crypto scheme is supported, or false otherwise. |
| */ |
| @UsedByNative |
| static boolean isWidevineCryptoSchemeSupported() { |
| return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID); |
| } |
| |
| /** |
| * Check whether the Widevine crypto scheme is supported for the given container. If |
| * |containerMimeType| is an empty string, we just return whether the crypto scheme is supported. |
| * |
| * @return true if the container and the crypto scheme is supported, or false otherwise. |
| */ |
| @UsedByNative |
| static boolean isWidevineCryptoSchemeSupported(String containerMimeType) { |
| if (containerMimeType.isEmpty()) { |
| return isWidevineCryptoSchemeSupported(); |
| } |
| return MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID, containerMimeType); |
| } |
| |
| /** Destroy the MediaDrmBridge object. */ |
| @UsedByNative |
| void destroy() { |
| mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE; |
| if (mMediaDrm != null) { |
| release(); |
| } |
| } |
| |
| @UsedByNative |
| void createSession(int ticket, byte[] initData, String mime) { |
| Log.d(TAG, "createSession()"); |
| |
| if (mMediaDrm == null) { |
| Log.e(TAG, "createSession() called when MediaDrm is null."); |
| return; |
| } |
| |
| boolean newSessionOpened = false; |
| byte[] sessionId = null; |
| try { |
| sessionId = openSession(); |
| if (sessionId == null) { |
| Log.e(TAG, "Open session failed."); |
| return; |
| } |
| newSessionOpened = true; |
| if (sessionExists(sessionId)) { |
| Log.e(TAG, "Opened session that already exists."); |
| return; |
| } |
| |
| MediaDrm.KeyRequest request = null; |
| request = getKeyRequest(sessionId, initData, mime); |
| if (request == null) { |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(sessionId); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed", e); |
| } |
| Log.e(TAG, "Generate request failed."); |
| return; |
| } |
| |
| // Success! |
| Log.d( |
| TAG, |
| String.format("createSession(): Session (%s) created.", bytesToHexString(sessionId))); |
| mSessionIds.put(ByteBuffer.wrap(sessionId), mime); |
| onSessionMessage(ticket, sessionId, request); |
| } catch (NotProvisionedException e) { |
| Log.e(TAG, "Device not provisioned", e); |
| if (newSessionOpened) { |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(sessionId); |
| } catch (Exception ex) { |
| Log.e(TAG, "closeSession failed", ex); |
| } |
| } |
| attemptProvisioning(); |
| } |
| } |
| |
| /** |
| * Update a session with response. |
| * |
| * @param sessionId Reference ID of session to be updated. |
| * @param response Response data from the server. |
| */ |
| @UsedByNative |
| UpdateSessionResult updateSession(int ticket, byte[] sessionId, byte[] response) { |
| Log.d(TAG, "updateSession()"); |
| if (mMediaDrm == null) { |
| Log.e(TAG, "updateSession() called when MediaDrm is null."); |
| return new UpdateSessionResult( |
| UpdateSessionResult.Status.FAILURE, |
| "Null MediaDrm object when calling updateSession(). StackTrace: " |
| + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| |
| if (!sessionExists(sessionId)) { |
| Log.e(TAG, "updateSession tried to update a session that does not exist."); |
| return new UpdateSessionResult( |
| UpdateSessionResult.Status.FAILURE, |
| "Failed to update session because it does not exist. StackTrace: " |
| + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| |
| try { |
| try { |
| mMediaDrm.provideKeyResponse(sessionId, response); |
| } catch (IllegalStateException e) { |
| // This is not really an exception. Some error codes are incorrectly |
| // reported as an exception. |
| Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse()", e); |
| } |
| Log.d( |
| TAG, String.format("Key successfully added for session %s", bytesToHexString(sessionId))); |
| if (Build.VERSION.SDK_INT < 23) { |
| // Pass null to indicate that KeyStatus isn't supported. |
| nativeOnKeyStatusChange(mNativeMediaDrmBridge, sessionId, null); |
| } |
| return new UpdateSessionResult(UpdateSessionResult.Status.SUCCESS, ""); |
| } catch (NotProvisionedException e) { |
| // TODO: Should we handle this? |
| Log.e(TAG, "Failed to provide key response", e); |
| release(); |
| return new UpdateSessionResult( |
| UpdateSessionResult.Status.FAILURE, |
| "Update session failed due to lack of provisioning. StackTrace: " |
| + android.util.Log.getStackTraceString(e)); |
| } catch (DeniedByServerException e) { |
| Log.e(TAG, "Failed to provide key response.", e); |
| release(); |
| return new UpdateSessionResult( |
| UpdateSessionResult.Status.FAILURE, |
| "Update session failed because we were denied by server. StackTrace: " |
| + android.util.Log.getStackTraceString(e)); |
| } catch (Exception e) { |
| Log.e(TAG, "", e); |
| release(); |
| return new UpdateSessionResult( |
| UpdateSessionResult.Status.FAILURE, |
| "Update session failed. Caught exception: " |
| + e.getMessage() |
| + " StackTrace: " |
| + android.util.Log.getStackTraceString(e)); |
| } |
| } |
| |
| /** |
| * Close a session that was previously created by createSession(). |
| * |
| * @param sessionId ID of session to be closed. |
| */ |
| @UsedByNative |
| void closeSession(byte[] sessionId) { |
| Log.d(TAG, "closeSession()"); |
| if (mMediaDrm == null) { |
| Log.e(TAG, "closeSession() called when MediaDrm is null."); |
| return; |
| } |
| |
| if (!sessionExists(sessionId)) { |
| Log.e(TAG, "Invalid sessionId in closeSession(): " + bytesToHexString(sessionId)); |
| return; |
| } |
| |
| try { |
| // Some implementations don't have removeKeys. |
| // https://bugs.chromium.org/p/chromium/issues/detail?id=475632 |
| mMediaDrm.removeKeys(sessionId); |
| } catch (Exception e) { |
| Log.e(TAG, "removeKeys failed: ", e); |
| } |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(sessionId); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed: ", e); |
| } |
| mSessionIds.remove(ByteBuffer.wrap(sessionId)); |
| Log.d(TAG, String.format("Session %s closed", bytesToHexString(sessionId))); |
| } |
| |
| @UsedByNative |
| byte[] getMetricsInBase64() { |
| if (Build.VERSION.SDK_INT < 28) { |
| return null; |
| } |
| byte[] metrics; |
| try { |
| metrics = mMediaDrm.getPropertyByteArray("metrics"); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to retrieve DRM Metrics."); |
| return null; |
| } |
| return Base64.encode(metrics, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); |
| } |
| |
| @UsedByNative |
| MediaCrypto getMediaCrypto() { |
| return mMediaCrypto; |
| } |
| |
| private MediaDrmBridge(UUID schemeUUID, long nativeMediaDrmBridge) |
| throws android.media.UnsupportedSchemeException { |
| mSchemeUUID = schemeUUID; |
| mMediaDrm = new MediaDrm(schemeUUID); |
| |
| mNativeMediaDrmBridge = nativeMediaDrmBridge; |
| if (!isNativeMediaDrmBridgeValid()) { |
| throw new IllegalArgumentException( |
| String.format("Invalid nativeMediaDrmBridge value: |%d|.", nativeMediaDrmBridge)); |
| } |
| |
| mMediaDrm.setOnEventListener( |
| new OnEventListener() { |
| @Override |
| public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { |
| if (sessionId == null) { |
| Log.e(TAG, "EventListener: Null session."); |
| return; |
| } |
| if (!sessionExists(sessionId)) { |
| Log.e( |
| TAG, |
| String.format("EventListener: Invalid session %s", bytesToHexString(sessionId))); |
| return; |
| } |
| switch (event) { |
| case MediaDrm.EVENT_KEY_REQUIRED: |
| Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED"); |
| String mime = mSessionIds.get(ByteBuffer.wrap(sessionId)); |
| MediaDrm.KeyRequest request = null; |
| try { |
| request = getKeyRequest(sessionId, data, mime); |
| } catch (NotProvisionedException e) { |
| Log.e(TAG, "Device not provisioned", e); |
| if (!attemptProvisioning()) { |
| Log.e(TAG, "Failed to provision device when responding to EVENT_KEY_REQUIRED"); |
| return; |
| } |
| // If we supposedly successfully provisioned ourselves, then try to create a |
| // request again. |
| try { |
| request = getKeyRequest(sessionId, data, mime); |
| } catch (NotProvisionedException e2) { |
| Log.e( |
| TAG, |
| "Device still not provisioned after supposedly successful provisioning", |
| e2); |
| return; |
| } |
| } |
| if (request != null) { |
| onSessionMessage(SB_DRM_TICKET_INVALID, sessionId, request); |
| } else { |
| Log.e(TAG, "EventListener: getKeyRequest failed."); |
| return; |
| } |
| break; |
| case MEDIA_DRM_EVENT_KEY_EXPIRED: |
| Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED"); |
| break; |
| case MediaDrm.EVENT_VENDOR_DEFINED: |
| Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED"); |
| break; |
| default: |
| Log.e(TAG, "Invalid DRM event " + event); |
| return; |
| } |
| } |
| }); |
| |
| if (Build.VERSION.SDK_INT >= 23) { |
| setOnKeyStatusChangeListenerV23(); |
| } |
| |
| mMediaDrm.setPropertyString("privacyMode", "enable"); |
| mMediaDrm.setPropertyString("sessionSharing", "enable"); |
| } |
| |
| @RequiresApi(23) |
| private void setOnKeyStatusChangeListenerV23() { |
| mMediaDrm.setOnKeyStatusChangeListener( |
| new MediaDrm.OnKeyStatusChangeListener() { |
| @Override |
| public void onKeyStatusChange( |
| MediaDrm md, |
| byte[] sessionId, |
| List<MediaDrm.KeyStatus> keyInformation, |
| boolean hasNewUsableKey) { |
| nativeOnKeyStatusChange( |
| mNativeMediaDrmBridge, |
| sessionId, |
| keyInformation.toArray(new MediaDrm.KeyStatus[keyInformation.size()])); |
| } |
| }, |
| null); |
| } |
| |
| /** Convert byte array to hex string for logging. */ |
| private static String bytesToHexString(byte[] bytes) { |
| StringBuilder hexString = new StringBuilder(); |
| for (int i = 0; i < bytes.length; ++i) { |
| hexString.append(HEX_CHAR_LOOKUP[bytes[i] >>> 4]); |
| hexString.append(HEX_CHAR_LOOKUP[bytes[i] & 0xf]); |
| } |
| return hexString.toString(); |
| } |
| |
| private void onSessionMessage( |
| int ticket, final byte[] sessionId, final MediaDrm.KeyRequest request) { |
| if (!isNativeMediaDrmBridgeValid()) { |
| return; |
| } |
| |
| int requestType = MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| requestType = request.getRequestType(); |
| } else { |
| // Prior to M, getRequestType() is not supported. Do our best guess here: Assume |
| // requests with a URL are renewals and all others are initial requests. |
| requestType = |
| request.getDefaultUrl().isEmpty() |
| ? MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL |
| : MediaDrm.KeyRequest.REQUEST_TYPE_RENEWAL; |
| } |
| |
| nativeOnSessionMessage( |
| mNativeMediaDrmBridge, ticket, sessionId, requestType, request.getData()); |
| } |
| |
| /** |
| * Get a key request. |
| * |
| * @param sessionId ID of session on which we need to get the key request. |
| * @param data Data needed to get the key request. |
| * @param mime Mime type to get the key request. |
| * @return the key request. |
| */ |
| private MediaDrm.KeyRequest getKeyRequest(byte[] sessionId, byte[] data, String mime) |
| throws android.media.NotProvisionedException { |
| if (mMediaDrm == null) { |
| throw new IllegalStateException("mMediaDrm cannot be null in getKeyRequest"); |
| } |
| if (mMediaCryptoSession == null) { |
| throw new IllegalStateException("mMediaCryptoSession cannot be null in getKeyRequest."); |
| } |
| // TODO: Cannot do this during provisioning pending. |
| |
| HashMap<String, String> optionalParameters = new HashMap<>(); |
| MediaDrm.KeyRequest request = null; |
| try { |
| request = |
| mMediaDrm.getKeyRequest( |
| sessionId, data, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters); |
| } catch (IllegalStateException e) { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP |
| && e instanceof android.media.MediaDrm.MediaDrmStateException) { |
| Log.e(TAG, "MediaDrmStateException fired during getKeyRequest().", e); |
| } |
| } |
| |
| String result = (request != null) ? "succeeded" : "failed"; |
| Log.d(TAG, String.format("getKeyRequest %s!", result)); |
| |
| return request; |
| } |
| |
| /** |
| * Create a MediaCrypto object. |
| * |
| * @return false upon fatal error in creating MediaCrypto. Returns true otherwise, including the |
| * following two cases: 1. MediaCrypto is successfully created and notified. 2. Device is not |
| * provisioned and MediaCrypto creation will be tried again after the provisioning process is |
| * completed. |
| * <p>When false is returned, the caller should call release(), which will notify the native |
| * code with a null MediaCrypto, if needed. |
| */ |
| private boolean createMediaCrypto() { |
| if (mMediaDrm == null) { |
| throw new IllegalStateException("Cannot create media crypto with null mMediaDrm."); |
| } |
| if (mMediaCryptoSession != null) { |
| throw new IllegalStateException( |
| "Cannot create media crypto with non-null mMediaCryptoSession."); |
| } |
| // TODO: Cannot do this during provisioning pending. |
| |
| // Open media crypto session. |
| try { |
| mMediaCryptoSession = openSession(); |
| } catch (NotProvisionedException e) { |
| Log.d(TAG, "Device not provisioned", e); |
| if (!attemptProvisioning()) { |
| Log.e(TAG, "Failed to provision device during MediaCrypto creation."); |
| return false; |
| } |
| try { |
| mMediaCryptoSession = openSession(); |
| } catch (NotProvisionedException e2) { |
| Log.e(TAG, "Device still not provisioned after supposedly successful provisioning", e2); |
| return false; |
| } |
| } |
| |
| if (mMediaCryptoSession == null) { |
| Log.e(TAG, "Cannot create MediaCrypto Session."); |
| return false; |
| } |
| Log.d( |
| TAG, |
| String.format("MediaCrypto Session created: %s", bytesToHexString(mMediaCryptoSession))); |
| |
| // Create MediaCrypto object. |
| try { |
| if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { |
| MediaCrypto mediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession); |
| Log.d(TAG, "MediaCrypto successfully created!"); |
| mMediaCrypto = mediaCrypto; |
| return true; |
| } else { |
| Log.e(TAG, "Cannot create MediaCrypto for unsupported scheme."); |
| } |
| } catch (MediaCryptoException e) { |
| Log.e(TAG, "Cannot create MediaCrypto", e); |
| } |
| |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(mMediaCryptoSession); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed: ", e); |
| } |
| mMediaCryptoSession = null; |
| |
| return false; |
| } |
| |
| /** |
| * Open a new session. |
| * |
| * @return ID of the session opened. Returns null if unexpected error happened. |
| */ |
| private byte[] openSession() throws android.media.NotProvisionedException { |
| Log.d(TAG, "openSession()"); |
| if (mMediaDrm == null) { |
| throw new IllegalStateException("mMediaDrm cannot be null in openSession"); |
| } |
| try { |
| byte[] sessionId = mMediaDrm.openSession(); |
| // Make a clone here in case the underlying byte[] is modified. |
| return sessionId.clone(); |
| } catch (RuntimeException e) { // TODO: Drop this? |
| Log.e(TAG, "Cannot open a new session", e); |
| release(); |
| return null; |
| } catch (NotProvisionedException e) { |
| // Throw NotProvisionedException so that we can attemptProvisioning(). |
| throw e; |
| } catch (MediaDrmException e) { |
| // Other MediaDrmExceptions (e.g. ResourceBusyException) are not |
| // recoverable. |
| Log.e(TAG, "Cannot open a new session", e); |
| release(); |
| return null; |
| } |
| } |
| |
| /** |
| * Attempt to get the device that we are currently running on provisioned. |
| * |
| * @return whether provisioning was successful or not. |
| */ |
| private boolean attemptProvisioning() { |
| Log.d(TAG, "attemptProvisioning()"); |
| MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest(); |
| String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); |
| byte[] response = new CobaltHttpHelper().performDrmHttpPost(url); |
| if (response == null) { |
| return false; |
| } |
| try { |
| mMediaDrm.provideProvisionResponse(response); |
| return true; |
| } catch (android.media.DeniedByServerException e) { |
| Log.e(TAG, "failed to provide provision response", e); |
| } catch (java.lang.IllegalStateException e) { |
| Log.e(TAG, "failed to provide provision response", e); |
| } |
| return false; |
| } |
| |
| /** |
| * Check whether |sessionId| is an existing session ID, excluding the media crypto session. |
| * |
| * @param sessionId Crypto session Id. |
| * @return true if |sessionId| exists, false otherwise. |
| */ |
| private boolean sessionExists(byte[] sessionId) { |
| if (mMediaCryptoSession == null) { |
| if (!mSessionIds.isEmpty()) { |
| throw new IllegalStateException( |
| "mSessionIds must be empty if crypto session does not exist."); |
| } |
| Log.e(TAG, "Session doesn't exist because media crypto session is not created."); |
| return false; |
| } |
| return !Arrays.equals(sessionId, mMediaCryptoSession) |
| && mSessionIds.containsKey(ByteBuffer.wrap(sessionId)); |
| } |
| |
| /** Release all allocated resources and finish all pending operations. */ |
| private void release() { |
| // Note that mNativeMediaDrmBridge may have already been reset (see destroy()). |
| if (mMediaDrm == null) { |
| throw new IllegalStateException("Called release with null mMediaDrm."); |
| } |
| |
| // Close all open sessions. |
| for (ByteBuffer sessionId : mSessionIds.keySet()) { |
| try { |
| // Some implementations don't have removeKeys. |
| // https://bugs.chromium.org/p/chromium/issues/detail?id=475632 |
| mMediaDrm.removeKeys(sessionId.array()); |
| } catch (Exception e) { |
| Log.e(TAG, "removeKeys failed: ", e); |
| } |
| |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(sessionId.array()); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed: ", e); |
| } |
| Log.d( |
| TAG, |
| String.format("Successfully closed session (%s)", bytesToHexString(sessionId.array()))); |
| } |
| mSessionIds.clear(); |
| mSessionIds = null; |
| |
| // Close mMediaCryptoSession if it's open. |
| if (mMediaCryptoSession != null) { |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(mMediaCryptoSession); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed: ", e); |
| } |
| mMediaCryptoSession = null; |
| } |
| |
| if (mMediaDrm != null) { |
| if (Build.VERSION.SDK_INT >= 28) { |
| closeMediaDrmV28(mMediaDrm); |
| } else { |
| releaseMediaDrmDeprecated(mMediaDrm); |
| } |
| mMediaDrm = null; |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void releaseMediaDrmDeprecated(MediaDrm mediaDrm) { |
| mediaDrm.release(); |
| } |
| |
| @RequiresApi(28) |
| private void closeMediaDrmV28(MediaDrm mediaDrm) { |
| mediaDrm.close(); |
| } |
| |
| private boolean isNativeMediaDrmBridgeValid() { |
| return mNativeMediaDrmBridge != INVALID_NATIVE_MEDIA_DRM_BRIDGE; |
| } |
| |
| private native void nativeOnSessionMessage( |
| long nativeMediaDrmBridge, int ticket, byte[] sessionId, int requestType, byte[] message); |
| |
| private native void nativeOnKeyStatusChange( |
| long nativeMediaDrmBridge, byte[] sessionId, MediaDrm.KeyStatus[] keyInformation); |
| } |