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. |
4 | |
5 | package org.chromium.media; |
6 | |
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; |
12 | |
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; |
21 | |
22 | import java.io.IOException; |
23 | import java.util.HashMap; |
24 | import java.util.UUID; |
25 | |
26 | /** |
27 | * A wrapper of the android MediaDrm class. Each MediaDrmBridge manages multiple |
28 | * sessions for a single MediaSourcePlayer. |
29 | */ |
30 | @JNINamespace("media") |
31 | class MediaDrmBridge { |
32 | |
33 | private static final String TAG = "MediaDrmBridge"; |
34 | private MediaDrm mMediaDrm; |
35 | private UUID mSchemeUUID; |
36 | private int mNativeMediaDrmBridge; |
37 | // TODO(qinmin): we currently only support one session per DRM bridge. |
38 | // Change this to a HashMap if we start to support multiple sessions. |
39 | private String mSessionId; |
40 | private MediaCrypto mMediaCrypto; |
41 | private String mMimeType; |
42 | private Handler mhandler; |
43 | |
44 | private static UUID getUUIDFromBytes(byte[] data) { |
45 | if (data.length != 16) { |
46 | return null; |
47 | } |
48 | long mostSigBits = 0; |
49 | long leastSigBits = 0; |
50 | for (int i = 0; i < 8; i++) { |
51 | mostSigBits = (mostSigBits << 8) | (data[i] & 0xff); |
52 | } |
53 | for (int i = 8; i < 16; i++) { |
54 | leastSigBits = (leastSigBits << 8) | (data[i] & 0xff); |
55 | } |
56 | return new UUID(mostSigBits, leastSigBits); |
57 | } |
58 | |
59 | private MediaDrmBridge(UUID schemeUUID, int nativeMediaDrmBridge) { |
60 | try { |
61 | mSchemeUUID = schemeUUID; |
62 | mMediaDrm = new MediaDrm(schemeUUID); |
63 | mNativeMediaDrmBridge = nativeMediaDrmBridge; |
64 | mMediaDrm.setOnEventListener(new MediaDrmListener()); |
65 | mSessionId = openSession(); |
66 | mhandler = new Handler(); |
67 | } catch (android.media.UnsupportedSchemeException e) { |
68 | Log.e(TAG, "Unsupported DRM scheme " + e.toString()); |
69 | } |
70 | } |
71 | |
72 | /** |
73 | * Open a new session and return the sessionId. |
74 | * |
75 | * @return ID of the session. |
76 | */ |
77 | private String openSession() { |
78 | String session = null; |
79 | try { |
80 | final byte[] sessionId = mMediaDrm.openSession(); |
81 | session = new String(sessionId, "UTF-8"); |
82 | } catch (android.media.NotProvisionedException e) { |
83 | Log.e(TAG, "Cannot open a new session " + e.toString()); |
84 | } catch (java.io.UnsupportedEncodingException e) { |
85 | Log.e(TAG, "Cannot open a new session " + e.toString()); |
86 | } |
87 | return session; |
88 | } |
89 | |
90 | /** |
91 | * Create a new MediaDrmBridge from the crypto scheme UUID. |
92 | * |
93 | * @param schemeUUID Crypto scheme UUID. |
94 | * @param nativeMediaDrmBridge Native object of this class. |
95 | */ |
96 | @CalledByNative |
97 | private static MediaDrmBridge create(byte[] schemeUUID, int nativeMediaDrmBridge) { |
98 | UUID cryptoScheme = getUUIDFromBytes(schemeUUID); |
99 | if (cryptoScheme != null && MediaDrm.isCryptoSchemeSupported(cryptoScheme)) { |
100 | return new MediaDrmBridge(cryptoScheme, nativeMediaDrmBridge); |
101 | } |
102 | return null; |
103 | } |
104 | |
105 | /** |
106 | * Create a new MediaCrypto object from the session Id. |
107 | * |
108 | * @param sessionId Crypto session Id. |
109 | */ |
110 | @CalledByNative |
111 | private MediaCrypto getMediaCrypto() { |
112 | if (mMediaCrypto != null) { |
113 | return mMediaCrypto; |
114 | } |
115 | try { |
116 | final byte[] session = mSessionId.getBytes("UTF-8"); |
117 | if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { |
118 | mMediaCrypto = new MediaCrypto(mSchemeUUID, session); |
119 | } |
120 | } catch (android.media.MediaCryptoException e) { |
121 | Log.e(TAG, "Cannot create MediaCrypto " + e.toString()); |
122 | } catch (java.io.UnsupportedEncodingException e) { |
123 | Log.e(TAG, "Cannot create MediaCrypto " + e.toString()); |
124 | } |
125 | return mMediaCrypto; |
126 | } |
127 | |
128 | /** |
129 | * Release the MediaDrmBridge object. |
130 | */ |
131 | @CalledByNative |
132 | private void release() { |
133 | if (mMediaCrypto != null) { |
134 | mMediaCrypto.release(); |
135 | } |
136 | if (mSessionId != null) { |
137 | try { |
138 | final byte[] session = mSessionId.getBytes("UTF-8"); |
139 | mMediaDrm.closeSession(session); |
140 | } catch (java.io.UnsupportedEncodingException e) { |
141 | Log.e(TAG, "Failed to close session " + e.toString()); |
142 | } |
143 | } |
144 | mMediaDrm.release(); |
145 | } |
146 | |
147 | /** |
148 | * Generate a key request and post an asynchronous task to the native side |
149 | * with the response message. |
150 | * |
151 | * @param initData Data needed to generate the key request. |
152 | * @param mime Mime type. |
153 | */ |
154 | @CalledByNative |
155 | private void generateKeyRequest(byte[] initData, String mime) { |
156 | if (mSessionId == null) { |
157 | return; |
158 | } |
159 | try { |
160 | final byte[] session = mSessionId.getBytes("UTF-8"); |
161 | mMimeType = mime; |
162 | HashMap<String, String> optionalParameters = new HashMap<String, String>(); |
163 | final MediaDrm.KeyRequest request = mMediaDrm.getKeyRequest( |
164 | session, initData, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters); |
165 | mhandler.post(new Runnable(){ |
166 | public void run() { |
167 | nativeOnKeyMessage(mNativeMediaDrmBridge, mSessionId, |
168 | request.getData(), request.getDefaultUrl()); |
169 | } |
170 | }); |
171 | return; |
172 | } catch (android.media.NotProvisionedException e) { |
173 | Log.e(TAG, "Cannot get key request " + e.toString()); |
174 | } catch (java.io.UnsupportedEncodingException e) { |
175 | Log.e(TAG, "Cannot get key request " + e.toString()); |
176 | } |
177 | onKeyError(); |
178 | } |
179 | |
180 | /** |
181 | * Cancel a key request for a session Id. |
182 | * |
183 | * @param sessionId Crypto session Id. |
184 | */ |
185 | @CalledByNative |
186 | private void cancelKeyRequest(String sessionId) { |
187 | if (mSessionId == null || !mSessionId.equals(sessionId)) { |
188 | return; |
189 | } |
190 | try { |
191 | final byte[] session = sessionId.getBytes("UTF-8"); |
192 | mMediaDrm.removeKeys(session); |
193 | } catch (java.io.UnsupportedEncodingException e) { |
194 | Log.e(TAG, "Cannot cancel key request " + e.toString()); |
195 | } |
196 | } |
197 | |
198 | /** |
199 | * Add a key for a session Id. |
200 | * |
201 | * @param sessionId Crypto session Id. |
202 | * @param key Response data from the server. |
203 | */ |
204 | @CalledByNative |
205 | private void addKey(String sessionId, byte[] key) { |
206 | if (mSessionId == null || !mSessionId.equals(sessionId)) { |
207 | return; |
208 | } |
209 | try { |
210 | final byte[] session = sessionId.getBytes("UTF-8"); |
211 | mMediaDrm.provideKeyResponse(session, key); |
212 | mhandler.post(new Runnable() { |
213 | public void run() { |
214 | nativeOnKeyAdded(mNativeMediaDrmBridge, mSessionId); |
215 | } |
216 | }); |
217 | return; |
218 | } catch (android.media.NotProvisionedException e) { |
219 | Log.e(TAG, "failed to provide key response " + e.toString()); |
220 | } catch (android.media.DeniedByServerException e) { |
221 | Log.e(TAG, "failed to provide key response " + e.toString()); |
222 | } catch (java.io.UnsupportedEncodingException e) { |
223 | Log.e(TAG, "failed to provide key response " + e.toString()); |
224 | } |
225 | onKeyError(); |
226 | } |
227 | |
228 | /** |
229 | * Called when the provision response is received. |
230 | * |
231 | * @param response Response data from the provision server. |
232 | */ |
233 | private void onProvisionResponse(byte[] response) { |
234 | try { |
235 | mMediaDrm.provideProvisionResponse(response); |
236 | } catch (android.media.DeniedByServerException e) { |
237 | Log.e(TAG, "failed to provide key response " + e.toString()); |
238 | } |
239 | } |
240 | |
241 | private void onKeyError() { |
242 | // TODO(qinmin): pass the error code to native. |
243 | mhandler.post(new Runnable() { |
244 | public void run() { |
245 | nativeOnKeyError(mNativeMediaDrmBridge, mSessionId); |
246 | } |
247 | }); |
248 | } |
249 | |
250 | private class MediaDrmListener implements MediaDrm.OnEventListener { |
251 | @Override |
252 | public void onEvent(MediaDrm mediaDrm, byte[] sessionId, int event, int extra, |
253 | byte[] data) { |
254 | switch(event) { |
255 | case MediaDrm.EVENT_PROVISION_REQUIRED: |
256 | MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest(); |
257 | PostRequestTask postTask = new PostRequestTask(request.getData()); |
258 | postTask.execute(request.getDefaultUrl()); |
259 | break; |
260 | case MediaDrm.EVENT_KEY_REQUIRED: |
261 | generateKeyRequest(data, mMimeType); |
262 | break; |
263 | case MediaDrm.EVENT_KEY_EXPIRED: |
264 | onKeyError(); |
265 | break; |
266 | case MediaDrm.EVENT_VENDOR_DEFINED: |
267 | assert(false); |
268 | break; |
269 | default: |
270 | Log.e(TAG, "Invalid DRM event " + (int)event); |
271 | return; |
272 | } |
273 | } |
274 | } |
275 | |
276 | private class PostRequestTask extends AsyncTask<String, Void, Void> { |
277 | private static final String TAG = "PostRequestTask"; |
278 | |
279 | private byte[] mDrmRequest; |
280 | private byte[] mResponseBody; |
281 | |
282 | public PostRequestTask(byte[] drmRequest) { |
283 | mDrmRequest = drmRequest; |
284 | } |
285 | |
286 | @Override |
287 | protected Void doInBackground(String... urls) { |
288 | mResponseBody = postRequest(urls[0], mDrmRequest); |
289 | if (mResponseBody != null) { |
290 | Log.d(TAG, "response length=" + mResponseBody.length); |
291 | } |
292 | return null; |
293 | } |
294 | |
295 | private byte[] postRequest(String url, byte[] drmRequest) { |
296 | HttpClient httpClient = new DefaultHttpClient(); |
297 | HttpPost httpPost = new HttpPost(url + "&signedRequest=" + new String(drmRequest)); |
298 | |
299 | Log.d(TAG, "PostRequest:" + httpPost.getRequestLine()); |
300 | try { |
301 | // Add data |
302 | httpPost.setHeader("Accept", "*/*"); |
303 | httpPost.setHeader("User-Agent", "Widevine CDM v1.0"); |
304 | httpPost.setHeader("Content-Type", "application/json"); |
305 | |
306 | // Execute HTTP Post Request |
307 | HttpResponse response = httpClient.execute(httpPost); |
308 | |
309 | byte[] responseBody; |
310 | int responseCode = response.getStatusLine().getStatusCode(); |
311 | if (responseCode == 200) { |
312 | responseBody = EntityUtils.toByteArray(response.getEntity()); |
313 | } else { |
314 | Log.d(TAG, "Server returned HTTP error code " + responseCode); |
315 | return null; |
316 | } |
317 | return responseBody; |
318 | } catch (ClientProtocolException e) { |
319 | e.printStackTrace(); |
320 | } catch (IOException e) { |
321 | e.printStackTrace(); |
322 | } |
323 | return null; |
324 | } |
325 | |
326 | @Override |
327 | protected void onPostExecute(Void v) { |
328 | onProvisionResponse(mResponseBody); |
329 | } |
330 | } |
331 | |
332 | private native void nativeOnKeyMessage(int nativeMediaDrmBridge, String sessionId, |
333 | byte[] message, String destinationUrl); |
334 | |
335 | private native void nativeOnKeyAdded(int nativeMediaDrmBridge, String sessionId); |
336 | |
337 | private native void nativeOnKeyError(int nativeMediaDrmBridge, String sessionId); |
338 | } |