mirror of
https://github.com/androidx/media.git
synced 2025-05-04 06:00:37 +08:00
Work around incorrect ClearKey encoding prior to O-MR1
Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175006223
This commit is contained in:
parent
15543f13b7
commit
acf5247f6e
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
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 static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]");
|
||||||
|
|
||||||
|
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 rather
|
||||||
|
// than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request
|
||||||
|
// as a result were not escaped as "\/". We know the exact request format from the platform's
|
||||||
|
// InitDataParser.cpp, so we can use a regexp rather than parsing the JSON.
|
||||||
|
String requestString = Util.fromUtf8Bytes(request);
|
||||||
|
Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString);
|
||||||
|
if (!requestKidsMatcher.find()) {
|
||||||
|
Log.e(TAG, "Failed to adjust request data: " + requestString);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
int kidsStartIndex = requestKidsMatcher.start(1);
|
||||||
|
int kidsEndIndex = requestKidsMatcher.end(1);
|
||||||
|
StringBuilder adjustedRequestBuilder = new StringBuilder(requestString);
|
||||||
|
base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex);
|
||||||
|
return Util.getUtf8Bytes(adjustedRequestBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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].
|
||||||
|
try {
|
||||||
|
JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response));
|
||||||
|
JSONArray keysArray = responseJson.getJSONArray("keys");
|
||||||
|
for (int i = 0; i < keysArray.length(); i++) {
|
||||||
|
JSONObject key = keysArray.getJSONObject(i);
|
||||||
|
key.put("k", base64UrlToBase64(key.getString("k")));
|
||||||
|
key.put("kid", base64UrlToBase64(key.getString("kid")));
|
||||||
|
}
|
||||||
|
return Util.getUtf8Bytes(responseJson.toString());
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) {
|
||||||
|
for (int i = startIndex; i < endIndex; i++) {
|
||||||
|
switch (base64.charAt(i)) {
|
||||||
|
case '+':
|
||||||
|
base64.setCharAt(i, '-');
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
base64.setCharAt(i, '_');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String base64UrlToBase64(String base64) {
|
||||||
|
return base64.replace('-', '+').replace('_', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -362,6 +362,20 @@ import java.util.UUID;
|
|||||||
try {
|
try {
|
||||||
KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
|
KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
|
||||||
optionalKeyRequestParameters);
|
optionalKeyRequestParameters);
|
||||||
|
if (C.CLEARKEY_UUID.equals(uuid)) {
|
||||||
|
final byte[] data = ClearKeyUtil.adjustRequestData(request.getData());
|
||||||
|
final String defaultUrl = request.getDefaultUrl();
|
||||||
|
request = new KeyRequest() {
|
||||||
|
@Override
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String getDefaultUrl() {
|
||||||
|
return defaultUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
|
postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
onKeysError(e);
|
onKeysError(e);
|
||||||
@ -380,8 +394,12 @@ import java.util.UUID;
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
byte[] responseData = (byte[]) response;
|
||||||
|
if (C.CLEARKEY_UUID.equals(uuid)) {
|
||||||
|
responseData = ClearKeyUtil.adjustResponseData(responseData);
|
||||||
|
}
|
||||||
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
|
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
|
||||||
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
|
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
|
||||||
if (eventHandler != null && eventListener != null) {
|
if (eventHandler != null && eventListener != null) {
|
||||||
eventHandler.post(new Runnable() {
|
eventHandler.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -391,7 +409,7 @@ import java.util.UUID;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
|
||||||
if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
|
if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
|
||||||
|| (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null))
|
|| (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null))
|
||||||
&& keySetId != null && keySetId.length != 0) {
|
&& keySetId != null && keySetId.length != 0) {
|
||||||
|
@ -25,6 +25,7 @@ import android.media.MediaDrmException;
|
|||||||
import android.media.NotProvisionedException;
|
import android.media.NotProvisionedException;
|
||||||
import android.media.UnsupportedSchemeException;
|
import android.media.UnsupportedSchemeException;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
@ -74,7 +75,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
|
|||||||
final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
|
final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
|
||||||
mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() {
|
mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(@NonNull MediaDrm md, @NonNull byte[] sessionId, int event, int extra,
|
public void onEvent(@NonNull MediaDrm md, @Nullable byte[] sessionId, int event, int extra,
|
||||||
byte[] data) {
|
byte[] data) {
|
||||||
listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data);
|
listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data);
|
||||||
}
|
}
|
||||||
|
@ -246,6 +246,16 @@ public final class Util {
|
|||||||
return language == null ? null : new Locale(language).getLanguage();
|
return language == null ? null : new Locale(language).getLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link String} constructed by decoding UTF-8 encoded bytes.
|
||||||
|
*
|
||||||
|
* @param bytes The UTF-8 encoded bytes to decode.
|
||||||
|
* @return The string.
|
||||||
|
*/
|
||||||
|
public static String fromUtf8Bytes(byte[] bytes) {
|
||||||
|
return new String(bytes, Charset.forName(C.UTF8_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
|
* Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for {@link ClearKeyUtil}.
|
||||||
|
*/
|
||||||
|
// TODO: When API level 27 is supported, add tests that check the adjust methods are no-ops.
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public final class ClearKeyUtilTest {
|
||||||
|
|
||||||
|
@Config(sdk = 26, manifest = Config.NONE)
|
||||||
|
@Test
|
||||||
|
public void testAdjustResponseDataV26() {
|
||||||
|
byte[] data = ("{\"keys\":[{"
|
||||||
|
+ "\"k\":\"abc_def-\","
|
||||||
|
+ "\"kid\":\"ab_cde-f\"}],"
|
||||||
|
+ "\"type\":\"abc_def-"
|
||||||
|
+ "\"}").getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
// We expect "-" and "_" to be replaced with "+" and "\/" (forward slashes need to be escaped in
|
||||||
|
// JSON respectively, for "k" and "kid" only.
|
||||||
|
byte[] expected = ("{\"keys\":[{"
|
||||||
|
+ "\"k\":\"abc\\/def+\","
|
||||||
|
+ "\"kid\":\"ab\\/cde+f\"}],"
|
||||||
|
+ "\"type\":\"abc_def-"
|
||||||
|
+ "\"}").getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
assertThat(Arrays.equals(expected, ClearKeyUtil.adjustResponseData(data))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Config(sdk = 26, manifest = Config.NONE)
|
||||||
|
@Test
|
||||||
|
public void testAdjustRequestDataV26() {
|
||||||
|
byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}"
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
// We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids".
|
||||||
|
byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}"
|
||||||
|
.getBytes(Charset.forName(C.UTF8_NAME));
|
||||||
|
assertThat(Arrays.equals(expected, ClearKeyUtil.adjustRequestData(data))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user