/* * 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 { 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 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 = "" + MOCK_LA_URL_VALUE + ""; 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 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 listener) { if (Util.SDK_INT < 23) { throw new UnsupportedOperationException(); } mediaDrm.setOnKeyStatusChangeListener( listener == null ? null : (mediaDrm, sessionId, keyInfo, hasNewUsableKey) -> { List 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 schemeDatas, int keyType, @Nullable HashMap 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 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 getExoMediaCryptoType() { return FrameworkMediaCrypto.class; } private static SchemeData getSchemeData(UUID uuid, List 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. * *

See GitHub issue #4413. */ 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 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(""); if (endOfDataTagIndex == -1) { Log.w(TAG, "Could not find the 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(); } }