Implement multi session to support DRM key rotation.

Spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_Android_Using_Key_Rotation.pdf

1. Implement multisession to support drm key rotation
2. Put MediaDrmEventListener back to manager since this is a per mediaDrm thing.
3. It seems diffrenciate between single/multi session is unnecessary.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=167602965
This commit is contained in:
zhihuichen 2017-09-05 11:06:03 -07:00 committed by Oliver Woodman
parent e16610a82c
commit b62eab63a4
4 changed files with 301 additions and 150 deletions

View File

@ -89,6 +89,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_LICENSE_URL = "drm_license_url";
public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties";
public static final String DRM_MULTI_SESSION = "drm_multi_session";
public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders";
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
@ -264,13 +265,14 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
if (drmSchemeUuid != null) { if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false);
int errorStringId = R.string.error_drm_unknown; int errorStringId = R.string.error_drm_unknown;
if (Util.SDK_INT < 18) { if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_not_supported; errorStringId = R.string.error_drm_not_supported;
} else { } else {
try { try {
drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl,
keyRequestPropertiesArray); keyRequestPropertiesArray, multiSession);
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
@ -379,7 +381,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
} }
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(UUID uuid, private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(UUID uuid,
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException {
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false)); buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) { if (keyRequestPropertiesArray != null) {
@ -389,7 +392,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi
} }
} }
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback,
null, mainHandler, eventLogger); null, mainHandler, eventLogger, multiSession);
} }
private void releasePlayer() { private void releasePlayer() {

View File

@ -182,6 +182,7 @@ public class SampleChooserActivity extends Activity {
UUID drmUuid = null; UUID drmUuid = null;
String drmLicenseUrl = null; String drmLicenseUrl = null;
String[] drmKeyRequestProperties = null; String[] drmKeyRequestProperties = null;
boolean drmMultiSession = false;
boolean preferExtensionDecoders = false; boolean preferExtensionDecoders = false;
ArrayList<UriSample> playlistSamples = null; ArrayList<UriSample> playlistSamples = null;
String adTagUri = null; String adTagUri = null;
@ -220,6 +221,9 @@ public class SampleChooserActivity extends Activity {
reader.endObject(); reader.endObject();
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
break; break;
case "drm_multi_session":
drmMultiSession = reader.nextBoolean();
break;
case "prefer_extension_decoders": case "prefer_extension_decoders":
Assertions.checkState(!insidePlaylist, Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: prefer_extension_decoders"); "Invalid attribute on nested item: prefer_extension_decoders");
@ -242,15 +246,16 @@ public class SampleChooserActivity extends Activity {
} }
} }
reader.endObject(); reader.endObject();
DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl,
drmKeyRequestProperties, drmMultiSession);
if (playlistSamples != null) { if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray( UriSample[] playlistSamplesArray = playlistSamples.toArray(
new UriSample[playlistSamples.size()]); new UriSample[playlistSamples.size()]);
return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo,
preferExtensionDecoders, playlistSamplesArray); playlistSamplesArray);
} else { } else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension,
preferExtensionDecoders, uri, extension, adTagUri); adTagUri);
} }
} }
@ -372,31 +377,47 @@ public class SampleChooserActivity extends Activity {
} }
private abstract static class Sample { private static final class DrmInfo {
public final String name;
public final boolean preferExtensionDecoders;
public final UUID drmSchemeUuid; public final UUID drmSchemeUuid;
public final String drmLicenseUrl; public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties; public final String[] drmKeyRequestProperties;
public final boolean drmMultiSession;
public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { String[] drmKeyRequestProperties, boolean drmMultiSession) {
this.name = name;
this.drmSchemeUuid = drmSchemeUuid; this.drmSchemeUuid = drmSchemeUuid;
this.drmLicenseUrl = drmLicenseUrl; this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties; this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmMultiSession = drmMultiSession;
}
public void updateIntent(Intent intent) {
Assertions.checkNotNull(intent);
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString());
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession);
}
}
private abstract static class Sample {
public final String name;
public final boolean preferExtensionDecoders;
public final DrmInfo drmInfo;
public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) {
this.name = name;
this.preferExtensionDecoders = preferExtensionDecoders; this.preferExtensionDecoders = preferExtensionDecoders;
this.drmInfo = drmInfo;
} }
public Intent buildIntent(Context context) { public Intent buildIntent(Context context) {
Intent intent = new Intent(context, PlayerActivity.class); Intent intent = new Intent(context, PlayerActivity.class);
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders);
if (drmSchemeUuid != null) { if (drmInfo != null) {
intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); drmInfo.updateIntent(intent);
intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties);
} }
return intent; return intent;
} }
@ -408,10 +429,9 @@ public class SampleChooserActivity extends Activity {
public final String extension; public final String extension;
public final String adTagUri; public final String adTagUri;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
String extension, String adTagUri) { String extension, String adTagUri) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); super(name, preferExtensionDecoders, drmInfo);
this.uri = uri; this.uri = uri;
this.extension = extension; this.extension = extension;
this.adTagUri = adTagUri; this.adTagUri = adTagUri;
@ -432,10 +452,9 @@ public class SampleChooserActivity extends Activity {
public final UriSample[] children; public final UriSample[] children;
public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders,
UriSample... children) { UriSample... children) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); super(name, preferExtensionDecoders, drmInfo);
this.children = children; this.children = children;
} }

View File

@ -27,29 +27,36 @@ import android.os.Message;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import java.util.Arrays;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* A {@link DrmSession} that supports playbacks using {@link MediaDrm}. * A {@link DrmSession} that supports playbacks using {@link MediaDrm}.
*/ */
@TargetApi(18) @TargetApi(18)
/* package */ class DefaultDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> { /* package */ class DefaultDrmSession<T extends ExoMediaCrypto> implements DrmSession<T> {
private static final String TAG = "DefaultDrmSession";
private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /**
* Listener of {@link DefaultDrmSession} events.
*/
public interface EventListener {
/**
* Called each time provision is completed.
*/
void onProvisionCompleted();
}
private static final String TAG = "DefaultDrmSession";
private static final int MSG_PROVISION = 0; private static final int MSG_PROVISION = 0;
private static final int MSG_KEYS = 1; private static final int MSG_KEYS = 1;
private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
private final Handler eventHandler; private final Handler eventHandler;
@ -58,7 +65,6 @@ import java.util.UUID;
private final HashMap<String, String> optionalKeyRequestParameters; private final HashMap<String, String> optionalKeyRequestParameters;
/* package */ final MediaDrmCallback callback; /* package */ final MediaDrmCallback callback;
/* package */ final UUID uuid; /* package */ final UUID uuid;
/* package */ MediaDrmHandler mediaDrmHandler;
/* package */ PostResponseHandler postResponseHandler; /* package */ PostResponseHandler postResponseHandler;
private HandlerThread requestHandlerThread; private HandlerThread requestHandlerThread;
private Handler postRequestHandler; private Handler postRequestHandler;
@ -66,13 +72,14 @@ import java.util.UUID;
@DefaultDrmSessionManager.Mode @DefaultDrmSessionManager.Mode
private final int mode; private final int mode;
private int openCount; private int openCount;
private boolean provisioningInProgress; private final AtomicBoolean provisioningInProgress;
private final EventListener sessionEventListener;
@DrmSession.State @DrmSession.State
private int state; private int state;
private T mediaCrypto; private T mediaCrypto;
private DrmSessionException lastException; private DrmSessionException lastException;
private final byte[] schemeInitData; private final byte[] initData;
private final String schemeMimeType; private final String mimeType;
private byte[] sessionId; private byte[] sessionId;
private byte[] offlineLicenseKeySetId; private byte[] offlineLicenseKeySetId;
@ -90,11 +97,12 @@ import java.util.UUID;
* @param eventHandler The handler to post listener events. * @param eventHandler The handler to post listener events.
* @param eventListener The DRM session manager event listener. * @param eventListener The DRM session manager event listener.
*/ */
public DefaultDrmSession(UUID uuid, ExoMediaDrm<T> mediaDrm, DrmInitData initData, public DefaultDrmSession(UUID uuid, ExoMediaDrm<T> mediaDrm, byte[] initData, String mimeType,
@DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId,
HashMap<String, String> optionalKeyRequestParameters, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, MediaDrmCallback callback,
Looper playbackLooper, Handler eventHandler, Looper playbackLooper, Handler eventHandler,
DefaultDrmSessionManager.EventListener eventListener) { DefaultDrmSessionManager.EventListener eventListener, AtomicBoolean provisioningInProgress,
EventListener sessionEventListener) {
this.uuid = uuid; this.uuid = uuid;
this.mediaDrm = mediaDrm; this.mediaDrm = mediaDrm;
this.mode = mode; this.mode = mode;
@ -104,45 +112,23 @@ import java.util.UUID;
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
this.provisioningInProgress = provisioningInProgress;
this.sessionEventListener = sessionEventListener;
state = STATE_OPENING; state = STATE_OPENING;
mediaDrmHandler = new MediaDrmHandler(playbackLooper);
mediaDrm.setOnEventListener(new MediaDrmEventListener());
postResponseHandler = new PostResponseHandler(playbackLooper); postResponseHandler = new PostResponseHandler(playbackLooper);
requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread = new HandlerThread("DrmRequestHandler");
requestHandlerThread.start(); requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
// Parse init data.
byte[] schemeInitData = null;
String schemeMimeType = null;
if (offlineLicenseKeySetId == null) { if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(initData, uuid); this.initData = initData;
if (data == null) { this.mimeType = mimeType;
onError(new IllegalStateException("Media does not support uuid: " + uuid));
} else { } else {
schemeInitData = data.data; this.initData = null;
schemeMimeType = data.mimeType; this.mimeType = null;
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;
} }
} }
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;
}
}
}
this.schemeInitData = schemeInitData;
this.schemeMimeType = schemeMimeType;
}
// Life cycle. // Life cycle.
@ -163,9 +149,6 @@ import java.util.UUID;
public boolean release() { public boolean release() {
if (--openCount == 0) { if (--openCount == 0) {
state = STATE_RELEASED; state = STATE_RELEASED;
provisioningInProgress = false;
mediaDrmHandler.removeCallbacksAndMessages(null);
mediaDrmHandler = null;
postResponseHandler.removeCallbacksAndMessages(null); postResponseHandler.removeCallbacksAndMessages(null);
postRequestHandler.removeCallbacksAndMessages(null); postRequestHandler.removeCallbacksAndMessages(null);
postRequestHandler = null; postRequestHandler = null;
@ -182,6 +165,14 @@ import java.util.UUID;
return false; return false;
} }
public boolean canReuse(byte[] initData) {
return Arrays.equals(this.initData, initData);
}
public boolean hasSessionId(byte[] sessionId) {
return Arrays.equals(this.sessionId, sessionId);
}
// DrmSession Implementation. // DrmSession Implementation.
@Override @Override
@ -245,21 +236,15 @@ import java.util.UUID;
} }
private void postProvisionRequest() { private void postProvisionRequest() {
if (provisioningInProgress) { if (provisioningInProgress.getAndSet(true)) {
return; return;
} }
provisioningInProgress = true;
ProvisionRequest request = mediaDrm.getProvisionRequest(); ProvisionRequest request = mediaDrm.getProvisionRequest();
postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
} }
private void onProvisionResponse(Object response) { private void onProvisionResponse(Object response) {
provisioningInProgress = false; provisioningInProgress.set(false);
if (state != STATE_OPENING && !isOpen()) {
// This event is stale.
return;
}
if (response instanceof Exception) { if (response instanceof Exception) {
onError((Exception) response); onError((Exception) response);
return; return;
@ -267,11 +252,24 @@ import java.util.UUID;
try { try {
mediaDrm.provideProvisionResponse((byte[]) response); mediaDrm.provideProvisionResponse((byte[]) response);
if (openInternal(false)) {
doLicense();
}
} catch (DeniedByServerException e) { } catch (DeniedByServerException e) {
onError(e); onError(e);
return;
}
if (sessionEventListener != null) {
sessionEventListener.onProvisionCompleted();
}
}
public void onProvisionCompleted() {
if (state != STATE_OPENING && !isOpen()) {
// This event is stale.
return;
}
if (openInternal(false)) {
doLicense();
} }
} }
@ -322,6 +320,8 @@ import java.util.UUID;
postKeyRequest(MediaDrm.KEY_TYPE_RELEASE); postKeyRequest(MediaDrm.KEY_TYPE_RELEASE);
} }
break; break;
default:
break;
} }
} }
@ -347,7 +347,7 @@ import java.util.UUID;
private void postKeyRequest(int type) { private void postKeyRequest(int type) {
byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId;
try { try {
KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type, KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
optionalKeyRequestParameters); optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget();
} catch (Exception e) { } catch (Exception e) {
@ -433,20 +433,12 @@ import java.util.UUID;
return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS;
} }
@SuppressLint("HandlerLeak")
private class MediaDrmHandler extends Handler {
public MediaDrmHandler(Looper looper) {
super(looper);
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override public void onMediaDrmEvent(int what) {
public void handleMessage(Message msg) {
if (!isOpen()) { if (!isOpen()) {
return; return;
} }
switch (msg.what) { switch (what) {
case MediaDrm.EVENT_KEY_REQUIRED: case MediaDrm.EVENT_KEY_REQUIRED:
doLicense(); doLicense();
break; break;
@ -460,19 +452,8 @@ import java.util.UUID;
state = STATE_OPENED; state = STATE_OPENED;
postProvisionRequest(); postProvisionRequest();
break; break;
} default:
} break;
}
private class MediaDrmEventListener implements OnEventListener<T> {
@Override
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
byte[] data) {
if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) {
mediaDrmHandler.sendEmptyMessage(event);
}
} }
} }
@ -493,6 +474,9 @@ import java.util.UUID;
case MSG_KEYS: case MSG_KEYS:
onKeyResponse(msg.obj); onKeyResponse(msg.obj);
break; break;
default:
break;
} }
} }
@ -527,20 +511,4 @@ import java.util.UUID;
} }
/**
* Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}.
*
* @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
* @param uuid The UUID of the scheme.
* @return The extracted {@link SchemeData}, or null if no suitable data is present.
*/
public static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) {
SchemeData schemeData = drmInitData.get(uuid);
if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) {
// If present, the Common PSSH box should be used for ClearKey.
schemeData = drmInitData.get(C.COMMON_PSSH_UUID);
}
return schemeData;
}
} }

View File

@ -15,27 +15,36 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
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.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
*/ */
@TargetApi(18) @TargetApi(18)
public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> { public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
DefaultDrmSession.EventListener {
/** /**
* Listener of {@link DefaultDrmSessionManager} events. * Listener of {@link DefaultDrmSessionManager} events.
@ -70,6 +79,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* The key to use when passing CustomData to a PlayReady instance in an optional parameter map. * The key to use when passing CustomData to a PlayReady instance in an optional parameter map.
*/ */
public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
/** Determines the action to be done after a session acquired. */ /** Determines the action to be done after a session acquired. */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@ -93,14 +103,17 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
private final EventListener eventListener; private final EventListener eventListener;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
private final HashMap<String, String> optionalKeyRequestParameters; private final HashMap<String, String> optionalKeyRequestParameters;
private final MediaDrmCallback callback; private final MediaDrmCallback callback;
private final UUID uuid; private final UUID uuid;
private final boolean multiSession;
private Looper playbackLooper; private Looper playbackLooper;
private int mode; private int mode;
private byte[] offlineLicenseKeySetId; private byte[] offlineLicenseKeySetId;
private DefaultDrmSession<T> session;
private final List<DefaultDrmSession<T>> sessions;
private final AtomicBoolean provisioningInProgress;
/* package */ MediaDrmHandler mediaDrmHandler;
/** /**
* Instantiates a new instance using the Widevine scheme. * Instantiates a new instance using the Widevine scheme.
@ -163,7 +176,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
optionalKeyRequestParameters, eventHandler, eventListener); optionalKeyRequestParameters, eventHandler, eventListener, false);
} }
/** /**
@ -179,7 +192,27 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler, HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) { EventListener eventListener) {
this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener,
false);
}
/**
* @param uuid The UUID of the drm scheme.
* @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param multiSession A boolean that specify whether multiple key session support is enabled.
* Default is false.
*/
public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener, boolean multiSession) {
Assertions.checkNotNull(uuid); Assertions.checkNotNull(uuid);
Assertions.checkNotNull(mediaDrm);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
this.uuid = uuid; this.uuid = uuid;
this.mediaDrm = mediaDrm; this.mediaDrm = mediaDrm;
@ -187,7 +220,13 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.optionalKeyRequestParameters = optionalKeyRequestParameters;
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
this.multiSession = multiSession;
mode = MODE_PLAYBACK; mode = MODE_PLAYBACK;
sessions = new ArrayList<>();
provisioningInProgress = new AtomicBoolean(false);
if (multiSession) {
mediaDrm.setPropertyString("sessionSharing", "enable");
}
} }
/** /**
@ -261,7 +300,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
*/ */
public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
Assertions.checkState(session == null); Assertions.checkState(sessions.isEmpty());
if (mode == MODE_QUERY || mode == MODE_RELEASE) { if (mode == MODE_QUERY || mode == MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId); Assertions.checkNotNull(offlineLicenseKeySetId);
} }
@ -273,7 +312,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
@Override @Override
public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { public boolean canAcquireSession(@NonNull DrmInitData drmInitData) {
SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid); SchemeData schemeData = getSchemeData(drmInitData, uuid);
if (schemeData == null) { if (schemeData == null) {
// No data for this manager's scheme. // No data for this manager's scheme.
return false; return false;
@ -294,22 +333,144 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
@Override @Override
public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) { public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
if (session == null) { if (sessions.isEmpty()) {
this.playbackLooper = playbackLooper; this.playbackLooper = playbackLooper;
session = new DefaultDrmSession<T>(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId, mediaDrmHandler = new MediaDrmHandler(playbackLooper);
optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener); mediaDrm.setOnEventListener(new MediaDrmEventListener());
} }
DefaultDrmSession<T> session = null;
byte[] initData = null;
String mimeType = null;
if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid);
if (data == null) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrmSessionManagerError(new IllegalStateException(
"Media does not support uuid: " + uuid));
}
});
}
} else {
initData = getSchemeInitData(data, uuid);
mimeType = getSchemeMimeType(data, uuid);
}
}
for (DefaultDrmSession<T> s : sessions) {
if (!multiSession || s.canReuse(initData)) {
session = s;
break;
}
}
if (session == null) {
session = new DefaultDrmSession<T>(uuid, mediaDrm, initData, mimeType, mode,
offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper,
eventHandler, eventListener, provisioningInProgress, this);
sessions.add(session);
}
session.acquire(); session.acquire();
return session; return session;
} }
@Override @Override
public void releaseSession(DrmSession<T> session) { public void releaseSession(DrmSession<T> session) {
Assertions.checkState(session == this.session); DefaultDrmSession<T> drmSession = (DefaultDrmSession<T>) session;
if (this.session.release()) { if (drmSession.release()) {
this.session = null; sessions.remove(drmSession);
}
if (sessions.isEmpty()) {
mediaDrm.setOnEventListener(null);
mediaDrmHandler.removeCallbacksAndMessages(null);
mediaDrmHandler = null;
playbackLooper = null;
}
}
@Override
public void onProvisionCompleted() {
for (DefaultDrmSession<T> session : sessions) {
session.onProvisionCompleted();
}
}
/**
* Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}.
*
* @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
* @param uuid The UUID.
* @return The extracted {@link SchemeData}, or null if no suitable data is present.
*/
private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) {
SchemeData schemeData = drmInitData.get(uuid);
if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) {
// If present, the Common PSSH box should be used for ClearKey.
schemeData = drmInitData.get(C.COMMON_PSSH_UUID);
}
return schemeData;
}
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 {
public MediaDrmHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
byte[] sessionId = (byte[]) msg.obj;
for (DefaultDrmSession<T> session : sessions) {
if (session.hasSessionId(sessionId)) {
session.onMediaDrmEvent(msg.what);
return;
}
} }
} }
} }
private class MediaDrmEventListener implements OnEventListener<T> {
@Override
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
byte[] data) {
if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) {
mediaDrmHandler.obtainMessage(event, sessionId).sendToTarget();
}
}
}
}