1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.media;
7 import android.media.MediaCrypto;
8 import android.media.MediaDrm;
9 import android.os.AsyncTask;
10 import android.os.Handler;
11 import android.util.Log;
13 import org.apache.http.HttpResponse;
14 import org.apache.http.client.methods.HttpPost;
15 import org.apache.http.client.HttpClient;
16 import org.apache.http.client.ClientProtocolException;
17 import org.apache.http.impl.client.DefaultHttpClient;
18 import org.apache.http.util.EntityUtils;
19 import org.chromium.base.CalledByNative;
20 import org.chromium.base.JNINamespace;
22 import java.io.IOException;
23 import java.util.HashMap;
24 import java.util.UUID;
27 * A wrapper of the android MediaDrm class. Each MediaDrmBridge manages multiple
28 * sessions for a single MediaSourcePlayer.
30 @JNINamespace("media")
31 class MediaDrmBridge {
33 private static final String TAG = "MediaDrmBridge";
34 private static final String SECURITY_LEVEL = "securityLevel";
35 private static final String PRIVACY_MODE = "privacyMode";
36 private MediaDrm mMediaDrm;
37 private UUID mSchemeUUID;
38 private int mNativeMediaDrmBridge;
39 // TODO(qinmin): we currently only support one session per DRM bridge.
40 // Change this to a HashMap if we start to support multiple sessions.
41 private String mSessionId;
42 private MediaCrypto mMediaCrypto;
43 private String mMimeType;
44 private Handler mHandler;
45 private byte[] mPendingInitData;
46 private boolean mResetDeviceCredentialsPending;
48 private static UUID getUUIDFromBytes(byte[] data) {
49 if (data.length != 16) {
53 long leastSigBits = 0;
54 for (int i = 0; i < 8; i++) {
55 mostSigBits = (mostSigBits << 8) | (data[i] & 0xff);
57 for (int i = 8; i < 16; i++) {
58 leastSigBits = (leastSigBits << 8) | (data[i] & 0xff);
60 return new UUID(mostSigBits, leastSigBits);
63 private MediaDrmBridge(UUID schemeUUID, String securityLevel, int nativeMediaDrmBridge)
64 throws android.media.UnsupportedSchemeException {
65 mSchemeUUID = schemeUUID;
66 mMediaDrm = new MediaDrm(schemeUUID);
67 mHandler = new Handler();
68 mNativeMediaDrmBridge = nativeMediaDrmBridge;
69 mResetDeviceCredentialsPending = false;
70 mMediaDrm.setOnEventListener(new MediaDrmListener());
71 mMediaDrm.setPropertyString(PRIVACY_MODE, "enable");
72 String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL);
73 Log.e(TAG, "Security level: current " + currentSecurityLevel + ", new " + securityLevel);
74 if (!securityLevel.equals(currentSecurityLevel))
75 mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel);
79 * Create a MediaCrypto object.
81 * @return if a MediaCrypto object is successfully created.
83 private boolean createMediaCrypto() {
84 assert(mSessionId != null);
85 assert(mMediaCrypto == null);
87 final byte[] session = mSessionId.getBytes("UTF-8");
88 if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
89 mMediaCrypto = new MediaCrypto(mSchemeUUID, session);
91 } catch (android.media.MediaCryptoException e) {
92 Log.e(TAG, "Cannot create MediaCrypto " + e.toString());
94 } catch (java.io.UnsupportedEncodingException e) {
95 Log.e(TAG, "Cannot create MediaCrypto " + e.toString());
99 assert(mMediaCrypto != null);
100 nativeOnMediaCryptoReady(mNativeMediaDrmBridge);
105 * Open a new session and return the sessionId.
107 * @return false if unexpected error happens. Return true if a new session
108 * is successfully opened, or if provisioning is required to open a session.
110 private boolean openSession() {
111 assert(mSessionId == null);
113 if (mMediaDrm == null) {
118 final byte[] sessionId = mMediaDrm.openSession();
119 mSessionId = new String(sessionId, "UTF-8");
120 } catch (android.media.NotProvisionedException e) {
121 Log.e(TAG, "Cannot open a new session: " + e.toString());
123 } catch (Exception e) {
124 Log.e(TAG, "Cannot open a new session: " + e.toString());
128 assert(mSessionId != null);
129 return createMediaCrypto();
133 * Check whether the crypto scheme is supported for the given container.
134 * If |containerMimeType| is an empty string, we just return whether
135 * the crypto scheme is supported.
136 * TODO(qinmin): Implement the checking for container.
138 * @return true if the container and the crypto scheme is supported, or
142 private static boolean isCryptoSchemeSupported(byte[] schemeUUID, String containerMimeType) {
143 UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
144 return MediaDrm.isCryptoSchemeSupported(cryptoScheme);
148 * Create a new MediaDrmBridge from the crypto scheme UUID.
150 * @param schemeUUID Crypto scheme UUID.
151 * @param securityLevel Security level to be used.
152 * @param nativeMediaDrmBridge Native object of this class.
155 private static MediaDrmBridge create(
156 byte[] schemeUUID, String securityLevel, int nativeMediaDrmBridge) {
157 UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
158 if (cryptoScheme == null || !MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
162 MediaDrmBridge media_drm_bridge = null;
164 media_drm_bridge = new MediaDrmBridge(
165 cryptoScheme, securityLevel, nativeMediaDrmBridge);
166 } catch (android.media.UnsupportedSchemeException e) {
167 Log.e(TAG, "Unsupported DRM scheme: " + e.toString());
168 } catch (java.lang.IllegalArgumentException e) {
169 Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString());
170 } catch (java.lang.IllegalStateException e) {
171 Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString());
174 return media_drm_bridge;
178 * Return the MediaCrypto object if available.
181 private MediaCrypto getMediaCrypto() {
186 * Reset the device DRM credentials.
189 private void resetDeviceCredentials() {
190 mResetDeviceCredentialsPending = true;
191 MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
192 PostRequestTask postTask = new PostRequestTask(request.getData());
193 postTask.execute(request.getDefaultUrl());
197 * Release the MediaDrmBridge object.
200 private void release() {
201 if (mMediaCrypto != null) {
202 mMediaCrypto.release();
205 if (mSessionId != null) {
207 final byte[] session = mSessionId.getBytes("UTF-8");
208 mMediaDrm.closeSession(session);
209 } catch (java.io.UnsupportedEncodingException e) {
210 Log.e(TAG, "Failed to close session: " + e.toString());
214 if (mMediaDrm != null) {
221 * Generate a key request and post an asynchronous task to the native side
222 * with the response message.
224 * @param initData Data needed to generate the key request.
225 * @param mime Mime type.
228 private void generateKeyRequest(byte[] initData, String mime) {
229 Log.d(TAG, "generateKeyRequest().");
231 if (mMimeType == null) {
233 } else if (!mMimeType.equals(mime)) {
238 if (mSessionId == null) {
239 if (!openSession()) {
244 // NotProvisionedException happened during openSession().
245 if (mSessionId == null) {
246 if (mPendingInitData != null) {
247 Log.e(TAG, "generateKeyRequest called when another call is pending.");
252 // We assume MediaDrm.EVENT_PROVISION_REQUIRED is always fired if
253 // NotProvisionedException is throwed in openSession().
254 // generateKeyRequest() will be resumed after provisioning is finished.
255 // TODO(xhwang): Double check if this assumption is true. Otherwise we need
256 // to handle the exception in openSession more carefully.
257 mPendingInitData = initData;
263 final byte[] session = mSessionId.getBytes("UTF-8");
264 HashMap<String, String> optionalParameters = new HashMap<String, String>();
265 final MediaDrm.KeyRequest request = mMediaDrm.getKeyRequest(
266 session, initData, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
267 mHandler.post(new Runnable(){
269 nativeOnKeyMessage(mNativeMediaDrmBridge, mSessionId,
270 request.getData(), request.getDefaultUrl());
274 } catch (android.media.NotProvisionedException e) {
275 // MediaDrm.EVENT_PROVISION_REQUIRED is also fired in this case.
276 // Provisioning is handled in the handler of that event.
277 Log.e(TAG, "Cannot get key request: " + e.toString());
279 } catch (java.io.UnsupportedEncodingException e) {
280 Log.e(TAG, "Cannot get key request: " + e.toString());
286 * Cancel a key request for a session Id.
288 * @param sessionId Crypto session Id.
291 private void cancelKeyRequest(String sessionId) {
292 if (mSessionId == null || !mSessionId.equals(sessionId)) {
296 final byte[] session = sessionId.getBytes("UTF-8");
297 mMediaDrm.removeKeys(session);
298 } catch (java.io.UnsupportedEncodingException e) {
299 Log.e(TAG, "Cannot cancel key request: " + e.toString());
304 * Add a key for a session Id.
306 * @param sessionId Crypto session Id.
307 * @param key Response data from the server.
310 private void addKey(String sessionId, byte[] key) {
311 if (mSessionId == null || !mSessionId.equals(sessionId)) {
315 final byte[] session = sessionId.getBytes("UTF-8");
317 mMediaDrm.provideKeyResponse(session, key);
318 } catch (java.lang.IllegalStateException e) {
319 // This is not really an exception. Some error code are incorrectly
320 // reported as an exception.
321 // TODO(qinmin): remove this exception catch when b/10495563 is fixed.
322 Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse() "
325 mHandler.post(new Runnable() {
327 nativeOnKeyAdded(mNativeMediaDrmBridge, mSessionId);
331 } catch (android.media.NotProvisionedException e) {
332 Log.e(TAG, "failed to provide key response: " + e.toString());
333 } catch (android.media.DeniedByServerException e) {
334 Log.e(TAG, "failed to provide key response: " + e.toString());
335 } catch (java.io.UnsupportedEncodingException e) {
336 Log.e(TAG, "failed to provide key response: " + e.toString());
342 * Return the security level of this DRM object.
345 private String getSecurityLevel() {
346 return mMediaDrm.getPropertyString("securityLevel");
350 * Called when the provision response is received.
352 * @param response Response data from the provision server.
354 private void onProvisionResponse(byte[] response) {
355 Log.d(TAG, "onProvisionResponse()");
357 // If |mMediaDrm| is released, there is no need to callback native.
358 if (mMediaDrm == null) {
362 boolean success = provideProvisionResponse(response);
363 if (mResetDeviceCredentialsPending) {
364 nativeOnResetDeviceCredentialsCompleted(mNativeMediaDrmBridge, success);
365 mResetDeviceCredentialsPending = false;
375 * Provide the provisioning response to MediaDrm.
376 * @returns false if the response is invalid or on error, true otherwise.
378 boolean provideProvisionResponse(byte[] response) {
379 if (response == null || response.length == 0) {
380 Log.e(TAG, "Invalid provision response.");
385 mMediaDrm.provideProvisionResponse(response);
386 } catch (android.media.DeniedByServerException e) {
387 Log.e(TAG, "failed to provide provision response: " + e.toString());
389 } catch (java.lang.IllegalStateException e) {
390 Log.e(TAG, "failed to provide provision response: " + e.toString());
394 if (mPendingInitData != null) {
395 assert(!mResetDeviceCredentialsPending);
396 byte[] initData = mPendingInitData;
397 mPendingInitData = null;
398 generateKeyRequest(initData, mMimeType);
403 private void onKeyError() {
404 // TODO(qinmin): pass the error code to native.
405 mHandler.post(new Runnable() {
407 nativeOnKeyError(mNativeMediaDrmBridge, mSessionId);
412 private class MediaDrmListener implements MediaDrm.OnEventListener {
414 public void onEvent(MediaDrm mediaDrm, byte[] sessionId, int event, int extra,
417 case MediaDrm.EVENT_PROVISION_REQUIRED:
418 Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED.");
419 MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
420 PostRequestTask postTask = new PostRequestTask(request.getData());
421 postTask.execute(request.getDefaultUrl());
423 case MediaDrm.EVENT_KEY_REQUIRED:
424 Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED.");
425 generateKeyRequest(data, mMimeType);
427 case MediaDrm.EVENT_KEY_EXPIRED:
428 Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED.");
431 case MediaDrm.EVENT_VENDOR_DEFINED:
432 Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED.");
436 Log.e(TAG, "Invalid DRM event " + (int)event);
442 private class PostRequestTask extends AsyncTask<String, Void, Void> {
443 private static final String TAG = "PostRequestTask";
445 private byte[] mDrmRequest;
446 private byte[] mResponseBody;
448 public PostRequestTask(byte[] drmRequest) {
449 mDrmRequest = drmRequest;
453 protected Void doInBackground(String... urls) {
454 mResponseBody = postRequest(urls[0], mDrmRequest);
455 if (mResponseBody != null) {
456 Log.d(TAG, "response length=" + mResponseBody.length);
461 private byte[] postRequest(String url, byte[] drmRequest) {
462 HttpClient httpClient = new DefaultHttpClient();
463 HttpPost httpPost = new HttpPost(url + "&signedRequest=" + new String(drmRequest));
465 Log.d(TAG, "PostRequest:" + httpPost.getRequestLine());
468 httpPost.setHeader("Accept", "*/*");
469 httpPost.setHeader("User-Agent", "Widevine CDM v1.0");
470 httpPost.setHeader("Content-Type", "application/json");
472 // Execute HTTP Post Request
473 HttpResponse response = httpClient.execute(httpPost);
476 int responseCode = response.getStatusLine().getStatusCode();
477 if (responseCode == 200) {
478 responseBody = EntityUtils.toByteArray(response.getEntity());
480 Log.d(TAG, "Server returned HTTP error code " + responseCode);
484 } catch (ClientProtocolException e) {
486 } catch (IOException e) {
493 protected void onPostExecute(Void v) {
494 onProvisionResponse(mResponseBody);
498 private native void nativeOnMediaCryptoReady(int nativeMediaDrmBridge);
500 private native void nativeOnKeyMessage(int nativeMediaDrmBridge, String sessionId,
501 byte[] message, String destinationUrl);
503 private native void nativeOnKeyAdded(int nativeMediaDrmBridge, String sessionId);
505 private native void nativeOnKeyError(int nativeMediaDrmBridge, String sessionId);
507 private native void nativeOnResetDeviceCredentialsCompleted(
508 int nativeMediaDrmBridge, boolean success);