diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c3e1c78b0..46a1117202 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes # +### 2.8.3 ### + +* DRM: + * Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). + ### 2.8.2 ### * IMA: Don't advertise support for video/mpeg ad media, as we don't have an diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index c57b023139..fbbbcbc9ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -22,11 +22,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; -import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.Arrays; @@ -77,8 +78,7 @@ import java.util.UUID; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; - private final byte[] initData; - private final String mimeType; + private final SchemeData schemeData; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; @@ -103,9 +103,11 @@ import java.util.UUID; * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. - * @param initData The DRM init data. + * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is + * provided. * @param mode The DRM mode. - * @param offlineLicenseKeySetId The offlineLicense KeySetId. + * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using + * offline keys. * @param optionalKeyRequestParameters The optional key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. @@ -117,10 +119,9 @@ import java.util.UUID; UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, - byte[] initData, - String mimeType, + @Nullable SchemeData schemeData, @DefaultDrmSessionManager.Mode int mode, - byte[] offlineLicenseKeySetId, + @Nullable byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, @@ -131,6 +132,7 @@ import java.util.UUID; this.mediaDrm = mediaDrm; this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.schemeData = offlineLicenseKeySetId == null ? schemeData : null; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; @@ -141,14 +143,6 @@ import java.util.UUID; requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - this.initData = initData; - this.mimeType = mimeType; - } else { - this.initData = null; - this.mimeType = null; - } } // Life cycle. @@ -187,13 +181,37 @@ import java.util.UUID; } public boolean hasInitData(byte[] initData) { - return Arrays.equals(this.initData, initData); + return Arrays.equals(schemeData != null ? schemeData.data : null, initData); } public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; + } + switch (what) { + case ExoMediaDrm.EVENT_KEY_REQUIRED: + doLicense(false); + break; + case ExoMediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + provisioningManager.provisionRequired(this); + break; + default: + break; + } + } + // Provisioning implementation. public void provision() { @@ -356,14 +374,19 @@ import java.util.UUID; private void postKeyRequest(int type, boolean allowRetry) { byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] initData = null; + String mimeType = null; + String licenseServerUrl = null; + if (schemeData != null) { + initData = schemeData.data; + mimeType = schemeData.mimeType; + licenseServerUrl = schemeData.licenseServerUrl; + } try { - KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, - optionalKeyRequestParameters); - if (C.CLEARKEY_UUID.equals(uuid)) { - request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), - request.getDefaultUrl()); - } - postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); + KeyRequest request = + mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + Pair arguments = Pair.create(request, licenseServerUrl); + postRequestHandler.obtainMessage(MSG_KEYS, arguments, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); } @@ -382,9 +405,6 @@ import java.util.UUID; try { byte[] responseData = (byte[]) response; - if (C.CLEARKEY_UUID.equals(uuid)) { - responseData = ClearKeyUtil.adjustResponseData(responseData); - } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); eventDispatcher.drmKeysRemoved(); @@ -430,30 +450,7 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressWarnings("deprecation") - public void onMediaDrmEvent(int what) { - if (!isOpen()) { - return; - } - switch (what) { - case ExoMediaDrm.EVENT_KEY_REQUIRED: - doLicense(false); - break; - case ExoMediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case ExoMediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - provisioningManager.provisionRequired(this); - break; - default: - break; - } - - } + // Internal classes. @SuppressLint("HandlerLeak") private class PostResponseHandler extends Handler { @@ -492,6 +489,7 @@ import java.util.UUID; } @Override + @SuppressWarnings("unchecked") public void handleMessage(Message msg) { Object response; try { @@ -500,7 +498,8 @@ import java.util.UUID; response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + Pair arguments = (Pair) msg.obj; + response = callback.executeKeyRequest(uuid, arguments.first, arguments.second); break; default: throw new RuntimeException(); @@ -534,5 +533,4 @@ import java.util.UUID; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 66c9e5cde7..28fd7e15ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -89,7 +88,6 @@ public class DefaultDrmSessionManager implements DrmSe public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; private static final String TAG = "DefaultDrmSessionMgr"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; private final ExoMediaDrm mediaDrm; @@ -509,17 +507,14 @@ public class DefaultDrmSessionManager implements DrmSe } } - byte[] initData = null; - String mimeType = null; + SchemeData schemeData = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(drmInitData, uuid, false); - if (data == null) { + schemeData = getSchemeData(drmInitData, uuid, false); + if (schemeData == null) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); eventDispatcher.drmSessionManagerError(error); return new ErrorStateDrmSession<>(new DrmSessionException(error)); } - initData = getSchemeInitData(data, uuid); - mimeType = getSchemeMimeType(data, uuid); } DefaultDrmSession session; @@ -528,6 +523,7 @@ public class DefaultDrmSessionManager implements DrmSe } else { // Only use an existing session if it has matching init data. session = null; + byte[] initData = schemeData != null ? schemeData.data : null; for (DefaultDrmSession existingSession : sessions) { if (existingSession.hasInitData(initData)) { session = existingSession; @@ -543,8 +539,7 @@ public class DefaultDrmSessionManager implements DrmSe uuid, mediaDrm, this, - initData, - mimeType, + schemeData, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, @@ -650,31 +645,6 @@ public class DefaultDrmSessionManager implements DrmSe return matchingSchemeDatas.get(0); } - private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { - byte[] schemeInitData = data.data; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - return schemeInitData; - } - - private static String getSchemeMimeType(SchemeData data, UUID uuid) { - String schemeMimeType = data.mimeType; - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - return schemeMimeType; - } - @SuppressLint("HandlerLeak") private class MediaDrmHandler extends Handler { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index c2de662010..cd7adea1e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; - /** - * The mimeType of {@link #data}. - */ + /** The URL of the server to which license requests should be made. May be null if unknown. */ + public final @Nullable String licenseServerUrl; + /** The mimeType of {@link #data}. */ public final String mimeType; /** * The initialization data. May be null for scheme support checks only. @@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator, Parcelable { * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption); + } + + /** + * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is + * universal (i.e. applies to all schemes). + * @param licenseServerUrl See {@link #licenseServerUrl}. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. + * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. + */ + public SchemeData( + UUID uuid, + @Nullable String licenseServerUrl, + String mimeType, + byte[] data, + boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); + this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); this.data = data; this.requiresSecureDecryption = requiresSecureDecryption; @@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); + licenseServerUrl = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator, Parcelable { return true; } SchemeData other = (SchemeData) obj; - return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) + return Util.areEqual(licenseServerUrl, other.licenseServerUrl) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(uuid, other.uuid) && Arrays.equals(data, other.data); } @@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator, Parcelable { public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index f960cd637f..9f30cef0f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -26,7 +26,9 @@ import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.HashMap; @@ -40,6 +42,8 @@ import java.util.UUID; @TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final MediaDrm mediaDrm; @@ -116,14 +120,43 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { + + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + if (Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) { + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + init = psshData; + } + } + + // 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))) { + mimeType = CENC_SCHEME_MIME_TYPE; + } + final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, optionalParameters); - return new DefaultKeyRequest(request.getData(), request.getDefaultUrl()); + + byte[] requestData = request.getData(); + if (C.CLEARKEY_UUID.equals(uuid)) { + requestData = ClearKeyUtil.adjustRequestData(requestData); + } + + return new DefaultKeyRequest(requestData, request.getDefaultUrl()); } @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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 9150a72b53..fc1e62a89c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; @@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = mediaProvidedLicenseServerUrl; + } if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { url = defaultLicenseUrl; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java index 7b9aeca30a..7ed4a61a60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.util.Assertions; @@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { return keyResponse; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java index 617e168f9a..4405d6e538 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.UUID; @@ -38,10 +39,13 @@ public interface MediaDrmCallback { * Executes a key request. * * @param uuid The UUID of the content protection scheme. - * @param request The request. + * @param request The request generated by the content decryption module. + * @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the + * media does not include any license server URL. * @return The response data. * @throws Exception If an error occurred executing the request. */ - byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; - + byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception; }