Index: media/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
diff --git a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
index c84061cdc0e57e2d3c67304840cfd5a21b64ed98..df74868ac47437fcf8e939a737d10eac4aa5ed3b 100644 |
--- a/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
+++ b/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java |
@@ -97,6 +97,41 @@ public class MediaDrmBridge { |
// Boolean to track if 'ORIGIN' is set in MediaDrm. |
private boolean mOriginSet = false; |
+ // Delay the MediaDrm event handle if present. |
+ private SessionEventDeferrer mSessionEventDeferrer = null; |
+ |
+ // Block MediaDrm event for |mSessionId|. MediaDrm may fire event before the |
+ // functions return. This may break Chromium CDM API's assumption. For |
+ // example, when loading session, 'restoreKeys' will trigger key status |
+ // change event. But the session isn't known to Chromium CDM because the |
+ // promise isn't resolved. The class can block and collect these events and |
+ // fire these events later. |
+ private static class SessionEventDeferrer { |
+ private final SessionId mSessionId; |
+ private final ArrayList<Runnable> mEventHandlers; |
+ |
+ SessionEventDeferrer(SessionId sessionId) { |
+ mSessionId = sessionId; |
+ mEventHandlers = new ArrayList<>(); |
+ } |
+ |
+ boolean shouldDefer(SessionId sessionId) { |
+ return mSessionId.isEqual(sessionId); |
+ } |
+ |
+ void defer(Runnable handler) { |
+ mEventHandlers.add(handler); |
+ } |
+ |
+ void fire() { |
+ for (Runnable r : mEventHandlers) { |
+ r.run(); |
+ } |
+ |
+ mEventHandlers.clear(); |
+ } |
+ } |
+ |
/** |
* An equivalent of MediaDrm.KeyStatus, which is only available on M+. |
*/ |
@@ -573,8 +608,10 @@ public class MediaDrmBridge { |
MediaDrm.KeyRequest request = null; |
try { |
- request = mMediaDrm.getKeyRequest( |
- sessionId.drmId(), data, mime, keyType, optionalParameters); |
+ byte[] scopeId = |
+ keyType == MediaDrm.KEY_TYPE_RELEASE ? sessionId.keySetId() : sessionId.drmId(); |
+ assert scopeId != null; |
+ request = mMediaDrm.getKeyRequest(scopeId, data, mime, keyType, optionalParameters); |
} catch (IllegalStateException e) { |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && e |
instanceof android.media.MediaDrm.MediaDrmStateException) { |
@@ -825,19 +862,27 @@ public class MediaDrmBridge { |
try { |
SessionInfo sessionInfo = mSessionManager.get(sessionId); |
- byte[] keySetId = mMediaDrm.provideKeyResponse(sessionId.drmId(), response); |
- |
- if (keySetId != null && keySetId.length > 0) { |
- assert sessionInfo.keyType() == MediaDrm.KEY_TYPE_OFFLINE; |
- mSessionManager.setKeySetId(sessionId, keySetId, new Callback<Boolean>() { |
- @Override |
- public void onResult(Boolean success) { |
- onKeyUpdated(sessionId, promiseId, success); |
- } |
- }); |
+ boolean isKeyRelease = sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE; |
+ |
+ byte[] keySetId = null; |
+ if (isKeyRelease) { |
+ Log.d(TAG, "updateSession() for key release"); |
+ assert sessionId.keySetId() != null; |
+ mMediaDrm.provideKeyResponse(sessionId.keySetId(), response); |
+ } else { |
+ keySetId = mMediaDrm.provideKeyResponse(sessionId.drmId(), response); |
+ } |
+ |
+ KeyUpdatedCallback cb = new KeyUpdatedCallback(sessionId, promiseId, isKeyRelease); |
+ |
+ if (isKeyRelease) { |
+ mSessionManager.clearPersistentSessionInfo(sessionId, cb); |
+ } else if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_OFFLINE && keySetId != null |
+ && keySetId.length > 0) { |
+ mSessionManager.setKeySetId(sessionId, keySetId, cb); |
} else { |
// This can be either temporary license update or server certificate update. |
- onKeyUpdated(sessionId, promiseId, true); |
+ cb.onResult(true); |
} |
return; |
@@ -853,18 +898,139 @@ public class MediaDrmBridge { |
release(); |
} |
- private void onKeyUpdated(SessionId sessionId, long promiseId, boolean success) { |
- if (!success) { |
- onPromiseRejected(promiseId, "failed to update key after response accepted"); |
+ /** |
+ * Load persistent license from storage. |
+ */ |
+ @CalledByNative |
+ private void loadSession(byte[] emeId, final long promiseId) { |
+ Log.d(TAG, "loadSession()"); |
+ if (mProvisioningPending) { |
+ onPersistentLicenseNoExist(promiseId); |
return; |
} |
- Log.d(TAG, "Key successfully added for session %s", sessionId.toHexString()); |
- onPromiseResolved(promiseId); |
+ mSessionManager.load(emeId, new Callback<SessionId>() { |
+ @Override |
+ public void onResult(SessionId sessionId) { |
+ if (sessionId == null) { |
+ onPersistentLicenseNoExist(promiseId); |
+ return; |
+ } |
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
- onSessionKeysChange( |
- sessionId, getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_USABLE).toArray(), true); |
+ loadSessionWithLoadedStorage(sessionId, promiseId); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Load session back to memory with MediaDrm. Load persistent storage |
+ * before calling this. It will fail if persistent storage isn't loaded. |
+ */ |
+ private void loadSessionWithLoadedStorage(SessionId sessionId, final long promiseId) { |
+ byte[] drmId = null; |
+ try { |
+ drmId = openSession(); |
+ if (drmId == null) { |
+ onPromiseRejected(promiseId, "Failed to open session to load license"); |
+ return; |
+ } |
+ |
+ mSessionManager.setDrmId(sessionId, drmId); |
+ |
+ // Defer event handlers until license is loaded. |
+ assert mSessionEventDeferrer == null; |
+ mSessionEventDeferrer = new SessionEventDeferrer(sessionId); |
+ |
+ assert sessionId.keySetId() != null; |
+ mMediaDrm.restoreKeys(sessionId.drmId(), sessionId.keySetId()); |
+ |
+ onPromiseResolvedWithSession(promiseId, sessionId); |
+ |
+ mSessionEventDeferrer.fire(); |
+ mSessionEventDeferrer = null; |
+ |
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
+ onSessionKeysChange(sessionId, |
+ getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_USABLE).toArray(), true, false); |
+ } |
+ } catch (android.media.NotProvisionedException e) { |
+ // If device isn't provisioned, storage loading should fail. |
+ assert false; |
+ } catch (java.lang.IllegalStateException e) { |
+ // license doesn't exist |
+ if (sessionId.drmId() == null) { |
+ // TODO(yucliu): Check if the license is released or doesn't exist. |
+ onPersistentLicenseNoExist(promiseId); |
+ return; |
+ } |
+ |
+ closeSessionNoException(sessionId); |
+ mSessionManager.clearPersistentSessionInfo(sessionId, new Callback<Boolean>() { |
+ @Override |
+ public void onResult(Boolean success) { |
+ if (!success) { |
+ Log.w(TAG, "Failed to clear persistent storage for non-exist license"); |
+ } |
+ |
+ onPersistentLicenseNoExist(promiseId); |
+ } |
+ }); |
+ } |
+ } |
+ |
+ private void onPersistentLicenseNoExist(long promiseId) { |
+ // Chromium CDM API requires resolve the promise with empty session id for non-exist |
+ // license. See media/base/content_decryption_module.h LoadSession for more details. |
+ onPromiseResolvedWithSession(promiseId, SessionId.createNoExistSessionId()); |
+ } |
+ |
+ /** |
+ * Remove session from device. This will mark the key as released and |
+ * generate a key release request. The license is removed from the device |
+ * when the session is updated with a license release response. |
+ */ |
+ @CalledByNative |
+ private void removeSession(byte[] emeId, long promiseId) { |
+ Log.d(TAG, "removeSession()"); |
+ SessionId sessionId = getSessionIdByEmeId(emeId); |
+ |
+ if (sessionId == null) { |
+ onPromiseRejected(promiseId, "Session doesn't exist"); |
+ return; |
+ } |
+ |
+ SessionInfo sessionInfo = mSessionManager.get(sessionId); |
+ if (sessionInfo.keyType() != MediaDrm.KEY_TYPE_OFFLINE) { |
+ // TODO(yucliu): Support 'remove' of temporary session. |
+ onPromiseRejected(promiseId, "Removing temporary session isn't implemented"); |
+ return; |
+ } |
+ |
+ assert sessionId.keySetId() != null; |
+ |
+ mSessionManager.markKeyReleased(sessionId); |
+ |
+ try { |
+ // Get key release request. |
+ MediaDrm.KeyRequest request = getKeyRequest( |
+ sessionId, null, sessionInfo.mimeType(), MediaDrm.KEY_TYPE_RELEASE, null); |
+ |
+ if (request == null) { |
+ onPromiseRejected(promiseId, "Fail to generate key release request"); |
+ return; |
+ } |
+ |
+ // According to EME spec: |
+ // https://www.w3.org/TR/encrypted-media/#dom-mediakeysession-remove |
+ // 5.5 ... run the Queue a "message" Event ... |
+ // 5.6 Resolve promise |
+ // Since event is queued, JS will receive event after promise is |
+ // resolved. So resolve the promise before firing the event here. |
+ onPromiseResolved(promiseId); |
+ onSessionMessage(sessionId, request); |
+ } catch (android.media.NotProvisionedException e) { |
+ Log.e(TAG, "removeSession called on unprovisioned device"); |
+ onPromiseRejected(promiseId, "Unknown failure"); |
} |
} |
@@ -969,6 +1135,19 @@ public class MediaDrmBridge { |
return false; |
} |
+ /** |
+ * Delay session event handler if |mSessionEventDeferrer| exists and |
+ * matches |sessionId|. Otherwise run the handler immediately. |
+ */ |
+ private void deferEventHandleIfNeeded(SessionId sessionId, Runnable handler) { |
+ if (mSessionEventDeferrer != null && mSessionEventDeferrer.shouldDefer(sessionId)) { |
+ mSessionEventDeferrer.defer(handler); |
+ return; |
+ } |
+ |
+ handler.run(); |
+ } |
+ |
// Helper functions to make native calls. |
private void onMediaCryptoReady(MediaCrypto mediaCrypto) { |
@@ -1022,10 +1201,10 @@ public class MediaDrmBridge { |
} |
private void onSessionKeysChange(final SessionId sessionId, final Object[] keysInfo, |
- final boolean hasAdditionalUsableKey) { |
+ final boolean hasAdditionalUsableKey, final boolean isKeyRelease) { |
if (isNativeMediaDrmBridgeValid()) { |
- nativeOnSessionKeysChange( |
- mNativeMediaDrmBridge, sessionId.emeId(), keysInfo, hasAdditionalUsableKey); |
+ nativeOnSessionKeysChange(mNativeMediaDrmBridge, sessionId.emeId(), keysInfo, |
+ hasAdditionalUsableKey, isKeyRelease); |
} |
} |
@@ -1059,13 +1238,13 @@ public class MediaDrmBridge { |
return; |
} |
+ SessionInfo sessionInfo = mSessionManager.get(sessionId); |
switch(event) { |
case MediaDrm.EVENT_KEY_REQUIRED: |
Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED"); |
if (mProvisioningPending) { |
return; |
} |
- SessionInfo sessionInfo = mSessionManager.get(sessionId); |
MediaDrm.KeyRequest request = null; |
try { |
request = getKeyRequest(sessionId, data, sessionInfo.mimeType(), |
@@ -1082,7 +1261,7 @@ public class MediaDrmBridge { |
onSessionKeysChange(sessionId, |
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_INTERNAL_ERROR) |
.toArray(), |
- false); |
+ false, false); |
} |
Log.e(TAG, "EventListener: getKeyRequest failed."); |
return; |
@@ -1093,7 +1272,7 @@ public class MediaDrmBridge { |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
onSessionKeysChange(sessionId, |
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_EXPIRED).toArray(), |
- false); |
+ false, sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE); |
} |
break; |
case MediaDrm.EVENT_VENDOR_DEFINED: |
@@ -1120,14 +1299,25 @@ public class MediaDrmBridge { |
@Override |
public void onKeyStatusChange(MediaDrm md, byte[] drmSessionId, |
- List<MediaDrm.KeyStatus> keyInformation, boolean hasNewUsableKey) { |
- SessionId sessionId = getSessionIdByDrmId(drmSessionId); |
+ final List<MediaDrm.KeyStatus> keyInformation, final boolean hasNewUsableKey) { |
+ final SessionId sessionId = getSessionIdByDrmId(drmSessionId); |
assert sessionId != null; |
- |
- Log.d(TAG, "KeysStatusChange: " + sessionId.toHexString() + ", " + hasNewUsableKey); |
- |
- onSessionKeysChange(sessionId, getKeysInfo(keyInformation).toArray(), hasNewUsableKey); |
+ assert mSessionManager.get(sessionId) != null; |
+ |
+ final boolean isKeyRelease = |
+ mSessionManager.get(sessionId).keyType() == MediaDrm.KEY_TYPE_RELEASE; |
+ |
+ deferEventHandleIfNeeded(sessionId, new Runnable() { |
+ @Override |
+ public void run() { |
+ Log.d(TAG, |
+ "KeysStatusChange: " + sessionId.toHexString() + ", " |
+ + hasNewUsableKey); |
+ onSessionKeysChange(sessionId, getKeysInfo(keyInformation).toArray(), |
+ hasNewUsableKey, isKeyRelease); |
+ } |
+ }); |
} |
} |
@@ -1135,13 +1325,51 @@ public class MediaDrmBridge { |
@MainDex |
private class ExpirationUpdateListener implements MediaDrm.OnExpirationUpdateListener { |
@Override |
- public void onExpirationUpdate(MediaDrm md, byte[] drmSessionId, long expirationTime) { |
- SessionId sessionId = getSessionIdByDrmId(drmSessionId); |
+ public void onExpirationUpdate( |
+ MediaDrm md, byte[] drmSessionId, final long expirationTime) { |
+ final SessionId sessionId = getSessionIdByDrmId(drmSessionId); |
assert sessionId != null; |
- Log.d(TAG, "ExpirationUpdate: " + sessionId.toHexString() + ", " + expirationTime); |
- onSessionExpirationUpdate(sessionId, expirationTime); |
+ deferEventHandleIfNeeded(sessionId, new Runnable() { |
+ @Override |
+ public void run() { |
+ Log.d(TAG, |
+ "ExpirationUpdate: " + sessionId.toHexString() + ", " + expirationTime); |
+ onSessionExpirationUpdate(sessionId, expirationTime); |
+ } |
+ }); |
+ } |
+ } |
+ |
+ @MainDex |
+ private class KeyUpdatedCallback extends Callback<Boolean> { |
+ private final SessionId mSessionId; |
+ private final long mPromiseId; |
+ private final boolean mIsKeyRelease; |
+ |
+ KeyUpdatedCallback(SessionId sessionId, long promiseId, boolean isKeyRelease) { |
+ mSessionId = sessionId; |
+ mPromiseId = promiseId; |
+ mIsKeyRelease = isKeyRelease; |
+ } |
+ |
+ @Override |
+ public void onResult(Boolean success) { |
+ if (!success) { |
+ onPromiseRejected(mPromiseId, "failed to update key after response accepted"); |
+ return; |
+ } |
+ |
+ Log.d(TAG, "Key successfully %s for session %s", mIsKeyRelease ? "released" : "added", |
+ mSessionId.toHexString()); |
+ onPromiseResolved(mPromiseId); |
+ |
+ if (!mIsKeyRelease && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
+ onSessionKeysChange(mSessionId, |
+ getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_USABLE).toArray(), true, |
+ mIsKeyRelease); |
+ } |
} |
} |
@@ -1163,7 +1391,7 @@ public class MediaDrmBridge { |
long nativeMediaDrmBridge, byte[] emeSessionId, int requestType, byte[] message); |
private native void nativeOnSessionClosed(long nativeMediaDrmBridge, byte[] emeSessionId); |
private native void nativeOnSessionKeysChange(long nativeMediaDrmBridge, byte[] emeSessionId, |
- Object[] keysInfo, boolean hasAdditionalUsableKey); |
+ Object[] keysInfo, boolean hasAdditionalUsableKey, boolean isKeyRelease); |
private native void nativeOnSessionExpirationUpdate( |
long nativeMediaDrmBridge, byte[] emeSessionId, long expirationTime); |