summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java440
1 files changed, 440 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
new file mode 100644
index 0000000000..10ca857448
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.PersistableBundle;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */
+@TargetApi(23)
+@RequiresApi(18)
+public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
+
+ private static final String TAG = "FrameworkMediaDrm";
+
+ /**
+ * {@link ExoMediaDrm.Provider} that returns a new {@link FrameworkMediaDrm} for the requested
+ * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID
+ * is not supported by the device.
+ */
+ public static final Provider<FrameworkMediaCrypto> DEFAULT_PROVIDER =
+ uuid -> {
+ try {
+ return newInstance(uuid);
+ } catch (UnsupportedDrmException e) {
+ Log.e(TAG, "Failed to instantiate a FrameworkMediaDrm for uuid: " + uuid + ".");
+ return new DummyExoMediaDrm<>();
+ }
+ };
+
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+ private static final String MOCK_LA_URL_VALUE = "https://x";
+ private static final String MOCK_LA_URL = "<LA_URL>" + MOCK_LA_URL_VALUE + "</LA_URL>";
+ private static final int UTF_16_BYTES_PER_CHARACTER = 2;
+
+ private final UUID uuid;
+ private final MediaDrm mediaDrm;
+ private int referenceCount;
+
+ /**
+ * Creates an instance with an initial reference count of 1. {@link #release()} must be called on
+ * the instance when it's no longer required.
+ *
+ * @param uuid The scheme uuid.
+ * @return The created instance.
+ * @throws UnsupportedDrmException If the DRM scheme is unsupported or cannot be instantiated.
+ */
+ public static FrameworkMediaDrm newInstance(UUID uuid) throws UnsupportedDrmException {
+ try {
+ return new FrameworkMediaDrm(uuid);
+ } catch (UnsupportedSchemeException e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
+ } catch (Exception e) {
+ throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
+ }
+ }
+
+ private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
+ Assertions.checkNotNull(uuid);
+ Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
+ this.uuid = uuid;
+ this.mediaDrm = new MediaDrm(adjustUuid(uuid));
+ // Creators of an instance automatically acquire ownership of the created instance.
+ referenceCount = 1;
+ if (C.WIDEVINE_UUID.equals(uuid) && needsForceWidevineL3Workaround()) {
+ forceWidevineL3(mediaDrm);
+ }
+ }
+
+ @Override
+ public void setOnEventListener(
+ final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
+ mediaDrm.setOnEventListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, event, extra, data) ->
+ listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data));
+ }
+
+ @Override
+ public void setOnKeyStatusChangeListener(
+ final ExoMediaDrm.OnKeyStatusChangeListener<? super FrameworkMediaCrypto> listener) {
+ if (Util.SDK_INT < 23) {
+ throw new UnsupportedOperationException();
+ }
+
+ mediaDrm.setOnKeyStatusChangeListener(
+ listener == null
+ ? null
+ : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> {
+ List<KeyStatus> exoKeyInfo = new ArrayList<>();
+ for (MediaDrm.KeyStatus keyStatus : keyInfo) {
+ exoKeyInfo.add(new KeyStatus(keyStatus.getStatusCode(), keyStatus.getKeyId()));
+ }
+ listener.onKeyStatusChange(
+ FrameworkMediaDrm.this, sessionId, exoKeyInfo, hasNewUsableKey);
+ },
+ null);
+ }
+
+ @Override
+ public byte[] openSession() throws MediaDrmException {
+ return mediaDrm.openSession();
+ }
+
+ @Override
+ public void closeSession(byte[] sessionId) {
+ mediaDrm.closeSession(sessionId);
+ }
+
+ @Override
+ public KeyRequest getKeyRequest(
+ byte[] scope,
+ @Nullable List<DrmInitData.SchemeData> schemeDatas,
+ int keyType,
+ @Nullable HashMap<String, String> optionalParameters)
+ throws NotProvisionedException {
+ SchemeData schemeData = null;
+ byte[] initData = null;
+ String mimeType = null;
+ if (schemeDatas != null) {
+ schemeData = getSchemeData(uuid, schemeDatas);
+ initData = adjustRequestInitData(uuid, Assertions.checkNotNull(schemeData.data));
+ mimeType = adjustRequestMimeType(uuid, schemeData.mimeType);
+ }
+ MediaDrm.KeyRequest request =
+ mediaDrm.getKeyRequest(scope, initData, mimeType, keyType, optionalParameters);
+
+ byte[] requestData = adjustRequestData(uuid, request.getData());
+
+ String licenseServerUrl = request.getDefaultUrl();
+ if (MOCK_LA_URL_VALUE.equals(licenseServerUrl)) {
+ licenseServerUrl = "";
+ }
+ if (TextUtils.isEmpty(licenseServerUrl)
+ && schemeData != null
+ && !TextUtils.isEmpty(schemeData.licenseServerUrl)) {
+ licenseServerUrl = schemeData.licenseServerUrl;
+ }
+
+ return new KeyRequest(requestData, licenseServerUrl);
+ }
+
+ @Nullable
+ @Override
+ public byte[] provideKeyResponse(byte[] scope, byte[] response)
+ throws NotProvisionedException, DeniedByServerException {
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ response = ClearKeyUtil.adjustResponseData(response);
+ }
+
+ return mediaDrm.provideKeyResponse(scope, response);
+ }
+
+ @Override
+ public ProvisionRequest getProvisionRequest() {
+ final MediaDrm.ProvisionRequest request = mediaDrm.getProvisionRequest();
+ return new ProvisionRequest(request.getData(), request.getDefaultUrl());
+ }
+
+ @Override
+ public void provideProvisionResponse(byte[] response) throws DeniedByServerException {
+ mediaDrm.provideProvisionResponse(response);
+ }
+
+ @Override
+ public Map<String, String> queryKeyStatus(byte[] sessionId) {
+ return mediaDrm.queryKeyStatus(sessionId);
+ }
+
+ @Override
+ public synchronized void acquire() {
+ Assertions.checkState(referenceCount > 0);
+ referenceCount++;
+ }
+
+ @Override
+ public synchronized void release() {
+ if (--referenceCount == 0) {
+ mediaDrm.release();
+ }
+ }
+
+ @Override
+ public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+ mediaDrm.restoreKeys(sessionId, keySetId);
+ }
+
+ @Override
+ @Nullable
+ @TargetApi(28)
+ public PersistableBundle getMetrics() {
+ if (Util.SDK_INT < 28) {
+ return null;
+ }
+ return mediaDrm.getMetrics();
+ }
+
+ @Override
+ public String getPropertyString(String propertyName) {
+ return mediaDrm.getPropertyString(propertyName);
+ }
+
+ @Override
+ public byte[] getPropertyByteArray(String propertyName) {
+ return mediaDrm.getPropertyByteArray(propertyName);
+ }
+
+ @Override
+ public void setPropertyString(String propertyName, String value) {
+ mediaDrm.setPropertyString(propertyName, value);
+ }
+
+ @Override
+ public void setPropertyByteArray(String propertyName, byte[] value) {
+ mediaDrm.setPropertyByteArray(propertyName, value);
+ }
+
+ @Override
+ public FrameworkMediaCrypto createMediaCrypto(byte[] initData) throws MediaCryptoException {
+ // Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still
+ // indicate that it required secure video decoders [Internal ref: b/11428937].
+ boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
+ && C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
+ return new FrameworkMediaCrypto(
+ adjustUuid(uuid), initData, forceAllowInsecureDecoderComponents);
+ }
+
+ @Override
+ public Class<FrameworkMediaCrypto> getExoMediaCryptoType() {
+ return FrameworkMediaCrypto.class;
+ }
+
+ private static SchemeData getSchemeData(UUID uuid, List<SchemeData> schemeDatas) {
+ if (!C.WIDEVINE_UUID.equals(uuid)) {
+ // For non-Widevine CDMs always use the first scheme data.
+ return schemeDatas.get(0);
+ }
+
+ if (Util.SDK_INT >= 28 && schemeDatas.size() > 1) {
+ // For API level 28 and above, concatenate multiple PSSH scheme datas if possible.
+ SchemeData firstSchemeData = schemeDatas.get(0);
+ int concatenatedDataLength = 0;
+ boolean canConcatenateData = true;
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ byte[] schemeDataData = Util.castNonNull(schemeData.data);
+ if (Util.areEqual(schemeData.mimeType, firstSchemeData.mimeType)
+ && Util.areEqual(schemeData.licenseServerUrl, firstSchemeData.licenseServerUrl)
+ && PsshAtomUtil.isPsshAtom(schemeDataData)) {
+ concatenatedDataLength += schemeDataData.length;
+ } else {
+ canConcatenateData = false;
+ break;
+ }
+ }
+ if (canConcatenateData) {
+ byte[] concatenatedData = new byte[concatenatedDataLength];
+ int concatenatedDataPosition = 0;
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ byte[] schemeDataData = Util.castNonNull(schemeData.data);
+ int schemeDataLength = schemeDataData.length;
+ System.arraycopy(
+ schemeDataData, 0, concatenatedData, concatenatedDataPosition, schemeDataLength);
+ concatenatedDataPosition += schemeDataLength;
+ }
+ return firstSchemeData.copyWithData(concatenatedData);
+ }
+ }
+
+ // For API levels 23 - 27, prefer the first V1 PSSH box. For API levels 22 and earlier, prefer
+ // the first V0 box.
+ for (int i = 0; i < schemeDatas.size(); i++) {
+ SchemeData schemeData = schemeDatas.get(i);
+ int version = PsshAtomUtil.parseVersion(Util.castNonNull(schemeData.data));
+ if (Util.SDK_INT < 23 && version == 0) {
+ return schemeData;
+ } else if (Util.SDK_INT >= 23 && version == 1) {
+ return schemeData;
+ }
+ }
+
+ // If all else fails, use the first scheme data.
+ return schemeDatas.get(0);
+ }
+
+ private static UUID adjustUuid(UUID uuid) {
+ // ClearKey had to be accessed using the Common PSSH UUID prior to API level 27.
+ return Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;
+ }
+
+ private static byte[] adjustRequestInitData(UUID uuid, byte[] initData) {
+ // TODO: Add API level check once [Internal ref: b/112142048] is fixed.
+ if (C.PLAYREADY_UUID.equals(uuid)) {
+ byte[] schemeSpecificData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);
+ if (schemeSpecificData == null) {
+ // The init data is not contained in a pssh box.
+ schemeSpecificData = initData;
+ }
+ initData =
+ PsshAtomUtil.buildPsshAtom(
+ C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData));
+ }
+
+ // Prior to API level 21, the Widevine CDM required scheme specific data to be extracted from
+ // the PSSH atom. We also extract the data on API levels 21 and 22 because these API levels
+ // don't handle V1 PSSH atoms, but do handle scheme specific data regardless of whether it's
+ // extracted from a V0 or a V1 PSSH atom. Hence extracting the data allows us to support content
+ // that only provides V1 PSSH atoms. API levels 23 and above understand V0 and V1 PSSH atoms,
+ // and so we do not extract the data.
+ // Some Amazon devices also require data to be extracted from the PSSH atom for PlayReady.
+ if ((Util.SDK_INT < 23 && C.WIDEVINE_UUID.equals(uuid))
+ || (C.PLAYREADY_UUID.equals(uuid)
+ && "Amazon".equals(Util.MANUFACTURER)
+ && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
+ || "AFTS".equals(Util.MODEL) // Fire TV Gen 2
+ || "AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
+ || "AFTT".equals(Util.MODEL)))) { // Fire TV Stick Gen 2
+ byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);
+ if (psshData != null) {
+ // Extraction succeeded, so return the extracted data.
+ return psshData;
+ }
+ }
+ return initData;
+ }
+
+ private static String adjustRequestMimeType(UUID uuid, String mimeType) {
+ // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
+ if (Util.SDK_INT < 26
+ && C.CLEARKEY_UUID.equals(uuid)
+ && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {
+ return CENC_SCHEME_MIME_TYPE;
+ }
+ return mimeType;
+ }
+
+ private static byte[] adjustRequestData(UUID uuid, byte[] requestData) {
+ if (C.CLEARKEY_UUID.equals(uuid)) {
+ return ClearKeyUtil.adjustRequestData(requestData);
+ }
+ return requestData;
+ }
+
+ @SuppressLint("WrongConstant") // Suppress spurious lint error [Internal ref: b/32137960]
+ private static void forceWidevineL3(MediaDrm mediaDrm) {
+ mediaDrm.setPropertyString("securityLevel", "L3");
+ }
+
+ /**
+ * Returns whether the device codec is known to fail if security level L1 is used.
+ *
+ * <p>See <a href="https://github.com/google/ExoPlayer/issues/4413">GitHub issue #4413</a>.
+ */
+ private static boolean needsForceWidevineL3Workaround() {
+ return "ASUS_Z00AD".equals(Util.MODEL);
+ }
+
+ /**
+ * If the LA_URL tag is missing, injects a mock LA_URL value to avoid causing the CDM to throw
+ * when creating the key request. The LA_URL attribute is optional but some Android PlayReady
+ * implementations are known to require it. Does nothing it the provided {@code data} already
+ * contains an LA_URL value.
+ */
+ private static byte[] addLaUrlAttributeIfMissing(byte[] data) {
+ ParsableByteArray byteArray = new ParsableByteArray(data);
+ // See https://docs.microsoft.com/en-us/playready/specifications/specifications for more
+ // information about the init data format.
+ int length = byteArray.readLittleEndianInt();
+ int objectRecordCount = byteArray.readLittleEndianShort();
+ int recordType = byteArray.readLittleEndianShort();
+ if (objectRecordCount != 1 || recordType != 1) {
+ Log.i(TAG, "Unexpected record count or type. Skipping LA_URL workaround.");
+ return data;
+ }
+ int recordLength = byteArray.readLittleEndianShort();
+ String xml = byteArray.readString(recordLength, Charset.forName(C.UTF16LE_NAME));
+ if (xml.contains("<LA_URL>")) {
+ // LA_URL already present. Do nothing.
+ return data;
+ }
+ // This PlayReady object record does not include an LA_URL. We add a mock value for it.
+ int endOfDataTagIndex = xml.indexOf("</DATA>");
+ if (endOfDataTagIndex == -1) {
+ Log.w(TAG, "Could not find the </DATA> tag. Skipping LA_URL workaround.");
+ }
+ String xmlWithMockLaUrl =
+ xml.substring(/* beginIndex= */ 0, /* endIndex= */ endOfDataTagIndex)
+ + MOCK_LA_URL
+ + xml.substring(/* beginIndex= */ endOfDataTagIndex);
+ int extraBytes = MOCK_LA_URL.length() * UTF_16_BYTES_PER_CHARACTER;
+ ByteBuffer newData = ByteBuffer.allocate(length + extraBytes);
+ newData.order(ByteOrder.LITTLE_ENDIAN);
+ newData.putInt(length + extraBytes);
+ newData.putShort((short) objectRecordCount);
+ newData.putShort((short) recordType);
+ newData.putShort((short) (xmlWithMockLaUrl.length() * UTF_16_BYTES_PER_CHARACTER));
+ newData.put(xmlWithMockLaUrl.getBytes(Charset.forName(C.UTF16LE_NAME)));
+ return newData.array();
+ }
+}