diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index b2750a93bb..c2c4df9ea8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -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_LICENSE_URL = "drm_license_url"; 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 ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; @@ -264,13 +265,14 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); + boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); int errorStringId = R.string.error_drm_unknown; if (Util.SDK_INT < 18) { errorStringId = R.string.error_drm_not_supported; } else { try { drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); + keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? 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 buildDrmSessionManagerV18(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) + throws UnsupportedDrmException { HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { @@ -389,7 +392,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, mainHandler, eventLogger); + null, mainHandler, eventLogger, multiSession); } private void releasePlayer() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index c0edb1d1b8..1f84b1f29c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -182,6 +182,7 @@ public class SampleChooserActivity extends Activity { UUID drmUuid = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + boolean drmMultiSession = false; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; @@ -220,6 +221,9 @@ public class SampleChooserActivity extends Activity { reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; case "prefer_extension_decoders": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: prefer_extension_decoders"); @@ -242,15 +246,16 @@ public class SampleChooserActivity extends Activity { } } reader.endObject(); - + DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl, + drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, playlistSamplesArray); + return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo, + playlistSamplesArray); } else { - return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension, adTagUri); + return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension, + adTagUri); } } @@ -372,31 +377,47 @@ public class SampleChooserActivity extends Activity { } - private abstract static class Sample { - - public final String name; - public final boolean preferExtensionDecoders; + private static final class DrmInfo { public final UUID drmSchemeUuid; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; - public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { - this.name = name; + public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl, + String[] drmKeyRequestProperties, boolean drmMultiSession) { this.drmSchemeUuid = drmSchemeUuid; this.drmLicenseUrl = drmLicenseUrl; 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.drmInfo = drmInfo; } public Intent buildIntent(Context context) { Intent intent = new Intent(context, PlayerActivity.class); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); - if (drmSchemeUuid != null) { - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + if (drmInfo != null) { + drmInfo.updateIntent(intent); } + return intent; } @@ -408,10 +429,9 @@ public class SampleChooserActivity extends Activity { public final String extension; public final String adTagUri; - public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, + public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri, String extension, String adTagUri) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; @@ -432,10 +452,9 @@ public class SampleChooserActivity extends Activity { public final UriSample[] children; - public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, + public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, UriSample... children) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.children = children; } 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 b4dab7b971..d6776c8ed0 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 @@ -27,29 +27,36 @@ import android.os.Message; import android.util.Log; import android.util.Pair; 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.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; -import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { - 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_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; private final Handler eventHandler; @@ -58,7 +65,6 @@ import java.util.UUID; private final HashMap optionalKeyRequestParameters; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; - /* package */ MediaDrmHandler mediaDrmHandler; /* package */ PostResponseHandler postResponseHandler; private HandlerThread requestHandlerThread; private Handler postRequestHandler; @@ -66,13 +72,14 @@ import java.util.UUID; @DefaultDrmSessionManager.Mode private final int mode; private int openCount; - private boolean provisioningInProgress; + private final AtomicBoolean provisioningInProgress; + private final EventListener sessionEventListener; @DrmSession.State private int state; private T mediaCrypto; private DrmSessionException lastException; - private final byte[] schemeInitData; - private final String schemeMimeType; + private final byte[] initData; + private final String mimeType; private byte[] sessionId; private byte[] offlineLicenseKeySetId; @@ -90,11 +97,12 @@ import java.util.UUID; * @param eventHandler The handler to post listener events. * @param eventListener The DRM session manager event listener. */ - public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, DrmInitData initData, + public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, byte[] initData, String mimeType, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener) { + DefaultDrmSessionManager.EventListener eventListener, AtomicBoolean provisioningInProgress, + EventListener sessionEventListener) { this.uuid = uuid; this.mediaDrm = mediaDrm; this.mode = mode; @@ -104,44 +112,22 @@ import java.util.UUID; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.provisioningInProgress = provisioningInProgress; + this.sessionEventListener = sessionEventListener; state = STATE_OPENING; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - mediaDrm.setOnEventListener(new MediaDrmEventListener()); postResponseHandler = new PostResponseHandler(playbackLooper); requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - // Parse init data. - byte[] schemeInitData = null; - String schemeMimeType = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(initData, uuid); - if (data == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - } else { - schemeInitData = data.data; - schemeMimeType = data.mimeType; - 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.initData = initData; + this.mimeType = mimeType; + } else { + this.initData = null; + this.mimeType = null; } - this.schemeInitData = schemeInitData; - this.schemeMimeType = schemeMimeType; } // Life cycle. @@ -163,9 +149,6 @@ import java.util.UUID; public boolean release() { if (--openCount == 0) { state = STATE_RELEASED; - provisioningInProgress = false; - mediaDrmHandler.removeCallbacksAndMessages(null); - mediaDrmHandler = null; postResponseHandler.removeCallbacksAndMessages(null); postRequestHandler.removeCallbacksAndMessages(null); postRequestHandler = null; @@ -182,6 +165,14 @@ import java.util.UUID; 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. @Override @@ -245,21 +236,15 @@ import java.util.UUID; } private void postProvisionRequest() { - if (provisioningInProgress) { + if (provisioningInProgress.getAndSet(true)) { return; } - provisioningInProgress = true; ProvisionRequest request = mediaDrm.getProvisionRequest(); postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); } private void onProvisionResponse(Object response) { - provisioningInProgress = false; - if (state != STATE_OPENING && !isOpen()) { - // This event is stale. - return; - } - + provisioningInProgress.set(false); if (response instanceof Exception) { onError((Exception) response); return; @@ -267,11 +252,24 @@ import java.util.UUID; try { mediaDrm.provideProvisionResponse((byte[]) response); - if (openInternal(false)) { - doLicense(); - } } catch (DeniedByServerException 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); } break; + default: + break; } } @@ -347,7 +347,7 @@ import java.util.UUID; private void postKeyRequest(int type) { byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { - KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type, + KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); } catch (Exception e) { @@ -433,46 +433,27 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressLint("HandlerLeak") - private class MediaDrmHandler extends Handler { - - public MediaDrmHandler(Looper looper) { - super(looper); + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; } - - @SuppressWarnings("deprecation") - @Override - public void handleMessage(Message msg) { - if (!isOpen()) { - return; - } - switch (msg.what) { - case MediaDrm.EVENT_KEY_REQUIRED: - doLicense(); - break; - case MediaDrm.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 MediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - postProvisionRequest(); - break; - } - } - - } - - private class MediaDrmEventListener implements OnEventListener { - - @Override - public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { - mediaDrmHandler.sendEmptyMessage(event); - } + switch (what) { + case MediaDrm.EVENT_KEY_REQUIRED: + doLicense(); + break; + case MediaDrm.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 MediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + postProvisionRequest(); + break; + default: + break; } } @@ -493,6 +474,9 @@ import java.util.UUID; case MSG_KEYS: onKeyResponse(msg.obj); 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; - } - } 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 9ea696e074..67931d106b 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 @@ -15,27 +15,36 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; 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.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager { +public class DefaultDrmSessionManager implements DrmSessionManager, + DefaultDrmSession.EventListener { /** * Listener of {@link DefaultDrmSessionManager} events. @@ -70,6 +79,7 @@ public class DefaultDrmSessionManager implements DrmSe * 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"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /** Determines the action to be done after a session acquired. */ @Retention(RetentionPolicy.SOURCE) @@ -93,14 +103,17 @@ public class DefaultDrmSessionManager implements DrmSe private final EventListener eventListener; private final ExoMediaDrm mediaDrm; private final HashMap optionalKeyRequestParameters; - private final MediaDrmCallback callback; private final UUID uuid; + private final boolean multiSession; private Looper playbackLooper; private int mode; private byte[] offlineLicenseKeySetId; - private DefaultDrmSession session; + + private final List> sessions; + private final AtomicBoolean provisioningInProgress; + /* package */ MediaDrmHandler mediaDrmHandler; /** * Instantiates a new instance using the Widevine scheme. @@ -163,7 +176,7 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener); + optionalKeyRequestParameters, eventHandler, eventListener, false); } /** @@ -179,7 +192,27 @@ public class DefaultDrmSessionManager implements DrmSe public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, 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 mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener, boolean multiSession) { Assertions.checkNotNull(uuid); + Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; this.mediaDrm = mediaDrm; @@ -187,7 +220,13 @@ public class DefaultDrmSessionManager implements DrmSe this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.multiSession = multiSession; mode = MODE_PLAYBACK; + sessions = new ArrayList<>(); + provisioningInProgress = new AtomicBoolean(false); + if (multiSession) { + mediaDrm.setPropertyString("sessionSharing", "enable"); + } } /** @@ -261,7 +300,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { - Assertions.checkState(session == null); + Assertions.checkState(sessions.isEmpty()); if (mode == MODE_QUERY || mode == MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } @@ -273,7 +312,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid); + SchemeData schemeData = getSchemeData(drmInitData, uuid); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -294,22 +333,144 @@ public class DefaultDrmSessionManager implements DrmSe @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (session == null) { + if (sessions.isEmpty()) { this.playbackLooper = playbackLooper; - session = new DefaultDrmSession(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId, - optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener); + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + mediaDrm.setOnEventListener(new MediaDrmEventListener()); } + DefaultDrmSession 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 s : sessions) { + if (!multiSession || s.canReuse(initData)) { + session = s; + break; + } + } + + if (session == null) { + session = new DefaultDrmSession(uuid, mediaDrm, initData, mimeType, mode, + offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, + eventHandler, eventListener, provisioningInProgress, this); + sessions.add(session); + } session.acquire(); return session; } @Override public void releaseSession(DrmSession session) { - Assertions.checkState(session == this.session); - if (this.session.release()) { - this.session = null; + DefaultDrmSession drmSession = (DefaultDrmSession) session; + if (drmSession.release()) { + sessions.remove(drmSession); + } + + if (sessions.isEmpty()) { + mediaDrm.setOnEventListener(null); + mediaDrmHandler.removeCallbacksAndMessages(null); + mediaDrmHandler = null; + playbackLooper = null; } } + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession 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 session : sessions) { + if (session.hasSessionId(sessionId)) { + session.onMediaDrmEvent(msg.what); + return; + } + } + } + + } + + private class MediaDrmEventListener implements OnEventListener { + + @Override + public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { + mediaDrmHandler.obtainMessage(event, sessionId).sendToTarget(); + } + } + + } + }