| // 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.Locale; |
| 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; |
| |
| // Deprecated in API 23, but we still log it on earlier devices. |
| @SuppressWarnings("deprecation") |
| private static final int MEDIA_DRM_EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED; |
| |
| // Added in API 23. |
| private static final int MEDIA_DRM_EVENT_SESSION_RECLAIMED = MediaDrm.EVENT_SESSION_RECLAIMED; |
| |
| 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(String keySystem, long nativeMediaDrmBridge) { |
| UUID cryptoScheme = WIDEVINE_UUID; |
| if (!MediaDrm.isCryptoSchemeSupported(cryptoScheme)) { |
| return null; |
| } |
| |
| MediaDrmBridge mediaDrmBridge = null; |
| try { |
| mediaDrmBridge = new MediaDrmBridge(keySystem, 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 `cbcs` scheme is supported. |
| * |
| * @return true if the `cbcs` encryption is supported, or false otherwise. |
| */ |
| @UsedByNative |
| static boolean isCbcsSchemeSupported() { |
| // While 'cbcs' scheme was originally implemented in N, there was a bug (in the |
| // DRM code) which means that it didn't really work properly until N-MR1). |
| return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1; |
| } |
| |
| /** 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))); |
| 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(String keySystem, UUID schemeUUID, long nativeMediaDrmBridge) |
| throws android.media.UnsupportedSchemeException { |
| mSchemeUUID = schemeUUID; |
| mMediaDrm = new MediaDrm(schemeUUID); |
| |
| // Get info of hdcp connection |
| if (Build.VERSION.SDK_INT >= 29) { |
| getConnectedHdcpLevelInfoV29(mMediaDrm); |
| } |
| |
| 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; |
| } |
| |
| if (event == 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; |
| } |
| } else if (event == MEDIA_DRM_EVENT_KEY_EXPIRED) { |
| Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED"); |
| } else if (event == MediaDrm.EVENT_VENDOR_DEFINED) { |
| Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED"); |
| } else if (event == MEDIA_DRM_EVENT_PROVISION_REQUIRED) { |
| Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED"); |
| } else if (event == MEDIA_DRM_EVENT_SESSION_RECLAIMED) { |
| Log.d(TAG, "MediaDrm.EVENT_SESSION_RECLAIMED"); |
| } else { |
| Log.e(TAG, "Invalid DRM event " + event); |
| return; |
| } |
| } |
| }); |
| |
| 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); |
| |
| mMediaDrm.setPropertyString("privacyMode", "disable"); |
| mMediaDrm.setPropertyString("sessionSharing", "enable"); |
| if (keySystem.equals("com.youtube.widevine.l3") |
| && mMediaDrm.getPropertyString("securityLevel") != "L3") { |
| mMediaDrm.setPropertyString("securityLevel", "L3"); |
| } |
| } |
| |
| /** 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 = request.getRequestType(); |
| |
| 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 (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."); |
| } |
| // Create MediaCrypto object. |
| try { |
| if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { |
| MediaCrypto mediaCrypto = new MediaCrypto(mSchemeUUID, new byte[0]); |
| 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); |
| } |
| |
| 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; |
| } |
| } |
| |
| @UsedByNative |
| boolean createMediaCryptoSession() { |
| if (mMediaCryptoSession != null) { |
| return true; |
| } |
| Log.w(TAG, "MediaDrmBridge createMediaCryptoSession"); |
| if (mMediaCrypto == null) { |
| throw new IllegalStateException("Cannot create media crypto session with null mMediaCrypto."); |
| } |
| |
| // Open media crypto session. |
| try { |
| mMediaCryptoSession = openSession(); |
| } catch (NotProvisionedException e) { |
| Log.w(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; |
| } |
| |
| try { |
| mMediaCrypto.setMediaDrmSession(mMediaCryptoSession); |
| } catch (MediaCryptoException e3) { |
| Log.e(TAG, "Unable to set media drm session", e3); |
| try { |
| // Some implementations let this method throw exceptions. |
| mMediaDrm.closeSession(mMediaCryptoSession); |
| } catch (Exception e) { |
| Log.e(TAG, "closeSession failed: ", e); |
| } |
| mMediaCryptoSession = null; |
| return false; |
| } |
| |
| Log.d( |
| TAG, |
| String.format("MediaCrypto Session created: %s", bytesToHexString(mMediaCryptoSession))); |
| |
| return true; |
| } |
| |
| /** |
| * 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(); |
| |
| // 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(); |
| } |
| |
| @RequiresApi(29) |
| private void getConnectedHdcpLevelInfoV29(MediaDrm mediaDrm) { |
| int hdcpLevel = mediaDrm.getConnectedHdcpLevel(); |
| switch (hdcpLevel) { |
| case MediaDrm.HDCP_V1: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_V1."); |
| break; |
| case MediaDrm.HDCP_V2: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2."); |
| break; |
| case MediaDrm.HDCP_V2_1: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_1."); |
| break; |
| case MediaDrm.HDCP_V2_2: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_2."); |
| break; |
| case MediaDrm.HDCP_V2_3: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_3."); |
| break; |
| case MediaDrm.HDCP_NONE: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_NONE."); |
| break; |
| case MediaDrm.HDCP_NO_DIGITAL_OUTPUT: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_NO_DIGITAL_OUTPUT."); |
| break; |
| case MediaDrm.HDCP_LEVEL_UNKNOWN: |
| Log.i(TAG, "MediaDrm HDCP Level is HDCP_LEVEL_UNKNOWN."); |
| break; |
| default: |
| Log.i(TAG, String.format(Locale.US, "Unknown MediaDrm HDCP level %d.", hdcpLevel)); |
| break; |
| } |
| } |
| |
| 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); |
| } |