summaryrefslogtreecommitdiffstats
path: root/dom/media/eme
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/eme/CDMCaps.cpp112
-rw-r--r--dom/media/eme/CDMCaps.h82
-rw-r--r--dom/media/eme/CDMProxy.h325
-rw-r--r--dom/media/eme/DecryptorProxyCallback.h54
-rw-r--r--dom/media/eme/DetailedPromise.cpp87
-rw-r--r--dom/media/eme/DetailedPromise.h104
-rw-r--r--dom/media/eme/EMEUtils.cpp248
-rw-r--r--dom/media/eme/EMEUtils.h109
-rw-r--r--dom/media/eme/KeySystemConfig.cpp360
-rw-r--r--dom/media/eme/KeySystemConfig.h189
-rw-r--r--dom/media/eme/KeySystemNames.h48
-rw-r--r--dom/media/eme/MediaEncryptedEvent.cpp106
-rw-r--r--dom/media/eme/MediaEncryptedEvent.h60
-rw-r--r--dom/media/eme/MediaKeyError.cpp27
-rw-r--r--dom/media/eme/MediaKeyError.h33
-rw-r--r--dom/media/eme/MediaKeyMessageEvent.cpp101
-rw-r--r--dom/media/eme/MediaKeyMessageEvent.h64
-rw-r--r--dom/media/eme/MediaKeySession.cpp622
-rw-r--r--dom/media/eme/MediaKeySession.h142
-rw-r--r--dom/media/eme/MediaKeyStatusMap.cpp99
-rw-r--r--dom/media/eme/MediaKeyStatusMap.h94
-rw-r--r--dom/media/eme/MediaKeySystemAccess.cpp1161
-rw-r--r--dom/media/eme/MediaKeySystemAccess.h84
-rw-r--r--dom/media/eme/MediaKeySystemAccessManager.cpp702
-rw-r--r--dom/media/eme/MediaKeySystemAccessManager.h237
-rw-r--r--dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp91
-rw-r--r--dom/media/eme/MediaKeySystemAccessPermissionRequest.h74
-rw-r--r--dom/media/eme/MediaKeys.cpp847
-rw-r--r--dom/media/eme/MediaKeys.h236
-rw-r--r--dom/media/eme/clearkey/ArrayUtils.h22
-rw-r--r--dom/media/eme/clearkey/BigEndian.h59
-rw-r--r--dom/media/eme/clearkey/ClearKeyBase64.cpp90
-rw-r--r--dom/media/eme/clearkey/ClearKeyBase64.h29
-rw-r--r--dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp289
-rw-r--r--dom/media/eme/clearkey/ClearKeyDecryptionManager.h111
-rw-r--r--dom/media/eme/clearkey/ClearKeyPersistence.cpp153
-rw-r--r--dom/media/eme/clearkey/ClearKeyPersistence.h64
-rw-r--r--dom/media/eme/clearkey/ClearKeySession.cpp72
-rw-r--r--dom/media/eme/clearkey/ClearKeySession.h56
-rw-r--r--dom/media/eme/clearkey/ClearKeySessionManager.cpp713
-rw-r--r--dom/media/eme/clearkey/ClearKeySessionManager.h138
-rw-r--r--dom/media/eme/clearkey/ClearKeyStorage.cpp194
-rw-r--r--dom/media/eme/clearkey/ClearKeyStorage.h42
-rw-r--r--dom/media/eme/clearkey/ClearKeyUtils.cpp661
-rw-r--r--dom/media/eme/clearkey/ClearKeyUtils.h107
-rw-r--r--dom/media/eme/clearkey/RefCounted.h85
-rw-r--r--dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp81
-rw-r--r--dom/media/eme/clearkey/gtest/moz.build15
-rw-r--r--dom/media/eme/clearkey/moz.build37
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp115
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h63
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp453
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMProxy.h186
-rw-r--r--dom/media/eme/mediadrm/MediaDrmProxySupport.cpp272
-rw-r--r--dom/media/eme/mediadrm/MediaDrmProxySupport.h68
-rw-r--r--dom/media/eme/mediadrm/moz.build19
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMImpl.cpp151
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMImpl.h124
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxy.cpp414
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxy.h149
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp72
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxyCallback.h38
-rw-r--r--dom/media/eme/mediafoundation/moz.build21
-rw-r--r--dom/media/eme/moz.build56
64 files changed, 11717 insertions, 0 deletions
diff --git a/dom/media/eme/CDMCaps.cpp b/dom/media/eme/CDMCaps.cpp
new file mode 100644
index 0000000000..2752ada476
--- /dev/null
+++ b/dom/media/eme/CDMCaps.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/CDMCaps.h"
+#include "mozilla/EMEUtils.h"
+#include "nsThreadUtils.h"
+#include "SamplesWaitingForKey.h"
+
+namespace mozilla {
+
+CDMCaps::CDMCaps() = default;
+
+CDMCaps::~CDMCaps() = default;
+
+// Keys with MediaKeyStatus::Usable, MediaKeyStatus::Output_downscaled,
+// or MediaKeyStatus::Output_restricted status can be used by the CDM
+// to decrypt or decrypt-and-decode samples.
+static bool IsUsableStatus(dom::MediaKeyStatus aStatus) {
+ return aStatus == dom::MediaKeyStatus::Usable ||
+ aStatus == dom::MediaKeyStatus::Output_restricted ||
+ aStatus == dom::MediaKeyStatus::Output_downscaled;
+}
+
+bool CDMCaps::IsKeyUsable(const CencKeyId& aKeyId) {
+ for (const KeyStatus& keyStatus : mKeyStatuses) {
+ if (keyStatus.mId == aKeyId) {
+ return IsUsableStatus(keyStatus.mStatus);
+ }
+ }
+ return false;
+}
+
+bool CDMCaps::SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus) {
+ if (!aStatus.WasPassed()) {
+ // Called from ForgetKeyStatus.
+ // Return true if the element is found to notify key changes.
+ return mKeyStatuses.RemoveElement(
+ KeyStatus(aKeyId, aSessionId, dom::MediaKeyStatus::Internal_error));
+ }
+
+ KeyStatus key(aKeyId, aSessionId, aStatus.Value());
+ auto index = mKeyStatuses.IndexOf(key);
+ if (index != mKeyStatuses.NoIndex) {
+ if (mKeyStatuses[index].mStatus == aStatus.Value()) {
+ // No change.
+ return false;
+ }
+ auto oldStatus = mKeyStatuses[index].mStatus;
+ mKeyStatuses[index].mStatus = aStatus.Value();
+ // The old key status was one for which we can decrypt media. We don't
+ // need to do the "notify usable" step below, as it should be impossible
+ // for us to have anything waiting on this key to become usable, since it
+ // was already usable.
+ if (IsUsableStatus(oldStatus)) {
+ return true;
+ }
+ } else {
+ mKeyStatuses.AppendElement(key);
+ }
+
+ // Only call NotifyUsable() for a key when we are going from non-usable
+ // to usable state.
+ if (!IsUsableStatus(aStatus.Value())) {
+ return true;
+ }
+
+ auto& waiters = mWaitForKeys;
+ size_t i = 0;
+ while (i < waiters.Length()) {
+ auto& w = waiters[i];
+ if (w.mKeyId == aKeyId) {
+ w.mListener->NotifyUsable(aKeyId);
+ waiters.RemoveElementAt(i);
+ } else {
+ i++;
+ }
+ }
+ return true;
+}
+
+void CDMCaps::NotifyWhenKeyIdUsable(const CencKeyId& aKey,
+ SamplesWaitingForKey* aListener) {
+ MOZ_ASSERT(!IsKeyUsable(aKey));
+ MOZ_ASSERT(aListener);
+ mWaitForKeys.AppendElement(WaitForKeys(aKey, aListener));
+}
+
+void CDMCaps::GetKeyStatusesForSession(const nsAString& aSessionId,
+ nsTArray<KeyStatus>& aOutKeyStatuses) {
+ for (const KeyStatus& keyStatus : mKeyStatuses) {
+ if (keyStatus.mSessionId.Equals(aSessionId)) {
+ aOutKeyStatuses.AppendElement(keyStatus);
+ }
+ }
+}
+
+bool CDMCaps::RemoveKeysForSession(const nsString& aSessionId) {
+ bool changed = false;
+ nsTArray<KeyStatus> statuses;
+ GetKeyStatusesForSession(aSessionId, statuses);
+ for (const KeyStatus& status : statuses) {
+ changed |= SetKeyStatus(status.mId, aSessionId,
+ dom::Optional<dom::MediaKeyStatus>());
+ }
+ return changed;
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/CDMCaps.h b/dom/media/eme/CDMCaps.h
new file mode 100644
index 0000000000..b16c5153b1
--- /dev/null
+++ b/dom/media/eme/CDMCaps.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CDMCaps_h_
+#define CDMCaps_h_
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "SamplesWaitingForKey.h"
+
+#include "mozilla/Monitor.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/BindingDeclarations.h" // For Optional
+
+namespace mozilla {
+
+// CDM capabilities; what keys a CDMProxy can use.
+// Must be locked to access state.
+class CDMCaps {
+ public:
+ CDMCaps();
+ ~CDMCaps();
+
+ struct KeyStatus {
+ KeyStatus(const CencKeyId& aId, const nsString& aSessionId,
+ dom::MediaKeyStatus aStatus)
+ : mId(aId.Clone()), mSessionId(aSessionId), mStatus(aStatus) {}
+ KeyStatus(const KeyStatus& aOther)
+ : mId(aOther.mId.Clone()),
+ mSessionId(aOther.mSessionId),
+ mStatus(aOther.mStatus) {}
+ bool operator==(const KeyStatus& aOther) const {
+ return mId == aOther.mId && mSessionId == aOther.mSessionId;
+ };
+
+ CencKeyId mId;
+ nsString mSessionId;
+ dom::MediaKeyStatus mStatus;
+ };
+
+ bool IsKeyUsable(const CencKeyId& aKeyId);
+
+ // Returns true if key status changed,
+ // i.e. the key status changed from usable to expired.
+ bool SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus);
+
+ void GetKeyStatusesForSession(const nsAString& aSessionId,
+ nsTArray<KeyStatus>& aOutKeyStatuses);
+
+ // Ensures all keys for a session are marked as 'unknown', i.e. removed.
+ // Returns true if a key status was changed.
+ bool RemoveKeysForSession(const nsString& aSessionId);
+
+ // Notifies the SamplesWaitingForKey when key become usable.
+ void NotifyWhenKeyIdUsable(const CencKeyId& aKey,
+ SamplesWaitingForKey* aSamplesWaiting);
+
+ private:
+ struct WaitForKeys {
+ WaitForKeys(const CencKeyId& aKeyId, SamplesWaitingForKey* aListener)
+ : mKeyId(aKeyId.Clone()), mListener(aListener) {}
+ CencKeyId mKeyId;
+ RefPtr<SamplesWaitingForKey> mListener;
+ };
+
+ nsTArray<KeyStatus> mKeyStatuses;
+
+ nsTArray<WaitForKeys> mWaitForKeys;
+
+ // It is not safe to copy this object.
+ CDMCaps(const CDMCaps&) = delete;
+ CDMCaps& operator=(const CDMCaps&) = delete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/CDMProxy.h b/dom/media/eme/CDMProxy.h
new file mode 100644
index 0000000000..27ebef27de
--- /dev/null
+++ b/dom/media/eme/CDMProxy.h
@@ -0,0 +1,325 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CDMProxy_h_
+#define CDMProxy_h_
+
+#include "mozilla/CDMCaps.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/MozPromise.h"
+
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeys.h"
+
+#include "nsIThread.h"
+
+namespace mozilla {
+class ErrorResult;
+class MediaRawData;
+class ChromiumCDMProxy;
+#ifdef MOZ_WMF_CDM
+class WMFCDMProxy;
+#endif
+
+namespace eme {
+enum DecryptStatus {
+ Ok = 0,
+ GenericErr = 1,
+ NoKeyErr = 2,
+ AbortedErr = 3,
+};
+}
+
+using eme::DecryptStatus;
+
+struct DecryptResult {
+ DecryptResult(DecryptStatus aStatus, MediaRawData* aSample)
+ : mStatus(aStatus), mSample(aSample) {}
+ DecryptStatus mStatus;
+ RefPtr<MediaRawData> mSample;
+};
+
+typedef MozPromise<DecryptResult, DecryptResult, /* IsExclusive = */ true>
+ DecryptPromise;
+
+class CDMKeyInfo {
+ public:
+ explicit CDMKeyInfo(const nsTArray<uint8_t>& aKeyId)
+ : mKeyId(aKeyId.Clone()), mStatus() {}
+
+ CDMKeyInfo(const nsTArray<uint8_t>& aKeyId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus)
+ : mKeyId(aKeyId.Clone()), mStatus(aStatus.Value()) {}
+
+ // The copy-ctor and copy-assignment operator for Optional<T> are declared as
+ // delete, so override CDMKeyInfo copy-ctor for nsTArray operations.
+ CDMKeyInfo(const CDMKeyInfo& aKeyInfo) {
+ mKeyId = aKeyInfo.mKeyId.Clone();
+ if (aKeyInfo.mStatus.WasPassed()) {
+ mStatus.Construct(aKeyInfo.mStatus.Value());
+ }
+ }
+
+ nsTArray<uint8_t> mKeyId;
+ dom::Optional<dom::MediaKeyStatus> mStatus;
+};
+
+// Time is defined as the number of milliseconds since the
+// Epoch (00:00:00 UTC, January 1, 1970).
+typedef int64_t UnixTime;
+
+// Proxies calls CDM, and proxies calls back.
+// Note: Promises are passed in via a PromiseId, so that the ID can be
+// passed via IPC to the CDM, which can then signal when to reject or
+// resolve the promise using its PromiseId.
+class CDMProxy {
+ protected:
+ typedef dom::PromiseId PromiseId;
+ typedef dom::MediaKeySessionType MediaKeySessionType;
+
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Main thread only.
+ // Loads the CDM corresponding to mKeySystem.
+ // Calls MediaKeys::OnCDMCreated() when the CDM is created.
+ virtual void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) = 0;
+
+ // Main thread only.
+ // Uses the CDM to create a key session.
+ // Calls MediaKeys::OnSessionActivated() when session is created.
+ // Assumes ownership of (std::move()s) aInitData's contents.
+ virtual void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) = 0;
+
+ // Main thread only.
+ // Uses the CDM to load a presistent session stored on disk.
+ // Calls MediaKeys::OnSessionActivated() when session is loaded.
+ virtual void LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ // Sends a new certificate to the CDM.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // Assumes ownership of (std::move()s) aCert's contents.
+ virtual void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) = 0;
+
+ // Main thread only.
+ // Sends an update to the CDM.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // Assumes ownership of (std::move()s) aResponse's contents.
+ virtual void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) = 0;
+
+ // Main thread only.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // If processing this operation results in the session actually closing,
+ // we also call MediaKeySession::OnClosed(), which in turn calls
+ // MediaKeys::OnSessionClosed().
+ virtual void CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) = 0;
+
+ // Main thread only.
+ // Removes all data for a persisent session.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ virtual void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) = 0;
+
+ // Main thread only.
+ // Called to signal a request for output protection information from the CDM.
+ // This should forward the call up the stack where the query should be
+ // performed and then responded to via `NotifyOutputProtectionStatus`.
+ virtual void QueryOutputProtectionStatus() = 0;
+
+ // NotifyOutputProtectionStatus enums. Explicit values are specified to make
+ // it easy to match values in logs.
+ enum class OutputProtectionCheckStatus : uint8_t {
+ CheckFailed = 0,
+ CheckSuccessful = 1,
+ };
+
+ enum class OutputProtectionCaptureStatus : uint8_t {
+ CapturePossilbe = 0,
+ CaptureNotPossible = 1,
+ Unused = 2,
+ };
+ // End NotifyOutputProtectionStatus enums
+
+ // Main thread only.
+ // Notifies this proxy of the protection status for the media the CDM is
+ // associated with. This can be called in response to
+ // `QueryOutputProtectionStatus`, but can also be called without an
+ // associated query. In both cases the information will be forwarded to
+ // the CDM host machinery and used to handle requests from the CDM.
+ // @param aCheckStatus did the check succeed or not.
+ // @param aCaptureStatus if the check succeeded, this reflects if capture
+ // of media could take place. This doesn't mean capture is taking place.
+ // Callers should be conservative with this value such that it's okay to pass
+ // CapturePossilbe even if capture is not happening, but should never pass
+ // CaptureNotPossible if it could happen. If the check failed, this value is
+ // not used, and callers should pass Unused to indicate this.
+ virtual void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) = 0;
+
+ // Main thread only.
+ virtual void Shutdown() = 0;
+
+ // Main thread only.
+ virtual void Terminated() = 0;
+
+ // Threadsafe.
+ const nsCString& GetNodeId() const { return mNodeId; };
+
+ // Main thread only.
+ virtual void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ virtual void OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) = 0;
+
+ // Main thread only.
+ virtual void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) = 0;
+
+ // Main thread only.
+ virtual void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) = 0;
+
+ // Main thread only.
+ virtual void OnSessionClosed(const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ virtual void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) = 0;
+
+ // Main thread only.
+ virtual void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) = 0;
+
+ virtual RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) = 0;
+
+ // Owner thread only.
+ virtual void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) = 0;
+
+ // Reject promise with the given ErrorResult.
+ //
+ // Can be called from any thread.
+ virtual void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) = 0;
+
+ // Resolves promise with "undefined".
+ // Can be called from any thread.
+ virtual void ResolvePromise(PromiseId aId) = 0;
+
+ // Threadsafe.
+ const nsString& KeySystem() const { return mKeySystem; };
+
+ DataMutex<CDMCaps>& Capabilites() { return mCapabilites; };
+
+ // Main thread only.
+ virtual void OnKeyStatusesChange(const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ // Calls MediaKeys->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus) after
+ // the CDM has processed the request.
+ virtual void GetStatusForPolicy(PromiseId aPromiseId,
+ const dom::HDCPVersion& aMinHdcpVersion) = 0;
+
+#ifdef DEBUG
+ virtual bool IsOnOwnerThread() = 0;
+#endif
+
+ virtual ChromiumCDMProxy* AsChromiumCDMProxy() { return nullptr; }
+
+#ifdef MOZ_WMF_CDM
+ virtual WMFCDMProxy* AsWMFCDMProxy() { return nullptr; }
+#endif
+
+ virtual bool IsHardwareDecryptionSupported() const { return false; }
+
+ protected:
+ // Main thread only.
+ CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired, bool aPersistentStateRequired)
+ : mKeys(aKeys),
+ mKeySystem(aKeySystem),
+ mCapabilites("CDMProxy::mCDMCaps"),
+ mDistinctiveIdentifierRequired(aDistinctiveIdentifierRequired),
+ mPersistentStateRequired(aPersistentStateRequired),
+ mMainThread(GetMainThreadSerialEventTarget()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ virtual ~CDMProxy() {}
+
+ // Helper to enforce that a raw pointer is only accessed on the main thread.
+ template <class Type>
+ class MainThreadOnlyRawPtr {
+ public:
+ explicit MainThreadOnlyRawPtr(Type* aPtr) : mPtr(aPtr) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ bool IsNull() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !mPtr;
+ }
+
+ void Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPtr = nullptr;
+ }
+
+ Type* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPtr;
+ }
+
+ private:
+ Type* mPtr;
+ };
+
+ // Our reference back to the MediaKeys object.
+ // WARNING: This is a non-owning reference that is cleared by MediaKeys
+ // destructor. only use on main thread, and always nullcheck before using!
+ MainThreadOnlyRawPtr<dom::MediaKeys> mKeys;
+
+ const nsString mKeySystem;
+
+ // Onwer specified thread. e.g. Gecko Media Plugin thread.
+ // All interactions with the out-of-process EME plugin must come from this
+ // thread.
+ RefPtr<nsIThread> mOwnerThread;
+
+ nsCString mNodeId;
+
+ DataMutex<CDMCaps> mCapabilites;
+
+ const bool mDistinctiveIdentifierRequired;
+ const bool mPersistentStateRequired;
+
+ // The main thread associated with the root document.
+ const nsCOMPtr<nsISerialEventTarget> mMainThread;
+};
+
+} // namespace mozilla
+
+#endif // CDMProxy_h_
diff --git a/dom/media/eme/DecryptorProxyCallback.h b/dom/media/eme/DecryptorProxyCallback.h
new file mode 100644
index 0000000000..88acdd7967
--- /dev/null
+++ b/dom/media/eme/DecryptorProxyCallback.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecryptorProxyCallback_h_
+#define DecryptorProxyCallback_h_
+
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType
+#include "mozilla/CDMProxy.h"
+
+namespace mozilla {
+class ErrorResult;
+}
+
+class DecryptorProxyCallback {
+ public:
+ virtual ~DecryptorProxyCallback() {}
+
+ virtual void SetSessionId(uint32_t aCreateSessionId,
+ const nsCString& aSessionId) = 0;
+
+ virtual void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) = 0;
+
+ virtual void ResolvePromise(uint32_t aPromiseId) = 0;
+
+ virtual void RejectPromise(uint32_t aPromiseId,
+ mozilla::ErrorResult&& aException,
+ const nsCString& aSessionId) = 0;
+
+ virtual void SessionMessage(const nsCString& aSessionId,
+ mozilla::dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) = 0;
+
+ virtual void ExpirationChange(const nsCString& aSessionId,
+ mozilla::UnixTime aExpiryTime) = 0;
+
+ virtual void SessionClosed(const nsCString& aSessionId) = 0;
+
+ virtual void SessionError(const nsCString& aSessionId, nsresult aException,
+ uint32_t aSystemCode,
+ const nsCString& aMessage) = 0;
+
+ virtual void Decrypted(uint32_t aId, mozilla::DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) = 0;
+
+ virtual void BatchedKeyStatusChanged(
+ const nsCString& aSessionId,
+ const nsTArray<mozilla::CDMKeyInfo>& aKeyInfos) = 0;
+};
+
+#endif
diff --git a/dom/media/eme/DetailedPromise.cpp b/dom/media/eme/DetailedPromise.cpp
new file mode 100644
index 0000000000..0692a1dd07
--- /dev/null
+++ b/dom/media/eme/DetailedPromise.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DetailedPromise.h"
+
+#include "VideoUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::dom {
+
+DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal,
+ const nsACString& aName)
+ : Promise(aGlobal),
+ mName(aName),
+ mResponded(false),
+ mStartTime(TimeStamp::Now()) {}
+
+DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal,
+ const nsACString& aName,
+ Telemetry::HistogramID aSuccessLatencyProbe,
+ Telemetry::HistogramID aFailureLatencyProbe)
+ : DetailedPromise(aGlobal, aName) {
+ mSuccessLatencyProbe.Construct(aSuccessLatencyProbe);
+ mFailureLatencyProbe.Construct(aFailureLatencyProbe);
+}
+
+DetailedPromise::~DetailedPromise() {
+ // It would be nice to assert that mResponded is identical to
+ // GetPromiseState() == PromiseState::Rejected. But by now we've been
+ // unlinked, so don't have a reference to our actual JS Promise object
+ // anymore.
+ MaybeReportTelemetry(kFailed);
+}
+
+void DetailedPromise::LogRejectionReason(uint32_t aErrorCode,
+ const nsACString& aReason) {
+ nsPrintfCString msg("%s promise rejected 0x%" PRIx32 " '%s'", mName.get(),
+ aErrorCode, PromiseFlatCString(aReason).get());
+ EME_LOG("%s", msg.get());
+
+ MaybeReportTelemetry(kFailed);
+
+ LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg));
+}
+
+void DetailedPromise::MaybeReject(nsresult aArg, const nsACString& aReason) {
+ LogRejectionReason(static_cast<uint32_t>(aArg), aReason);
+
+ Promise::MaybeRejectWithDOMException(aArg, aReason);
+}
+
+void DetailedPromise::MaybeReject(ErrorResult&& aArg,
+ const nsACString& aReason) {
+ LogRejectionReason(aArg.ErrorCodeAsInt(), aReason);
+ Promise::MaybeReject(std::move(aArg));
+}
+
+/* static */
+already_AddRefed<DetailedPromise> DetailedPromise::Create(
+ nsIGlobalObject* aGlobal, ErrorResult& aRv, const nsACString& aName) {
+ RefPtr<DetailedPromise> promise = new DetailedPromise(aGlobal, aName);
+ promise->CreateWrapper(aRv);
+ return aRv.Failed() ? nullptr : promise.forget();
+}
+
+void DetailedPromise::MaybeReportTelemetry(eStatus aStatus) {
+ if (mResponded) {
+ return;
+ }
+ mResponded = true;
+ if (!mSuccessLatencyProbe.WasPassed() || !mFailureLatencyProbe.WasPassed()) {
+ return;
+ }
+ uint32_t latency = (TimeStamp::Now() - mStartTime).ToMilliseconds();
+ EME_LOG("%s %s latency %ums reported via telemetry", mName.get(),
+ ((aStatus == kSucceeded) ? "succcess" : "failure"), latency);
+ Telemetry::HistogramID tid = (aStatus == kSucceeded)
+ ? mSuccessLatencyProbe.Value()
+ : mFailureLatencyProbe.Value();
+ Telemetry::Accumulate(tid, latency);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/DetailedPromise.h b/dom/media/eme/DetailedPromise.h
new file mode 100644
index 0000000000..02c774755f
--- /dev/null
+++ b/dom/media/eme/DetailedPromise.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __DetailedPromise_h__
+#define __DetailedPromise_h__
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Telemetry.h"
+#include "EMEUtils.h"
+
+namespace mozilla::dom {
+
+/*
+ * This is pretty horrible; bug 1160445.
+ * Extend Promise to add custom DOMException messages on rejection.
+ * Get rid of this once we've ironed out EME errors in the wild.
+ */
+class DetailedPromise : public Promise {
+ public:
+ static already_AddRefed<DetailedPromise> Create(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv,
+ const nsACString& aName);
+
+ template <typename T>
+ void MaybeResolve(T&& aArg) {
+ EME_LOG("%s promise resolved", mName.get());
+ MaybeReportTelemetry(eStatus::kSucceeded);
+ Promise::MaybeResolve(std::forward<T>(aArg));
+ }
+
+ void MaybeReject(nsresult aArg) = delete;
+ void MaybeReject(nsresult aArg, const nsACString& aReason);
+
+ void MaybeReject(ErrorResult&& aArg) = delete;
+ void MaybeReject(ErrorResult&& aArg, const nsACString& aReason);
+
+ // Facilities for rejecting with various spec-defined exception values.
+#define DOMEXCEPTION(name, err) \
+ inline void MaybeRejectWith##name(const nsACString& aMessage) { \
+ LogRejectionReason(static_cast<uint32_t>(err), aMessage); \
+ Promise::MaybeRejectWith##name(aMessage); \
+ } \
+ template <int N> \
+ void MaybeRejectWith##name(const char(&aMessage)[N]) { \
+ MaybeRejectWith##name(nsLiteralCString(aMessage)); \
+ }
+
+#include "mozilla/dom/DOMExceptionNames.h"
+
+#undef DOMEXCEPTION
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithTypeError(Ts&&... aMessageArgs) = delete;
+
+ inline void MaybeRejectWithTypeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowTypeError(aMessage);
+ MaybeReject(std::move(res), aMessage);
+ }
+
+ template <int N>
+ void MaybeRejectWithTypeError(const char (&aMessage)[N]) {
+ MaybeRejectWithTypeError(nsLiteralCString(aMessage));
+ }
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithRangeError(Ts&&... aMessageArgs) = delete;
+
+ inline void MaybeRejectWithRangeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowRangeError(aMessage);
+ MaybeReject(std::move(res), aMessage);
+ }
+
+ template <int N>
+ void MaybeRejectWithRangeError(const char (&aMessage)[N]) {
+ MaybeRejectWithRangeError(nsLiteralCString(aMessage));
+ }
+
+ private:
+ explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName);
+
+ explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName,
+ Telemetry::HistogramID aSuccessLatencyProbe,
+ Telemetry::HistogramID aFailureLatencyProbe);
+ virtual ~DetailedPromise();
+
+ enum eStatus { kSucceeded, kFailed };
+ void MaybeReportTelemetry(eStatus aStatus);
+ void LogRejectionReason(uint32_t aErrorCode, const nsACString& aReason);
+
+ nsCString mName;
+ bool mResponded;
+ TimeStamp mStartTime;
+ Optional<Telemetry::HistogramID> mSuccessLatencyProbe;
+ Optional<Telemetry::HistogramID> mFailureLatencyProbe;
+};
+
+} // namespace mozilla::dom
+
+#endif // __DetailedPromise_h__
diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp
new file mode 100644
index 0000000000..294951e7e6
--- /dev/null
+++ b/dom/media/eme/EMEUtils.cpp
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EMEUtils.h"
+
+#include "jsfriendapi.h"
+#include "MediaData.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/dom/UnionTypes.h"
+
+#ifdef MOZ_WMF_CDM
+# include "mozilla/PMFCDM.h"
+# include "KeySystemConfig.h"
+#endif
+
+namespace mozilla {
+
+LogModule* GetEMELog() {
+ static LazyLogModule log("EME");
+ return log;
+}
+
+LogModule* GetEMEVerboseLog() {
+ static LazyLogModule log("EMEV");
+ return log;
+}
+
+void CopyArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView,
+ nsTArray<uint8_t>& aOutData) {
+ aOutData.Clear();
+ Unused << dom::AppendTypedArrayDataTo(aBufferOrView, aOutData);
+}
+
+bool IsClearkeyKeySystem(const nsAString& aKeySystem) {
+ if (StaticPrefs::media_clearkey_test_key_systems_enabled()) {
+ return aKeySystem.EqualsLiteral(kClearKeyKeySystemName) ||
+ aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName);
+ }
+ return aKeySystem.EqualsLiteral(kClearKeyKeySystemName);
+}
+
+bool IsWidevineKeySystem(const nsAString& aKeySystem) {
+ return aKeySystem.EqualsLiteral(kWidevineKeySystemName);
+}
+
+#ifdef MOZ_WMF_CDM
+bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem) {
+ if (!StaticPrefs::media_eme_playready_enabled()) {
+ return false;
+ }
+ // 1=enabled encrypted and clear, 2=enabled encrytped.
+ if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
+ StaticPrefs::media_wmf_media_engine_enabled() != 2) {
+ return false;
+ }
+ return IsPlayReadyKeySystem(aKeySystem);
+}
+
+bool IsPlayReadyKeySystem(const nsAString& aKeySystem) {
+ return aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) ||
+ aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware) ||
+ aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName);
+}
+
+bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem) {
+ if (!StaticPrefs::media_eme_widevine_experiment_enabled()) {
+ return false;
+ }
+ // 1=enabled encrypted and clear, 2=enabled encrytped.
+ if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
+ StaticPrefs::media_wmf_media_engine_enabled() != 2) {
+ return false;
+ }
+ return IsWidevineExperimentKeySystem(aKeySystem);
+}
+
+bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem) {
+ return aKeySystem.EqualsLiteral(kWidevineExperimentKeySystemName) ||
+ aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName);
+}
+
+bool IsWMFClearKeySystemAndSupported(const nsAString& aKeySystem) {
+ if (!StaticPrefs::media_eme_wmf_clearkey_enabled()) {
+ return false;
+ }
+ // 1=enabled encrypted and clear, 2=enabled encrytped.
+ if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
+ StaticPrefs::media_wmf_media_engine_enabled() != 2) {
+ return false;
+ }
+ return aKeySystem.EqualsLiteral(kClearKeyKeySystemName);
+}
+#endif
+
+nsString KeySystemToProxyName(const nsAString& aKeySystem) {
+ if (IsClearkeyKeySystem(aKeySystem)) {
+#ifdef MOZ_WMF_CDM
+ if (StaticPrefs::media_eme_wmf_clearkey_enabled()) {
+ return u"mfcdm-clearkey"_ns;
+ }
+#endif
+ return u"gmp-clearkey"_ns;
+ }
+ if (IsWidevineKeySystem(aKeySystem)) {
+ return u"gmp-widevinecdm"_ns;
+ }
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem)) {
+ return u"mfcdm-playready"_ns;
+ }
+ if (IsWidevineExperimentKeySystemAndSupported(aKeySystem)) {
+ return u"mfcdm-widevine"_ns;
+ }
+#endif
+ MOZ_ASSERT_UNREACHABLE("Not supported key system!");
+ return u""_ns;
+}
+
+#define ENUM_TO_STR(enumVal) \
+ case enumVal: \
+ return #enumVal
+
+const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus) {
+ switch (aStatus) {
+ ENUM_TO_STR(dom::MediaKeyStatus::Usable);
+ ENUM_TO_STR(dom::MediaKeyStatus::Expired);
+ ENUM_TO_STR(dom::MediaKeyStatus::Released);
+ ENUM_TO_STR(dom::MediaKeyStatus::Output_restricted);
+ ENUM_TO_STR(dom::MediaKeyStatus::Output_downscaled);
+ ENUM_TO_STR(dom::MediaKeyStatus::Status_pending);
+ ENUM_TO_STR(dom::MediaKeyStatus::Internal_error);
+ default:
+ return "Undefined MediaKeyStatus!";
+ }
+}
+
+#undef ENUM_TO_STR
+
+bool IsHardwareDecryptionSupported(
+ const dom::MediaKeySystemConfiguration& aConfig) {
+ bool supportHardwareDecryption = false;
+ for (const auto& capabilities : aConfig.mAudioCapabilities) {
+ if (capabilities.mRobustness.EqualsLiteral("HW_SECURE_ALL")) {
+ supportHardwareDecryption = true;
+ break;
+ }
+ }
+ for (const auto& capabilities : aConfig.mVideoCapabilities) {
+ if (capabilities.mRobustness.EqualsLiteral("3000") ||
+ capabilities.mRobustness.EqualsLiteral("HW_SECURE_ALL") ||
+ capabilities.mRobustness.EqualsLiteral("HW_SECURE_DECODE")) {
+ supportHardwareDecryption = true;
+ break;
+ }
+ }
+ return supportHardwareDecryption;
+}
+
+const char* EncryptionSchemeStr(const CryptoScheme& aScheme) {
+ switch (aScheme) {
+ case CryptoScheme::None:
+ return "none";
+ case CryptoScheme::Cenc:
+ return "cenc";
+ case CryptoScheme::Cbcs:
+ return "cbcs";
+ default:
+ return "not-defined!";
+ }
+}
+
+#ifdef MOZ_WMF_CDM
+void MFCDMCapabilitiesIPDLToKeySystemConfig(
+ const MFCDMCapabilitiesIPDL& aCDMConfig,
+ KeySystemConfig& aKeySystemConfig) {
+ aKeySystemConfig.mKeySystem = aCDMConfig.keySystem();
+
+ for (const auto& type : aCDMConfig.initDataTypes()) {
+ aKeySystemConfig.mInitDataTypes.AppendElement(type);
+ }
+
+ for (const auto& type : aCDMConfig.sessionTypes()) {
+ aKeySystemConfig.mSessionTypes.AppendElement(type);
+ }
+
+ for (const auto& c : aCDMConfig.videoCapabilities()) {
+ if (!c.robustness().IsEmpty() &&
+ !aKeySystemConfig.mVideoRobustness.Contains(c.robustness())) {
+ aKeySystemConfig.mVideoRobustness.AppendElement(c.robustness());
+ }
+ aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
+ NS_ConvertUTF16toUTF8(c.contentType()));
+ }
+ for (const auto& c : aCDMConfig.audioCapabilities()) {
+ if (!c.robustness().IsEmpty() &&
+ !aKeySystemConfig.mAudioRobustness.Contains(c.robustness())) {
+ aKeySystemConfig.mAudioRobustness.AppendElement(c.robustness());
+ }
+ aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
+ NS_ConvertUTF16toUTF8(c.contentType()));
+ }
+ aKeySystemConfig.mPersistentState = aCDMConfig.persistentState();
+ aKeySystemConfig.mDistinctiveIdentifier = aCDMConfig.distinctiveID();
+ for (const auto& scheme : aCDMConfig.encryptionSchemes()) {
+ aKeySystemConfig.mEncryptionSchemes.AppendElement(
+ NS_ConvertUTF8toUTF16(EncryptionSchemeStr(scheme)));
+ }
+ aKeySystemConfig.mIsHDCP22Compatible = aCDMConfig.isHDCP22Compatible();
+ EME_LOG("New Capabilities=%s",
+ NS_ConvertUTF16toUTF8(aKeySystemConfig.GetDebugInfo()).get());
+}
+#endif
+
+bool DoesKeySystemSupportClearLead(const nsAString& aKeySystem) {
+ // I believe that Widevine L3 supports clear-lead, but I couldn't find any
+ // official documentation to prove that. The only one I can find is that Shaka
+ // player mentions the clear lead feature. So we expect L3 should have that as
+ // well. But for HWDRM, Widevine L1 and SL3000 needs to rely on special checks
+ // to know whether clearlead is supported. That will be implemented by
+ // querying for special key system names.
+ // https://shaka-project.github.io/shaka-packager/html/documentation.html
+#ifdef MOZ_WMF_CDM
+ if (aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName) ||
+ aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName)) {
+ return true;
+ }
+#endif
+ return aKeySystem.EqualsLiteral(kWidevineKeySystemName);
+}
+
+bool CheckIfHarewareDRMConfigExists(
+ const nsTArray<dom::MediaKeySystemConfiguration>& aConfigs) {
+ bool foundHWDRMconfig = false;
+ for (const auto& config : aConfigs) {
+ if (IsHardwareDecryptionSupported(config)) {
+ foundHWDRMconfig = true;
+ break;
+ }
+ }
+ return foundHWDRMconfig;
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h
new file mode 100644
index 0000000000..ca5c684a9f
--- /dev/null
+++ b/dom/media/eme/EMEUtils.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EME_LOG_H_
+#define EME_LOG_H_
+
+#include "mozilla/Logging.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+enum class CryptoScheme : uint8_t;
+#ifdef MOZ_WMF_CDM
+class MFCDMCapabilitiesIPDL;
+#endif
+struct KeySystemConfig;
+
+namespace dom {
+class ArrayBufferViewOrArrayBuffer;
+}
+
+#ifndef EME_LOG
+LogModule* GetEMELog();
+# define EME_LOG(...) \
+ MOZ_LOG(GetEMELog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define EME_LOG_ENABLED() MOZ_LOG_TEST(GetEMELog(), mozilla::LogLevel::Debug)
+#endif
+
+#ifndef EME_VERBOSE_LOG
+LogModule* GetEMEVerboseLog();
+# define EME_VERBOSE_LOG(...) \
+ MOZ_LOG(GetEMEVerboseLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# ifndef EME_LOG
+# define EME_LOG(...)
+# endif
+
+# ifndef EME_VERBOSE_LOG
+# define EME_VERBOSE_LOG(...)
+# endif
+#endif
+
+// Helper function to extract a copy of data coming in from JS in an
+// (ArrayBuffer or ArrayBufferView) IDL typed function argument.
+//
+// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer.
+void CopyArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView,
+ nsTArray<uint8_t>& aOutData);
+
+nsString KeySystemToProxyName(const nsAString& aKeySystem);
+
+bool IsClearkeyKeySystem(const nsAString& aKeySystem);
+
+bool IsWidevineKeySystem(const nsAString& aKeySystem);
+
+#ifdef MOZ_WMF_CDM
+bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem);
+
+bool IsPlayReadyKeySystem(const nsAString& aKeySystem);
+
+bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem);
+
+bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem);
+
+bool IsWMFClearKeySystemAndSupported(const nsAString& aKeySystem);
+#endif
+
+// Note: Primetime is now unsupported, but we leave it in the enum so
+// that the telemetry enum values are not changed; doing so would break
+// existing telemetry probes.
+enum CDMType {
+ eClearKey = 0,
+ ePrimetime = 1, // Note: Unsupported.
+ eWidevine = 2,
+ eUnknown = 3
+};
+
+CDMType ToCDMTypeTelemetryEnum(const nsString& aKeySystem);
+
+const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus);
+
+// Return true if given config supports hardware decryption (SL3000 or L1).
+bool IsHardwareDecryptionSupported(
+ const dom::MediaKeySystemConfiguration& aConfig);
+
+const char* EncryptionSchemeStr(const CryptoScheme& aScheme);
+
+#ifdef MOZ_WMF_CDM
+void MFCDMCapabilitiesIPDLToKeySystemConfig(
+ const MFCDMCapabilitiesIPDL& aCDMConfig, KeySystemConfig& aKeySystemConfig);
+#endif
+
+bool DoesKeySystemSupportClearLead(const nsAString& aKeySystem);
+
+// Return true if there is any config in the given configs has hardware DRM
+// associated robustness.
+bool CheckIfHarewareDRMConfigExists(
+ const nsTArray<dom::MediaKeySystemConfiguration>& aConfigs);
+
+} // namespace mozilla
+
+#endif // EME_LOG_H_
diff --git a/dom/media/eme/KeySystemConfig.cpp b/dom/media/eme/KeySystemConfig.cpp
new file mode 100644
index 0000000000..b1b1f9ff63
--- /dev/null
+++ b/dom/media/eme/KeySystemConfig.cpp
@@ -0,0 +1,360 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "KeySystemConfig.h"
+
+#include "EMEUtils.h"
+#include "GMPUtils.h"
+#include "KeySystemNames.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsPrintfCString.h"
+
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidDecoderModule.h"
+# include "mozilla/java/MediaDrmProxyWrappers.h"
+# include "nsMimeTypes.h"
+#endif
+
+#ifdef MOZ_WMF_CDM
+# include "mediafoundation/WMFCDMImpl.h"
+#endif
+
+namespace mozilla {
+
+/* static */
+bool KeySystemConfig::Supports(const nsAString& aKeySystem) {
+#ifdef MOZ_WIDGET_ANDROID
+ // No GMP on Android, check if we can use MediaDrm for this keysystem.
+ if (mozilla::java::MediaDrmProxy::IsSchemeSupported(
+ NS_ConvertUTF16toUTF8(aKeySystem))) {
+ return true;
+ }
+#else
+# ifdef MOZ_WMF_CDM
+ // Test only, pretend we have already installed CDMs.
+ if (StaticPrefs::media_eme_wmf_use_mock_cdm_for_external_cdms()) {
+ return true;
+ }
+# endif
+ // Check if Widevine L3 or Clearkey has been downloaded via GMP downloader.
+ if (IsWidevineKeySystem(aKeySystem) || IsClearkeyKeySystem(aKeySystem)) {
+ return HaveGMPFor(nsCString(CHROMIUM_CDM_API),
+ {NS_ConvertUTF16toUTF8(aKeySystem)});
+ }
+#endif
+
+#if MOZ_WMF_CDM
+ // Check if Widevine L1 has been downloaded via GMP downloader.
+ if (IsWidevineExperimentKeySystemAndSupported(aKeySystem)) {
+ return HaveGMPFor(nsCString(kWidevineExperimentAPIName),
+ {nsCString(kWidevineExperimentKeySystemName)});
+ }
+
+ if ((IsPlayReadyKeySystemAndSupported(aKeySystem) ||
+ IsWMFClearKeySystemAndSupported(aKeySystem)) &&
+ WMFCDMImpl::Supports(aKeySystem)) {
+ return true;
+ }
+#endif
+
+ return false;
+}
+
+/* static */
+bool KeySystemConfig::CreateKeySystemConfigs(
+ const nsAString& aKeySystem, nsTArray<KeySystemConfig>& aOutConfigs) {
+ if (!Supports(aKeySystem)) {
+ return false;
+ }
+
+ if (IsClearkeyKeySystem(aKeySystem)) {
+ KeySystemConfig* config = aOutConfigs.AppendElement();
+ config->mKeySystem = aKeySystem;
+ config->mInitDataTypes.AppendElement(u"cenc"_ns);
+ config->mInitDataTypes.AppendElement(u"keyids"_ns);
+ config->mInitDataTypes.AppendElement(u"webm"_ns);
+ config->mPersistentState = Requirement::Optional;
+ config->mDistinctiveIdentifier = Requirement::NotAllowed;
+ config->mSessionTypes.AppendElement(SessionType::Temporary);
+ config->mEncryptionSchemes.AppendElement(u"cenc"_ns);
+ config->mEncryptionSchemes.AppendElement(u"cbcs"_ns);
+ config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns);
+ if (StaticPrefs::media_clearkey_persistent_license_enabled()) {
+ config->mSessionTypes.AppendElement(SessionType::PersistentLicense);
+ }
+#if defined(XP_WIN)
+ // Clearkey CDM uses WMF's H.264 decoder on Windows.
+ if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) {
+ config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+ } else {
+ config->mMP4.SetCanDecrypt(EME_CODEC_H264);
+ }
+#else
+ config->mMP4.SetCanDecrypt(EME_CODEC_H264);
+#endif
+ config->mMP4.SetCanDecrypt(EME_CODEC_AAC);
+ config->mMP4.SetCanDecrypt(EME_CODEC_FLAC);
+ config->mMP4.SetCanDecrypt(EME_CODEC_OPUS);
+ config->mMP4.SetCanDecrypt(EME_CODEC_VP9);
+ config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+ config->mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+ config->mWebM.SetCanDecrypt(EME_CODEC_VP8);
+ config->mWebM.SetCanDecrypt(EME_CODEC_VP9);
+
+ if (StaticPrefs::media_clearkey_test_key_systems_enabled()) {
+ // Add testing key systems. These offer the same capabilities as the
+ // base clearkey system, so just clone clearkey and change the name.
+ KeySystemConfig clearkeyWithProtectionQuery{*config};
+ clearkeyWithProtectionQuery.mKeySystem.AssignLiteral(
+ kClearKeyWithProtectionQueryKeySystemName);
+ aOutConfigs.AppendElement(std::move(clearkeyWithProtectionQuery));
+ }
+ return true;
+ }
+
+ if (IsWidevineKeySystem(aKeySystem)) {
+ KeySystemConfig* config = aOutConfigs.AppendElement();
+ config->mKeySystem = aKeySystem;
+ config->mInitDataTypes.AppendElement(u"cenc"_ns);
+ config->mInitDataTypes.AppendElement(u"keyids"_ns);
+ config->mInitDataTypes.AppendElement(u"webm"_ns);
+ config->mPersistentState = Requirement::Optional;
+ config->mDistinctiveIdentifier = Requirement::NotAllowed;
+ config->mSessionTypes.AppendElement(SessionType::Temporary);
+#ifdef MOZ_WIDGET_ANDROID
+ config->mSessionTypes.AppendElement(SessionType::PersistentLicense);
+#endif
+ config->mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns);
+ config->mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns);
+ config->mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns);
+ config->mEncryptionSchemes.AppendElement(u"cenc"_ns);
+ config->mEncryptionSchemes.AppendElement(u"cbcs"_ns);
+ config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ // MediaDrm.isCryptoSchemeSupported only allows passing
+ // "video/mp4" or "video/webm" for mimetype string.
+ // See
+ // https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID,
+ // java.lang.String) for more detail.
+ typedef struct {
+ const nsCString& mMimeType;
+ const nsCString& mEMECodecType;
+ const char16_t* mCodecType;
+ KeySystemConfig::ContainerSupport* mSupportType;
+ } DataForValidation;
+
+ DataForValidation validationList[] = {
+ {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC,
+ &config->mMP4},
+ {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC,
+ &config->mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC,
+ &config->mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC,
+ &config->mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS,
+ &config->mMP4},
+ {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8,
+ &config->mWebM},
+ {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9,
+ &config->mWebM},
+ {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS,
+ &config->mWebM},
+ {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS,
+ &config->mWebM},
+ };
+
+ for (const auto& data : validationList) {
+ if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName,
+ data.mMimeType)) {
+ if (!AndroidDecoderModule::SupportsMimeType(data.mMimeType).isEmpty()) {
+ data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType);
+ } else {
+ data.mSupportType->SetCanDecrypt(data.mEMECodecType);
+ }
+ }
+ }
+#else
+# if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't
+ // decode AAC, and a codec wasn't specified, be conservative
+ // and reject the MediaKeys request, since we assume Widevine
+ // will be used with AAC.
+ if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ config->mMP4.SetCanDecrypt(EME_CODEC_AAC);
+ }
+# else
+ config->mMP4.SetCanDecrypt(EME_CODEC_AAC);
+# endif
+ config->mMP4.SetCanDecrypt(EME_CODEC_FLAC);
+ config->mMP4.SetCanDecrypt(EME_CODEC_OPUS);
+ config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+ config->mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9);
+ config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+ config->mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+ config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8);
+ config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9);
+#endif
+ return true;
+ }
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem) ||
+ IsWidevineExperimentKeySystemAndSupported(aKeySystem)) {
+ RefPtr<WMFCDMImpl> cdm = MakeRefPtr<WMFCDMImpl>(aKeySystem);
+ return cdm->GetCapabilities(aOutConfigs);
+ }
+#endif
+ return false;
+}
+
+bool KeySystemConfig::IsSameKeySystem(const nsAString& aKeySystem) const {
+#ifdef MOZ_WMF_CDM
+ // We want to map Widevine experiment key system to normal Widevine key system
+ // as well.
+ if (IsWidevineExperimentKeySystemAndSupported(mKeySystem)) {
+ return mKeySystem.Equals(aKeySystem) ||
+ aKeySystem.EqualsLiteral(kWidevineKeySystemName);
+ }
+#endif
+ return mKeySystem.Equals(aKeySystem);
+}
+
+/* static */
+void KeySystemConfig::GetGMPKeySystemConfigs(dom::Promise* aPromise) {
+ MOZ_ASSERT(aPromise);
+ nsTArray<KeySystemConfig> keySystemConfigs;
+ const nsTArray<nsString> keySystemNames{
+ NS_ConvertUTF8toUTF16(kClearKeyKeySystemName),
+ NS_ConvertUTF8toUTF16(kWidevineKeySystemName),
+ };
+ FallibleTArray<dom::CDMInformation> cdmInfo;
+ for (const auto& name : keySystemNames) {
+#ifdef MOZ_WMF_CDM
+ if (IsWMFClearKeySystemAndSupported(name)) {
+ // Using wmf clearkey, not gmp clearkey.
+ continue;
+ }
+#endif
+ if (KeySystemConfig::CreateKeySystemConfigs(name, keySystemConfigs)) {
+ auto* info = cdmInfo.AppendElement(fallible);
+ if (!info) {
+ aPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ MOZ_ASSERT(keySystemConfigs.Length() == cdmInfo.Length());
+ info->mKeySystemName = name;
+ info->mCapabilities = keySystemConfigs.LastElement().GetDebugInfo();
+ info->mClearlead = DoesKeySystemSupportClearLead(name);
+ // TODO : ask real CDM
+ info->mIsHDCP22Compatible = false;
+ }
+ }
+ aPromise->MaybeResolve(cdmInfo);
+}
+
+nsString KeySystemConfig::GetDebugInfo() const {
+ nsString debugInfo;
+ debugInfo.AppendLiteral(" key-system=");
+ debugInfo.Append(mKeySystem);
+ debugInfo.AppendLiteral(" init-data-type=[");
+ for (size_t idx = 0; idx < mInitDataTypes.Length(); idx++) {
+ debugInfo.Append(mInitDataTypes[idx]);
+ if (idx + 1 < mInitDataTypes.Length()) {
+ debugInfo.AppendLiteral(",");
+ }
+ }
+ debugInfo.AppendLiteral("]");
+ debugInfo.AppendASCII(
+ nsPrintfCString(" persistent=%s", RequirementToStr(mPersistentState))
+ .get());
+ debugInfo.AppendASCII(
+ nsPrintfCString(" distinctive=%s",
+ RequirementToStr(mDistinctiveIdentifier))
+ .get());
+ debugInfo.AppendLiteral(" sessionType=[");
+ for (size_t idx = 0; idx < mSessionTypes.Length(); idx++) {
+ debugInfo.AppendASCII(
+ nsPrintfCString("%s", SessionTypeToStr(mSessionTypes[idx])).get());
+ if (idx + 1 < mSessionTypes.Length()) {
+ debugInfo.AppendLiteral(",");
+ }
+ }
+ debugInfo.AppendLiteral("]");
+ debugInfo.AppendLiteral(" video-robustness=");
+ for (size_t idx = 0; idx < mVideoRobustness.Length(); idx++) {
+ debugInfo.Append(mVideoRobustness[idx]);
+ if (idx + 1 < mVideoRobustness.Length()) {
+ debugInfo.AppendLiteral(",");
+ }
+ }
+ debugInfo.AppendLiteral(" audio-robustness=");
+ for (size_t idx = 0; idx < mAudioRobustness.Length(); idx++) {
+ debugInfo.Append(mAudioRobustness[idx]);
+ if (idx + 1 < mAudioRobustness.Length()) {
+ debugInfo.AppendLiteral(",");
+ }
+ }
+ debugInfo.AppendLiteral(" scheme=[");
+ for (size_t idx = 0; idx < mEncryptionSchemes.Length(); idx++) {
+ debugInfo.Append(mEncryptionSchemes[idx]);
+ if (idx + 1 < mEncryptionSchemes.Length()) {
+ debugInfo.AppendLiteral(",");
+ }
+ }
+ debugInfo.AppendLiteral("]");
+ debugInfo.AppendLiteral(" MP4={");
+ debugInfo.Append(NS_ConvertUTF8toUTF16(mMP4.GetDebugInfo()));
+ debugInfo.AppendLiteral("}");
+ debugInfo.AppendLiteral(" WEBM={");
+ debugInfo.Append(NS_ConvertUTF8toUTF16(mWebM.GetDebugInfo()));
+ debugInfo.AppendLiteral("}");
+ debugInfo.AppendASCII(
+ nsPrintfCString(" isHDCP22Compatible=%d", mIsHDCP22Compatible));
+ return debugInfo;
+}
+
+KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType(
+ dom::MediaKeySessionType aType) {
+ switch (aType) {
+ case dom::MediaKeySessionType::Temporary:
+ return KeySystemConfig::SessionType::Temporary;
+ case dom::MediaKeySessionType::Persistent_license:
+ return KeySystemConfig::SessionType::PersistentLicense;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid session type");
+ return KeySystemConfig::SessionType::Temporary;
+ }
+}
+
+const char* SessionTypeToStr(KeySystemConfig::SessionType aType) {
+ switch (aType) {
+ case KeySystemConfig::SessionType::Temporary:
+ return "Temporary";
+ case KeySystemConfig::SessionType::PersistentLicense:
+ return "PersistentLicense";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid session type");
+ return "Invalid";
+ }
+}
+
+const char* RequirementToStr(KeySystemConfig::Requirement aRequirement) {
+ switch (aRequirement) {
+ case KeySystemConfig::Requirement::Required:
+ return "required";
+ case KeySystemConfig::Requirement::Optional:
+ return "optional";
+ default:
+ return "not-allowed";
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/KeySystemConfig.h b/dom/media/eme/KeySystemConfig.h
new file mode 100644
index 0000000000..8bd7d98217
--- /dev/null
+++ b/dom/media/eme/KeySystemConfig.h
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
+#define DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+
+namespace mozilla {
+
+struct KeySystemConfig {
+ public:
+ // EME MediaKeysRequirement:
+ // https://www.w3.org/TR/encrypted-media/#dom-mediakeysrequirement
+ enum class Requirement {
+ Required = 1,
+ Optional = 2,
+ NotAllowed = 3,
+ };
+
+ // EME MediaKeySessionType:
+ // https://www.w3.org/TR/encrypted-media/#dom-mediakeysessiontype
+ enum class SessionType {
+ Temporary = 1,
+ PersistentLicense = 2,
+ };
+
+ using EMECodecString = nsCString;
+ static constexpr auto EME_CODEC_AAC = "aac"_ns;
+ static constexpr auto EME_CODEC_OPUS = "opus"_ns;
+ static constexpr auto EME_CODEC_VORBIS = "vorbis"_ns;
+ static constexpr auto EME_CODEC_FLAC = "flac"_ns;
+ static constexpr auto EME_CODEC_H264 = "h264"_ns;
+ static constexpr auto EME_CODEC_VP8 = "vp8"_ns;
+ static constexpr auto EME_CODEC_VP9 = "vp9"_ns;
+ static constexpr auto EME_CODEC_HEVC = "hevc"_ns;
+
+ using EMEEncryptionSchemeString = nsCString;
+ static constexpr auto EME_ENCRYPTION_SCHEME_CENC = "cenc"_ns;
+ static constexpr auto EME_ENCRYPTION_SCHEME_CBCS = "cbcs"_ns;
+
+ // A codec can be decrypted-and-decoded by the CDM, or only decrypted
+ // by the CDM and decoded by Gecko. Not both.
+ struct ContainerSupport {
+ ContainerSupport() = default;
+ ~ContainerSupport() = default;
+ ContainerSupport(const ContainerSupport& aOther) {
+ mCodecsDecoded = aOther.mCodecsDecoded.Clone();
+ mCodecsDecrypted = aOther.mCodecsDecrypted.Clone();
+ }
+ ContainerSupport& operator=(const ContainerSupport& aOther) {
+ if (this == &aOther) {
+ return *this;
+ }
+ mCodecsDecoded = aOther.mCodecsDecoded.Clone();
+ mCodecsDecrypted = aOther.mCodecsDecrypted.Clone();
+ return *this;
+ }
+ ContainerSupport(ContainerSupport&& aOther) = default;
+ ContainerSupport& operator=(ContainerSupport&&) = default;
+
+ bool IsSupported() const {
+ return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty();
+ }
+
+ // CDM decrypts and decodes using a DRM robust decoder, and passes decoded
+ // samples back to Gecko for rendering.
+ bool DecryptsAndDecodes(const EMECodecString& aCodec) const {
+ return mCodecsDecoded.Contains(aCodec);
+ }
+
+ // CDM decrypts and passes the decrypted samples back to Gecko for decoding.
+ bool Decrypts(const EMECodecString& aCodec) const {
+ return mCodecsDecrypted.Contains(aCodec);
+ }
+
+ void SetCanDecryptAndDecode(const EMECodecString& aCodec) {
+ // Can't both decrypt and decrypt-and-decode a codec.
+ MOZ_ASSERT(!Decrypts(aCodec));
+ // Prevent duplicates.
+ MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
+ mCodecsDecoded.AppendElement(aCodec);
+ }
+
+ void SetCanDecrypt(const EMECodecString& aCodec) {
+ // Prevent duplicates.
+ MOZ_ASSERT(!Decrypts(aCodec));
+ // Can't both decrypt and decrypt-and-decode a codec.
+ MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
+ mCodecsDecrypted.AppendElement(aCodec);
+ }
+
+ EMECodecString GetDebugInfo() const {
+ EMECodecString info;
+ info.AppendLiteral("decoding-and-decrypting:[");
+ for (size_t idx = 0; idx < mCodecsDecoded.Length(); idx++) {
+ info.Append(mCodecsDecoded[idx]);
+ if (idx + 1 < mCodecsDecoded.Length()) {
+ info.AppendLiteral(",");
+ }
+ }
+ info.AppendLiteral("],");
+ info.AppendLiteral("decrypting-only:[");
+ for (size_t idx = 0; idx < mCodecsDecrypted.Length(); idx++) {
+ info.Append(mCodecsDecrypted[idx]);
+ if (idx + 1 < mCodecsDecrypted.Length()) {
+ info.AppendLiteral(",");
+ }
+ }
+ info.AppendLiteral("]");
+ return info;
+ }
+
+ private:
+ nsTArray<EMECodecString> mCodecsDecoded;
+ nsTArray<EMECodecString> mCodecsDecrypted;
+ };
+
+ // Return true if given key system is supported on the current device.
+ static bool Supports(const nsAString& aKeySystem);
+ static bool CreateKeySystemConfigs(const nsAString& aKeySystem,
+ nsTArray<KeySystemConfig>& aOutConfigs);
+ static void GetGMPKeySystemConfigs(dom::Promise* aPromise);
+
+ KeySystemConfig() = default;
+ ~KeySystemConfig() = default;
+ explicit KeySystemConfig(const KeySystemConfig& aOther) {
+ mKeySystem = aOther.mKeySystem;
+ mInitDataTypes = aOther.mInitDataTypes.Clone();
+ mPersistentState = aOther.mPersistentState;
+ mDistinctiveIdentifier = aOther.mDistinctiveIdentifier;
+ mSessionTypes = aOther.mSessionTypes.Clone();
+ mVideoRobustness = aOther.mVideoRobustness.Clone();
+ mAudioRobustness = aOther.mAudioRobustness.Clone();
+ mEncryptionSchemes = aOther.mEncryptionSchemes.Clone();
+ mMP4 = aOther.mMP4;
+ mWebM = aOther.mWebM;
+ }
+ KeySystemConfig& operator=(const KeySystemConfig& aOther) {
+ if (this == &aOther) {
+ return *this;
+ }
+ mKeySystem = aOther.mKeySystem;
+ mInitDataTypes = aOther.mInitDataTypes.Clone();
+ mPersistentState = aOther.mPersistentState;
+ mDistinctiveIdentifier = aOther.mDistinctiveIdentifier;
+ mSessionTypes = aOther.mSessionTypes.Clone();
+ mVideoRobustness = aOther.mVideoRobustness.Clone();
+ mAudioRobustness = aOther.mAudioRobustness.Clone();
+ mEncryptionSchemes = aOther.mEncryptionSchemes.Clone();
+ mMP4 = aOther.mMP4;
+ mWebM = aOther.mWebM;
+ return *this;
+ }
+ KeySystemConfig(KeySystemConfig&&) = default;
+ KeySystemConfig& operator=(KeySystemConfig&&) = default;
+
+ nsString GetDebugInfo() const;
+
+ // Return true if the given key system is equal to `mKeySystem`, or it can be
+ // mapped to the same key system
+ bool IsSameKeySystem(const nsAString& aKeySystem) const;
+
+ nsString mKeySystem;
+ nsTArray<nsString> mInitDataTypes;
+ Requirement mPersistentState = Requirement::NotAllowed;
+ Requirement mDistinctiveIdentifier = Requirement::NotAllowed;
+ nsTArray<SessionType> mSessionTypes;
+ nsTArray<nsString> mVideoRobustness;
+ nsTArray<nsString> mAudioRobustness;
+ nsTArray<nsString> mEncryptionSchemes;
+ ContainerSupport mMP4;
+ ContainerSupport mWebM;
+ bool mIsHDCP22Compatible = false;
+};
+
+KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType(
+ dom::MediaKeySessionType aType);
+const char* SessionTypeToStr(KeySystemConfig::SessionType aType);
+const char* RequirementToStr(KeySystemConfig::Requirement aRequirement);
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
diff --git a/dom/media/eme/KeySystemNames.h b/dom/media/eme/KeySystemNames.h
new file mode 100644
index 0000000000..182fe231e2
--- /dev/null
+++ b/dom/media/eme/KeySystemNames.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
+#define DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
+
+// Header for key system names. Keep these separate from some of our other
+// EME utils because want to use these strings in contexts where other utils
+// may not build correctly. Specifically at time of writing:
+// - The GMP doesn't have prefs available, so we want to avoid utils that
+// touch the pref service.
+// - The clear key CDM links a limited subset of what normal Fx does, so we
+// need to avoid any utils that touch things like XUL.
+
+namespace mozilla {
+// EME Key System Strings.
+inline constexpr char kClearKeyKeySystemName[] = "org.w3.clearkey";
+inline constexpr char kClearKeyWithProtectionQueryKeySystemName[] =
+ "org.mozilla.clearkey_with_protection_query";
+inline constexpr char kWidevineKeySystemName[] = "com.widevine.alpha";
+#ifdef MOZ_WMF_CDM
+// A sub key system of `kWidevineKeySystem` only used in experiments.
+inline constexpr char kWidevineExperimentKeySystemName[] =
+ "com.widevine.alpha.experiment";
+// A sub key system of `kWidevineKeySystem` only used in experiments to support
+// hardware decryption with codecs that support clear lead.
+inline constexpr char kWidevineExperiment2KeySystemName[] =
+ "com.widevine.alpha.experiment2";
+// API name used for searching GMP Wideivine L1 plugin.
+inline constexpr char kWidevineExperimentAPIName[] = "windows-mf-cdm";
+
+// https://learn.microsoft.com/en-us/playready/overview/key-system-strings
+inline constexpr char kPlayReadyKeySystemName[] =
+ "com.microsoft.playready.recommendation";
+inline constexpr char kPlayReadyKeySystemHardware[] =
+ "com.microsoft.playready.recommendation.3000";
+
+// A sub key system of `kPlayReadyKeySystemName` only used in experiments to
+// support hardware decryption with codecs that support clear lead.
+inline constexpr char kPlayReadyHardwareClearLeadKeySystemName[] =
+ "com.microsoft.playready.recommendation.3000.clearlead";
+#endif
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
diff --git a/dom/media/eme/MediaEncryptedEvent.cpp b/dom/media/eme/MediaEncryptedEvent.cpp
new file mode 100644
index 0000000000..bef98bcd55
--- /dev/null
+++ b/dom/media/eme/MediaEncryptedEvent.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEncryptedEvent.h"
+#include "mozilla/dom/MediaEncryptedEventBinding.h"
+#include "nsContentUtils.h"
+#include "js/ArrayBuffer.h"
+#include "jsfriendapi.h"
+#include "nsINode.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/HoldDropJSObjects.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaEncryptedEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaEncryptedEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaEncryptedEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mInitData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaEncryptedEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaEncryptedEvent::MediaEncryptedEvent(EventTarget* aOwner)
+ : Event(aOwner, nullptr, nullptr) {
+ mozilla::HoldJSObjects(this);
+}
+
+MediaEncryptedEvent::~MediaEncryptedEvent() { mozilla::DropJSObjects(this); }
+
+JSObject* MediaEncryptedEvent::WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaEncryptedEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ EventTarget* aOwner) {
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner);
+ e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo);
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ EventTarget* aOwner, const nsAString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData) {
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner);
+ e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo);
+ e->mInitDataType = aInitDataType;
+ e->mRawInitData = aInitData.Clone();
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(owner);
+ bool trusted = e->Init(owner);
+ e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+ e->mInitDataType = aEventInitDict.mInitDataType;
+ if (!aEventInitDict.mInitData.IsNull()) {
+ JS::Rooted<JSObject*> buffer(aGlobal.Context(),
+ aEventInitDict.mInitData.Value().Obj());
+ e->mInitData = JS::CopyArrayBuffer(aGlobal.Context(), buffer);
+ if (!e->mInitData) {
+ aRv.NoteJSContextException(aGlobal.Context());
+ return nullptr;
+ }
+ }
+ e->SetTrusted(trusted);
+ return e.forget();
+}
+
+void MediaEncryptedEvent::GetInitDataType(nsString& aRetVal) const {
+ aRetVal = mInitDataType;
+}
+
+void MediaEncryptedEvent::GetInitData(JSContext* cx,
+ JS::MutableHandle<JSObject*> aData,
+ ErrorResult& aRv) {
+ if (mRawInitData.Length()) {
+ mInitData = ArrayBuffer::Create(cx, this, mRawInitData, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ mRawInitData.Clear();
+ }
+ aData.set(mInitData);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaEncryptedEvent.h b/dom/media/eme/MediaEncryptedEvent.h
new file mode 100644
index 0000000000..0fcb5db051
--- /dev/null
+++ b/dom/media/eme/MediaEncryptedEvent.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyNeededEvent_h__
+#define mozilla_dom_MediaKeyNeededEvent_h__
+
+#include <cstdint>
+#include "js/RootingAPI.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Event.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+struct MediaKeyNeededEventInit;
+
+class MediaEncryptedEvent final : public Event {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaEncryptedEvent,
+ Event)
+ protected:
+ virtual ~MediaEncryptedEvent();
+ explicit MediaEncryptedEvent(EventTarget* aOwner);
+
+ nsString mInitDataType;
+ JS::Heap<JSObject*> mInitData;
+
+ public:
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(EventTarget* aOwner);
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(
+ EventTarget* aOwner, const nsAString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData);
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv);
+
+ void GetInitDataType(nsString& aRetVal) const;
+
+ void GetInitData(JSContext* cx, JS::MutableHandle<JSObject*> aData,
+ ErrorResult& aRv);
+
+ private:
+ nsTArray<uint8_t> mRawInitData;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MediaKeyNeededEvent_h__
diff --git a/dom/media/eme/MediaKeyError.cpp b/dom/media/eme/MediaKeyError.cpp
new file mode 100644
index 0000000000..7172b0d05b
--- /dev/null
+++ b/dom/media/eme/MediaKeyError.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeyError.h"
+#include "mozilla/dom/MediaKeyErrorBinding.h"
+#include "nsContentUtils.h"
+
+namespace mozilla::dom {
+
+MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode)
+ : Event(aOwner, nullptr, nullptr), mSystemCode(aSystemCode) {
+ InitEvent(u"error"_ns, CanBubble::eNo, Cancelable::eNo);
+}
+
+MediaKeyError::~MediaKeyError() = default;
+
+uint32_t MediaKeyError::SystemCode() const { return mSystemCode; }
+
+JSObject* MediaKeyError::WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyError_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyError.h b/dom/media/eme/MediaKeyError.h
new file mode 100644
index 0000000000..ef4ecf1375
--- /dev/null
+++ b/dom/media/eme/MediaKeyError.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyError_h
+#define mozilla_dom_MediaKeyError_h
+
+#include "mozilla/Attributes.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/Event.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla::dom {
+
+class MediaKeyError final : public Event {
+ public:
+ MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode);
+ ~MediaKeyError();
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t SystemCode() const;
+
+ private:
+ uint32_t mSystemCode;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/eme/MediaKeyMessageEvent.cpp b/dom/media/eme/MediaKeyMessageEvent.cpp
new file mode 100644
index 0000000000..0884e3b0e0
--- /dev/null
+++ b/dom/media/eme/MediaKeyMessageEvent.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "js/ArrayBuffer.h"
+#include "js/RootingAPI.h"
+#include "jsfriendapi.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/PrimitiveConversions.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/TypedArray.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/MediaKeys.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyMessageEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaKeyMessageEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMessage)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyMessageEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner)
+ : Event(aOwner, nullptr, nullptr),
+ mMessageType(static_cast<MediaKeyMessageType>(0)) {
+ mozilla::HoldJSObjects(this);
+}
+
+MediaKeyMessageEvent::~MediaKeyMessageEvent() { mozilla::DropJSObjects(this); }
+
+MediaKeyMessageEvent* MediaKeyMessageEvent::AsMediaKeyMessageEvent() {
+ return this;
+}
+
+JSObject* MediaKeyMessageEvent::WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyMessageEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaKeyMessageEvent> MediaKeyMessageEvent::Constructor(
+ EventTarget* aOwner, MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(aOwner);
+ e->InitEvent(u"message"_ns, false, false);
+ e->mMessageType = aMessageType;
+ e->mRawMessage = aMessage.Clone();
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaKeyMessageEvent> MediaKeyMessageEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(owner);
+ bool trusted = e->Init(owner);
+ e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+ JS::Rooted<JSObject*> buffer(aGlobal.Context(),
+ aEventInitDict.mMessage.Obj());
+ e->mMessage = JS::CopyArrayBuffer(aGlobal.Context(), buffer);
+ if (!e->mMessage) {
+ aRv.NoteJSContextException(aGlobal.Context());
+ return nullptr;
+ }
+ e->mMessageType = aEventInitDict.mMessageType;
+ e->SetTrusted(trusted);
+ e->SetComposed(aEventInitDict.mComposed);
+ return e.forget();
+}
+
+void MediaKeyMessageEvent::GetMessage(JSContext* cx,
+ JS::MutableHandle<JSObject*> aMessage,
+ ErrorResult& aRv) {
+ if (!mMessage) {
+ mMessage = ArrayBuffer::Create(cx, this, mRawMessage, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ mRawMessage.Clear();
+ }
+ aMessage.set(mMessage);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyMessageEvent.h b/dom/media/eme/MediaKeyMessageEvent.h
new file mode 100644
index 0000000000..400439b065
--- /dev/null
+++ b/dom/media/eme/MediaKeyMessageEvent.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyMessageEvent_h__
+#define mozilla_dom_MediaKeyMessageEvent_h__
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TypedArray.h"
+#include "js/TypeDecls.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+struct MediaKeyMessageEventInit;
+
+class MediaKeyMessageEvent final : public Event {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyMessageEvent,
+ Event)
+ protected:
+ virtual ~MediaKeyMessageEvent();
+ explicit MediaKeyMessageEvent(EventTarget* aOwner);
+
+ MediaKeyMessageType mMessageType;
+ JS::Heap<JSObject*> mMessage;
+
+ public:
+ virtual MediaKeyMessageEvent* AsMediaKeyMessageEvent();
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<MediaKeyMessageEvent> Constructor(
+ EventTarget* aOwner, MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage);
+
+ static already_AddRefed<MediaKeyMessageEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv);
+
+ MediaKeyMessageType MessageType() const { return mMessageType; }
+
+ void GetMessage(JSContext* cx, JS::MutableHandle<JSObject*> aMessage,
+ ErrorResult& aRv);
+
+ private:
+ nsTArray<uint8_t> mRawMessage;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeyMessageEvent_h__
diff --git a/dom/media/eme/MediaKeySession.cpp b/dom/media/eme/MediaKeySession.cpp
new file mode 100644
index 0000000000..908df5f7c8
--- /dev/null
+++ b/dom/media/eme/MediaKeySession.cpp
@@ -0,0 +1,622 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySession.h"
+
+#include <ctime>
+#include <utility>
+
+#include "GMPUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/KeyIdsInitDataBinding.h"
+#include "mozilla/dom/MediaEncryptedEvent.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsPrintfCString.h"
+#include "psshparser/PsshParser.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, DOMEventTargetHelper,
+ mMediaKeyError, mKeys, mKeyStatusMap,
+ mClosed)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySession)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper)
+
+// Count of number of instances. Used to give each instance a
+// unique token.
+static uint32_t sMediaKeySessionNum = 0;
+
+// Max length of keyId in EME "keyIds" or WebM init data format, as enforced
+// by web platform tests.
+static const uint32_t MAX_KEY_ID_LENGTH = 512;
+
+// Max length of CENC PSSH init data tolerated, as enforced by web
+// platform tests.
+static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024;
+
+MediaKeySession::MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys,
+ const nsAString& aKeySystem,
+ MediaKeySessionType aSessionType,
+ ErrorResult& aRv)
+ : DOMEventTargetHelper(aParent),
+ mKeys(aKeys),
+ mKeySystem(aKeySystem),
+ mSessionType(aSessionType),
+ mToken(sMediaKeySessionNum++),
+ mIsClosed(false),
+ mUninitialized(true),
+ mKeyStatusMap(new MediaKeyStatusMap(aParent)),
+ mExpiration(JS::GenericNaN()) {
+ EME_LOG("MediaKeySession[%p,''] ctor", this);
+
+ MOZ_ASSERT(aParent);
+ if (aRv.Failed()) {
+ return;
+ }
+ mClosed = MakePromise(aRv, "MediaKeys.createSession"_ns);
+}
+
+void MediaKeySession::SetSessionId(const nsAString& aSessionId) {
+ EME_LOG("MediaKeySession[%p,'%s'] session Id set", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+
+ if (NS_WARN_IF(!mSessionId.IsEmpty())) {
+ return;
+ }
+ mSessionId = aSessionId;
+ mKeys->OnSessionIdReady(this);
+}
+
+MediaKeySession::~MediaKeySession() {
+ EME_LOG("MediaKeySession[%p,'%s'] dtor", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+}
+
+MediaKeyError* MediaKeySession::GetError() const { return mMediaKeyError; }
+
+void MediaKeySession::GetSessionId(nsString& aSessionId) const {
+ aSessionId = GetSessionId();
+}
+
+const nsString& MediaKeySession::GetSessionId() const { return mSessionId; }
+
+JSObject* MediaKeySession::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeySession_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+double MediaKeySession::Expiration() const { return mExpiration; }
+
+Promise* MediaKeySession::Closed() const { return mClosed; }
+
+void MediaKeySession::UpdateKeyStatusMap() {
+ MOZ_ASSERT(!IsClosed());
+ if (!mKeys->GetCDMProxy()) {
+ return;
+ }
+
+ nsTArray<CDMCaps::KeyStatus> keyStatuses;
+ {
+ auto caps = mKeys->GetCDMProxy()->Capabilites().Lock();
+ caps->GetKeyStatusesForSession(mSessionId, keyStatuses);
+ }
+
+ mKeyStatusMap->Update(keyStatuses);
+
+ if (EME_LOG_ENABLED()) {
+ nsAutoCString message(
+ nsPrintfCString("MediaKeySession[%p,'%s'] key statuses change {", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get()));
+ for (const CDMCaps::KeyStatus& status : keyStatuses) {
+ message.AppendPrintf(
+ " (%s,%s)", ToHexString(status.mId).get(),
+ nsCString(MediaKeyStatusValues::GetString(status.mStatus)).get());
+ }
+ message.AppendLiteral(" }");
+ // Use %s so we aren't exposing random strings to printf interpolation.
+ EME_LOG("%s", message.get());
+ }
+}
+
+MediaKeyStatusMap* MediaKeySession::KeyStatuses() const {
+ return mKeyStatusMap;
+}
+
+// The user agent MUST thoroughly validate the Initialization Data before
+// passing it to the CDM. This includes verifying that the length and
+// values of fields are reasonable, verifying that values are within
+// reasonable limits, and stripping irrelevant, unsupported, or unknown
+// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize,
+// and/or generate a fully sanitized version of the Initialization Data.
+// If the Initialization Data format specified by initDataType supports
+// multiple entries, the user agent SHOULD remove entries that are not
+// needed by the CDM. The user agent MUST NOT re-order entries within
+// the Initialization Data.
+static bool ValidateInitData(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) {
+ if (aInitDataType.LowerCaseEqualsLiteral("webm")) {
+ // WebM initData consists of a single keyId. Ensure it's of reasonable
+ // length.
+ return aInitData.Length() <= MAX_KEY_ID_LENGTH;
+ } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) {
+ // Limit initData to less than 64KB.
+ if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) {
+ return false;
+ }
+ std::vector<std::vector<uint8_t>> keyIds;
+ return ParseCENCInitData(aInitData.Elements(), aInitData.Length(), keyIds);
+ } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) {
+ if (aInitData.Length() > MAX_KEY_ID_LENGTH) {
+ return false;
+ }
+ // Ensure that init data matches the expected JSON format.
+ mozilla::dom::KeyIdsInitData keyIds;
+ nsString json;
+ nsDependentCSubstring raw(
+ reinterpret_cast<const char*>(aInitData.Elements()),
+ aInitData.Length());
+ if (NS_FAILED(UTF_8_ENCODING->DecodeWithBOMRemoval(raw, json))) {
+ return false;
+ }
+ if (!keyIds.Init(json)) {
+ return false;
+ }
+ if (keyIds.mKids.Length() == 0) {
+ return false;
+ }
+ for (const auto& kid : keyIds.mKids) {
+ if (kid.IsEmpty()) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+// Generates a license request based on the initData. A message of type
+// "license-request" or "individualization-request" will always be queued
+// if the algorithm succeeds and the promise is resolved.
+already_AddRefed<Promise> MediaKeySession::GenerateRequest(
+ const nsAString& aInitDataType,
+ const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.generateRequest"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // If this object is closed, return a promise rejected with an
+ // InvalidStateError.
+ if (IsClosed()) {
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, closed", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed in MediaKeySession.generateRequest()");
+ return promise.forget();
+ }
+
+ // If this object's uninitialized value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!mUninitialized) {
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, uninitialized",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is already initialized in MediaKeySession.generateRequest()");
+ return promise.forget();
+ }
+
+ // Let this object's uninitialized value be false.
+ mUninitialized = false;
+
+ // If initDataType is the empty string, return a promise rejected
+ // with a newly created TypeError.
+ if (aInitDataType.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty initDataType passed to MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initDataType",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // If initData is an empty array, return a promise rejected with
+ // a newly created TypeError.
+ nsTArray<uint8_t> data;
+ CopyArrayBufferViewOrArrayBufferData(aInitData, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty initData passed to MediaKeySession.generateRequest()");
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initData",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // If the Key System implementation represented by this object's
+ // cdm implementation value does not support initDataType as an
+ // Initialization Data Type, return a promise rejected with a
+ // NotSupportedError. String comparison is case-sensitive.
+ if (!MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem,
+ aInitDataType)) {
+ promise->MaybeRejectWithNotSupportedError(
+ "Unsupported initDataType passed to MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported "
+ "initDataType",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Let init data be a copy of the contents of the initData parameter.
+ // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above.
+
+ // Let session type be this object's session type.
+
+ // Let promise be a new promise.
+
+ // Run the following steps in parallel:
+
+ // If the init data is not valid for initDataType, reject promise with
+ // a newly created TypeError.
+ if (!ValidateInitData(data, aInitDataType)) {
+ // If the preceding step failed, reject promise with a newly created
+ // TypeError.
+ promise->MaybeRejectWithTypeError(
+ "initData sanitization failed in MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization "
+ "failed",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Let sanitized init data be a validated and sanitized version of init data.
+
+ // If sanitized init data is empty, reject promise with a NotSupportedError.
+
+ // Note: Remaining steps of generateRequest method continue in CDM.
+
+ // Convert initData to hex for easier logging.
+ // Note: CreateSession() std::move()s the data out of the array, so we have
+ // to copy it here.
+ nsAutoCString hexInitData(ToHexString(data));
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->ConnectPendingPromiseIdWithToken(pid, Token());
+ mKeys->GetCDMProxy()->CreateSession(Token(), mSessionType, pid, aInitDataType,
+ data);
+
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() sent, "
+ "promiseId=%d initData='%s' initDataType='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexInitData.get(),
+ NS_ConvertUTF16toUTF8(aInitDataType).get());
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Load(const nsAString& aSessionId,
+ ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.load"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // 1. If this object is closed, return a promise rejected with an
+ // InvalidStateError.
+ if (IsClosed()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed in MediaKeySession.load()");
+ EME_LOG("MediaKeySession[%p,'%s'] Load() failed, closed", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ return promise.forget();
+ }
+
+ // 2.If this object's uninitialized value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!mUninitialized) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is already initialized in MediaKeySession.load()");
+ EME_LOG("MediaKeySession[%p,'%s'] Load() failed, uninitialized", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ return promise.forget();
+ }
+
+ // 3.Let this object's uninitialized value be false.
+ mUninitialized = false;
+
+ // 4. If sessionId is the empty string, return a promise rejected with a newly
+ // created TypeError.
+ if (aSessionId.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Trying to load a session with empty session ID");
+ // "The sessionId parameter is empty."
+ EME_LOG("MediaKeySession[%p,''] Load() failed, no sessionId", this);
+ return promise.forget();
+ }
+
+ // 5. If the result of running the Is persistent session type? algorithm
+ // on this object's session type is false, return a promise rejected with
+ // a newly created TypeError.
+ if (mSessionType == MediaKeySessionType::Temporary) {
+ promise->MaybeRejectWithTypeError(
+ "Trying to load() into a non-persistent session");
+ EME_LOG(
+ "MediaKeySession[%p,''] Load() failed, can't load in a non-persistent "
+ "session",
+ this);
+ return promise.forget();
+ }
+
+ // Note: We don't support persistent sessions in any keysystem, so all calls
+ // to Load() should reject with a TypeError in the preceding check. Omitting
+ // implementing the rest of the specified MediaKeySession::Load() algorithm.
+
+ // We now know the sessionId being loaded into this session. Remove the
+ // session from its owning MediaKey's set of sessions awaiting a sessionId.
+ RefPtr<MediaKeySession> session(mKeys->GetPendingSession(Token()));
+ MOZ_ASSERT(session == this, "Session should be awaiting id on its own token");
+
+ // Associate with the known sessionId.
+ SetSessionId(aSessionId);
+
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->LoadSession(pid, mSessionType, aSessionId);
+
+ EME_LOG("MediaKeySession[%p,'%s'] Load() sent to CDM, promiseId=%d", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Update(
+ const ArrayBufferViewOrArrayBuffer& aResponse, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.update"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!IsCallable()) {
+ // If this object's callable value is false, return a promise rejected
+ // with a new DOMException whose name is InvalidStateError.
+ EME_LOG(
+ "MediaKeySession[%p,''] Update() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Update() called before sessionId set by CDM");
+ return promise.forget();
+ }
+
+ nsTArray<uint8_t> data;
+ if (IsClosed() || !mKeys->GetCDMProxy()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed or was not properly initialized");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] Update() failed, session is closed or was "
+ "not properly initialised.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ CopyArrayBufferViewOrArrayBufferData(aResponse, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty response buffer passed to MediaKeySession.update()");
+ EME_LOG("MediaKeySession[%p,'%s'] Update() failed, empty response buffer",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Convert response to hex for easier logging.
+ // Note: UpdateSession() std::move()s the data out of the array, so we have
+ // to copy it here.
+ nsAutoCString hexResponse(ToHexString(data));
+
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->UpdateSession(mSessionId, pid, data);
+
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] Update() sent to CDM, "
+ "promiseId=%d Response='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexResponse.get());
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Close(ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.close"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ // 1. Let session be the associated MediaKeySession object.
+ // 2. If session is closed, return a resolved promise.
+ if (IsClosed()) {
+ EME_LOG("MediaKeySession[%p,'%s'] Close() already closed", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeResolveWithUndefined();
+ return promise.forget();
+ }
+ // 3. If session's callable value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!IsCallable()) {
+ EME_LOG("MediaKeySession[%p,''] Close() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Close() called before sessionId set by CDM");
+ return promise.forget();
+ }
+ if (!mKeys->GetCDMProxy()) {
+ EME_LOG("MediaKeySession[%p,'%s'] Close() null CDMProxy", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Close() lost reference to CDM");
+ return promise.forget();
+ }
+ // 4. Let promise be a new promise.
+ PromiseId pid = mKeys->StorePromise(promise);
+ // 5. Run the following steps in parallel:
+ // 5.1 Let cdm be the CDM instance represented by session's cdm instance
+ // value. 5.2 Use cdm to close the session associated with session.
+ mKeys->GetCDMProxy()->CloseSession(mSessionId, pid);
+
+ EME_LOG("MediaKeySession[%p,'%s'] Close() sent to CDM, promiseId=%d", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ // Session Closed algorithm is run when CDM causes us to run
+ // OnSessionClosed().
+
+ // 6. Return promise.
+ return promise.forget();
+}
+
+void MediaKeySession::OnClosed() {
+ if (IsClosed()) {
+ return;
+ }
+ EME_LOG("MediaKeySession[%p,'%s'] session close operation complete.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ mIsClosed = true;
+ mKeys->OnSessionClosed(this);
+ mKeys = nullptr;
+ mClosed->MaybeResolveWithUndefined();
+}
+
+bool MediaKeySession::IsClosed() const { return mIsClosed; }
+
+already_AddRefed<Promise> MediaKeySession::Remove(ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.remove"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ if (!IsCallable()) {
+ // If this object's callable value is false, return a promise rejected
+ // with a new DOMException whose name is InvalidStateError.
+ EME_LOG(
+ "MediaKeySession[%p,''] Remove() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Remove() called before sessionId set by CDM");
+ return promise.forget();
+ }
+ if (mSessionType != MediaKeySessionType::Persistent_license) {
+ promise->MaybeRejectWithInvalidAccessError(
+ "Calling MediaKeySession.remove() on non-persistent session");
+ // "The operation is not supported on session type sessions."
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, sesion not persisrtent.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ if (IsClosed() || !mKeys->GetCDMProxy()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.remove() called but session is not active");
+ // "The session is closed."
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, already session closed.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->RemoveSession(mSessionId, pid);
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() sent to CDM, promiseId=%d.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ return promise.forget();
+}
+
+void MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ if (EME_LOG_ENABLED()) {
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] DispatchKeyMessage() type=%s message='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(),
+ nsCString(MediaKeyMessageTypeValues::GetString(aMessageType)).get(),
+ ToHexString(aMessage).get());
+ }
+
+ RefPtr<MediaKeyMessageEvent> event(
+ MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage));
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event.forget());
+ asyncDispatcher->PostDOMEvent();
+}
+
+void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) {
+ EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode);
+
+ auto event = MakeRefPtr<MediaKeyError>(this, aSystemCode);
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event.forget());
+ asyncDispatcher->PostDOMEvent();
+}
+
+void MediaKeySession::DispatchKeyStatusesChange() {
+ if (IsClosed()) {
+ return;
+ }
+
+ UpdateKeyStatusMap();
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"keystatuseschange"_ns, CanBubble::eNo);
+ asyncDispatcher->PostDOMEvent();
+}
+
+uint32_t MediaKeySession::Token() const { return mToken; }
+
+already_AddRefed<DetailedPromise> MediaKeySession::MakePromise(
+ ErrorResult& aRv, const nsACString& aName) {
+ nsCOMPtr<nsIGlobalObject> global = GetParentObject();
+ if (!global) {
+ NS_WARNING("Passed non-global to MediaKeys ctor!");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return DetailedPromise::Create(global, aRv, aName);
+}
+
+void MediaKeySession::SetExpiration(double aExpiration) {
+ EME_LOG("MediaKeySession[%p,'%s'] SetExpiry(%.12lf) (%.2lf hours from now)",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), aExpiration,
+ (aExpiration - 1000.0 * double(time(0))) / (1000.0 * 60 * 60));
+ mExpiration = aExpiration;
+}
+
+EventHandlerNonNull* MediaKeySession::GetOnkeystatuseschange() {
+ return GetEventHandler(nsGkAtoms::onkeystatuseschange);
+}
+
+void MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::onkeystatuseschange, aCallback);
+}
+
+EventHandlerNonNull* MediaKeySession::GetOnmessage() {
+ return GetEventHandler(nsGkAtoms::onmessage);
+}
+
+void MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::onmessage, aCallback);
+}
+
+nsCString ToCString(MediaKeySessionType aType) {
+ return nsCString(MediaKeySessionTypeValues::GetString(aType));
+}
+
+nsString ToString(MediaKeySessionType aType) {
+ return NS_ConvertUTF8toUTF16(ToCString(aType));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h
new file mode 100644
index 0000000000..e19488c311
--- /dev/null
+++ b/dom/media/eme/MediaKeySession.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeySession_h
+#define mozilla_dom_MediaKeySession_h
+
+#include "DecoderDoctorLogger.h"
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/dom/MediaKeySessionBinding.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+
+struct JSContext;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class MediaKeySession;
+} // namespace dom
+DDLoggedTypeName(dom::MediaKeySession);
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+class MediaKeyError;
+class MediaKeyStatusMap;
+
+nsCString ToCString(MediaKeySessionType aType);
+
+nsString ToString(MediaKeySessionType aType);
+
+class MediaKeySession final : public DOMEventTargetHelper,
+ public DecoderDoctorLifeLogger<MediaKeySession> {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession,
+ DOMEventTargetHelper)
+ public:
+ MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys,
+ const nsAString& aKeySystem, MediaKeySessionType aSessionType,
+ ErrorResult& aRv);
+
+ void SetSessionId(const nsAString& aSessionId);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Mark this as resultNotAddRefed to return raw pointers
+ MediaKeyError* GetError() const;
+
+ MediaKeyStatusMap* KeyStatuses() const;
+
+ void GetSessionId(nsString& aRetval) const;
+
+ const nsString& GetSessionId() const;
+
+ // Number of ms since epoch at which expiration occurs, or NaN if unknown.
+ // TODO: The type of this attribute is still under contention.
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25902
+ double Expiration() const;
+
+ Promise* Closed() const;
+
+ already_AddRefed<Promise> GenerateRequest(
+ const nsAString& aInitDataType,
+ const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv);
+
+ already_AddRefed<Promise> Load(const nsAString& aSessionId, ErrorResult& aRv);
+
+ already_AddRefed<Promise> Update(const ArrayBufferViewOrArrayBuffer& response,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> Close(ErrorResult& aRv);
+
+ already_AddRefed<Promise> Remove(ErrorResult& aRv);
+
+ void DispatchKeyMessage(MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage);
+
+ void DispatchKeyError(uint32_t system_code);
+
+ void DispatchKeyStatusesChange();
+
+ void OnClosed();
+
+ bool IsClosed() const;
+
+ void SetExpiration(double aExpiry);
+
+ mozilla::dom::EventHandlerNonNull* GetOnkeystatuseschange();
+ void SetOnkeystatuseschange(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ mozilla::dom::EventHandlerNonNull* GetOnmessage();
+ void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ // Process-unique identifier.
+ uint32_t Token() const;
+
+ private:
+ ~MediaKeySession();
+
+ void UpdateKeyStatusMap();
+
+ bool IsCallable() const {
+ // The EME spec sets the "callable value" to true whenever the CDM sets
+ // the sessionId. When the session is initialized, sessionId is empty and
+ // callable is thus false.
+ return !mSessionId.IsEmpty();
+ }
+
+ already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv,
+ const nsACString& aName);
+
+ RefPtr<DetailedPromise> mClosed;
+
+ RefPtr<MediaKeyError> mMediaKeyError;
+ RefPtr<MediaKeys> mKeys;
+ const nsString mKeySystem;
+ nsString mSessionId;
+ const MediaKeySessionType mSessionType;
+ const uint32_t mToken;
+ bool mIsClosed;
+ bool mUninitialized;
+ RefPtr<MediaKeyStatusMap> mKeyStatusMap;
+ double mExpiration;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/MediaKeyStatusMap.cpp b/dom/media/eme/MediaKeyStatusMap.cpp
new file mode 100644
index 0000000000..6ee9d76b77
--- /dev/null
+++ b/dom/media/eme/MediaKeyStatusMap.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/EMEUtils.h"
+#include "GMPUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeyStatusMap)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeyStatusMap)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyStatusMap)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeyStatusMap, mParent)
+
+MediaKeyStatusMap::MediaKeyStatusMap(nsPIDOMWindowInner* aParent)
+ : mParent(aParent) {}
+
+MediaKeyStatusMap::~MediaKeyStatusMap() = default;
+
+JSObject* MediaKeyStatusMap::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyStatusMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* MediaKeyStatusMap::GetParentObject() const {
+ return mParent;
+}
+
+const MediaKeyStatusMap::KeyStatus* MediaKeyStatusMap::FindKey(
+ const ArrayBufferViewOrArrayBuffer& aKey) const {
+ MOZ_ASSERT(aKey.IsArrayBuffer() || aKey.IsArrayBufferView());
+
+ return ProcessTypedArrays(aKey,
+ [&](const Span<const uint8_t>& aData,
+ JS::AutoCheckCannotGC&&) -> const KeyStatus* {
+ for (const KeyStatus& status : mStatuses) {
+ if (aData == Span(status.mKeyId)) {
+ return &status;
+ }
+ }
+ return nullptr;
+ });
+}
+
+void MediaKeyStatusMap::Get(const ArrayBufferViewOrArrayBuffer& aKey,
+ OwningMediaKeyStatusOrUndefined& aOutValue,
+ ErrorResult& aOutRv) const {
+ const KeyStatus* status = FindKey(aKey);
+ if (!status) {
+ aOutValue.SetUndefined();
+ return;
+ }
+
+ aOutValue.SetAsMediaKeyStatus() = status->mStatus;
+}
+
+bool MediaKeyStatusMap::Has(const ArrayBufferViewOrArrayBuffer& aKey) const {
+ return FindKey(aKey);
+}
+
+uint32_t MediaKeyStatusMap::GetIterableLength() const {
+ return mStatuses.Length();
+}
+
+TypedArrayCreator<ArrayBuffer> MediaKeyStatusMap::GetKeyAtIndex(
+ uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return TypedArrayCreator<ArrayBuffer>(mStatuses[aIndex].mKeyId);
+}
+
+nsString MediaKeyStatusMap::GetKeyIDAsHexString(uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return NS_ConvertUTF8toUTF16(ToHexString(mStatuses[aIndex].mKeyId));
+}
+
+MediaKeyStatus MediaKeyStatusMap::GetValueAtIndex(uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return mStatuses[aIndex].mStatus;
+}
+
+uint32_t MediaKeyStatusMap::Size() const { return mStatuses.Length(); }
+
+void MediaKeyStatusMap::Update(const nsTArray<CDMCaps::KeyStatus>& aKeys) {
+ mStatuses.Clear();
+ for (const auto& key : aKeys) {
+ mStatuses.InsertElementSorted(KeyStatus(key.mId, key.mStatus));
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyStatusMap.h b/dom/media/eme/MediaKeyStatusMap.h
new file mode 100644
index 0000000000..1ffab4331e
--- /dev/null
+++ b/dom/media/eme/MediaKeyStatusMap.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyStatuses_h
+#define mozilla_dom_MediaKeyStatuses_h
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+#include "mozilla/CDMCaps.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+
+// The MediaKeyStatusMap WebIDL interface; maps a keyId to its status.
+// Note that the underlying "map" is stored in an array, since we assume
+// that a MediaKeySession won't have many key statuses to report.
+class MediaKeyStatusMap final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeyStatusMap)
+
+ public:
+ explicit MediaKeyStatusMap(nsPIDOMWindowInner* aParent);
+
+ protected:
+ ~MediaKeyStatusMap();
+
+ public:
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void Get(const ArrayBufferViewOrArrayBuffer& aKey,
+ OwningMediaKeyStatusOrUndefined& aOutValue,
+ ErrorResult& aOutRv) const;
+ bool Has(const ArrayBufferViewOrArrayBuffer& aKey) const;
+ uint32_t Size() const;
+
+ uint32_t GetIterableLength() const;
+ TypedArrayCreator<ArrayBuffer> GetKeyAtIndex(uint32_t aIndex) const;
+ nsString GetKeyIDAsHexString(uint32_t aIndex) const;
+ MediaKeyStatus GetValueAtIndex(uint32_t aIndex) const;
+
+ void Update(const nsTArray<CDMCaps::KeyStatus>& keys);
+
+ private:
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+
+ struct KeyStatus {
+ KeyStatus(const nsTArray<uint8_t>& aKeyId, MediaKeyStatus aStatus)
+ : mKeyId(aKeyId.Clone()), mStatus(aStatus) {}
+ bool operator==(const KeyStatus& aOther) const {
+ return aOther.mKeyId == mKeyId;
+ }
+ bool operator<(const KeyStatus& aOther) const {
+ // Copy chromium and compare keys' bytes.
+ // Update once https://github.com/w3c/encrypted-media/issues/69
+ // is resolved.
+ const nsTArray<uint8_t>& other = aOther.mKeyId;
+ const nsTArray<uint8_t>& self = mKeyId;
+ size_t length = std::min<size_t>(other.Length(), self.Length());
+ int cmp = memcmp(self.Elements(), other.Elements(), length);
+ if (cmp != 0) {
+ return cmp < 0;
+ }
+ return self.Length() <= other.Length();
+ }
+ nsTArray<uint8_t> mKeyId;
+ MediaKeyStatus mStatus;
+ };
+
+ const KeyStatus* FindKey(const ArrayBufferViewOrArrayBuffer& aKey) const;
+
+ nsTArray<KeyStatus> mStatuses;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/MediaKeySystemAccess.cpp b/dom/media/eme/MediaKeySystemAccess.cpp
new file mode 100644
index 0000000000..b58ff76424
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccess.cpp
@@ -0,0 +1,1161 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySystemAccess.h"
+
+#include <functional>
+
+#include "DecoderDoctorDiagnostics.h"
+#include "DecoderTraits.h"
+#include "KeySystemConfig.h"
+#include "MP4Decoder.h"
+#include "MediaContainerType.h"
+#include "WebMDecoder.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeySystemAccessManager.h"
+#include "mozilla/dom/MediaSource.h"
+#include "nsDOMString.h"
+#include "nsIObserverService.h"
+#include "nsMimeTypes.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/java/MediaDrmProxyWrappers.h"
+#endif
+
+namespace mozilla::dom {
+
+#define LOG(msg, ...) \
+ EME_LOG("MediaKeySystemAccess::%s " msg, __func__, ##__VA_ARGS__)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+static nsCString ToCString(const MediaKeySystemConfiguration& aConfig);
+
+MediaKeySystemAccess::MediaKeySystemAccess(
+ nsPIDOMWindowInner* aParent, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig)
+ : mParent(aParent), mKeySystem(aKeySystem), mConfig(aConfig) {
+ LOG("Created MediaKeySystemAccess for keysystem=%s config=%s",
+ NS_ConvertUTF16toUTF8(mKeySystem).get(),
+ mozilla::dom::ToCString(mConfig).get());
+}
+
+MediaKeySystemAccess::~MediaKeySystemAccess() = default;
+
+JSObject* MediaKeySystemAccess::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeySystemAccess_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* MediaKeySystemAccess::GetParentObject() const {
+ return mParent;
+}
+
+void MediaKeySystemAccess::GetKeySystem(nsString& aOutKeySystem) const {
+ aOutKeySystem.Assign(mKeySystem);
+}
+
+void MediaKeySystemAccess::GetConfiguration(
+ MediaKeySystemConfiguration& aConfig) {
+ aConfig = mConfig;
+}
+
+already_AddRefed<Promise> MediaKeySystemAccess::CreateMediaKeys(
+ ErrorResult& aRv) {
+ RefPtr<MediaKeys> keys(new MediaKeys(mParent, mKeySystem, mConfig));
+ return keys->Init(aRv);
+}
+
+enum class SecureLevel {
+ Software,
+ Hardware,
+};
+
+static MediaKeySystemStatus EnsureCDMInstalled(const nsAString& aKeySystem,
+ const SecureLevel aSecure,
+ nsACString& aOutMessage) {
+ if (aSecure == SecureLevel::Software &&
+ !KeySystemConfig::Supports(aKeySystem)) {
+ aOutMessage = "CDM is not installed"_ns;
+ return MediaKeySystemStatus::Cdm_not_installed;
+ }
+
+#ifdef MOZ_WMF_CDM
+ if (aSecure == SecureLevel::Hardware) {
+ // Ensure we check the hardware key system name.
+ nsAutoString hardwareKeySystem;
+ if (IsWidevineKeySystem(aKeySystem) ||
+ IsWidevineExperimentKeySystemAndSupported(aKeySystem)) {
+ hardwareKeySystem =
+ NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName);
+ } else if (IsPlayReadyKeySystemAndSupported(aKeySystem)) {
+ hardwareKeySystem = NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Not supported key system for HWDRM!");
+ }
+ if (!KeySystemConfig::Supports(hardwareKeySystem)) {
+ aOutMessage = "CDM is not installed"_ns;
+ return MediaKeySystemStatus::Cdm_not_installed;
+ }
+ }
+#endif
+
+ return MediaKeySystemStatus::Available;
+}
+
+/* static */
+MediaKeySystemStatus MediaKeySystemAccess::GetKeySystemStatus(
+ const MediaKeySystemAccessRequest& aRequest, nsACString& aOutMessage) {
+ const nsString& keySystem = aRequest.mKeySystem;
+
+ MOZ_ASSERT(StaticPrefs::media_eme_enabled() ||
+ IsClearkeyKeySystem(keySystem));
+
+ LOG("checking if CDM is installed or disabled for %s",
+ NS_ConvertUTF16toUTF8(keySystem).get());
+ if (IsClearkeyKeySystem(keySystem)) {
+ return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage);
+ }
+
+ // This is used to determine if we need to download Widevine L1.
+ bool shouldCheckL1Installation = false;
+#ifdef MOZ_WMF_CDM
+ if (StaticPrefs::media_eme_widevine_experiment_enabled()) {
+ shouldCheckL1Installation =
+ CheckIfHarewareDRMConfigExists(aRequest.mConfigs) ||
+ IsWidevineExperimentKeySystemAndSupported(keySystem);
+ }
+#endif
+
+ // Check Widevine L3
+ if (IsWidevineKeySystem(keySystem) && !shouldCheckL1Installation) {
+ if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) {
+ if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) {
+ aOutMessage = "Widevine EME disabled"_ns;
+ return MediaKeySystemStatus::Cdm_disabled;
+ }
+ return EnsureCDMInstalled(keySystem, SecureLevel::Software, aOutMessage);
+#ifdef MOZ_WIDGET_ANDROID
+ } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible",
+ false)) {
+ bool supported =
+ mozilla::java::MediaDrmProxy::IsSchemeSupported(keySystem);
+ if (!supported) {
+ aOutMessage = nsLiteralCString(
+ "KeySystem or Minimum API level not met for Widevine EME");
+ return MediaKeySystemStatus::Cdm_not_supported;
+ }
+ return MediaKeySystemStatus::Available;
+#endif
+ }
+ }
+
+#ifdef MOZ_WMF_CDM
+ // Check PlayReady, which is a built-in CDM on most Windows versions.
+ if (IsPlayReadyKeySystemAndSupported(keySystem) &&
+ KeySystemConfig::Supports(keySystem)) {
+ return MediaKeySystemStatus::Available;
+ }
+
+ // Check Widevine L1. This can be a request for experimental key systems, or
+ // for a normal key system with hardware robustness.
+ if ((IsWidevineExperimentKeySystemAndSupported(keySystem) ||
+ IsWidevineKeySystem(keySystem)) &&
+ shouldCheckL1Installation) {
+ // TODO : if L3 hasn't been installed as well, should we fallback to install
+ // L3?
+ if (!Preferences::GetBool("media.gmp-widevinecdm-l1.enabled", false)) {
+ aOutMessage = "Widevine L1 EME disabled"_ns;
+ return MediaKeySystemStatus::Cdm_disabled;
+ }
+ return EnsureCDMInstalled(keySystem, SecureLevel::Hardware, aOutMessage);
+ }
+#endif
+
+ return MediaKeySystemStatus::Cdm_not_supported;
+}
+
+static KeySystemConfig::EMECodecString ToEMEAPICodecString(
+ const nsString& aCodec) {
+ if (IsAACCodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_AAC;
+ }
+ if (aCodec.EqualsLiteral("opus")) {
+ return KeySystemConfig::EME_CODEC_OPUS;
+ }
+ if (aCodec.EqualsLiteral("vorbis")) {
+ return KeySystemConfig::EME_CODEC_VORBIS;
+ }
+ if (aCodec.EqualsLiteral("flac")) {
+ return KeySystemConfig::EME_CODEC_FLAC;
+ }
+ if (IsH264CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_H264;
+ }
+ if (IsVP8CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_VP8;
+ }
+ if (IsVP9CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_VP9;
+ }
+#ifdef MOZ_WMF
+ if (IsH265CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_HEVC;
+ }
+#endif
+ return ""_ns;
+}
+
+static nsTArray<KeySystemConfig> GetSupportedKeySystems() {
+ nsTArray<KeySystemConfig> keySystemConfigs;
+
+ const nsTArray<nsString> keySystemNames{
+ NS_ConvertUTF8toUTF16(kClearKeyKeySystemName),
+ NS_ConvertUTF8toUTF16(kWidevineKeySystemName),
+#ifdef MOZ_WMF_CDM
+ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName),
+ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware),
+ NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName),
+ NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName),
+ NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName),
+#endif
+ };
+ for (const auto& name : keySystemNames) {
+ Unused << KeySystemConfig::CreateKeySystemConfigs(name, keySystemConfigs);
+ }
+ return keySystemConfigs;
+}
+
+static bool GetKeySystemConfigs(
+ const nsAString& aKeySystem,
+ nsTArray<KeySystemConfig>& aOutKeySystemConfig) {
+ bool foundConfigs = false;
+ for (auto& config : GetSupportedKeySystems()) {
+ if (config.IsSameKeySystem(aKeySystem)) {
+ aOutKeySystemConfig.AppendElement(std::move(config));
+ foundConfigs = true;
+ }
+ }
+ return foundConfigs;
+}
+
+/* static */
+bool MediaKeySystemAccess::KeySystemSupportsInitDataType(
+ const nsAString& aKeySystem, const nsAString& aInitDataType) {
+ nsTArray<KeySystemConfig> implementations;
+ GetKeySystemConfigs(aKeySystem, implementations);
+ bool containInitType = false;
+ for (const auto& config : implementations) {
+ if (config.mInitDataTypes.Contains(aInitDataType)) {
+ containInitType = true;
+ break;
+ }
+ }
+ return containInitType;
+}
+
+enum CodecType { Audio, Video, Invalid };
+
+static bool CanDecryptAndDecode(
+ const nsString& aKeySystem, const nsString& aContentType,
+ CodecType aCodecType,
+ const KeySystemConfig::ContainerSupport& aContainerSupport,
+ const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+ MOZ_ASSERT(aCodecType != Invalid);
+ for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
+ MOZ_ASSERT(!codec.IsEmpty());
+
+ if (aContainerSupport.DecryptsAndDecodes(codec)) {
+ // GMP can decrypt-and-decode this codec.
+ continue;
+ }
+
+ if (aContainerSupport.Decrypts(codec)) {
+ IgnoredErrorResult rv;
+ MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv);
+ if (!rv.Failed()) {
+ // GMP can decrypt and is allowed to return compressed samples to
+ // Gecko to decode, and Gecko has a decoder.
+ continue;
+ }
+ }
+
+ // Neither the GMP nor Gecko can both decrypt and decode. We don't
+ // support this codec.
+
+#if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't
+ // decode AAC, and a codec wasn't specified, be conservative
+ // and reject the MediaKeys request, since we assume Widevine
+ // will be used with AAC.
+ if (codec == KeySystemConfig::EME_CODEC_AAC &&
+ IsWidevineKeySystem(aKeySystem) &&
+ !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ if (aDiagnostics) {
+ aDiagnostics->SetKeySystemIssue(
+ DecoderDoctorDiagnostics::eWidevineWithNoWMF);
+ }
+ }
+#endif
+ return false;
+ }
+ return true;
+}
+
+// Returns if an encryption scheme is supported per:
+// https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md
+// To be supported the scheme should be one of:
+// - null
+// - missing (which will result in the nsString being set to void and thus null)
+// - one of the schemes supported by the CDM
+// If the pref to enable this behavior is not set, then the value should be
+// empty/null, as the dict member will not be exposed. In this case we will
+// always report support as we would before this feature was implemented.
+static bool SupportsEncryptionScheme(
+ const nsString& aEncryptionScheme,
+ const nsTArray<nsString>& aSupportedEncryptionSchemes) {
+ MOZ_ASSERT(
+ DOMStringIsNull(aEncryptionScheme) ||
+ StaticPrefs::media_eme_encrypted_media_encryption_scheme_enabled(),
+ "Encryption scheme checking support must be preffed on for "
+ "encryptionScheme to be a non-null string");
+ if (DOMStringIsNull(aEncryptionScheme)) {
+ // "A missing or null value indicates that any encryption scheme is
+ // acceptable."
+ return true;
+ }
+ return aSupportedEncryptionSchemes.Contains(aEncryptionScheme);
+}
+
+static bool ToSessionType(const nsAString& aSessionType,
+ MediaKeySessionType& aOutType) {
+ if (aSessionType.Equals(ToString(MediaKeySessionType::Temporary))) {
+ aOutType = MediaKeySessionType::Temporary;
+ return true;
+ }
+ if (aSessionType.Equals(ToString(MediaKeySessionType::Persistent_license))) {
+ aOutType = MediaKeySessionType::Persistent_license;
+ return true;
+ }
+ return false;
+}
+
+// 5.1.1 Is persistent session type?
+static bool IsPersistentSessionType(MediaKeySessionType aSessionType) {
+ return aSessionType == MediaKeySessionType::Persistent_license;
+}
+
+static bool ContainsSessionType(
+ const nsTArray<KeySystemConfig::SessionType>& aTypes,
+ const MediaKeySessionType& aSessionType) {
+ return (aSessionType == MediaKeySessionType::Persistent_license &&
+ aTypes.Contains(KeySystemConfig::SessionType::PersistentLicense)) ||
+ (aSessionType == MediaKeySessionType::Temporary &&
+ aTypes.Contains(KeySystemConfig::SessionType::Temporary));
+}
+
+CodecType GetMajorType(const MediaMIMEType& aMIMEType) {
+ if (aMIMEType.HasAudioMajorType()) {
+ return Audio;
+ }
+ if (aMIMEType.HasVideoMajorType()) {
+ return Video;
+ }
+ return Invalid;
+}
+
+static CodecType GetCodecType(const KeySystemConfig::EMECodecString& aCodec) {
+ if (aCodec.Equals(KeySystemConfig::EME_CODEC_AAC) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_OPUS) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VORBIS) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_FLAC)) {
+ return Audio;
+ }
+ if (aCodec.Equals(KeySystemConfig::EME_CODEC_H264) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VP8) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VP9) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_HEVC)) {
+ return Video;
+ }
+ return Invalid;
+}
+
+static bool AllCodecsOfType(
+ const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
+ const CodecType aCodecType) {
+ for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
+ if (GetCodecType(codec) != aCodecType) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool IsParameterUnrecognized(const nsAString& aContentType) {
+ nsAutoString contentType(aContentType);
+ contentType.StripWhitespace();
+
+ nsTArray<nsString> params;
+ nsAString::const_iterator start, end, semicolon, equalSign;
+ contentType.BeginReading(start);
+ contentType.EndReading(end);
+ semicolon = start;
+ // Find any substring between ';' & '='.
+ while (semicolon != end) {
+ if (FindCharInReadable(';', semicolon, end)) {
+ equalSign = ++semicolon;
+ if (FindCharInReadable('=', equalSign, end)) {
+ params.AppendElement(Substring(semicolon, equalSign));
+ semicolon = equalSign;
+ }
+ }
+ }
+
+ for (auto param : params) {
+ if (!param.LowerCaseEqualsLiteral("codecs") &&
+ !param.LowerCaseEqualsLiteral("profiles")) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// 3.1.1.3 Get Supported Capabilities for Audio/Video Type
+static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
+ const CodecType aCodecType,
+ const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities,
+ const MediaKeySystemConfiguration& aPartialConfig,
+ const KeySystemConfig& aKeySystem, DecoderDoctorDiagnostics* aDiagnostics,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ // Let local accumulated configuration be a local copy of partial
+ // configuration. (Note: It's not necessary for us to maintain a local copy,
+ // as we don't need to test whether capabilites from previous calls to this
+ // algorithm work with the capabilities currently being considered in this
+ // call. )
+
+ // Let supported media capabilities be an empty sequence of
+ // MediaKeySystemMediaCapability dictionaries.
+ Sequence<MediaKeySystemMediaCapability> supportedCapabilities;
+
+ // For each requested media capability in requested media capabilities:
+ for (const MediaKeySystemMediaCapability& capabilities :
+ aRequestedCapabilities) {
+ // Let content type be requested media capability's contentType member.
+ const nsString& contentTypeString = capabilities.mContentType;
+ // Let robustness be requested media capability's robustness member.
+ const nsString& robustness = capabilities.mRobustness;
+ // Optional encryption scheme extension, see
+ // https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md
+ // This will only be exposed to JS if
+ // media.eme.encrypted-media-encryption-scheme.enabled is preffed on.
+ const nsString encryptionScheme = capabilities.mEncryptionScheme;
+ // If content type is the empty string, return null.
+ if (contentTypeString.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') rejected; "
+ "audio or video capability has empty contentType.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ return Sequence<MediaKeySystemMediaCapability>();
+ }
+ // If content type is an invalid or unrecognized MIME type, continue
+ // to the next iteration.
+ Maybe<MediaContainerType> maybeContainerType =
+ MakeMediaContainerType(contentTypeString);
+ if (!maybeContainerType) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "failed to parse contentTypeString as MIME type.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ const MediaContainerType& containerType = *maybeContainerType;
+ bool invalid = false;
+ nsTArray<KeySystemConfig::EMECodecString> codecs;
+ for (const auto& codecString :
+ containerType.ExtendedType().Codecs().Range()) {
+ KeySystemConfig::EMECodecString emeCodec =
+ ToEMEAPICodecString(nsString(codecString));
+ if (emeCodec.IsEmpty()) {
+ invalid = true;
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "'%s' is an invalid codec string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get(),
+ NS_ConvertUTF16toUTF8(codecString).get());
+ break;
+ }
+ codecs.AppendElement(emeCodec);
+ }
+ if (invalid) {
+ continue;
+ }
+
+ // If the user agent does not support container, continue to the next
+ // iteration. The case-sensitivity of string comparisons is determined by
+ // the appropriate RFC. (Note: Per RFC 6838 [RFC6838], "Both top-level type
+ // and subtype names are case-insensitive."'. We're using
+ // nsContentTypeParser and that is case-insensitive and converts all its
+ // parameter outputs to lower case.)
+ const bool supportedInMP4 =
+ MP4Decoder::IsSupportedType(containerType, aDiagnostics);
+ if (supportedInMP4 && !aKeySystem.mMP4.IsSupported()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MP4 requested but unsupported.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ const bool isWebM = WebMDecoder::IsSupportedType(containerType);
+ if (isWebM && !aKeySystem.mWebM.IsSupported()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s,'%s') unsupported; "
+ "WebM requested but unsupported.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (!supportedInMP4 && !isWebM) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "Unsupported or unrecognized container requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // Let parameters be the RFC 6381[RFC6381] parameters, if any, specified by
+ // content type.
+ // If the user agent does not recognize one or more parameters, continue to
+ // the next iteration.
+ if (IsParameterUnrecognized(contentTypeString)) {
+ continue;
+ }
+
+ // Let media types be the set of codecs and codec constraints specified by
+ // parameters. The case-sensitivity of string comparisons is determined by
+ // the appropriate RFC or other specification.
+ // (Note: codecs array is 'parameter').
+
+ // If media types is empty:
+ if (codecs.IsEmpty()) {
+ // Log deprecation warning to encourage authors to not do this!
+ aDeprecationLogFn("MediaEMENoCodecsDeprecatedWarning");
+ // TODO: Remove this once we're sure it doesn't break the web.
+ // If container normatively implies a specific set of codecs and codec
+ // constraints: Let parameters be that set.
+ if (supportedInMP4) {
+ if (aCodecType == Audio) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_AAC);
+ } else if (aCodecType == Video) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_H264);
+ }
+ } else if (isWebM) {
+ if (aCodecType == Audio) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_VORBIS);
+ } else if (aCodecType == Video) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_VP8);
+ }
+ }
+ // Otherwise: Continue to the next iteration.
+ // (Note: all containers we support have implied codecs, so don't continue
+ // here.)
+ }
+
+ // If container type is not strictly a audio/video type, continue to the
+ // next iteration.
+ const auto majorType = GetMajorType(containerType.Type());
+ if (majorType == Invalid) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MIME type is not an audio or video MIME type.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MIME type mixes audio codecs in video capabilities "
+ "or video codecs in audio capabilities.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ // If robustness is not the empty string and contains an unrecognized
+ // value or a value not supported by implementation, continue to the
+ // next iteration. String comparison is case-sensitive.
+ if (!robustness.IsEmpty()) {
+ if (majorType == Audio &&
+ !aKeySystem.mAudioRobustness.Contains(robustness)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "unsupported robustness string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (majorType == Video &&
+ !aKeySystem.mVideoRobustness.Contains(robustness)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "unsupported robustness string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ // Note: specified robustness requirements are satisfied.
+ }
+
+ // If preffed on: "In the Get Supported Capabilities for Audio/Video Type
+ // algorithm, implementations must skip capabilities specifying unsupported
+ // encryption schemes."
+ if (!SupportsEncryptionScheme(encryptionScheme,
+ aKeySystem.mEncryptionSchemes)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "encryption scheme unsupported by CDM requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // If the user agent and implementation definitely support playback of
+ // encrypted media data for the combination of container, media types,
+ // robustness and local accumulated configuration in combination with
+ // restrictions...
+ const auto& containerSupport =
+ supportedInMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM;
+ if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString,
+ majorType, containerSupport, codecs,
+ aDiagnostics)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "codec unsupported by CDM requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // ... add requested media capability to supported media capabilities.
+ if (!supportedCapabilities.AppendElement(capabilities, mozilla::fallible)) {
+ NS_WARNING("GetSupportedCapabilities: Malloc failure");
+ return Sequence<MediaKeySystemMediaCapability>();
+ }
+
+ // Note: omitting steps 3.13.2, our robustness is not sophisticated enough
+ // to require considering all requirements together.
+ }
+ return supportedCapabilities;
+}
+
+// "Get Supported Configuration and Consent" algorithm, steps 4-7 for
+// distinctive identifier, and steps 8-11 for persistent state. The steps
+// are the same for both requirements/features, so we factor them out into
+// a single function.
+static bool CheckRequirement(
+ const MediaKeysRequirement aRequirement,
+ const KeySystemConfig::Requirement aKeySystemRequirement,
+ MediaKeysRequirement& aOutRequirement) {
+ // Let requirement be the value of candidate configuration's member.
+ MediaKeysRequirement requirement = aRequirement;
+ // If requirement is "optional" and feature is not allowed according to
+ // restrictions, set requirement to "not-allowed".
+ if (aRequirement == MediaKeysRequirement::Optional &&
+ aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) {
+ requirement = MediaKeysRequirement::Not_allowed;
+ }
+
+ // Follow the steps for requirement from the following list:
+ switch (requirement) {
+ case MediaKeysRequirement::Required: {
+ // If the implementation does not support use of requirement in
+ // combination with accumulated configuration and restrictions, return
+ // NotSupported.
+ if (aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) {
+ return false;
+ }
+ break;
+ }
+ case MediaKeysRequirement::Optional: {
+ // Continue with the following steps.
+ break;
+ }
+ case MediaKeysRequirement::Not_allowed: {
+ // If the implementation requires use of feature in combination with
+ // accumulated configuration and restrictions, return NotSupported.
+ if (aKeySystemRequirement == KeySystemConfig::Requirement::Required) {
+ return false;
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+
+ // Set the requirement member of accumulated configuration to equal
+ // calculated requirement.
+ aOutRequirement = requirement;
+
+ return true;
+}
+
+// 3.1.1.2, step 12
+// Follow the steps for the first matching condition from the following list:
+// If the sessionTypes member is present in candidate configuration.
+// Let session types be candidate configuration's sessionTypes member.
+// Otherwise let session types be ["temporary"].
+// Note: This returns an empty array on malloc failure.
+static Sequence<nsString> UnboxSessionTypes(
+ const Optional<Sequence<nsString>>& aSessionTypes) {
+ Sequence<nsString> sessionTypes;
+ if (aSessionTypes.WasPassed()) {
+ sessionTypes = aSessionTypes.Value();
+ } else {
+ // Note: fallible. Results in an empty array.
+ (void)sessionTypes.AppendElement(ToString(MediaKeySessionType::Temporary),
+ mozilla::fallible);
+ }
+ return sessionTypes;
+}
+
+// 3.1.1.2 Get Supported Configuration and Consent
+static bool GetSupportedConfig(
+ const KeySystemConfig& aKeySystem,
+ const MediaKeySystemConfiguration& aCandidate,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aInPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ EME_LOG("Compare implementation '%s'\n with request '%s'",
+ NS_ConvertUTF16toUTF8(aKeySystem.GetDebugInfo()).get(),
+ ToCString(aCandidate).get());
+ // Let accumulated configuration be a new MediaKeySystemConfiguration
+ // dictionary.
+ MediaKeySystemConfiguration config;
+ // Set the label member of accumulated configuration to equal the label member
+ // of candidate configuration.
+ config.mLabel = aCandidate.mLabel;
+ // If the initDataTypes member of candidate configuration is non-empty, run
+ // the following steps:
+ if (!aCandidate.mInitDataTypes.IsEmpty()) {
+ // Let supported types be an empty sequence of DOMStrings.
+ nsTArray<nsString> supportedTypes;
+ // For each value in candidate configuration's initDataTypes member:
+ for (const nsString& initDataType : aCandidate.mInitDataTypes) {
+ // Let initDataType be the value.
+ // If the implementation supports generating requests based on
+ // initDataType, add initDataType to supported types. String comparison is
+ // case-sensitive. The empty string is never supported.
+ if (aKeySystem.mInitDataTypes.Contains(initDataType)) {
+ supportedTypes.AppendElement(initDataType);
+ }
+ }
+ // If supported types is empty, return NotSupported.
+ if (supportedTypes.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported initDataTypes provided.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the initDataTypes member of accumulated configuration to supported
+ // types.
+ if (!config.mInitDataTypes.Assign(supportedTypes)) {
+ return false;
+ }
+ }
+
+ if (!CheckRequirement(aCandidate.mDistinctiveIdentifier,
+ aKeySystem.mDistinctiveIdentifier,
+ config.mDistinctiveIdentifier)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "distinctiveIdentifier requirement not satisfied.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ if (!CheckRequirement(aCandidate.mPersistentState,
+ aKeySystem.mPersistentState, config.mPersistentState)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistentState requirement not satisfied.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ if (config.mPersistentState == MediaKeysRequirement::Required &&
+ aInPrivateBrowsing) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistentState requested in Private Browsing window.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ Sequence<nsString> sessionTypes(UnboxSessionTypes(aCandidate.mSessionTypes));
+ if (sessionTypes.IsEmpty()) {
+ // Malloc failure.
+ return false;
+ }
+
+ // For each value in session types:
+ for (const auto& sessionTypeString : sessionTypes) {
+ // Let session type be the value.
+ MediaKeySessionType sessionType;
+ if (!ToSessionType(sessionTypeString, sessionType)) {
+ // (Assume invalid sessionType is unsupported as per steps below).
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "invalid session type specified.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // If accumulated configuration's persistentState value is "not-allowed"
+ // and the Is persistent session type? algorithm returns true for session
+ // type return NotSupported.
+ if (config.mPersistentState == MediaKeysRequirement::Not_allowed &&
+ IsPersistentSessionType(sessionType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistent session requested but keysystem doesn't"
+ "support persistent state.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // If the implementation does not support session type in combination
+ // with accumulated configuration and restrictions for other reasons,
+ // return NotSupported.
+ if (!ContainsSessionType(aKeySystem.mSessionTypes, sessionType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "session type '%s' unsupported by keySystem.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get(),
+ NS_ConvertUTF16toUTF8(sessionTypeString).get());
+ return false;
+ }
+ // If accumulated configuration's persistentState value is "optional"
+ // and the result of running the Is persistent session type? algorithm
+ // on session type is true, change accumulated configuration's
+ // persistentState value to "required".
+ if (config.mPersistentState == MediaKeysRequirement::Optional &&
+ IsPersistentSessionType(sessionType)) {
+ config.mPersistentState = MediaKeysRequirement::Required;
+ }
+ }
+ // Set the sessionTypes member of accumulated configuration to session types.
+ config.mSessionTypes.Construct(std::move(sessionTypes));
+
+ // If the videoCapabilities and audioCapabilities members in candidate
+ // configuration are both empty, return NotSupported.
+ if (aCandidate.mAudioCapabilities.IsEmpty() &&
+ aCandidate.mVideoCapabilities.IsEmpty()) {
+ // TODO: Most sites using EME still don't pass capabilities, so we
+ // can't reject on it yet without breaking them. So add this later.
+ // Log deprecation warning to encourage authors to not do this!
+ aDeprecationLogFn("MediaEMENoCapabilitiesDeprecatedWarning");
+ }
+
+ // If the videoCapabilities member in candidate configuration is non-empty:
+ if (!aCandidate.mVideoCapabilities.IsEmpty()) {
+ // Let video capabilities be the result of executing the Get Supported
+ // Capabilities for Audio/Video Type algorithm on Video, candidate
+ // configuration's videoCapabilities member, accumulated configuration,
+ // and restrictions.
+ Sequence<MediaKeySystemMediaCapability> caps =
+ GetSupportedCapabilities(Video, aCandidate.mVideoCapabilities, config,
+ aKeySystem, aDiagnostics, aDeprecationLogFn);
+ // If video capabilities is null, return NotSupported.
+ if (caps.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported video capabilities.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the videoCapabilities member of accumulated configuration to video
+ // capabilities.
+ config.mVideoCapabilities = std::move(caps);
+ } else {
+ // Otherwise:
+ // Set the videoCapabilities member of accumulated configuration to an empty
+ // sequence.
+ }
+
+ // If the audioCapabilities member in candidate configuration is non-empty:
+ if (!aCandidate.mAudioCapabilities.IsEmpty()) {
+ // Let audio capabilities be the result of executing the Get Supported
+ // Capabilities for Audio/Video Type algorithm on Audio, candidate
+ // configuration's audioCapabilities member, accumulated configuration, and
+ // restrictions.
+ Sequence<MediaKeySystemMediaCapability> caps =
+ GetSupportedCapabilities(Audio, aCandidate.mAudioCapabilities, config,
+ aKeySystem, aDiagnostics, aDeprecationLogFn);
+ // If audio capabilities is null, return NotSupported.
+ if (caps.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported audio capabilities.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the audioCapabilities member of accumulated configuration to audio
+ // capabilities.
+ config.mAudioCapabilities = std::move(caps);
+ } else {
+ // Otherwise:
+ // Set the audioCapabilities member of accumulated configuration to an empty
+ // sequence.
+ }
+
+ // If accumulated configuration's distinctiveIdentifier value is "optional",
+ // follow the steps for the first matching condition from the following list:
+ if (config.mDistinctiveIdentifier == MediaKeysRequirement::Optional) {
+ // If the implementation requires use Distinctive Identifier(s) or
+ // Distinctive Permanent Identifier(s) for any of the combinations
+ // in accumulated configuration
+ if (aKeySystem.mDistinctiveIdentifier ==
+ KeySystemConfig::Requirement::Required) {
+ // Change accumulated configuration's distinctiveIdentifier value to
+ // "required".
+ config.mDistinctiveIdentifier = MediaKeysRequirement::Required;
+ } else {
+ // Otherwise, change accumulated configuration's distinctiveIdentifier
+ // value to "not-allowed".
+ config.mDistinctiveIdentifier = MediaKeysRequirement::Not_allowed;
+ }
+ }
+
+ // If accumulated configuration's persistentState value is "optional", follow
+ // the steps for the first matching condition from the following list:
+ if (config.mPersistentState == MediaKeysRequirement::Optional) {
+ // If the implementation requires persisting state for any of the
+ // combinations in accumulated configuration
+ if (aKeySystem.mPersistentState == KeySystemConfig::Requirement::Required) {
+ // Change accumulated configuration's persistentState value to "required".
+ config.mPersistentState = MediaKeysRequirement::Required;
+ } else {
+ // Otherwise, change accumulated configuration's persistentState
+ // value to "not-allowed".
+ config.mPersistentState = MediaKeysRequirement::Not_allowed;
+ }
+ }
+
+ // Note: Omitting steps 20-22. We don't ask for consent.
+
+#if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't decode AAC,
+ // and a codec wasn't specified, be conservative and reject the MediaKeys
+ // request.
+ if (IsWidevineKeySystem(aKeySystem.mKeySystem) &&
+ (aCandidate.mAudioCapabilities.IsEmpty() ||
+ aCandidate.mVideoCapabilities.IsEmpty()) &&
+ !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ if (aDiagnostics) {
+ aDiagnostics->SetKeySystemIssue(
+ DecoderDoctorDiagnostics::eWidevineWithNoWMF);
+ }
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "WMF required for Widevine decoding, but it's not available.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+#endif
+
+ // Return accumulated configuration.
+ aOutConfig = config;
+
+ return true;
+}
+
+/* static */
+bool MediaKeySystemAccess::GetSupportedConfig(
+ const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ nsTArray<KeySystemConfig> implementations;
+ if (!GetKeySystemConfigs(aKeySystem, implementations)) {
+ return false;
+ }
+ for (const auto& implementation : implementations) {
+ for (const MediaKeySystemConfiguration& candidate : aConfigs) {
+ if (mozilla::dom::GetSupportedConfig(
+ implementation, candidate, aOutConfig, aDiagnostics,
+ aIsPrivateBrowsing, aDeprecationLogFn)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/* static */
+void MediaKeySystemAccess::NotifyObservers(nsPIDOMWindowInner* aWindow,
+ const nsAString& aKeySystem,
+ MediaKeySystemStatus aStatus) {
+ RequestMediaKeySystemAccessNotification data;
+ data.mKeySystem = aKeySystem;
+ data.mStatus = aStatus;
+ nsAutoString json;
+ data.ToJSON(json);
+ EME_LOG("MediaKeySystemAccess::NotifyObservers() %s",
+ NS_ConvertUTF16toUTF8(json).get());
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aWindow, MediaKeys::kMediaKeysRequestTopic,
+ json.get());
+ }
+}
+
+static nsCString ToCString(const nsString& aString) {
+ nsCString str("'");
+ str.Append(NS_ConvertUTF16toUTF8(aString));
+ str.AppendLiteral("'");
+ return str;
+}
+
+static nsCString ToCString(const MediaKeysRequirement aValue) {
+ nsCString str("'");
+ str.AppendASCII(MediaKeysRequirementValues::GetString(aValue));
+ str.AppendLiteral("'");
+ return str;
+}
+
+static nsCString ToCString(const MediaKeySystemMediaCapability& aValue) {
+ nsCString str;
+ str.AppendLiteral("{contentType=");
+ str.Append(ToCString(aValue.mContentType));
+ str.AppendLiteral(", robustness=");
+ str.Append(ToCString(aValue.mRobustness));
+ str.AppendLiteral(", encryptionScheme=");
+ str.Append(ToCString(aValue.mEncryptionScheme));
+ str.AppendLiteral("}");
+ return str;
+}
+
+template <class Type>
+static nsCString ToCString(const Sequence<Type>& aSequence) {
+ nsCString str;
+ str.AppendLiteral("[");
+ StringJoinAppend(str, ","_ns, aSequence,
+ [](nsACString& dest, const Type& element) {
+ dest.Append(ToCString(element));
+ });
+ str.AppendLiteral("]");
+ return str;
+}
+
+template <class Type>
+static nsCString ToCString(const Optional<Sequence<Type>>& aOptional) {
+ nsCString str;
+ if (aOptional.WasPassed()) {
+ str.Append(ToCString(aOptional.Value()));
+ } else {
+ str.AppendLiteral("[]");
+ }
+ return str;
+}
+
+static nsCString ToCString(const MediaKeySystemConfiguration& aConfig) {
+ nsCString str;
+ str.AppendLiteral("{label=");
+ str.Append(ToCString(aConfig.mLabel));
+
+ str.AppendLiteral(", initDataTypes=");
+ str.Append(ToCString(aConfig.mInitDataTypes));
+
+ str.AppendLiteral(", audioCapabilities=");
+ str.Append(ToCString(aConfig.mAudioCapabilities));
+
+ str.AppendLiteral(", videoCapabilities=");
+ str.Append(ToCString(aConfig.mVideoCapabilities));
+
+ str.AppendLiteral(", distinctiveIdentifier=");
+ str.Append(ToCString(aConfig.mDistinctiveIdentifier));
+
+ str.AppendLiteral(", persistentState=");
+ str.Append(ToCString(aConfig.mPersistentState));
+
+ str.AppendLiteral(", sessionTypes=");
+ str.Append(ToCString(aConfig.mSessionTypes));
+
+ str.AppendLiteral("}");
+
+ return str;
+}
+
+/* static */
+nsCString MediaKeySystemAccess::ToCString(
+ const Sequence<MediaKeySystemConfiguration>& aConfig) {
+ return mozilla::dom::ToCString(aConfig);
+}
+
+#undef LOG
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h
new file mode 100644
index 0000000000..954ff7adf3
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccess.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeySystemAccess_h
+#define mozilla_dom_MediaKeySystemAccess_h
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeysRequestStatusBinding.h"
+
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+
+class DecoderDoctorDiagnostics;
+class ErrorResult;
+
+namespace dom {
+
+struct MediaKeySystemAccessRequest;
+
+class MediaKeySystemAccess final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeySystemAccess)
+
+ public:
+ explicit MediaKeySystemAccess(nsPIDOMWindowInner* aParent,
+ const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig);
+
+ protected:
+ ~MediaKeySystemAccess();
+
+ public:
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetKeySystem(nsString& aRetVal) const;
+
+ void GetConfiguration(MediaKeySystemConfiguration& aConfig);
+
+ already_AddRefed<Promise> CreateMediaKeys(ErrorResult& aRv);
+
+ static MediaKeySystemStatus GetKeySystemStatus(
+ const MediaKeySystemAccessRequest& aRequest,
+ nsACString& aOutExceptionMessage);
+
+ static void NotifyObservers(nsPIDOMWindowInner* aWindow,
+ const nsAString& aKeySystem,
+ MediaKeySystemStatus aStatus);
+
+ static bool GetSupportedConfig(
+ const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn);
+
+ static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem,
+ const nsAString& aInitDataType);
+
+ static nsCString ToCString(
+ const Sequence<MediaKeySystemConfiguration>& aConfig);
+
+ private:
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ const nsString mKeySystem;
+ const MediaKeySystemConfiguration mConfig;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeySystemAccess_h
diff --git a/dom/media/eme/MediaKeySystemAccessManager.cpp b/dom/media/eme/MediaKeySystemAccessManager.cpp
new file mode 100644
index 0000000000..2bc12d57d7
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.cpp
@@ -0,0 +1,702 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeySystemAccessManager.h"
+
+#include "DecoderDoctorDiagnostics.h"
+#include "MediaKeySystemAccessPermissionRequest.h"
+#include "VideoUtils.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Unused.h"
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsTHashMap.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla::dom {
+
+MediaKeySystemAccessManager::PendingRequest::PendingRequest(
+ DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs)
+ : MediaKeySystemAccessRequest(aKeySystem, aConfigs), mPromise(aPromise) {
+ MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+MediaKeySystemAccessManager::PendingRequest::~PendingRequest() {
+ MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+void MediaKeySystemAccessManager::PendingRequest::CancelTimer() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::
+ RejectPromiseWithInvalidAccessError(const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithInvalidAccessError(aReason);
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::
+ RejectPromiseWithNotSupportedError(const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithNotSupportedError(aReason);
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::RejectPromiseWithTypeError(
+ const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithTypeError(aReason);
+ }
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+ for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) {
+ tmp->mPendingInstallRequests[i]->CancelTimer();
+ tmp->mPendingInstallRequests[i]->RejectPromiseWithInvalidAccessError(
+ nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager GC"));
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingInstallRequests[i]->mPromise)
+ }
+ tmp->mPendingInstallRequests.Clear();
+ for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) {
+ tmp->mPendingAppApprovalRequests[i]->RejectPromiseWithInvalidAccessError(
+ nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager GC"));
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingAppApprovalRequests[i]->mPromise)
+ }
+ tmp->mPendingAppApprovalRequests.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+ for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingInstallRequests[i]->mPromise)
+ }
+ for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingAppApprovalRequests[i]->mPromise)
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+#define MKSAM_LOG_DEBUG(msg, ...) \
+ EME_LOG("MediaKeySystemAccessManager::%s " msg, __func__, ##__VA_ARGS__)
+
+MediaKeySystemAccessManager::MediaKeySystemAccessManager(
+ nsPIDOMWindowInner* aWindow)
+ : mWindow(aWindow) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+MediaKeySystemAccessManager::~MediaKeySystemAccessManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+ Shutdown();
+}
+
+void MediaKeySystemAccessManager::Request(
+ DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs) {
+ MOZ_ASSERT(NS_IsMainThread());
+ CheckDoesWindowSupportProtectedMedia(
+ MakeUnique<PendingRequest>(aPromise, aKeySystem, aConfigs));
+}
+
+void MediaKeySystemAccessManager::CheckDoesWindowSupportProtectedMedia(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ // In Windows OS, some Firefox windows that host content cannot support
+ // protected content, so check the status of support for this window.
+ // On other platforms windows should always support protected media.
+#ifdef XP_WIN
+ RefPtr<BrowserChild> browser(BrowserChild::GetFrom(mWindow));
+ if (!browser) {
+ if (!XRE_IsParentProcess() || XRE_IsE10sParentProcess()) {
+ // In this case, there is no browser because the Navigator object has
+ // been disconnected from its window. Thus, reject the promise.
+ aRequest->mPromise->MaybeRejectWithTypeError(
+ "Browsing context is no longer available");
+ } else {
+ // In this case, there is no browser because e10s is off. Proceed with
+ // the request with support since this scenario is always supported.
+ MKSAM_LOG_DEBUG("Allowing protected media on Windows with e10s off.");
+
+ OnDoesWindowSupportProtectedMedia(true, std::move(aRequest));
+ }
+
+ return;
+ }
+
+ RefPtr<MediaKeySystemAccessManager> self(this);
+
+ MKSAM_LOG_DEBUG(
+ "Checking with browser if this window supports protected media.");
+ browser->DoesWindowSupportProtectedMedia()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, request = std::move(aRequest)](
+ const BrowserChild::IsWindowSupportingProtectedMediaPromise::
+ ResolveOrRejectValue& value) mutable {
+ if (value.IsResolve()) {
+ self->OnDoesWindowSupportProtectedMedia(value.ResolveValue(),
+ std::move(request));
+ } else {
+ EME_LOG(
+ "MediaKeySystemAccessManager::DoesWindowSupportProtectedMedia-"
+ "ResolveOrRejectLambda Failed to make IPC call to "
+ "IsWindowSupportingProtectedMedia: "
+ "reason=%d",
+ static_cast<int>(value.RejectValue()));
+ // Treat as failure.
+ self->OnDoesWindowSupportProtectedMedia(false, std::move(request));
+ }
+ });
+
+#else
+ // Non-Windows OS windows always support protected media.
+ MKSAM_LOG_DEBUG(
+ "Allowing protected media because all non-Windows OS windows support "
+ "protected media.");
+ OnDoesWindowSupportProtectedMedia(true, std::move(aRequest));
+#endif
+}
+
+void MediaKeySystemAccessManager::OnDoesWindowSupportProtectedMedia(
+ bool aIsSupportedInWindow, UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s aRequest->mKeySystem=%s",
+ aIsSupportedInWindow ? "true" : "false",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!aIsSupportedInWindow) {
+ aRequest->RejectPromiseWithNotSupportedError(
+ "EME is not supported in this window"_ns);
+ return;
+ }
+
+ RequestMediaKeySystemAccess(std::move(aRequest));
+}
+
+void MediaKeySystemAccessManager::CheckDoesAppAllowProtectedMedia(
+ UniquePtr<PendingRequest> aRequest) {
+ // At time of writing, only GeckoView is expected to leverage the need to
+ // approve EME requests from the application. However, this functionality
+ // can be tested on all platforms by manipulating the
+ // media.eme.require-app-approval + test prefs associated with
+ // MediaKeySystemPermissionRequest.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!StaticPrefs::media_eme_require_app_approval()) {
+ MKSAM_LOG_DEBUG(
+ "media.eme.require-app-approval is false, allowing request.");
+ // We don't require app approval as the pref is not set. Treat as app
+ // approving by passing true to the handler.
+ OnDoesAppAllowProtectedMedia(true, std::move(aRequest));
+ return;
+ }
+
+ if (mAppAllowsProtectedMediaPromiseRequest.Exists()) {
+ // We already have a pending permission request, we don't need to fire
+ // another. Just wait for the existing permission request to be handled
+ // and the result from that will be used to handle the current request.
+ MKSAM_LOG_DEBUG(
+ "mAppAllowsProtectedMediaPromiseRequest already exists. aRequest "
+ "addded to queue and will be handled when exising permission request "
+ "is serviced.");
+ mPendingAppApprovalRequests.AppendElement(std::move(aRequest));
+ return;
+ }
+
+ RefPtr<MediaKeySystemAccessPermissionRequest> appApprovalRequest =
+ MediaKeySystemAccessPermissionRequest::Create(mWindow);
+ if (!appApprovalRequest) {
+ MKSAM_LOG_DEBUG(
+ "Failed to create app approval request! Blocking eme request as "
+ "fallback.");
+ aRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Failed to create approval request to send to app embedding Gecko."));
+ return;
+ }
+
+ // If we're not using testing prefs (which take precedence over cached
+ // values) and have a cached value, handle based on the cached value.
+ if (appApprovalRequest->CheckPromptPrefs() ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Pending &&
+ mAppAllowsProtectedMedia) {
+ MKSAM_LOG_DEBUG(
+ "Short circuiting based on mAppAllowsProtectedMedia cached value");
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(aRequest));
+ return;
+ }
+
+ // Store the approval request, it will be handled when we get a response
+ // from the app.
+ mPendingAppApprovalRequests.AppendElement(std::move(aRequest));
+
+ RefPtr<MediaKeySystemAccessPermissionRequest::RequestPromise> p =
+ appApprovalRequest->GetPromise();
+ p->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ // Allow callback
+ [this,
+ self = RefPtr<MediaKeySystemAccessManager>(this)](bool aRequestResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequestResult, "Result should be true on allow callback!");
+ mAppAllowsProtectedMediaPromiseRequest.Complete();
+ // Cache result.
+ mAppAllowsProtectedMedia = Some(aRequestResult);
+ // For each pending request, handle it based on the app's response.
+ for (UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(approvalRequest));
+ }
+ self->mPendingAppApprovalRequests.Clear();
+ },
+ // Cancel callback
+ [this,
+ self = RefPtr<MediaKeySystemAccessManager>(this)](bool aRequestResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aRequestResult,
+ "Result should be false on cancel callback!");
+ mAppAllowsProtectedMediaPromiseRequest.Complete();
+ // Cache result.
+ mAppAllowsProtectedMedia = Some(aRequestResult);
+ // For each pending request, handle it based on the app's response.
+ for (UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(approvalRequest));
+ }
+ self->mPendingAppApprovalRequests.Clear();
+ })
+ ->Track(mAppAllowsProtectedMediaPromiseRequest);
+
+ // Prefs not causing short circuit, no cached value, go ahead and request
+ // permission.
+ MKSAM_LOG_DEBUG("Dispatching async request for app approval");
+ if (NS_FAILED(appApprovalRequest->Start())) {
+ // This shouldn't happen unless we're shutting down or similar edge cases.
+ // If this is regularly being hit then something is wrong and should be
+ // investigated.
+ MKSAM_LOG_DEBUG(
+ "Failed to start app approval request! Eme approval will be left in "
+ "limbo!");
+ }
+}
+
+void MediaKeySystemAccessManager::OnDoesAppAllowProtectedMedia(
+ bool aIsAllowed, UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsAllowed=%s aRequest->mKeySystem=%s",
+ aIsAllowed ? "true" : "false",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+ if (!aIsAllowed) {
+ aRequest->RejectPromiseWithNotSupportedError(
+ nsLiteralCString("The application embedding this user agent has "
+ "blocked MediaKeySystemAccess"));
+ return;
+ }
+
+ ProvideAccess(std::move(aRequest));
+}
+
+void MediaKeySystemAccessManager::RequestMediaKeySystemAccess(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ // 1. If keySystem is the empty string, return a promise rejected with a newly
+ // created TypeError.
+ if (aRequest->mKeySystem.IsEmpty()) {
+ aRequest->mPromise->MaybeRejectWithTypeError("Key system string is empty");
+ // Don't notify DecoderDoctor, as there's nothing we or the user can
+ // do to fix this situation; the site is using the API wrong.
+ return;
+ }
+ // 2. If supportedConfigurations is empty, return a promise rejected with a
+ // newly created TypeError.
+ if (aRequest->mConfigs.IsEmpty()) {
+ aRequest->mPromise->MaybeRejectWithTypeError(
+ "Candidate MediaKeySystemConfigs is empty");
+ // Don't notify DecoderDoctor, as there's nothing we or the user can
+ // do to fix this situation; the site is using the API wrong.
+ return;
+ }
+
+ // 3. Let document be the calling context's Document.
+ // 4. Let origin be the origin of document.
+ // 5. Let promise be a new promise.
+ // 6. Run the following steps in parallel:
+
+ DecoderDoctorDiagnostics diagnostics;
+
+ // 1. If keySystem is not one of the Key Systems supported by the user
+ // agent, reject promise with a NotSupportedError. String comparison is
+ // case-sensitive.
+ if (!IsWidevineKeySystem(aRequest->mKeySystem) &&
+#ifdef MOZ_WMF_CDM
+ !IsPlayReadyKeySystemAndSupported(aRequest->mKeySystem) &&
+ !IsWidevineExperimentKeySystemAndSupported(aRequest->mKeySystem) &&
+#endif
+ !IsClearkeyKeySystem(aRequest->mKeySystem)) {
+ // Not to inform user, because nothing to do if the keySystem is not
+ // supported.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Key system is unsupported"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ if (!StaticPrefs::media_eme_enabled() &&
+ !IsClearkeyKeySystem(aRequest->mKeySystem)) {
+ // EME disabled by user, send notification to chrome so UI can inform user.
+ // Clearkey is allowed even when EME is disabled because we want the pref
+ // "media.eme.enabled" only taking effect on proprietary DRMs.
+ // We don't show the notification if the pref is locked.
+ if (!Preferences::IsLocked("media.eme.enabled")) {
+ MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem,
+ MediaKeySystemStatus::Api_disabled);
+ }
+ aRequest->RejectPromiseWithNotSupportedError("EME has been preffed off"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ nsAutoCString message;
+ MediaKeySystemStatus status =
+ MediaKeySystemAccess::GetKeySystemStatus(*aRequest, message);
+
+ nsPrintfCString msg(
+ "MediaKeySystemAccess::GetKeySystemStatus(%s) "
+ "result=%s msg='%s'",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get(),
+ nsCString(MediaKeySystemStatusValues::GetString(status)).get(),
+ message.get());
+ LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg));
+ EME_LOG("%s", msg.get());
+
+ // We may need to install Widevine CDM to continue.
+ if (status == MediaKeySystemStatus::Cdm_not_installed &&
+ (IsWidevineKeySystem(aRequest->mKeySystem)
+#ifdef MOZ_WMF_CDM
+ || IsWidevineExperimentKeySystemAndSupported(aRequest->mKeySystem)
+#endif
+ )) {
+ // These are cases which could be resolved by downloading a new(er) CDM.
+ // When we send the status to chrome, chrome's GMPProvider will attempt to
+ // download or update the CDM. In AwaitInstall() we add listeners to wait
+ // for the update to complete, and we'll call this function again with
+ // aType==Subsequent once the download has completed and the GMPService
+ // has had a new plugin added. AwaitInstall() sets a timer to fail if the
+ // update/download takes too long or fails.
+
+ if (aRequest->mRequestType != PendingRequest::RequestType::Initial) {
+ MOZ_ASSERT(aRequest->mRequestType ==
+ PendingRequest::RequestType::Subsequent);
+ // CDM is not installed, but this is a subsequent request. We've waited,
+ // but can't service this request! Give up. Chrome will still be showing a
+ // "I can't play, updating" notification.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Timed out while waiting for a CDM update"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ nsString keySystem = aRequest->mKeySystem;
+#ifdef MOZ_WMF_CDM
+ // If cdm-not-install is for HWDRM, that means we want to install Widevine
+ // L1, which requires using hardware key system name for GMP to look up the
+ // plugin.
+ if (CheckIfHarewareDRMConfigExists(aRequest->mConfigs)) {
+ keySystem = NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName);
+ }
+#endif
+ if (AwaitInstall(std::move(aRequest))) {
+ // Notify chrome that we're going to wait for the CDM to download/update.
+ EME_LOG("Await %s for installation",
+ NS_ConvertUTF16toUTF8(keySystem).get());
+ MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status);
+ } else {
+ // Failed to await the install. Log failure and give up trying to service
+ // this request.
+ EME_LOG("Failed to await %s for installation",
+ NS_ConvertUTF16toUTF8(keySystem).get());
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), keySystem,
+ false, __func__);
+ }
+ return;
+ }
+ if (status != MediaKeySystemStatus::Available) {
+ // Failed due to user disabling something, send a notification to
+ // chrome, so we can show some UI to explain how the user can rectify
+ // the situation.
+ EME_LOG("Notify CDM failure for %s and reject the promise",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+ MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem,
+ status);
+ aRequest->RejectPromiseWithNotSupportedError(message);
+ return;
+ }
+
+ nsCOMPtr<Document> doc = mWindow->GetExtantDoc();
+ nsTHashMap<nsCharPtrHashKey, bool> warnings;
+ std::function<void(const char*)> deprecationWarningLogFn =
+ [&](const char* aMsgName) {
+ EME_LOG(
+ "MediaKeySystemAccessManager::DeprecationWarningLambda Logging "
+ "deprecation warning '%s' to WebConsole.",
+ aMsgName);
+ warnings.InsertOrUpdate(aMsgName, true);
+ AutoTArray<nsString, 1> params;
+ nsString& uri = *params.AppendElement();
+ if (doc) {
+ Unused << doc->GetDocumentURI(uri);
+ }
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns,
+ doc, nsContentUtils::eDOM_PROPERTIES,
+ aMsgName, params);
+ };
+
+ bool isPrivateBrowsing =
+ mWindow->GetExtantDoc() &&
+ mWindow->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() > 0;
+ // 2. Let implementation be the implementation of keySystem.
+ // 3. For each value in supportedConfigurations:
+ // 1. Let candidate configuration be the value.
+ // 2. Let supported configuration be the result of executing the Get
+ // Supported Configuration algorithm on implementation, candidate
+ // configuration, and origin.
+ // 3. If supported configuration is not NotSupported, run the following
+ // steps:
+ // 1. Let access be a new MediaKeySystemAccess object, and initialize it
+ // as follows:
+ // 1. Set the keySystem attribute to keySystem.
+ // 2. Let the configuration value be supported configuration.
+ // 3. Let the cdm implementation value be implementation.
+ // 2. Resolve promise with access and abort the parallel steps of this
+ // algorithm.
+ MediaKeySystemConfiguration config;
+ if (MediaKeySystemAccess::GetSupportedConfig(
+ aRequest->mKeySystem, aRequest->mConfigs, config, &diagnostics,
+ isPrivateBrowsing, deprecationWarningLogFn)) {
+ aRequest->mSupportedConfig = Some(config);
+ // The app gets the final say on if we provide access or not.
+ CheckDoesAppAllowProtectedMedia(std::move(aRequest));
+ return;
+ }
+ // 4. Reject promise with a NotSupportedError.
+
+ // Not to inform user, because nothing to do if the corresponding keySystem
+ // configuration is not supported.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Key system configuration is not supported"_ns);
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(),
+ aRequest->mKeySystem, false, __func__);
+}
+
+void MediaKeySystemAccessManager::ProvideAccess(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(aRequest);
+ MOZ_ASSERT(
+ aRequest->mSupportedConfig,
+ "The request needs a supported config if we're going to provide access!");
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ DecoderDoctorDiagnostics diagnostics;
+
+ RefPtr<MediaKeySystemAccess> access(new MediaKeySystemAccess(
+ mWindow, aRequest->mKeySystem, aRequest->mSupportedConfig.ref()));
+ aRequest->mPromise->MaybeResolve(access);
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(),
+ aRequest->mKeySystem, true, __func__);
+}
+
+bool MediaKeySystemAccessManager::AwaitInstall(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!EnsureObserversAdded()) {
+ NS_WARNING("Failed to add pref observer");
+ aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString(
+ "Failed trying to setup CDM update: failed adding observers"));
+ return false;
+ }
+
+ nsCOMPtr<nsITimer> timer;
+ NS_NewTimerWithObserver(getter_AddRefs(timer), this, 60 * 1000,
+ nsITimer::TYPE_ONE_SHOT);
+ if (!timer) {
+ NS_WARNING("Failed to create timer to await CDM install.");
+ aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString(
+ "Failed trying to setup CDM update: failed timer creation"));
+ return false;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ aRequest->mTimer == nullptr,
+ "Timer should not already be set on a request we're about to await");
+ aRequest->mTimer = timer;
+
+ mPendingInstallRequests.AppendElement(std::move(aRequest));
+ return true;
+}
+
+void MediaKeySystemAccessManager::RetryRequest(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+ // Cancel and null timer if it exists.
+ aRequest->CancelTimer();
+ // Indicate that this is a request that's being retried.
+ aRequest->mRequestType = PendingRequest::RequestType::Subsequent;
+ RequestMediaKeySystemAccess(std::move(aRequest));
+}
+
+nsresult MediaKeySystemAccessManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MKSAM_LOG_DEBUG("%s", aTopic);
+
+ if (!strcmp(aTopic, "gmp-changed")) {
+ // Filter out the requests where the CDM's install-status is no longer
+ // "unavailable". This will be the CDMs which have downloaded since the
+ // initial request.
+ // Note: We don't have a way to communicate from chrome that the CDM has
+ // failed to download, so we'll just let the timeout fail us in that case.
+ nsTArray<UniquePtr<PendingRequest>> requests;
+ for (size_t i = mPendingInstallRequests.Length(); i-- > 0;) {
+ nsAutoCString message;
+ MediaKeySystemStatus status = MediaKeySystemAccess::GetKeySystemStatus(
+ *mPendingInstallRequests[i], message);
+ if (status == MediaKeySystemStatus::Cdm_not_installed) {
+ // Not yet installed, don't retry. Keep waiting until timeout.
+ continue;
+ }
+ // Status has changed, retry request.
+ requests.AppendElement(std::move(mPendingInstallRequests[i]));
+ mPendingInstallRequests.RemoveElementAt(i);
+ }
+ // Retry all pending requests, but this time fail if the CDM is not
+ // installed.
+ for (size_t i = requests.Length(); i-- > 0;) {
+ RetryRequest(std::move(requests[i]));
+ }
+ } else if (!strcmp(aTopic, "timer-callback")) {
+ // Find the timer that expired and re-run the request for it.
+ nsCOMPtr<nsITimer> timer(do_QueryInterface(aSubject));
+ for (size_t i = 0; i < mPendingInstallRequests.Length(); i++) {
+ if (mPendingInstallRequests[i]->mTimer == timer) {
+ EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request");
+ UniquePtr<PendingRequest> request =
+ std::move(mPendingInstallRequests[i]);
+ mPendingInstallRequests.RemoveElementAt(i);
+ RetryRequest(std::move(request));
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult MediaKeySystemAccessManager::GetName(nsACString& aName) {
+ aName.AssignLiteral("MediaKeySystemAccessManager");
+ return NS_OK;
+}
+
+bool MediaKeySystemAccessManager::EnsureObserversAdded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAddedObservers) {
+ return true;
+ }
+
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (NS_WARN_IF(!obsService)) {
+ return false;
+ }
+ mAddedObservers =
+ NS_SUCCEEDED(obsService->AddObserver(this, "gmp-changed", false));
+ return mAddedObservers;
+}
+
+void MediaKeySystemAccessManager::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MKSAM_LOG_DEBUG("");
+ for (const UniquePtr<PendingRequest>& installRequest :
+ mPendingInstallRequests) {
+ // Cancel all requests; we're shutting down.
+ installRequest->CancelTimer();
+ installRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager shutdown"));
+ }
+ mPendingInstallRequests.Clear();
+ for (const UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ approvalRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager shutdown"));
+ }
+ mPendingAppApprovalRequests.Clear();
+ mAppAllowsProtectedMediaPromiseRequest.DisconnectIfExists();
+ if (mAddedObservers) {
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->RemoveObserver(this, "gmp-changed");
+ mAddedObservers = false;
+ }
+ }
+}
+
+} // namespace mozilla::dom
+
+#undef MKSAM_LOG_DEBUG
diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h
new file mode 100644
index 0000000000..77feded701
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.h
@@ -0,0 +1,237 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
+#define DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
+
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "mozilla/MozPromise.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla::dom {
+
+class DetailedPromise;
+class TestGMPVideoDecoder;
+
+/**
+ * MediaKeySystemAccessManager implements the functionality for
+ * Navigator.requestMediaKeySystemAccess(). The navigator may perform its own
+ * logic before passing the request to this class, but the majority of
+ * processing happens the MediaKeySystemAccessManager. The manager is expected
+ * to be run entirely on the main thread of the content process for whichever
+ * window it is associated with.
+ *
+ * As well as implementing the Navigator.requestMediaKeySystemAccess()
+ * algorithm, the manager performs Gecko specific logic. For example, the EME
+ * specification does not specify a process to check if a CDM is installed as
+ * part of requesting access, but that is an important part of obtaining access
+ * for Gecko, and is handled by the manager.
+ *
+ * A request made to the manager can be thought of as entering a pipeline.
+ * In this pipeline the request must pass through various stages that can
+ * reject the request and remove it from the pipeline. If a request is not
+ * rejected by the end of the pipeline it is approved/resolved.
+ *
+ * The pipeline is structured in such a way that each step should be executed
+ * even if it will quickly be exited. For example, the step that checks if a
+ * window supports protected media is an instant approve on non-Windows OSes,
+ * but we want to execute the function representing that step to ensure a
+ * deterministic execution and logging path. The hope is this reduces
+ * complexity for when we need to debug or change the code.
+ *
+ * While the pipeline metaphor generally holds, the implementation details of
+ * the manager mean that processing is not always linear: a request may be
+ * re-injected earlier into the pipeline for reprocessing. This can happen
+ * if the request was pending some other operation, e.g. CDM install, after
+ * which we wish to reprocess that request. However, we strive to keep it
+ * as linear as possible.
+ *
+ * A high level version of the happy path pipeline is depicted below. If a
+ * request were to fail any of the steps below it would be rejected and ejected
+ * from the pipeline.
+ *
+ * Request arrives from navigator
+ * +
+ * |
+ * v
+ * Check if window supports protected media
+ * +
+ * +<-------------------+
+ * v |
+ * Check request args are sane |
+ * + |
+ * | Wait for CDM and retry
+ * v |
+ * Check if CDM is installed |
+ * + |
+ * | |
+ * +--------------------+
+ * |
+ * v
+ * Check if CDM supports args
+ * +
+ * |
+ * v
+ * Check if app allows protected media
+ * (used by GeckoView)
+ * +
+ * |
+ * v
+ * Provide access
+ *
+ */
+
+struct MediaKeySystemAccessRequest {
+ MediaKeySystemAccessRequest(
+ const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs)
+ : mKeySystem(aKeySystem), mConfigs(aConfigs) {}
+ virtual ~MediaKeySystemAccessRequest() = default;
+ // The KeySystem passed for this request.
+ const nsString mKeySystem;
+ // The config(s) passed for this request.
+ const Sequence<MediaKeySystemConfiguration> mConfigs;
+};
+
+class MediaKeySystemAccessManager final : public nsIObserver, public nsINamed {
+ public:
+ explicit MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager,
+ nsIObserver)
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSINAMED
+
+ // Entry point for the navigator to call into the manager.
+ void Request(DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfig);
+
+ void Shutdown();
+
+ private:
+ // Encapsulates the information for a Navigator.requestMediaKeySystemAccess()
+ // request that is being processed.
+ struct PendingRequest : public MediaKeySystemAccessRequest {
+ enum class RequestType { Initial, Subsequent };
+
+ PendingRequest(DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs);
+ ~PendingRequest();
+
+ // The JS promise associated with this request.
+ RefPtr<DetailedPromise> mPromise;
+
+ // If the request is
+ // - A first attempt request from JS: RequestType::Initial.
+ // - A request we're reprocessing due to a GMP being installed:
+ // RequestType::Subsequent.
+ RequestType mRequestType = RequestType::Initial;
+
+ // If we find a supported config for this request during processing it
+ // should be stored here. Only if we have a supported config should a
+ // request have access provided.
+ Maybe<MediaKeySystemConfiguration> mSupportedConfig;
+
+ // Will be set to trigger a timeout and re-processing of the request if the
+ // request is pending on some potentially time consuming operation, e.g.
+ // CDM install.
+ nsCOMPtr<nsITimer> mTimer = nullptr;
+
+ // Convenience methods to reject the wrapped promise.
+ void RejectPromiseWithInvalidAccessError(const nsACString& aReason);
+ void RejectPromiseWithNotSupportedError(const nsACString& aReason);
+ void RejectPromiseWithTypeError(const nsACString& aReason);
+
+ void CancelTimer();
+ };
+
+ // Check if the application (e.g. a GeckoView app) allows protected media in
+ // this window.
+ //
+ // This function is always expected to be executed as part of the pipeline of
+ // processing a request, but its behavior differs depending on prefs set.
+ //
+ // If the `media_eme_require_app_approval` pref is false, then the function
+ // assumes app approval and early returns. Otherwise the function will
+ // create a permission request to be approved by the embedding app. If the
+ // test prefs detailed in MediaKeySystemAccessPermissionRequest.h are set
+ // then they will control handling, otherwise it is up to the embedding
+ // app to handle the request.
+ //
+ // At the time of writing, only GeckoView based apps are expected to pref
+ // on this behavior.
+ //
+ // This function is expected to run late/last in the pipeline so that if we
+ // ask the app for permission we don't fail after the app okays the request.
+ // This is to avoid cases where a user may be prompted by the app to approve
+ // eme, this check then passes, but we fail later in the pipeline, leaving
+ // the user wondering why their approval didn't work.
+ void CheckDoesAppAllowProtectedMedia(UniquePtr<PendingRequest> aRequest);
+
+ // Handles the result of the app allowing or disallowing protected media.
+ // If there are pending requests in mPendingAppApprovalRequests then this
+ // needs to be called on each.
+ void OnDoesAppAllowProtectedMedia(bool aIsAllowed,
+ UniquePtr<PendingRequest> aRequest);
+
+ // Checks if the Window associated with this manager supports protected media
+ // and calls into OnDoesWindowSupportEncryptedMedia with the result.
+ void CheckDoesWindowSupportProtectedMedia(UniquePtr<PendingRequest> aRequest);
+
+ // Handle the result of checking if the window associated with this manager
+ // supports protected media. If the window doesn't support protected media
+ // this will reject the request, otherwise the request will continue to be
+ // processed.
+ void OnDoesWindowSupportProtectedMedia(bool aIsSupportedInWindow,
+ UniquePtr<PendingRequest> aRequest);
+
+ // Performs the 'requestMediaKeySystemAccess' algorithm detailed in the EME
+ // specification. Gecko may need to install a CDM to satisfy this check. If
+ // CDM install is needed this function may be called again for the same
+ // request once the CDM is installed or a timeout is reached.
+ void RequestMediaKeySystemAccess(UniquePtr<PendingRequest> aRequest);
+
+ // Approves aRequest and provides MediaKeySystemAccess by resolving the
+ // promise associated with the request.
+ void ProvideAccess(UniquePtr<PendingRequest> aRequest);
+
+ ~MediaKeySystemAccessManager();
+
+ bool EnsureObserversAdded();
+
+ bool AwaitInstall(UniquePtr<PendingRequest> aRequest);
+
+ void RetryRequest(UniquePtr<PendingRequest> aRequest);
+
+ // Requests waiting on approval from the application to be processed.
+ nsTArray<UniquePtr<PendingRequest>> mPendingAppApprovalRequests;
+
+ // Requests waiting on CDM installation to be processed.
+ nsTArray<UniquePtr<PendingRequest>> mPendingInstallRequests;
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ bool mAddedObservers = false;
+
+ // Has the app approved protected media playback? If it has we cache the
+ // value so we don't need to check again.
+ Maybe<bool> mAppAllowsProtectedMedia;
+
+ // If we're waiting for permission from the app to enable EME this holder
+ // should contain the request.
+ //
+ // Note the type in the holder should match
+ // MediaKeySystemAccessPermissionRequest::RequestPromise, but we can't
+ // include MediaKeySystemAccessPermissionRequest's header here without
+ // breaking the build, so we do this hack.
+ MozPromiseRequestHolder<MozPromise<bool, bool, true>>
+ mAppAllowsProtectedMediaPromiseRequest;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp
new file mode 100644
index 0000000000..063bf93f7e
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeySystemAccessPermissionRequest.h"
+
+#include "nsGlobalWindowInner.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySystemAccessPermissionRequest,
+ ContentPermissionRequestBase)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase)
+
+/* static */
+already_AddRefed<MediaKeySystemAccessPermissionRequest>
+MediaKeySystemAccessPermissionRequest::Create(nsPIDOMWindowInner* aWindow) {
+ // Could conceivably be created off main thread then used on main thread
+ // later. If we really need to do that at some point we could relax this
+ // assert.
+ AssertIsOnMainThread();
+ if (!aWindow) {
+ return nullptr;
+ }
+
+ nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow);
+ if (!innerWindow->GetPrincipal()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaKeySystemAccessPermissionRequest> request =
+ new MediaKeySystemAccessPermissionRequest(innerWindow);
+ return request.forget();
+}
+
+MediaKeySystemAccessPermissionRequest::MediaKeySystemAccessPermissionRequest(
+ nsGlobalWindowInner* aWindow)
+ : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow,
+ "media.eme.require-app-approval"_ns,
+ "media-key-system-access"_ns) {}
+
+MediaKeySystemAccessPermissionRequest::
+ ~MediaKeySystemAccessPermissionRequest() {
+ AssertIsOnMainThread();
+ // If a request has not been serviced by the time it is destroyed, treat it
+ // as if the request was denied.
+ Cancel();
+}
+
+already_AddRefed<MediaKeySystemAccessPermissionRequest::RequestPromise>
+MediaKeySystemAccessPermissionRequest::GetPromise() {
+ return mPromiseHolder.Ensure(__func__);
+}
+
+nsresult MediaKeySystemAccessPermissionRequest::Start() {
+ // Check test prefs to see if we should short circuit. We want to do this
+ // before checking the cached value so we can have pref changes take effect
+ // without refreshing the page.
+ MediaKeySystemAccessPermissionRequest::PromptResult promptResult =
+ CheckPromptPrefs();
+ if (promptResult ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Granted) {
+ return Allow(JS::UndefinedHandleValue);
+ }
+ if (promptResult ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Denied) {
+ return Cancel();
+ }
+
+ return nsContentPermissionUtils::AskPermission(this, mWindow);
+}
+
+NS_IMETHODIMP
+MediaKeySystemAccessPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
+ AssertIsOnMainThread();
+ mPromiseHolder.ResolveIfExists(true, __func__);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaKeySystemAccessPermissionRequest::Cancel() {
+ AssertIsOnMainThread();
+ mPromiseHolder.RejectIfExists(false, __func__);
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.h b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h
new file mode 100644
index 0000000000..088cf50ab6
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
+#define DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
+
+#include "mozilla/MozPromise.h"
+#include "nsContentPermissionHelper.h"
+
+class nsGlobalWindowInner;
+
+namespace mozilla::dom {
+
+/**
+ * This class encapsulates a permission request to allow media key system
+ * access. The intention is not for this class to be used in all cases of EME,
+ * but only when we need to seek explicit approval from an application using
+ * Gecko, such as an application embedding via GeckoView.
+ *
+ * media.eme.require-app-approval should be used to gate this functionality in
+ * gecko code, and is also used as the testing pref for
+ * ContentPermissionRequestBase. I.e. CheckPromptPrefs() will respond to having
+ * `media.eme.require-app-approval.prompt.testing` and
+ * `media.eme.require-app-approval.prompt.testing.allow` being set to true or
+ * false and will return an appropriate value to allow for test code to short
+ * circuit showing a prompt. Note that the code using this class needs to call
+ * CheckPromptPrefs and implement test specific logic, it is *not* handled by
+ * this class or ContentPermissionRequestBase.
+ *
+ * Expects to be used on main thread as ContentPermissionRequestBase uses
+ * PContentPermissionRequest which is managed by PContent which is main thread
+ * to main thread communication.
+ */
+class MediaKeySystemAccessPermissionRequest
+ : public ContentPermissionRequestBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase)
+
+ using RequestPromise = MozPromise<bool, bool, true /* IsExclusive*/>;
+
+ // Create a MediaKeySystemAccessPermissionRequest.
+ // @param aWindow The window associated with this request.
+ static already_AddRefed<MediaKeySystemAccessPermissionRequest> Create(
+ nsPIDOMWindowInner* aWindow);
+
+ // Returns a promise that will be resolved if this request is allowed or
+ // rejected in the case the request is denied. If allowed the promise
+ // will resolve with true, otherwise it will resolve with false.
+ already_AddRefed<RequestPromise> GetPromise();
+
+ // Helper function that triggers the request. This function will check
+ // prefs and cancel or allow the request if the appropriate prefs are set,
+ // otherwise it will fire the request to the associated window.
+ nsresult Start();
+
+ // nsIContentPermissionRequest methods
+ NS_IMETHOD Cancel(void) override;
+ NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override;
+
+ private:
+ explicit MediaKeySystemAccessPermissionRequest(nsGlobalWindowInner* aWindow);
+ ~MediaKeySystemAccessPermissionRequest();
+
+ MozPromiseHolder<RequestPromise> mPromiseHolder;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp
new file mode 100644
index 0000000000..c4340885a3
--- /dev/null
+++ b/dom/media/eme/MediaKeys.cpp
@@ -0,0 +1,847 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeys.h"
+
+#include "ChromiumCDMProxy.h"
+#include "GMPCrashHelper.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "nsContentCID.h"
+#include "nsContentTypeParser.h"
+#include "nsContentUtils.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/MediaDrmCDMProxy.h"
+#endif
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+#ifdef MOZ_WMF_CDM
+# include "mozilla/WMFCDMProxy.h"
+#endif
+
+namespace mozilla::dom {
+
+// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to
+// disconnect our MediaKeys instances from the inner window (mparent) before
+// we unlink it.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mKeySessions)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingSessions)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement)
+ tmp->DisconnectInnerWindow();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mKeySessions)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromises)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingSessions)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+MediaKeys::MediaKeys(nsPIDOMWindowInner* aParent, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig)
+ : mParent(aParent),
+ mKeySystem(aKeySystem),
+ mCreatePromiseId(0),
+ mConfig(aConfig) {
+ EME_LOG("MediaKeys[%p] constructed keySystem=%s", this,
+ NS_ConvertUTF16toUTF8(mKeySystem).get());
+}
+
+MediaKeys::~MediaKeys() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ DisconnectInnerWindow();
+ Shutdown();
+ EME_LOG("MediaKeys[%p] destroyed", this);
+}
+
+NS_IMETHODIMP MediaKeys::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aTopic, kMediaKeysResponseTopic),
+ "Should only listen for responses to MediaKey requests");
+ EME_LOG("MediaKeys[%p] observing message with aTopic=%s aData=%s", this,
+ aTopic, NS_ConvertUTF16toUTF8(aData).get());
+ if (!strcmp(aTopic, kMediaKeysResponseTopic)) {
+ if (!mProxy) {
+ // This may happen if we're notified during shutdown or startup. If this
+ // is happening outside of those scenarios there's a bug.
+ EME_LOG(
+ "MediaKeys[%p] can't notify CDM of observed message as mProxy is "
+ "unset",
+ this);
+ return NS_OK;
+ }
+
+ if (u"capture-possible"_ns.Equals(aData)) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckSuccessful,
+ CDMProxy::OutputProtectionCaptureStatus::CapturePossilbe);
+ } else if (u"capture-not-possible"_ns.Equals(aData)) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckSuccessful,
+ CDMProxy::OutputProtectionCaptureStatus::CaptureNotPossible);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("No code paths should lead to the failure case");
+ // This should be unreachable, but gracefully handle in case.
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ }
+ return NS_OK;
+}
+
+void MediaKeys::ConnectInnerWindow() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsPIDOMWindowInner> innerWindowParent = GetParentObject();
+ MOZ_ASSERT(innerWindowParent,
+ "We should only be connecting when we have an inner window!");
+ innerWindowParent->AddMediaKeysInstance(this);
+}
+
+void MediaKeys::DisconnectInnerWindow() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!GetParentObject()) {
+ // We don't have a parent. We've been cycle collected, or the window
+ // already notified us of its destruction and we cleared the ref.
+ return;
+ }
+
+ GetParentObject()->RemoveMediaKeysInstance(this);
+}
+
+void MediaKeys::OnInnerWindowDestroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ EME_LOG("MediaKeys[%p] OnInnerWindowDestroy()", this);
+
+ // The InnerWindow should clear its reference to this object after this call,
+ // so we don't need to explicitly call DisconnectInnerWindow before nulling.
+ mParent = nullptr;
+
+ // Don't call shutdown directly because (at time of writing) mProxy can
+ // spin the event loop when it's shutdown. This can change the world state
+ // in the middle of window destruction, which we do not want.
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod("MediaKeys::Shutdown", this, &MediaKeys::Shutdown));
+}
+
+void MediaKeys::Terminated() {
+ EME_LOG("MediaKeys[%p] CDM crashed unexpectedly", this);
+
+ KeySessionHashMap keySessions;
+ // Remove entries during iteration will screw it. Make a copy first.
+ for (const RefPtr<MediaKeySession>& session : mKeySessions.Values()) {
+ // XXX Could the RefPtr still be moved here?
+ keySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session});
+ }
+ for (const RefPtr<MediaKeySession>& session : keySessions.Values()) {
+ session->OnClosed();
+ }
+ keySessions.Clear();
+ MOZ_ASSERT(mKeySessions.Count() == 0);
+
+ // Notify the element about that CDM has terminated.
+ if (mElement) {
+ mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR);
+ }
+
+ Shutdown();
+}
+
+void MediaKeys::Shutdown() {
+ EME_LOG("MediaKeys[%p]::Shutdown()", this);
+ if (mProxy) {
+ mProxy->Shutdown();
+ mProxy = nullptr;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService && mObserverAdded) {
+ observerService->RemoveObserver(this, kMediaKeysResponseTopic);
+ }
+
+ // Hold a self reference to keep us alive after we clear the self reference
+ // for each promise. This ensures we stay alive until we're done shutting
+ // down.
+ RefPtr<MediaKeys> selfReference = this;
+
+ for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Promise still outstanding at MediaKeys shutdown");
+ Release();
+ }
+ mPromises.Clear();
+}
+
+nsPIDOMWindowInner* MediaKeys::GetParentObject() const { return mParent; }
+
+JSObject* MediaKeys::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeys_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaKeys::GetKeySystem(nsString& aOutKeySystem) const {
+ aOutKeySystem.Assign(mKeySystem);
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::SetServerCertificate(
+ const ArrayBufferViewOrArrayBuffer& aCert, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeys.setServerCertificate"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys without a CDM");
+ promise->MaybeRejectWithInvalidStateError(
+ "Null CDM in MediaKeys.setServerCertificate()");
+ return promise.forget();
+ }
+
+ nsTArray<uint8_t> data;
+ CopyArrayBufferViewOrArrayBufferData(aCert, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty certificate passed to MediaKeys.setServerCertificate()");
+ return promise.forget();
+ }
+
+ mProxy->SetServerCertificate(StorePromise(promise), data);
+ return promise.forget();
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::MakePromise(
+ ErrorResult& aRv, const nsACString& aName) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
+ if (!global) {
+ NS_WARNING("Passed non-global to MediaKeys ctor!");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return DetailedPromise::Create(global, aRv, aName);
+}
+
+PromiseId MediaKeys::StorePromise(DetailedPromise* aPromise) {
+ static uint32_t sEMEPromiseCount = 1;
+ MOZ_ASSERT(aPromise);
+ uint32_t id = sEMEPromiseCount++;
+
+ EME_LOG("MediaKeys[%p]::StorePromise() id=%" PRIu32, this, id);
+
+ // Keep MediaKeys alive for the lifetime of its promises. Any still-pending
+ // promises are rejected in Shutdown().
+ EME_LOG("MediaKeys[%p]::StorePromise() calling AddRef()", this);
+ AddRef();
+
+#ifdef DEBUG
+ // We should not have already stored this promise!
+ for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) {
+ MOZ_ASSERT(promise != aPromise);
+ }
+#endif
+
+ mPromises.InsertOrUpdate(id, RefPtr{aPromise});
+ return id;
+}
+
+void MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId,
+ uint32_t aToken) {
+ // Should only be called from MediaKeySession::GenerateRequest.
+ mPromiseIdToken.InsertOrUpdate(aId, aToken);
+ EME_LOG(
+ "MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)",
+ this, aId, aToken);
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::RetrievePromise(PromiseId aId) {
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ")", this, aId);
+ if (!mPromises.Contains(aId)) {
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32
+ ") tried to retrieve non-existent promise!",
+ this, aId);
+ NS_WARNING(nsPrintfCString(
+ "Tried to retrieve a non-existent promise id=%" PRIu32, aId)
+ .get());
+ return nullptr;
+ }
+ RefPtr<DetailedPromise> promise;
+ mPromises.Remove(aId, getter_AddRefs(promise));
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ") calling Release()",
+ this, aId);
+ Release();
+ return promise.forget();
+}
+
+void MediaKeys::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ uint32_t errorCodeAsInt = aException.ErrorCodeAsInt();
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 ")", this, aId,
+ errorCodeAsInt);
+
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32
+ ") couldn't retrieve promise! Bailing!",
+ this, aId, errorCodeAsInt);
+ return;
+ }
+
+ // This promise could be a createSession or loadSession promise,
+ // so we might have a pending session waiting to be resolved into
+ // the promise on success. We've been directed to reject to promise,
+ // so we can throw away the corresponding session object.
+ uint32_t token = 0;
+ if (mPromiseIdToken.Get(aId, &token)) {
+ MOZ_ASSERT(mPendingSessions.Contains(token));
+ mPendingSessions.Remove(token);
+ mPromiseIdToken.Remove(aId);
+ }
+
+ MOZ_ASSERT(aException.Failed());
+ promise->MaybeReject(std::move(aException), aReason);
+
+ if (mCreatePromiseId == aId) {
+ // Note: This will probably destroy the MediaKeys object!
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32
+ ") calling Release()",
+ this, aId, errorCodeAsInt);
+ Release();
+ }
+}
+
+void MediaKeys::OnSessionIdReady(MediaKeySession* aSession) {
+ if (!aSession) {
+ NS_WARNING("Invalid MediaKeySession passed to OnSessionIdReady()");
+ return;
+ }
+ if (mKeySessions.Contains(aSession->GetSessionId())) {
+ NS_WARNING("MediaKeySession's made ready multiple times!");
+ return;
+ }
+ if (mPendingSessions.Contains(aSession->Token())) {
+ NS_WARNING(
+ "MediaKeySession made ready when it wasn't waiting to be ready!");
+ return;
+ }
+ if (aSession->GetSessionId().IsEmpty()) {
+ NS_WARNING(
+ "MediaKeySession with invalid sessionId passed to OnSessionIdReady()");
+ return;
+ }
+ mKeySessions.InsertOrUpdate(aSession->GetSessionId(), RefPtr{aSession});
+}
+
+void MediaKeys::ResolvePromise(PromiseId aId) {
+ EME_LOG("MediaKeys[%p]::ResolvePromise(%" PRIu32 ")", this, aId);
+
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ MOZ_ASSERT(!mPromises.Contains(aId));
+ if (!promise) {
+ return;
+ }
+
+ uint32_t token = 0;
+ if (!mPromiseIdToken.Get(aId, &token)) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ } else if (!mPendingSessions.Contains(token)) {
+ // Pending session for CreateSession() should be removed when sessionId
+ // is ready.
+ promise->MaybeResolveWithUndefined();
+ mPromiseIdToken.Remove(aId);
+ return;
+ }
+ mPromiseIdToken.Remove(aId);
+
+ // We should only resolve LoadSession calls via this path,
+ // not CreateSession() promises.
+ RefPtr<MediaKeySession> session;
+ mPendingSessions.Remove(token, getter_AddRefs(session));
+ if (!session || session->GetSessionId().IsEmpty()) {
+ NS_WARNING("Received activation for non-existent session!");
+ promise->MaybeRejectWithInvalidAccessError(
+ "CDM LoadSession() returned a different session ID than requested");
+ return;
+ }
+ mKeySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session});
+ promise->MaybeResolve(session);
+}
+
+class MediaKeysGMPCrashHelper : public GMPCrashHelper {
+ public:
+ explicit MediaKeysGMPCrashHelper(MediaKeys* aMediaKeys)
+ : mMediaKeys(aMediaKeys) {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ }
+ already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ EME_LOG("MediaKeysGMPCrashHelper::GetPluginCrashedEventTarget()");
+ return (mMediaKeys && mMediaKeys->GetParentObject())
+ ? do_AddRef(mMediaKeys->GetParentObject())
+ : nullptr;
+ }
+
+ private:
+ WeakPtr<MediaKeys> mMediaKeys;
+};
+
+already_AddRefed<CDMProxy> MediaKeys::CreateCDMProxy() {
+ const bool isHardwareDecryptionSupported =
+ IsHardwareDecryptionSupported(mConfig);
+ EME_LOG("MediaKeys[%p]::CreateCDMProxy(), isHardwareDecryptionSupported=%d",
+ this, isHardwareDecryptionSupported);
+ RefPtr<CDMProxy> proxy;
+#ifdef MOZ_WIDGET_ANDROID
+ if (IsWidevineKeySystem(mKeySystem)) {
+ proxy = new MediaDrmCDMProxy(
+ this, mKeySystem,
+ mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required,
+ mConfig.mPersistentState == MediaKeysRequirement::Required);
+ } else
+#endif
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(mKeySystem) ||
+ IsWidevineExperimentKeySystemAndSupported(mKeySystem) ||
+ (IsWidevineKeySystem(mKeySystem) && isHardwareDecryptionSupported) ||
+ IsWMFClearKeySystemAndSupported(mKeySystem)) {
+ proxy = new WMFCDMProxy(this, mKeySystem, mConfig);
+ } else
+#endif
+ {
+ proxy = new ChromiumCDMProxy(
+ this, mKeySystem, new MediaKeysGMPCrashHelper(this),
+ mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required,
+ mConfig.mPersistentState == MediaKeysRequirement::Required);
+ }
+ return proxy.forget();
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::Init(ErrorResult& aRv) {
+ EME_LOG("MediaKeys[%p]::Init()", this);
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeys::Init()"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Determine principal (at creation time) of the MediaKeys object.
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject());
+ if (!sop) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get script principal in MediaKeys::Init");
+ return promise.forget();
+ }
+ mPrincipal = sop->GetPrincipal();
+
+ // Begin figuring out the top level principal.
+ nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject();
+
+ // If we're in a top level document, getting the top level principal is easy.
+ // However, we're not in a top level doc this becomes more complicated. If
+ // we're not top level we need to get the top level principal, this can be
+ // done by reading the principal of the load info, which we can get of a
+ // document's channel.
+ //
+ // There is an edge case we need to watch out for here where this code can be
+ // run in an about:blank document before it has done its async load. In this
+ // case the document will not yet have a load info. We address this below by
+ // walking up a level in the window context chain. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1675360
+ // for more info.
+ Document* document = window->GetExtantDoc();
+ if (!document) {
+ NS_WARNING("Failed to get document when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get document in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ WindowGlobalChild* windowGlobalChild = window->GetWindowGlobalChild();
+ if (!windowGlobalChild) {
+ NS_WARNING("Failed to get window global child when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get window global child in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ if (windowGlobalChild->SameOriginWithTop()) {
+ // We're in the same origin as the top window context, so our principal
+ // is also the top principal.
+ mTopLevelPrincipal = mPrincipal;
+ } else {
+ // We have a different origin than the top doc, try and find the top level
+ // principal by looking it up via load info, which we read off a channel.
+ nsIChannel* channel = document->GetChannel();
+
+ WindowContext* windowContext = document->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Failed to get window context when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get window context in MediaKeys::Init");
+ return promise.forget();
+ }
+ while (!channel) {
+ // We don't have a channel, this can happen if we're in an about:blank
+ // page that hasn't yet had its async load performed. Try and get
+ // the channel from our parent doc. We should be able to do this because
+ // an about:blank is considered the same origin as its parent. We do this
+ // recursively to cover pages do silly things like nesting blank iframes
+ // and not waiting for loads.
+
+ // Move our window context up a level.
+ windowContext = windowContext->GetParentWindowContext();
+ if (!windowContext || !windowContext->GetExtantDoc()) {
+ NS_WARNING(
+ "Failed to get parent window context's document when creating "
+ "MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get parent window context's document in "
+ "MediaKeys::Init (likely due to an nested about about:blank frame "
+ "that hasn't loaded yet)");
+ return promise.forget();
+ }
+
+ Document* parentDoc = windowContext->GetExtantDoc();
+ channel = parentDoc->GetChannel();
+ }
+
+ MOZ_RELEASE_ASSERT(
+ channel, "Should either have a channel or should have returned by now");
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ MOZ_RELEASE_ASSERT(loadInfo, "Channels should always have LoadInfo");
+ mTopLevelPrincipal = loadInfo->GetTopLevelPrincipal();
+ if (!mTopLevelPrincipal) {
+ NS_WARNING("Failed to get top level principal when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get top level principal in MediaKeys::Init");
+ return promise.forget();
+ }
+ }
+
+ // We should have figured out our top level principal.
+ if (!mPrincipal || !mTopLevelPrincipal) {
+ NS_WARNING("Failed to get principals when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get principal(s) in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ nsAutoCString origin;
+ nsresult rv = mPrincipal->GetOrigin(origin);
+ if (NS_FAILED(rv)) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get principal origin string in MediaKeys::Init");
+ return promise.forget();
+ }
+ nsAutoCString topLevelOrigin;
+ rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin);
+ if (NS_FAILED(rv)) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get top-level principal origin string in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ EME_LOG("MediaKeys[%p]::Create() (%s, %s)", this, origin.get(),
+ topLevelOrigin.get());
+
+ mProxy = CreateCDMProxy();
+
+ // The CDMProxy's initialization is asynchronous. The MediaKeys is
+ // refcounted, and its instance is returned to JS by promise once
+ // it's been initialized. No external refs exist to the MediaKeys while
+ // we're waiting for the promise to be resolved, so we must hold a
+ // reference to the new MediaKeys object until it's been created,
+ // or its creation has failed. Store the id of the promise returned
+ // here, and hold a self-reference until that promise is resolved or
+ // rejected.
+ MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!");
+ mCreatePromiseId = StorePromise(promise);
+ EME_LOG("MediaKeys[%p]::Init() calling AddRef()", this);
+ AddRef();
+ mProxy->Init(mCreatePromiseId, NS_ConvertUTF8toUTF16(origin),
+ NS_ConvertUTF8toUTF16(topLevelOrigin),
+ KeySystemToProxyName(mKeySystem));
+
+ ConnectInnerWindow();
+
+ return promise.forget();
+}
+
+void MediaKeys::OnCDMCreated(PromiseId aId, const uint32_t aPluginId) {
+ EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 ")",
+ this, aId, aPluginId);
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ RefPtr<MediaKeys> keys(this);
+
+ promise->MaybeResolve(keys);
+ if (mCreatePromiseId == aId) {
+ EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32
+ ") calling Release()",
+ this, aId, aPluginId);
+ Release();
+ }
+
+ MediaKeySystemAccess::NotifyObservers(mParent, mKeySystem,
+ MediaKeySystemStatus::Cdm_created);
+}
+
+static bool IsSessionTypeSupported(const MediaKeySessionType aSessionType,
+ const MediaKeySystemConfiguration& aConfig) {
+ if (aSessionType == MediaKeySessionType::Temporary) {
+ // Temporary is always supported.
+ return true;
+ }
+ if (!aConfig.mSessionTypes.WasPassed()) {
+ // No other session types supported.
+ return false;
+ }
+ return aConfig.mSessionTypes.Value().Contains(ToString(aSessionType));
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::CreateSession(
+ MediaKeySessionType aSessionType, ErrorResult& aRv) {
+ EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 ")", this,
+ static_cast<uint8_t>(aSessionType));
+ if (!IsSessionTypeSupported(aSessionType, mConfig)) {
+ EME_LOG("MediaKeys[%p]::CreateSession() failed, unsupported session type",
+ this);
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys which lost its CDM");
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ EME_LOG("MediaKeys[%p] Creating session", this);
+
+ RefPtr<MediaKeySession> session = new MediaKeySession(
+ GetParentObject(), this, mKeySystem, aSessionType, aRv);
+
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ DDLINKCHILD("session", session.get());
+
+ // Add session to the set of sessions awaiting their sessionId being ready.
+ EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8
+ ") putting session with token=%" PRIu32 " into mPendingSessions",
+ this, static_cast<uint8_t>(aSessionType), session->Token());
+ mPendingSessions.InsertOrUpdate(session->Token(), RefPtr{session});
+
+ return session.forget();
+}
+
+void MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) {
+ EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%" PRIu32, this,
+ aId);
+
+ ResolvePromiseWithResult(aId, aSuccess);
+}
+
+void MediaKeys::OnSessionClosed(MediaKeySession* aSession) {
+ nsAutoString id;
+ aSession->GetSessionId(id);
+ mKeySessions.Remove(id);
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::GetSession(
+ const nsAString& aSessionId) {
+ RefPtr<MediaKeySession> session;
+ mKeySessions.Get(aSessionId, getter_AddRefs(session));
+ return session.forget();
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::GetPendingSession(
+ uint32_t aToken) {
+ EME_LOG("MediaKeys[%p]::GetPendingSession(aToken=%" PRIu32 ")", this, aToken);
+ RefPtr<MediaKeySession> session;
+ mPendingSessions.Get(aToken, getter_AddRefs(session));
+ mPendingSessions.Remove(aToken);
+ return session.forget();
+}
+
+bool MediaKeys::IsBoundToMediaElement() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mElement != nullptr;
+}
+
+nsresult MediaKeys::Bind(HTMLMediaElement* aElement) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (IsBoundToMediaElement()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mElement = aElement;
+
+ return NS_OK;
+}
+
+void MediaKeys::Unbind() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mElement = nullptr;
+}
+
+void MediaKeys::CheckIsElementCapturePossible() {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("MediaKeys[%p]::IsElementCapturePossible()", this);
+ // Note, HTMLMediaElement prevents capture of its content via Capture APIs
+ // on the element if it has a media keys attached (see bug 1071482). So we
+ // don't need to check those cases here (they are covered by tests).
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (!observerService) {
+ // This can happen if we're in shutdown which means we may be going away
+ // soon anyway, but respond saying capture is possible since we can't
+ // forward the check further.
+ if (mProxy) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ return;
+ }
+ if (!mObserverAdded) {
+ nsresult rv =
+ observerService->AddObserver(this, kMediaKeysResponseTopic, false);
+ if (NS_FAILED(rv)) {
+ if (mProxy) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ return;
+ }
+ mObserverAdded = true;
+ }
+
+ if (mCaptureCheckRequestJson.IsEmpty()) {
+ // Lazily populate the JSON the first time we need it.
+ JSONStringWriteFunc<nsAutoCString> json;
+ JSONWriter jw{json};
+ jw.Start();
+ jw.StringProperty("status", "is-capture-possible");
+ jw.StringProperty("keySystem", NS_ConvertUTF16toUTF8(mKeySystem));
+ jw.End();
+ mCaptureCheckRequestJson = NS_ConvertUTF8toUTF16(json.StringCRef());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!mCaptureCheckRequestJson.IsEmpty());
+ observerService->NotifyObservers(mParent.get(), kMediaKeysRequestTopic,
+ mCaptureCheckRequestJson.get());
+}
+
+void MediaKeys::GetSessionsInfo(nsString& sessionsInfo) {
+ for (const auto& keySession : mKeySessions.Values()) {
+ nsString sessionID;
+ keySession->GetSessionId(sessionID);
+ sessionsInfo.AppendLiteral("(sid=");
+ sessionsInfo.Append(sessionID);
+ MediaKeyStatusMap* keyStatusMap = keySession->KeyStatuses();
+ for (uint32_t i = 0; i < keyStatusMap->GetIterableLength(); i++) {
+ nsString keyID = keyStatusMap->GetKeyIDAsHexString(i);
+ sessionsInfo.AppendLiteral("(kid=");
+ sessionsInfo.Append(keyID);
+ sessionsInfo.AppendLiteral(" status=");
+ sessionsInfo.AppendASCII(
+ MediaKeyStatusValues::GetString(keyStatusMap->GetValueAtIndex(i)));
+ sessionsInfo.AppendLiteral(")");
+ }
+ sessionsInfo.AppendLiteral(")");
+ }
+}
+
+// https://w3c.github.io/encrypted-media/#dom-mediakeys-getstatusforpolicy
+already_AddRefed<Promise> MediaKeys::GetStatusForPolicy(
+ const MediaKeysPolicy& aPolicy, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeys::GetStatusForPolicy()"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // 1. If policy has no present members, return a promise rejected with a newly
+ // created TypeError.
+ if (!aPolicy.mMinHdcpVersion.WasPassed()) {
+ promise->MaybeRejectWithTypeError("No minHdcpVersion in MediaKeysPolicy");
+ return promise.forget();
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys without a CDM");
+ promise->MaybeRejectWithInvalidStateError(
+ "Null CDM in MediaKeys.GetStatusForPolicy()");
+ return promise.forget();
+ }
+
+ EME_LOG("GetStatusForPolicy minHdcpVersion = %s.",
+ HDCPVersionValues::GetString(aPolicy.mMinHdcpVersion.Value()).data());
+ mProxy->GetStatusForPolicy(StorePromise(promise),
+ aPolicy.mMinHdcpVersion.Value());
+ return promise.forget();
+}
+
+void MediaKeys::ResolvePromiseWithKeyStatus(PromiseId aId,
+ MediaKeyStatus aMediaKeyStatus) {
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ RefPtr<MediaKeys> keys(this);
+ EME_LOG(
+ "MediaKeys[%p]::ResolvePromiseWithKeyStatus() resolve promise id=%" PRIu32
+ ", keystatus=%" PRIu8,
+ this, aId, static_cast<uint8_t>(aMediaKeyStatus));
+ promise->MaybeResolve(aMediaKeyStatus);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeys.h b/dom/media/eme/MediaKeys.h
new file mode 100644
index 0000000000..5a44b3c227
--- /dev/null
+++ b/dom/media/eme/MediaKeys.h
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_mediakeys_h__
+#define mozilla_dom_mediakeys_h__
+
+#include "DecoderDoctorLogger.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTHashMap.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+
+class CDMProxy;
+
+namespace dom {
+class MediaKeys;
+} // namespace dom
+DDLoggedTypeName(dom::MediaKeys);
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+class MediaKeySession;
+struct MediaKeysPolicy;
+class HTMLMediaElement;
+
+typedef nsRefPtrHashtable<nsStringHashKey, MediaKeySession> KeySessionHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, dom::DetailedPromise> PromiseHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, MediaKeySession>
+ PendingKeySessionsHashMap;
+typedef nsTHashMap<nsUint32HashKey, uint32_t> PendingPromiseIdTokenHashMap;
+typedef uint32_t PromiseId;
+
+// This class is used on the main thread only.
+// Note: its addref/release is not (and can't be) thread safe!
+class MediaKeys final : public nsIObserver,
+ public nsWrapperCache,
+ public SupportsWeakPtr,
+ public DecoderDoctorLifeLogger<MediaKeys> {
+ ~MediaKeys();
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys)
+
+ NS_DECL_NSIOBSERVER
+
+ MediaKeys(nsPIDOMWindowInner* aParentWindow, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig);
+
+ already_AddRefed<DetailedPromise> Init(ErrorResult& aRv);
+
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsresult Bind(HTMLMediaElement* aElement);
+ void Unbind();
+
+ // Checks if there's any activity happening that could capture the media
+ // the keys are associated with and then expose that media outside of the
+ // origin it is in.
+ //
+ // This method does not return the results of the check, but the MediaKeys
+ // will notify mProxy of the results using `NotifyOutputProtectionStatus`.
+ void CheckIsElementCapturePossible();
+
+ // Javascript: readonly attribute DOMString keySystem;
+ void GetKeySystem(nsString& retval) const;
+
+ // JavaScript: MediaKeys.createSession()
+ already_AddRefed<MediaKeySession> CreateSession(
+ MediaKeySessionType aSessionType, ErrorResult& aRv);
+
+ // JavaScript: MediaKeys.SetServerCertificate()
+ already_AddRefed<DetailedPromise> SetServerCertificate(
+ const ArrayBufferViewOrArrayBuffer& aServerCertificate, ErrorResult& aRv);
+
+ already_AddRefed<MediaKeySession> GetSession(const nsAString& aSessionId);
+
+ // Removes and returns MediaKeySession from the set of sessions awaiting
+ // their sessionId to be assigned.
+ already_AddRefed<MediaKeySession> GetPendingSession(uint32_t aToken);
+
+ // Called once a Init() operation succeeds.
+ void OnCDMCreated(PromiseId aId, const uint32_t aPluginId);
+
+ // Called once the CDM generates a sessionId while servicing a
+ // MediaKeySession.generateRequest() or MediaKeySession.load() call,
+ // once the sessionId of a MediaKeySession is known.
+ void OnSessionIdReady(MediaKeySession* aSession);
+
+ // Called once a LoadSession succeeds.
+ void OnSessionLoaded(PromiseId aId, bool aSuccess);
+
+ // Called once a session has closed.
+ void OnSessionClosed(MediaKeySession* aSession);
+
+ CDMProxy* GetCDMProxy() { return mProxy; }
+
+ // Makes a new promise, or nullptr on failure.
+ already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv,
+ const nsACString& aName);
+ // Stores promise in mPromises, returning an ID that can be used to retrieve
+ // it later. The ID is passed to the CDM, so that it can signal specific
+ // promises to be resolved.
+ PromiseId StorePromise(DetailedPromise* aPromise);
+
+ // Stores a map from promise id to pending session token. Using this
+ // mapping, when a promise is rejected via its ID, we can check if the
+ // promise corresponds to a pending session and retrieve that session
+ // via the mapped-to token, and remove the pending session from the
+ // list of sessions awaiting a session id.
+ void ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken);
+
+ // Reject promise with the given exception.
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason);
+ // Resolves promise with "undefined".
+ void ResolvePromise(PromiseId aId);
+
+ void Shutdown();
+
+ // Called by CDMProxy when CDM crashes or shuts down. It is different from
+ // Shutdown which is called from the script/dom side.
+ void Terminated();
+
+ // Returns true if this MediaKeys has been bound to a media element.
+ bool IsBoundToMediaElement() const;
+
+ // Indicates to a MediaKeys instance that the inner window parent of that
+ // instance is being destroyed, this should prompt the keys to shutdown.
+ void OnInnerWindowDestroy();
+
+ void GetSessionsInfo(nsString& sessionsInfo);
+
+ // JavaScript: MediaKeys.GetStatusForPolicy()
+ already_AddRefed<Promise> GetStatusForPolicy(const MediaKeysPolicy& aPolicy,
+ ErrorResult& aR);
+ // Called by CDMProxy when CDM successfully GetStatusForPolicy.
+ void ResolvePromiseWithKeyStatus(PromiseId aId,
+ dom::MediaKeyStatus aMediaKeyStatus);
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult) {
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ promise->MaybeResolve(aResult);
+ }
+
+ // The topic used for requests related to mediakeys -- observe this to be
+ // notified of such requests.
+ constexpr static const char* kMediaKeysRequestTopic = "mediakeys-request";
+
+ private:
+ // Instantiate CDMProxy instance.
+ // It could be MediaDrmCDMProxy (Widevine on Fennec) or ChromiumCDMProxy (the
+ // rest).
+ already_AddRefed<CDMProxy> CreateCDMProxy();
+
+ // Removes promise from mPromises, and returns it.
+ already_AddRefed<DetailedPromise> RetrievePromise(PromiseId aId);
+
+ // Helpers to connect and disconnect to the parent inner window. An inner
+ // window should track (via weak ptr) MediaKeys created within it so we can
+ // ensure MediaKeys are shutdown if that window is destroyed.
+ void ConnectInnerWindow();
+ void DisconnectInnerWindow();
+
+ // Owning ref to proxy. The proxy has a weak reference back to the MediaKeys,
+ // and the MediaKeys destructor clears the proxy's reference to the MediaKeys.
+ RefPtr<CDMProxy> mProxy;
+
+ // The HTMLMediaElement the MediaKeys are associated with. Note that a
+ // MediaKeys instance may not be associated with any HTMLMediaElement so
+ // this can be null (we also cannot rely on a media element to drive shutdown
+ // for this reason).
+ RefPtr<HTMLMediaElement> mElement;
+
+ // The inner window associated with an instance of MediaKeys. We will
+ // shutdown the media keys when this Window is destroyed. We do this from the
+ // window rather than a document to address the case where media keys can be
+ // created in an about:blank document that then performs an async load -- this
+ // recreates the document, but the inner window is preserved in such a case.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1675360 for more info.
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ const nsString mKeySystem;
+ KeySessionHashMap mKeySessions;
+ PromiseHashMap mPromises;
+ PendingKeySessionsHashMap mPendingSessions;
+ PromiseId mCreatePromiseId;
+
+ // The principal of the relevant settings object.
+ RefPtr<nsIPrincipal> mPrincipal;
+ // The principal of the top level page. This can differ from mPrincipal if
+ // we're in an iframe.
+ RefPtr<nsIPrincipal> mTopLevelPrincipal;
+
+ const MediaKeySystemConfiguration mConfig;
+
+ PendingPromiseIdTokenHashMap mPromiseIdToken;
+
+ // The topic a MediaKeys instance will observe to receive updates from
+ // EncryptedMediaChild.
+ constexpr static const char* kMediaKeysResponseTopic = "mediakeys-response";
+ // Tracks if we've added an observer for responses from the associated
+ // EncryptedMediaChild. When true an observer is already in place, otherwise
+ // the observer has not yet been added.
+ bool mObserverAdded = false;
+ // Stores the json request we will send to EncryptedMediaChild when querying
+ // output protection. Lazily populated upon first use.
+ nsString mCaptureCheckRequestJson;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_mediakeys_h__
diff --git a/dom/media/eme/clearkey/ArrayUtils.h b/dom/media/eme/clearkey/ArrayUtils.h
new file mode 100644
index 0000000000..ae5f33b68e
--- /dev/null
+++ b/dom/media/eme/clearkey/ArrayUtils.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ArrayUtils_h__
+#define __ArrayUtils_h__
+
+#define MOZ_ARRAY_LENGTH(array_) (sizeof(array_) / sizeof(array_[0]))
+
+#endif
diff --git a/dom/media/eme/clearkey/BigEndian.h b/dom/media/eme/clearkey/BigEndian.h
new file mode 100644
index 0000000000..5ea1b6042f
--- /dev/null
+++ b/dom/media/eme/clearkey/BigEndian.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __BigEndian_h__
+#define __BigEndian_h__
+
+#include <stdint.h>
+
+namespace mozilla {
+
+class BigEndian {
+ public:
+ static uint32_t readUint32(const void* aPtr) {
+ const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr);
+ return uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 |
+ uint32_t(p[3]);
+ }
+
+ static uint16_t readUint16(const void* aPtr) {
+ const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr);
+ return uint32_t(p[0]) << 8 | uint32_t(p[1]);
+ }
+
+ static uint64_t readUint64(const void* aPtr) {
+ const uint8_t* p = reinterpret_cast<const uint8_t*>(aPtr);
+ return uint64_t(p[0]) << 56 | uint64_t(p[1]) << 48 | uint64_t(p[2]) << 40 |
+ uint64_t(p[3]) << 32 | uint64_t(p[4]) << 24 | uint64_t(p[5]) << 16 |
+ uint64_t(p[6]) << 8 | uint64_t(p[7]);
+ }
+
+ static void writeUint64(void* aPtr, uint64_t aValue) {
+ uint8_t* p = reinterpret_cast<uint8_t*>(aPtr);
+ p[0] = uint8_t(aValue >> 56) & 0xff;
+ p[1] = uint8_t(aValue >> 48) & 0xff;
+ p[2] = uint8_t(aValue >> 40) & 0xff;
+ p[3] = uint8_t(aValue >> 32) & 0xff;
+ p[4] = uint8_t(aValue >> 24) & 0xff;
+ p[5] = uint8_t(aValue >> 16) & 0xff;
+ p[6] = uint8_t(aValue >> 8) & 0xff;
+ p[7] = uint8_t(aValue) & 0xff;
+ }
+};
+
+} // namespace mozilla
+
+#endif // __BigEndian_h__
diff --git a/dom/media/eme/clearkey/ClearKeyBase64.cpp b/dom/media/eme/clearkey/ClearKeyBase64.cpp
new file mode 100644
index 0000000000..bb610afd1b
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyBase64.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeyBase64.h"
+
+#include <algorithm>
+
+using std::string;
+using std::vector;
+
+/**
+ * Take a base64-encoded string, convert (in-place) each character to its
+ * corresponding value in the [0x00, 0x3f] range, and truncate any padding.
+ */
+static bool Decode6Bit(string& aStr) {
+ for (size_t i = 0; i < aStr.length(); i++) {
+ if (aStr[i] >= 'A' && aStr[i] <= 'Z') {
+ aStr[i] -= 'A';
+ } else if (aStr[i] >= 'a' && aStr[i] <= 'z') {
+ aStr[i] -= 'a' - 26;
+ } else if (aStr[i] >= '0' && aStr[i] <= '9') {
+ aStr[i] -= '0' - 52;
+ } else if (aStr[i] == '-' || aStr[i] == '+') {
+ aStr[i] = 62;
+ } else if (aStr[i] == '_' || aStr[i] == '/') {
+ aStr[i] = 63;
+ } else {
+ // Truncate '=' padding at the end of the aString.
+ if (aStr[i] != '=') {
+ aStr.erase(i, string::npos);
+ return false;
+ }
+ aStr[i] = '\0';
+ aStr.resize(i);
+ break;
+ }
+ }
+
+ return true;
+}
+
+bool DecodeBase64(const string& aEncoded, vector<uint8_t>& aOutDecoded) {
+ if (aEncoded.empty()) {
+ aOutDecoded.clear();
+ return true;
+ }
+ if (aEncoded.size() == 1) {
+ // Invalid Base64 encoding.
+ return false;
+ }
+ string encoded = aEncoded;
+ if (!Decode6Bit(encoded)) {
+ return false;
+ }
+
+ // The number of bytes we haven't yet filled in the current byte, mod 8.
+ int shift = 0;
+
+ aOutDecoded.resize((encoded.size() * 3) / 4);
+ vector<uint8_t>::iterator out = aOutDecoded.begin();
+ for (size_t i = 0; i < encoded.length(); i++) {
+ if (!shift) {
+ *out = encoded[i] << 2;
+ } else {
+ *out |= encoded[i] >> (6 - shift);
+ out++;
+ if (out == aOutDecoded.end()) {
+ // Hit last 6bit octed in encoded, which is padding and can be ignored.
+ break;
+ }
+ *out = encoded[i] << (shift + 2);
+ }
+ shift = (shift + 2) % 8;
+ }
+
+ return true;
+}
diff --git a/dom/media/eme/clearkey/ClearKeyBase64.h b/dom/media/eme/clearkey/ClearKeyBase64.h
new file mode 100644
index 0000000000..39309c031e
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyBase64.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyBase64_h__
+#define __ClearKeyBase64_h__
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+// Decodes a base64 encoded string. Returns true on success.
+bool DecodeBase64(const std::string& aEncoded,
+ std::vector<uint8_t>& aOutDecoded);
+
+#endif
diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp
new file mode 100644
index 0000000000..7eeca88985
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeyDecryptionManager.h"
+
+#include <assert.h>
+#include <string.h>
+
+#include <vector>
+#include <algorithm>
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/Span.h"
+#include "psshparser/PsshParser.h"
+
+using namespace cdm;
+
+bool AllZero(const std::vector<uint32_t>& aBytes) {
+ return all_of(aBytes.begin(), aBytes.end(),
+ [](uint32_t b) { return b == 0; });
+}
+
+class ClearKeyDecryptor : public RefCounted {
+ public:
+ ClearKeyDecryptor();
+
+ void InitKey(const Key& aKey);
+ bool HasKey() const { return !mKey.empty(); }
+
+ Status Decrypt(uint8_t* aBuffer, uint32_t aBufferSize,
+ const CryptoMetaData& aMetadata);
+
+ const Key& DecryptionKey() const { return mKey; }
+
+ private:
+ ~ClearKeyDecryptor();
+
+ Key mKey;
+};
+
+/* static */
+ClearKeyDecryptionManager* ClearKeyDecryptionManager::sInstance = nullptr;
+
+/* static */
+ClearKeyDecryptionManager* ClearKeyDecryptionManager::Get() {
+ if (!sInstance) {
+ sInstance = new ClearKeyDecryptionManager();
+ }
+ return sInstance;
+}
+
+ClearKeyDecryptionManager::ClearKeyDecryptionManager() {
+ CK_LOGD("ClearKeyDecryptionManager::ClearKeyDecryptionManager");
+}
+
+ClearKeyDecryptionManager::~ClearKeyDecryptionManager() {
+ CK_LOGD("ClearKeyDecryptionManager::~ClearKeyDecryptionManager");
+
+ sInstance = nullptr;
+
+ for (auto it = mDecryptors.begin(); it != mDecryptors.end(); it++) {
+ it->second->Release();
+ }
+ mDecryptors.clear();
+}
+
+bool ClearKeyDecryptionManager::HasSeenKeyId(const KeyId& aKeyId) const {
+ CK_LOGD("ClearKeyDecryptionManager::SeenKeyId %s",
+ mDecryptors.find(aKeyId) != mDecryptors.end() ? "t" : "f");
+ return mDecryptors.find(aKeyId) != mDecryptors.end();
+}
+
+bool ClearKeyDecryptionManager::IsExpectingKeyForKeyId(
+ const KeyId& aKeyId) const {
+ CK_LOGARRAY("ClearKeyDecryptionManager::IsExpectingKeyForId ", aKeyId.data(),
+ aKeyId.size());
+ const auto& decryptor = mDecryptors.find(aKeyId);
+ return decryptor != mDecryptors.end() && !decryptor->second->HasKey();
+}
+
+bool ClearKeyDecryptionManager::HasKeyForKeyId(const KeyId& aKeyId) const {
+ CK_LOGD("ClearKeyDecryptionManager::HasKeyForKeyId");
+ const auto& decryptor = mDecryptors.find(aKeyId);
+ return decryptor != mDecryptors.end() && decryptor->second->HasKey();
+}
+
+const Key& ClearKeyDecryptionManager::GetDecryptionKey(const KeyId& aKeyId) {
+ assert(HasKeyForKeyId(aKeyId));
+ return mDecryptors[aKeyId]->DecryptionKey();
+}
+
+void ClearKeyDecryptionManager::InitKey(KeyId aKeyId, Key aKey) {
+ CK_LOGD("ClearKeyDecryptionManager::InitKey ", aKeyId.data(), aKeyId.size());
+ if (IsExpectingKeyForKeyId(aKeyId)) {
+ CK_LOGARRAY("Initialized Key ", aKeyId.data(), aKeyId.size());
+ mDecryptors[aKeyId]->InitKey(aKey);
+ } else {
+ CK_LOGARRAY("Failed to initialize key ", aKeyId.data(), aKeyId.size());
+ }
+}
+
+void ClearKeyDecryptionManager::ExpectKeyId(KeyId aKeyId) {
+ CK_LOGD("ClearKeyDecryptionManager::ExpectKeyId ", aKeyId.data(),
+ aKeyId.size());
+ if (!HasSeenKeyId(aKeyId)) {
+ mDecryptors[aKeyId] = new ClearKeyDecryptor();
+ }
+ mDecryptors[aKeyId]->AddRef();
+}
+
+void ClearKeyDecryptionManager::ReleaseKeyId(KeyId aKeyId) {
+ CK_LOGD("ClearKeyDecryptionManager::ReleaseKeyId");
+ assert(HasSeenKeyId(aKeyId));
+
+ ClearKeyDecryptor* decryptor = mDecryptors[aKeyId];
+ if (!decryptor->Release()) {
+ mDecryptors.erase(aKeyId);
+ }
+}
+
+Status ClearKeyDecryptionManager::Decrypt(std::vector<uint8_t>& aBuffer,
+ const CryptoMetaData& aMetadata) {
+ return Decrypt(&aBuffer[0], aBuffer.size(), aMetadata);
+}
+
+Status ClearKeyDecryptionManager::Decrypt(uint8_t* aBuffer,
+ uint32_t aBufferSize,
+ const CryptoMetaData& aMetadata) {
+ CK_LOGD("ClearKeyDecryptionManager::Decrypt");
+ if (!HasKeyForKeyId(aMetadata.mKeyId)) {
+ CK_LOGARRAY("Unable to find decryptor for keyId: ", aMetadata.mKeyId.data(),
+ aMetadata.mKeyId.size());
+ return Status::kNoKey;
+ }
+
+ CK_LOGARRAY("Found decryptor for keyId: ", aMetadata.mKeyId.data(),
+ aMetadata.mKeyId.size());
+ return mDecryptors[aMetadata.mKeyId]->Decrypt(aBuffer, aBufferSize,
+ aMetadata);
+}
+
+ClearKeyDecryptor::ClearKeyDecryptor() { CK_LOGD("ClearKeyDecryptor ctor"); }
+
+ClearKeyDecryptor::~ClearKeyDecryptor() {
+ if (HasKey()) {
+ CK_LOGARRAY("ClearKeyDecryptor dtor; key = ", mKey.data(), mKey.size());
+ } else {
+ CK_LOGD("ClearKeyDecryptor dtor");
+ }
+}
+
+void ClearKeyDecryptor::InitKey(const Key& aKey) { mKey = aKey; }
+
+Status ClearKeyDecryptor::Decrypt(uint8_t* aBuffer, uint32_t aBufferSize,
+ const CryptoMetaData& aMetadata) {
+ CK_LOGD("ClearKeyDecryptor::Decrypt");
+ // If the sample is split up into multiple encrypted subsamples, we need to
+ // stitch them into one continuous buffer for decryption.
+ std::vector<uint8_t> tmp(aBufferSize);
+ static_assert(sizeof(uintptr_t) == sizeof(uint8_t*),
+ "We need uintptr_t to be exactly the same size as a pointer");
+
+ // Decrypt CBCS case:
+ if (aMetadata.mEncryptionScheme == EncryptionScheme::kCbcs) {
+ mozilla::CheckedInt<uintptr_t> data = reinterpret_cast<uintptr_t>(aBuffer);
+ if (!data.isValid()) {
+ return Status::kDecryptError;
+ }
+ const uintptr_t endBuffer =
+ reinterpret_cast<uintptr_t>(aBuffer + aBufferSize);
+
+ if (aMetadata.NumSubsamples() == 0) {
+ if (data.value() > endBuffer) {
+ return Status::kDecryptError;
+ }
+ mozilla::Span<uint8_t> encryptedSpan =
+ mozilla::Span(reinterpret_cast<uint8_t*>(data.value()), aBufferSize);
+ if (!ClearKeyUtils::DecryptCbcs(mKey, aMetadata.mIV, encryptedSpan,
+ aMetadata.mCryptByteBlock,
+ aMetadata.mSkipByteBlock)) {
+ return Status::kDecryptError;
+ }
+ return Status::kSuccess;
+ }
+
+ for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) {
+ data += aMetadata.mClearBytes[i];
+ if (!data.isValid() || data.value() > endBuffer) {
+ return Status::kDecryptError;
+ }
+ mozilla::CheckedInt<uintptr_t> dataAfterCipher =
+ data + aMetadata.mCipherBytes[i];
+ if (!dataAfterCipher.isValid() || dataAfterCipher.value() > endBuffer) {
+ // Trying to read past the end of the buffer!
+ return Status::kDecryptError;
+ }
+ mozilla::Span<uint8_t> encryptedSpan = mozilla::Span(
+ reinterpret_cast<uint8_t*>(data.value()), aMetadata.mCipherBytes[i]);
+ if (!ClearKeyUtils::DecryptCbcs(mKey, aMetadata.mIV, encryptedSpan,
+ aMetadata.mCryptByteBlock,
+ aMetadata.mSkipByteBlock)) {
+ return Status::kDecryptError;
+ }
+ data += aMetadata.mCipherBytes[i];
+ if (!data.isValid()) {
+ return Status::kDecryptError;
+ }
+ }
+ return Status::kSuccess;
+ }
+
+ // Decrypt CENC case:
+ if (aMetadata.NumSubsamples()) {
+ // Take all encrypted parts of subsamples and stitch them into one
+ // continuous encrypted buffer.
+ mozilla::CheckedInt<uintptr_t> data = reinterpret_cast<uintptr_t>(aBuffer);
+ const uintptr_t endBuffer =
+ reinterpret_cast<uintptr_t>(aBuffer + aBufferSize);
+ uint8_t* iter = &tmp[0];
+ for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) {
+ data += aMetadata.mClearBytes[i];
+ if (!data.isValid() || data.value() > endBuffer) {
+ // Trying to read past the end of the buffer!
+ return Status::kDecryptError;
+ }
+ const uint32_t& cipherBytes = aMetadata.mCipherBytes[i];
+ mozilla::CheckedInt<uintptr_t> dataAfterCipher = data + cipherBytes;
+ if (!dataAfterCipher.isValid() || dataAfterCipher.value() > endBuffer) {
+ // Trying to read past the end of the buffer!
+ return Status::kDecryptError;
+ }
+
+ memcpy(iter, reinterpret_cast<uint8_t*>(data.value()), cipherBytes);
+
+ data = dataAfterCipher;
+ iter += cipherBytes;
+ }
+
+ tmp.resize((size_t)(iter - &tmp[0]));
+ } else {
+ memcpy(&tmp[0], aBuffer, aBufferSize);
+ }
+
+ // It is possible that we could be passed an unencrypted sample, if all
+ // encrypted sample lengths are zero, and in this case, a zero length
+ // IV is allowed.
+ assert(aMetadata.mIV.size() == 8 || aMetadata.mIV.size() == 16 ||
+ (aMetadata.mIV.empty() && AllZero(aMetadata.mCipherBytes)));
+
+ std::vector<uint8_t> iv(aMetadata.mIV);
+ iv.insert(iv.end(), CENC_KEY_LEN - aMetadata.mIV.size(), 0);
+
+ if (!ClearKeyUtils::DecryptAES(mKey, tmp, iv)) {
+ return Status::kDecryptError;
+ }
+
+ if (aMetadata.NumSubsamples()) {
+ // Take the decrypted buffer, split up into subsamples, and insert those
+ // subsamples back into their original position in the original buffer.
+ uint8_t* data = aBuffer;
+ uint8_t* iter = &tmp[0];
+ for (size_t i = 0; i < aMetadata.NumSubsamples(); i++) {
+ data += aMetadata.mClearBytes[i];
+ uint32_t cipherBytes = aMetadata.mCipherBytes[i];
+
+ memcpy(data, iter, cipherBytes);
+
+ data += cipherBytes;
+ iter += cipherBytes;
+ }
+ } else {
+ memcpy(aBuffer, &tmp[0], aBufferSize);
+ }
+
+ return Status::kSuccess;
+}
diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.h b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h
new file mode 100644
index 0000000000..c21ff3119a
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.h
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyDecryptionManager_h__
+#define __ClearKeyDecryptionManager_h__
+
+// This include is required in order for content_decryption_module to work
+// on Unix systems.
+#include <stddef.h>
+
+#include <map>
+
+#include "content_decryption_module.h"
+
+#include "ClearKeyUtils.h"
+#include "RefCounted.h"
+
+class ClearKeyDecryptor;
+
+class CryptoMetaData {
+ public:
+ CryptoMetaData() = default;
+
+ explicit CryptoMetaData(const cdm::InputBuffer_2* aInputBuffer) {
+ Init(aInputBuffer);
+ }
+
+ void Init(const cdm::InputBuffer_2* aInputBuffer) {
+ if (!aInputBuffer) {
+ assert(!IsValid());
+ return;
+ }
+
+ mEncryptionScheme = aInputBuffer->encryption_scheme;
+ Assign(mKeyId, aInputBuffer->key_id, aInputBuffer->key_id_size);
+ Assign(mIV, aInputBuffer->iv, aInputBuffer->iv_size);
+ mCryptByteBlock = aInputBuffer->pattern.crypt_byte_block;
+ mSkipByteBlock = aInputBuffer->pattern.skip_byte_block;
+
+ for (uint32_t i = 0; i < aInputBuffer->num_subsamples; ++i) {
+ const cdm::SubsampleEntry& subsample = aInputBuffer->subsamples[i];
+ mClearBytes.push_back(subsample.clear_bytes);
+ mCipherBytes.push_back(subsample.cipher_bytes);
+ }
+ }
+
+ bool IsValid() const {
+ return !mKeyId.empty() && !mIV.empty() && !mCipherBytes.empty() &&
+ !mClearBytes.empty();
+ }
+
+ size_t NumSubsamples() const {
+ assert(mClearBytes.size() == mCipherBytes.size());
+ return mClearBytes.size();
+ }
+
+ cdm::EncryptionScheme mEncryptionScheme;
+ std::vector<uint8_t> mKeyId;
+ std::vector<uint8_t> mIV;
+ uint32_t mCryptByteBlock;
+ uint32_t mSkipByteBlock;
+ std::vector<uint32_t> mClearBytes;
+ std::vector<uint32_t> mCipherBytes;
+};
+
+class ClearKeyDecryptionManager : public RefCounted {
+ private:
+ ClearKeyDecryptionManager();
+ ~ClearKeyDecryptionManager();
+
+ static ClearKeyDecryptionManager* sInstance;
+
+ public:
+ static ClearKeyDecryptionManager* Get();
+
+ bool HasSeenKeyId(const KeyId& aKeyId) const;
+ bool HasKeyForKeyId(const KeyId& aKeyId) const;
+
+ const Key& GetDecryptionKey(const KeyId& aKeyId);
+
+ // Create a decryptor for the given KeyId if one does not already exist.
+ void InitKey(KeyId aKeyId, Key aKey);
+ void ExpectKeyId(KeyId aKeyId);
+ void ReleaseKeyId(KeyId aKeyId);
+
+ // Decrypts buffer *in place*.
+ cdm::Status Decrypt(uint8_t* aBuffer, uint32_t aBufferSize,
+ const CryptoMetaData& aMetadata);
+ cdm::Status Decrypt(std::vector<uint8_t>& aBuffer,
+ const CryptoMetaData& aMetadata);
+
+ private:
+ bool IsExpectingKeyForKeyId(const KeyId& aKeyId) const;
+
+ std::map<KeyId, ClearKeyDecryptor*> mDecryptors;
+};
+
+#endif // __ClearKeyDecryptionManager_h__
diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.cpp b/dom/media/eme/clearkey/ClearKeyPersistence.cpp
new file mode 100644
index 0000000000..0b81f81399
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyPersistence.cpp
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeyPersistence.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <sstream>
+
+#include "ClearKeySessionManager.h"
+#include "ClearKeyStorage.h"
+#include "ClearKeyUtils.h"
+#include "RefCounted.h"
+
+using namespace cdm;
+
+using std::function;
+using std::string;
+using std::stringstream;
+using std::vector;
+
+void ClearKeyPersistence::ReadAllRecordsFromIndex(
+ function<void()>&& aOnComplete) {
+ // Clear what we think the index file contains, we're about to read it again.
+ mPersistentSessionIds.clear();
+
+ // Hold a reference to the persistence manager, so it isn't released before
+ // we try and use it.
+ RefPtr<ClearKeyPersistence> self(this);
+ function<void(const uint8_t*, uint32_t)> onIndexSuccess =
+ [self, aOnComplete](const uint8_t* data, uint32_t size) {
+ CK_LOGD("ClearKeyPersistence: Loaded index file!");
+ const char* charData = (const char*)data;
+
+ stringstream ss(string(charData, charData + size));
+ string name;
+ while (getline(ss, name)) {
+ if (ClearKeyUtils::IsValidSessionId(name.data(), name.size())) {
+ self->mPersistentSessionIds.insert(atoi(name.c_str()));
+ }
+ }
+
+ self->mPersistentKeyState = PersistentKeyState::LOADED;
+ aOnComplete();
+ };
+
+ function<void()> onIndexFailed = [self, aOnComplete]() {
+ CK_LOGD(
+ "ClearKeyPersistence: Failed to load index file (it might not exist");
+ self->mPersistentKeyState = PersistentKeyState::LOADED;
+ aOnComplete();
+ };
+
+ string filename = "index";
+ ReadData(mHost, filename, std::move(onIndexSuccess),
+ std::move(onIndexFailed));
+}
+
+void ClearKeyPersistence::WriteIndex() {
+ function<void()> onIndexSuccess = []() {
+ CK_LOGD("ClearKeyPersistence: Wrote index file");
+ };
+
+ function<void()> onIndexFail = []() {
+ CK_LOGD("ClearKeyPersistence: Failed to write index file (this is bad)");
+ };
+
+ stringstream ss;
+
+ for (const uint32_t& sessionId : mPersistentSessionIds) {
+ ss << sessionId;
+ ss << '\n';
+ }
+
+ string dataString = ss.str();
+ uint8_t* dataArray = (uint8_t*)dataString.data();
+ vector<uint8_t> data(dataArray, dataArray + dataString.size());
+
+ string filename = "index";
+ WriteData(mHost, filename, data, std::move(onIndexSuccess),
+ std::move(onIndexFail));
+}
+
+ClearKeyPersistence::ClearKeyPersistence(Host_10* aHost) {
+ this->mHost = aHost;
+}
+
+void ClearKeyPersistence::EnsureInitialized(bool aPersistentStateAllowed,
+ function<void()>&& aOnInitialized) {
+ if (aPersistentStateAllowed &&
+ mPersistentKeyState == PersistentKeyState::UNINITIALIZED) {
+ mPersistentKeyState = LOADING;
+ ReadAllRecordsFromIndex(std::move(aOnInitialized));
+ } else {
+ mPersistentKeyState = PersistentKeyState::LOADED;
+ aOnInitialized();
+ }
+}
+
+bool ClearKeyPersistence::IsLoaded() const {
+ return mPersistentKeyState == PersistentKeyState::LOADED;
+}
+
+string ClearKeyPersistence::GetNewSessionId(SessionType aSessionType) {
+ static uint32_t sNextSessionId = 1;
+
+ // Ensure we don't re-use a session id that was persisted.
+ while (Contains(mPersistentSessionIds, sNextSessionId)) {
+ sNextSessionId++;
+ }
+
+ string sessionId;
+ stringstream ss;
+ ss << sNextSessionId;
+ ss >> sessionId;
+
+ if (aSessionType == SessionType::kPersistentLicense) {
+ mPersistentSessionIds.insert(sNextSessionId);
+
+ // Save the updated index file.
+ WriteIndex();
+ }
+
+ sNextSessionId++;
+
+ return sessionId;
+}
+
+bool ClearKeyPersistence::IsPersistentSessionId(const string& aSessionId) {
+ return Contains(mPersistentSessionIds, atoi(aSessionId.c_str()));
+}
+
+void ClearKeyPersistence::PersistentSessionRemoved(string& aSessionId) {
+ mPersistentSessionIds.erase(atoi(aSessionId.c_str()));
+
+ // Update the index file.
+ WriteIndex();
+}
diff --git a/dom/media/eme/clearkey/ClearKeyPersistence.h b/dom/media/eme/clearkey/ClearKeyPersistence.h
new file mode 100644
index 0000000000..b832db8c11
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyPersistence.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyPersistence_h__
+#define __ClearKeyPersistence_h__
+
+// This include is required in order for content_decryption_module to work
+// on Unix systems.
+#include <stddef.h>
+
+#include <functional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "content_decryption_module.h"
+
+#include "RefCounted.h"
+
+class ClearKeySessionManager;
+
+// Whether we've loaded the persistent session ids yet.
+enum PersistentKeyState { UNINITIALIZED, LOADING, LOADED };
+
+class ClearKeyPersistence : public RefCounted {
+ public:
+ explicit ClearKeyPersistence(cdm::Host_10* aHost);
+
+ void EnsureInitialized(bool aPersistentStateAllowed,
+ std::function<void()>&& aOnInitialized);
+
+ bool IsLoaded() const;
+
+ std::string GetNewSessionId(cdm::SessionType aSessionType);
+
+ bool IsPersistentSessionId(const std::string& aSid);
+
+ void PersistentSessionRemoved(std::string& aSid);
+
+ private:
+ cdm::Host_10* mHost = nullptr;
+
+ PersistentKeyState mPersistentKeyState = PersistentKeyState::UNINITIALIZED;
+
+ std::set<uint32_t> mPersistentSessionIds;
+
+ void ReadAllRecordsFromIndex(std::function<void()>&& aOnComplete);
+ void WriteIndex();
+};
+
+#endif // __ClearKeyPersistence_h__
diff --git a/dom/media/eme/clearkey/ClearKeySession.cpp b/dom/media/eme/clearkey/ClearKeySession.cpp
new file mode 100644
index 0000000000..59e104fc54
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeySession.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <string.h>
+
+#include "BigEndian.h"
+#include "ClearKeyDecryptionManager.h"
+#include "ClearKeySession.h"
+#include "ClearKeyStorage.h"
+#include "ClearKeyUtils.h"
+#include "psshparser/PsshParser.h"
+
+using namespace mozilla;
+using namespace cdm;
+
+ClearKeySession::ClearKeySession(const std::string& aSessionId,
+ SessionType aSessionType)
+ : mSessionId(aSessionId), mSessionType(aSessionType) {
+ CK_LOGD("ClearKeySession ctor %p", this);
+}
+
+ClearKeySession::~ClearKeySession() {
+ CK_LOGD("ClearKeySession dtor %p", this);
+}
+
+bool ClearKeySession::Init(InitDataType aInitDataType, const uint8_t* aInitData,
+ uint32_t aInitDataSize) {
+ CK_LOGD("ClearKeySession::Init");
+
+ if (aInitDataType == InitDataType::kCenc) {
+ ParseCENCInitData(aInitData, aInitDataSize, mKeyIds);
+ } else if (aInitDataType == InitDataType::kKeyIds) {
+ ClearKeyUtils::ParseKeyIdsInitData(aInitData, aInitDataSize, mKeyIds);
+ } else if (aInitDataType == InitDataType::kWebM &&
+ aInitDataSize <= kMaxWebmInitDataSize) {
+ // "webm" initData format is simply the raw bytes of the keyId.
+ std::vector<uint8_t> keyId;
+ keyId.assign(aInitData, aInitData + aInitDataSize);
+ mKeyIds.push_back(keyId);
+ }
+
+ if (mKeyIds.empty()) {
+ CK_LOGD("ClearKeySession::Init, failed to get keyId");
+ return false;
+ }
+#ifdef WMF_CLEARKEY_DEBUG
+ for (const auto& keyId : mKeyIds) {
+ CK_LOGARRAY("ClearKeySession::Init, KeyId : ", keyId.data(), keyId.size());
+ }
+#endif
+ return true;
+}
+
+SessionType ClearKeySession::Type() const { return mSessionType; }
+
+void ClearKeySession::AddKeyId(const KeyId& aKeyId) {
+ mKeyIds.push_back(aKeyId);
+}
diff --git a/dom/media/eme/clearkey/ClearKeySession.h b/dom/media/eme/clearkey/ClearKeySession.h
new file mode 100644
index 0000000000..3a130aa45a
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeySession.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeySession_h__
+#define __ClearKeySession_h__
+
+// This include is required in order for content_decryption_module to work
+// on Unix systems.
+#include <stddef.h>
+
+#include <string>
+#include <vector>
+
+#include "content_decryption_module.h"
+
+#include "ClearKeyUtils.h"
+
+class ClearKeySession {
+ public:
+ explicit ClearKeySession(const std::string& aSessionId,
+ cdm::SessionType aSessionType);
+
+ ~ClearKeySession();
+
+ const std::vector<KeyId>& GetKeyIds() const { return mKeyIds; }
+
+ bool Init(cdm::InitDataType aInitDataType, const uint8_t* aInitData,
+ uint32_t aInitDataSize);
+
+ cdm::SessionType Type() const;
+
+ void AddKeyId(const KeyId& aKeyId);
+
+ const std::string& Id() const { return mSessionId; }
+
+ private:
+ const std::string mSessionId;
+ std::vector<KeyId> mKeyIds;
+
+ const cdm::SessionType mSessionType;
+};
+
+#endif // __ClearKeySession_h__
diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.cpp b/dom/media/eme/clearkey/ClearKeySessionManager.cpp
new file mode 100644
index 0000000000..54a15f4fcf
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeySessionManager.cpp
@@ -0,0 +1,713 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeySessionManager.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "content_decryption_module.h"
+
+#include "ClearKeyDecryptionManager.h"
+#include "ClearKeyPersistence.h"
+#include "ClearKeyStorage.h"
+#include "ClearKeyUtils.h"
+#include "psshparser/PsshParser.h"
+
+using namespace cdm;
+
+using std::function;
+using std::string;
+using std::vector;
+
+ClearKeySessionManager::ClearKeySessionManager(Host_10* aHost)
+ : mDecryptionManager(ClearKeyDecryptionManager::Get()) {
+ CK_LOGD("ClearKeySessionManager ctor %p", this);
+ AddRef();
+
+ mHost = aHost;
+ mPersistence = new ClearKeyPersistence(mHost);
+}
+
+ClearKeySessionManager::~ClearKeySessionManager() {
+ CK_LOGD("ClearKeySessionManager dtor %p", this);
+}
+
+void ClearKeySessionManager::Init(bool aDistinctiveIdentifierAllowed,
+ bool aPersistentStateAllowed) {
+ CK_LOGD("ClearKeySessionManager::Init");
+
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> onPersistentStateLoaded = [self]() {
+ while (!self->mDeferredInitialize.empty()) {
+ function<void()> func = self->mDeferredInitialize.front();
+ self->mDeferredInitialize.pop();
+
+ func();
+ }
+ if (self->mHost) {
+ // The session manager should be the last thing the ClearKey CDM is
+ // waiting on to be initialized.
+ self->mHost->OnInitialized(true);
+ }
+ };
+
+ mPersistence->EnsureInitialized(aPersistentStateAllowed,
+ std::move(onPersistentStateLoaded));
+}
+
+void ClearKeySessionManager::CreateSession(uint32_t aPromiseId,
+ InitDataType aInitDataType,
+ const uint8_t* aInitData,
+ uint32_t aInitDataSize,
+ SessionType aSessionType) {
+ CK_LOGD("ClearKeySessionManager::CreateSession type:%u", aInitDataType);
+
+ // Copy the init data so it is correctly captured by the lambda
+ vector<uint8_t> initData(aInitData, aInitData + aInitDataSize);
+
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> deferrer = [self, aPromiseId, aInitDataType, initData,
+ aSessionType]() {
+ self->CreateSession(aPromiseId, aInitDataType, initData.data(),
+ initData.size(), aSessionType);
+ };
+
+ // If we haven't loaded, don't do this yet
+ if (MaybeDeferTillInitialized(std::move(deferrer))) {
+ CK_LOGD("Deferring CreateSession");
+ return;
+ }
+
+ CK_LOGARRAY("ClearKeySessionManager::CreateSession initdata: ", aInitData,
+ aInitDataSize);
+
+ // If 'DecryptingComplete' has been called mHost will be null so we can't
+ // won't be able to resolve our promise
+ if (!mHost) {
+ CK_LOGD("ClearKeySessionManager::CreateSession: mHost is nullptr");
+ return;
+ }
+
+ // initDataType must be "cenc", "keyids", or "webm".
+ if (aInitDataType != InitDataType::kCenc &&
+ aInitDataType != InitDataType::kKeyIds &&
+ aInitDataType != InitDataType::kWebM) {
+ string message = "initDataType is not supported by ClearKey";
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionNotSupportedError,
+ 0, message.c_str(), message.size());
+
+ return;
+ }
+
+ string sessionId = mPersistence->GetNewSessionId(aSessionType);
+ assert(mSessions.find(sessionId) == mSessions.end());
+
+ ClearKeySession* session = new ClearKeySession(sessionId, aSessionType);
+
+ if (!session->Init(aInitDataType, aInitData, aInitDataSize)) {
+ CK_LOGD("Failed to initialize session: %s", sessionId.c_str());
+
+ const static char* message = "Failed to initialize session";
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionInvalidStateError,
+ 0, message, strlen(message));
+ delete session;
+
+ return;
+ }
+
+ mSessions[sessionId] = session;
+ mLastSessionId = sessionId;
+
+ const vector<KeyId>& sessionKeys = session->GetKeyIds();
+ vector<KeyId> neededKeys;
+
+ for (auto it = sessionKeys.begin(); it != sessionKeys.end(); it++) {
+ // Need to request this key ID from the client. We always send a key
+ // request, whether or not another session has sent a request with the same
+ // key ID. Otherwise a script can end up waiting for another script to
+ // respond to the request (which may not necessarily happen).
+ neededKeys.push_back(*it);
+ mDecryptionManager->ExpectKeyId(*it);
+ }
+
+ if (neededKeys.empty()) {
+ CK_LOGD("No keys needed from client.");
+ return;
+ }
+
+ // Send a request for needed key data.
+ string request;
+ ClearKeyUtils::MakeKeyRequest(neededKeys, request, aSessionType);
+
+ // Resolve the promise with the new session information.
+ mHost->OnResolveNewSessionPromise(aPromiseId, sessionId.c_str(),
+ sessionId.size());
+
+ mHost->OnSessionMessage(sessionId.c_str(), sessionId.size(),
+ MessageType::kLicenseRequest, request.c_str(),
+ request.size());
+}
+
+void ClearKeySessionManager::LoadSession(uint32_t aPromiseId,
+ const char* aSessionId,
+ uint32_t aSessionIdLength) {
+ CK_LOGD("ClearKeySessionManager::LoadSession");
+
+ // Copy the sessionId into a string so the lambda captures it properly.
+ string sessionId(aSessionId, aSessionId + aSessionIdLength);
+
+ // Hold a reference to the SessionManager so that it isn't released before
+ // we try to use it.
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> deferrer = [self, aPromiseId, sessionId]() {
+ self->LoadSession(aPromiseId, sessionId.data(), sessionId.size());
+ };
+
+ if (MaybeDeferTillInitialized(std::move(deferrer))) {
+ CK_LOGD("Deferring LoadSession");
+ return;
+ }
+
+ // If the SessionManager has been shutdown mHost will be null and we won't
+ // be able to resolve the promise.
+ if (!mHost) {
+ return;
+ }
+
+ if (!ClearKeyUtils::IsValidSessionId(aSessionId, aSessionIdLength)) {
+ mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0);
+ return;
+ }
+
+ if (!mPersistence->IsPersistentSessionId(sessionId)) {
+ mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0);
+ return;
+ }
+
+ function<void(const uint8_t*, uint32_t)> success =
+ [self, sessionId, aPromiseId](const uint8_t* data, uint32_t size) {
+ self->PersistentSessionDataLoaded(aPromiseId, sessionId, data, size);
+ };
+
+ function<void()> failure = [self, aPromiseId] {
+ if (!self->mHost) {
+ return;
+ }
+ // As per the API described in ContentDecryptionModule_8
+ self->mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0);
+ };
+
+ ReadData(mHost, sessionId, std::move(success), std::move(failure));
+}
+
+void ClearKeySessionManager::PersistentSessionDataLoaded(
+ uint32_t aPromiseId, const string& aSessionId, const uint8_t* aKeyData,
+ uint32_t aKeyDataSize) {
+ CK_LOGD("ClearKeySessionManager::PersistentSessionDataLoaded");
+
+ // Check that the SessionManager has not been shut down before we try and
+ // resolve any promises.
+ if (!mHost) {
+ return;
+ }
+
+ if (Contains(mSessions, aSessionId) ||
+ (aKeyDataSize % (2 * CENC_KEY_LEN)) != 0) {
+ // As per the instructions in ContentDecryptionModule_8
+ mHost->OnResolveNewSessionPromise(aPromiseId, nullptr, 0);
+ return;
+ }
+
+ ClearKeySession* session =
+ new ClearKeySession(aSessionId, SessionType::kPersistentLicense);
+
+ mSessions[aSessionId] = session;
+ mLastSessionId = aSessionId;
+
+ uint32_t numKeys = aKeyDataSize / (2 * CENC_KEY_LEN);
+
+ vector<KeyInformation> keyInfos;
+ vector<KeyIdPair> keyPairs;
+ for (uint32_t i = 0; i < numKeys; i++) {
+ const uint8_t* base = aKeyData + 2 * CENC_KEY_LEN * i;
+
+ KeyIdPair keyPair;
+
+ keyPair.mKeyId = KeyId(base, base + CENC_KEY_LEN);
+ assert(keyPair.mKeyId.size() == CENC_KEY_LEN);
+
+ keyPair.mKey = Key(base + CENC_KEY_LEN, base + 2 * CENC_KEY_LEN);
+ assert(keyPair.mKey.size() == CENC_KEY_LEN);
+
+ session->AddKeyId(keyPair.mKeyId);
+
+ mDecryptionManager->ExpectKeyId(keyPair.mKeyId);
+ mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey);
+ mKeyIds.insert(keyPair.mKey);
+ keyPairs.push_back(keyPair);
+
+ KeyInformation keyInfo = {};
+ keyInfo.key_id = &keyPairs.back().mKeyId[0];
+ keyInfo.key_id_size = keyPair.mKeyId.size();
+ keyInfo.status = KeyStatus::kUsable;
+
+ keyInfos.push_back(keyInfo);
+ }
+
+ mHost->OnSessionKeysChange(&aSessionId[0], aSessionId.size(), true,
+ keyInfos.data(), keyInfos.size());
+
+ mHost->OnResolveNewSessionPromise(aPromiseId, aSessionId.c_str(),
+ aSessionId.size());
+}
+
+void ClearKeySessionManager::UpdateSession(uint32_t aPromiseId,
+ const char* aSessionId,
+ uint32_t aSessionIdLength,
+ const uint8_t* aResponse,
+ uint32_t aResponseSize) {
+ CK_LOGD("ClearKeySessionManager::UpdateSession");
+
+ // Copy the method arguments so we can capture them in the lambda
+ string sessionId(aSessionId, aSessionId + aSessionIdLength);
+ vector<uint8_t> response(aResponse, aResponse + aResponseSize);
+
+ // Hold a reference to the SessionManager so it isn't released before we
+ // callback.
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> deferrer = [self, aPromiseId, sessionId, response]() {
+ self->UpdateSession(aPromiseId, sessionId.data(), sessionId.size(),
+ response.data(), response.size());
+ };
+
+ // If we haven't fully loaded, defer calling this method
+ if (MaybeDeferTillInitialized(std::move(deferrer))) {
+ CK_LOGD("Deferring LoadSession");
+ return;
+ }
+
+ // Make sure the SessionManager has not been shutdown before we try and
+ // resolve any promises.
+ if (!mHost) {
+ return;
+ }
+
+ CK_LOGD("Updating session: %s", sessionId.c_str());
+
+ auto itr = mSessions.find(sessionId);
+ if (itr == mSessions.end() || !(itr->second)) {
+ CK_LOGW("ClearKey CDM couldn't resolve session ID in UpdateSession.");
+ CK_LOGD("Unable to find session: %s", sessionId.c_str());
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ nullptr, 0);
+
+ return;
+ }
+ ClearKeySession* session = itr->second;
+
+ // Verify the size of session response.
+ if (aResponseSize >= kMaxSessionResponseLength) {
+ CK_LOGW("Session response size is not within a reasonable size.");
+ CK_LOGD("Failed to parse response for session %s", sessionId.c_str());
+
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ nullptr, 0);
+
+ return;
+ }
+
+ // Parse the response for any (key ID, key) pairs.
+ vector<KeyIdPair> keyPairs;
+ if (!ClearKeyUtils::ParseJWK(aResponse, aResponseSize, keyPairs,
+ session->Type())) {
+ CK_LOGW("ClearKey CDM failed to parse JSON Web Key.");
+
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ nullptr, 0);
+
+ return;
+ }
+
+ vector<KeyInformation> keyInfos;
+ for (size_t i = 0; i < keyPairs.size(); i++) {
+ KeyIdPair& keyPair = keyPairs[i];
+ mDecryptionManager->InitKey(keyPair.mKeyId, keyPair.mKey);
+ mKeyIds.insert(keyPair.mKeyId);
+
+ KeyInformation keyInfo = {};
+ keyInfo.key_id = &keyPair.mKeyId[0];
+ keyInfo.key_id_size = keyPair.mKeyId.size();
+ keyInfo.status = KeyStatus::kUsable;
+
+ keyInfos.push_back(keyInfo);
+ }
+
+ mHost->OnSessionKeysChange(aSessionId, aSessionIdLength, true,
+ keyInfos.data(), keyInfos.size());
+
+ if (session->Type() != SessionType::kPersistentLicense) {
+ mHost->OnResolvePromise(aPromiseId);
+ return;
+ }
+
+ // Store the keys on disk. We store a record whose name is the sessionId,
+ // and simply append each keyId followed by its key.
+ vector<uint8_t> keydata;
+ Serialize(session, keydata);
+
+ function<void()> resolve = [self, aPromiseId]() {
+ if (!self->mHost) {
+ return;
+ }
+ self->mHost->OnResolvePromise(aPromiseId);
+ };
+
+ function<void()> reject = [self, aPromiseId]() {
+ if (!self->mHost) {
+ return;
+ }
+
+ static const char* message = "Couldn't store cenc key init data";
+ self->mHost->OnRejectPromise(aPromiseId,
+ Exception::kExceptionInvalidStateError, 0,
+ message, strlen(message));
+ };
+
+ WriteData(mHost, sessionId, keydata, std::move(resolve), std::move(reject));
+}
+
+void ClearKeySessionManager::Serialize(const ClearKeySession* aSession,
+ std::vector<uint8_t>& aOutKeyData) {
+ const std::vector<KeyId>& keyIds = aSession->GetKeyIds();
+ for (size_t i = 0; i < keyIds.size(); i++) {
+ const KeyId& keyId = keyIds[i];
+ if (!mDecryptionManager->HasKeyForKeyId(keyId)) {
+ continue;
+ }
+ assert(keyId.size() == CENC_KEY_LEN);
+ aOutKeyData.insert(aOutKeyData.end(), keyId.begin(), keyId.end());
+ const Key& key = mDecryptionManager->GetDecryptionKey(keyId);
+ assert(key.size() == CENC_KEY_LEN);
+ aOutKeyData.insert(aOutKeyData.end(), key.begin(), key.end());
+ }
+}
+
+void ClearKeySessionManager::CloseSession(uint32_t aPromiseId,
+ const char* aSessionId,
+ uint32_t aSessionIdLength) {
+ CK_LOGD("ClearKeySessionManager::CloseSession");
+
+ // Copy the sessionId into a string so we capture it properly.
+ string sessionId(aSessionId, aSessionId + aSessionIdLength);
+ // Hold a reference to the session manager, so it doesn't get deleted
+ // before we need to use it.
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> deferrer = [self, aPromiseId, sessionId]() {
+ self->CloseSession(aPromiseId, sessionId.data(), sessionId.size());
+ };
+
+ // If we haven't loaded, call this method later.
+ if (MaybeDeferTillInitialized(std::move(deferrer))) {
+ CK_LOGD("Deferring CloseSession");
+ return;
+ }
+
+ // If DecryptingComplete has been called mHost will be null and we won't
+ // be able to resolve our promise.
+ if (!mHost) {
+ return;
+ }
+
+ auto itr = mSessions.find(sessionId);
+ if (itr == mSessions.end()) {
+ CK_LOGW("ClearKey CDM couldn't close non-existent session.");
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ nullptr, 0);
+
+ return;
+ }
+
+ ClearKeySession* session = itr->second;
+ assert(session);
+
+ ClearInMemorySessionData(session);
+
+ mHost->OnSessionClosed(aSessionId, aSessionIdLength);
+ mHost->OnResolvePromise(aPromiseId);
+}
+
+void ClearKeySessionManager::ClearInMemorySessionData(
+ ClearKeySession* aSession) {
+ mSessions.erase(aSession->Id());
+ delete aSession;
+}
+
+void ClearKeySessionManager::RemoveSession(uint32_t aPromiseId,
+ const char* aSessionId,
+ uint32_t aSessionIdLength) {
+ CK_LOGD("ClearKeySessionManager::RemoveSession");
+
+ // Copy the sessionId into a string so it can be captured for the lambda.
+ string sessionId(aSessionId, aSessionId + aSessionIdLength);
+
+ // Hold a reference to the SessionManager, so it isn't released before we
+ // try and use it.
+ RefPtr<ClearKeySessionManager> self(this);
+ function<void()> deferrer = [self, aPromiseId, sessionId]() {
+ self->RemoveSession(aPromiseId, sessionId.data(), sessionId.size());
+ };
+
+ // If we haven't fully loaded, defer calling this method.
+ if (MaybeDeferTillInitialized(std::move(deferrer))) {
+ CK_LOGD("Deferring RemoveSession");
+ return;
+ }
+
+ // Check that the SessionManager has not been shutdown before we try and
+ // resolve any promises.
+ if (!mHost) {
+ return;
+ }
+
+ auto itr = mSessions.find(sessionId);
+ if (itr == mSessions.end()) {
+ CK_LOGW("ClearKey CDM couldn't remove non-existent session.");
+
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ nullptr, 0);
+
+ return;
+ }
+
+ ClearKeySession* session = itr->second;
+ assert(session);
+ string sid = session->Id();
+ bool isPersistent = session->Type() == SessionType::kPersistentLicense;
+ ClearInMemorySessionData(session);
+
+ if (!isPersistent) {
+ mHost->OnResolvePromise(aPromiseId);
+ return;
+ }
+
+ mPersistence->PersistentSessionRemoved(sid);
+
+ vector<uint8_t> emptyKeydata;
+
+ function<void()> resolve = [self, aPromiseId]() {
+ if (!self->mHost) {
+ return;
+ }
+ self->mHost->OnResolvePromise(aPromiseId);
+ };
+
+ function<void()> reject = [self, aPromiseId]() {
+ if (!self->mHost) {
+ return;
+ }
+ static const char* message = "Could not remove session";
+ self->mHost->OnRejectPromise(aPromiseId, Exception::kExceptionTypeError, 0,
+ message, strlen(message));
+ };
+
+ WriteData(mHost, sessionId, emptyKeydata, std::move(resolve),
+ std::move(reject));
+}
+
+void ClearKeySessionManager::SetServerCertificate(uint32_t aPromiseId,
+ const uint8_t* aServerCert,
+ uint32_t aServerCertSize) {
+ // ClearKey CDM doesn't support this method by spec.
+ CK_LOGD("ClearKeySessionManager::SetServerCertificate");
+ mHost->OnRejectPromise(aPromiseId, Exception::kExceptionNotSupportedError, 0,
+ nullptr /* message */, 0 /* messageLen */);
+}
+
+Status ClearKeySessionManager::Decrypt(const InputBuffer_2& aBuffer,
+ DecryptedBlock* aDecryptedBlock) {
+ CK_LOGD("ClearKeySessionManager::Decrypt");
+
+ CK_LOGARRAY("Key: ", aBuffer.key_id, aBuffer.key_id_size);
+
+ Buffer* buffer = mHost->Allocate(aBuffer.data_size);
+ assert(buffer != nullptr);
+ assert(buffer->Data() != nullptr);
+ assert(buffer->Capacity() >= aBuffer.data_size);
+
+ memcpy(buffer->Data(), aBuffer.data, aBuffer.data_size);
+
+ Status status = Status::kSuccess;
+ // According to the comment `If |iv_size| = 0, the data is unencrypted.`
+ // Use iv_size to determine if the sample is encrypted.
+ if (aBuffer.iv_size != 0) {
+ status = mDecryptionManager->Decrypt(buffer->Data(), buffer->Size(),
+ CryptoMetaData(&aBuffer));
+ }
+
+ aDecryptedBlock->SetDecryptedBuffer(buffer);
+ aDecryptedBlock->SetTimestamp(aBuffer.timestamp);
+
+ return status;
+}
+
+void ClearKeySessionManager::DecryptingComplete() {
+ CK_LOGD("ClearKeySessionManager::DecryptingComplete %p", this);
+
+ for (auto it = mSessions.begin(); it != mSessions.end(); it++) {
+ delete it->second;
+ }
+ mSessions.clear();
+ mLastSessionId = std::nullopt;
+
+ mDecryptionManager = nullptr;
+ mHost = nullptr;
+
+ Release();
+}
+
+bool ClearKeySessionManager::MaybeDeferTillInitialized(
+ function<void()>&& aMaybeDefer) {
+ if (mPersistence->IsLoaded()) {
+ return false;
+ }
+
+ mDeferredInitialize.emplace(std::move(aMaybeDefer));
+ return true;
+}
+
+void ClearKeySessionManager::OnQueryOutputProtectionStatus(
+ QueryResult aResult, uint32_t aLinkMask, uint32_t aOutputProtectionMask) {
+ MOZ_ASSERT(mHasOutstandingOutputProtectionQuery,
+ "Should only be called if a query is outstanding");
+ CK_LOGD("ClearKeySessionManager::OnQueryOutputProtectionStatus");
+ mHasOutstandingOutputProtectionQuery = false;
+
+ if (aResult == QueryResult::kQueryFailed) {
+ // Indicate the query failed. This can happen if we're in shutdown.
+ NotifyOutputProtectionStatus(KeyStatus::kInternalError);
+ return;
+ }
+
+ if (aLinkMask & OutputLinkTypes::kLinkTypeNetwork) {
+ NotifyOutputProtectionStatus(KeyStatus::kOutputRestricted);
+ return;
+ }
+
+ NotifyOutputProtectionStatus(KeyStatus::kUsable);
+}
+
+void ClearKeySessionManager::QueryOutputProtectionStatusIfNeeded() {
+ MOZ_ASSERT(
+ mHost,
+ "Should not query protection status if we're shutdown (mHost == null)!");
+ CK_LOGD(
+ "ClearKeySessionManager::UpdateOutputProtectionStatusAndQueryIfNeeded");
+ if (mLastOutputProtectionQueryTime.IsNull()) {
+ // We haven't perfomed a check yet, get a query going.
+ MOZ_ASSERT(
+ !mHasOutstandingOutputProtectionQuery,
+ "Shouldn't have an outstanding query if we haven't recorded a time");
+ QueryOutputProtectionStatusFromHost();
+ return;
+ }
+
+ MOZ_ASSERT(!mLastOutputProtectionQueryTime.IsNull(),
+ "Should have already handled the case where we don't yet have a "
+ "previous check time");
+ const mozilla::TimeStamp now = mozilla::TimeStamp::NowLoRes();
+ const mozilla::TimeDuration timeSinceQuery =
+ now - mLastOutputProtectionQueryTime;
+
+ // The time between output protection checks to the host. I.e. if this amount
+ // of time has passed since the last check with the host, another should be
+ // performed (provided the first check has been handled).
+ static const mozilla::TimeDuration kOutputProtectionQueryInterval =
+ mozilla::TimeDuration::FromSeconds(0.2);
+ // The number of kOutputProtectionQueryInterval intervals we can miss before
+ // we decide a check has failed. I.e. if this value is 2, if we have not
+ // received a reply to a check after kOutputProtectionQueryInterval * 2
+ // time, we consider the check failed.
+ constexpr uint32_t kMissedIntervalsBeforeFailure = 2;
+ // The length of time after which we will restrict output until we get a
+ // query response.
+ static const mozilla::TimeDuration kTimeToWaitBeforeFailure =
+ kOutputProtectionQueryInterval * kMissedIntervalsBeforeFailure;
+
+ if ((timeSinceQuery > kOutputProtectionQueryInterval) &&
+ !mHasOutstandingOutputProtectionQuery) {
+ // We don't have an outstanding query and enough time has passed we should
+ // query again.
+ QueryOutputProtectionStatusFromHost();
+ return;
+ }
+
+ if ((timeSinceQuery > kTimeToWaitBeforeFailure) &&
+ mHasOutstandingOutputProtectionQuery) {
+ // A reponse was not received fast enough, notify.
+ NotifyOutputProtectionStatus(KeyStatus::kInternalError);
+ }
+}
+
+void ClearKeySessionManager::QueryOutputProtectionStatusFromHost() {
+ MOZ_ASSERT(
+ mHost,
+ "Should not query protection status if we're shutdown (mHost == null)!");
+ CK_LOGD("ClearKeySessionManager::QueryOutputProtectionStatusFromHost");
+ if (mHost) {
+ mLastOutputProtectionQueryTime = mozilla::TimeStamp::NowLoRes();
+ mHost->QueryOutputProtectionStatus();
+ mHasOutstandingOutputProtectionQuery = true;
+ }
+}
+
+void ClearKeySessionManager::NotifyOutputProtectionStatus(KeyStatus aStatus) {
+ MOZ_ASSERT(aStatus == KeyStatus::kUsable ||
+ aStatus == KeyStatus::kOutputRestricted ||
+ aStatus == KeyStatus::kInternalError,
+ "aStatus should have an expected value");
+ CK_LOGD("ClearKeySessionManager::NotifyOutputProtectionStatus");
+ if (!mLastSessionId.has_value()) {
+ // If we don't have a session id, either because we're too early, or are
+ // shutting down, don't notify.
+ return;
+ }
+
+ string& lastSessionId = mLastSessionId.value();
+
+ // Use 'output-protection' as the key ID. This helps tests disambiguate key
+ // status updates related to this.
+ const uint8_t kKeyId[] = {'o', 'u', 't', 'p', 'u', 't', '-', 'p', 'r',
+ 'o', 't', 'e', 'c', 't', 'i', 'o', 'n'};
+ KeyInformation keyInfo = {};
+ keyInfo.key_id = kKeyId;
+ keyInfo.key_id_size = std::size(kKeyId);
+ keyInfo.status = aStatus;
+
+ vector<KeyInformation> keyInfos;
+ keyInfos.push_back(keyInfo);
+
+ // At time of writing, Gecko's higher level handling doesn't use this arg.
+ // However, we set it to false to mimic Chromium's similar case. Since
+ // Clearkey is used to test the Chromium CDM path, it doesn't hurt to try
+ // and mimic their behaviour.
+ bool hasAdditionalUseableKey = false;
+ mHost->OnSessionKeysChange(lastSessionId.c_str(), lastSessionId.size(),
+ hasAdditionalUseableKey, keyInfos.data(),
+ keyInfos.size());
+}
diff --git a/dom/media/eme/clearkey/ClearKeySessionManager.h b/dom/media/eme/clearkey/ClearKeySessionManager.h
new file mode 100644
index 0000000000..3a930f666d
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeySessionManager.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyDecryptor_h__
+#define __ClearKeyDecryptor_h__
+
+// This include is required in order for content_decryption_module to work
+// on Unix systems.
+#include <stddef.h>
+
+#include <functional>
+#include <map>
+#include <optional>
+#include <queue>
+#include <set>
+#include <string>
+
+#include "content_decryption_module.h"
+
+#include "ClearKeyDecryptionManager.h"
+#include "ClearKeyPersistence.h"
+#include "ClearKeySession.h"
+#include "ClearKeyUtils.h"
+#include "RefCounted.h"
+#include "mozilla/TimeStamp.h"
+
+class ClearKeySessionManager final : public RefCounted {
+ public:
+ explicit ClearKeySessionManager(cdm::Host_10* aHost);
+
+ void Init(bool aDistinctiveIdentifierAllowed, bool aPersistentStateAllowed);
+
+ void CreateSession(uint32_t aPromiseId, cdm::InitDataType aInitDataType,
+ const uint8_t* aInitData, uint32_t aInitDataSize,
+ cdm::SessionType aSessionType);
+
+ void LoadSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdLength);
+
+ void UpdateSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdLength, const uint8_t* aResponse,
+ uint32_t aResponseSize);
+
+ void CloseSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdLength);
+
+ void RemoveSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdLength);
+
+ void SetServerCertificate(uint32_t aPromiseId, const uint8_t* aServerCert,
+ uint32_t aServerCertSize);
+
+ cdm::Status Decrypt(const cdm::InputBuffer_2& aBuffer,
+ cdm::DecryptedBlock* aDecryptedBlock);
+
+ void DecryptingComplete();
+
+ void PersistentSessionDataLoaded(uint32_t aPromiseId,
+ const std::string& aSessionId,
+ const uint8_t* aKeyData,
+ uint32_t aKeyDataSize);
+
+ // Receives the result of an output protection query from the user agent.
+ // This may trigger a key status change.
+ // @param aResult indicates if the query succeeded or not. If a query did
+ // not succeed then that other arguments are ignored.
+ // @param aLinkMask is used to indicate if output could be captured by the
+ // user agent. It should be set to `kLinkTypeNetwork` if capture is possible,
+ // otherwise it should be zero.
+ // @param aOutputProtectionMask this argument is unused.
+ void OnQueryOutputProtectionStatus(cdm::QueryResult aResult,
+ uint32_t aLinkMask,
+ uint32_t aOutputProtectionMask);
+
+ // Prompts the session manager to query the output protection status if we
+ // haven't yet, or if enough time has passed since the last check. Will also
+ // notify if a check has not been responded to on time.
+ void QueryOutputProtectionStatusIfNeeded();
+
+ private:
+ ~ClearKeySessionManager();
+
+ void ClearInMemorySessionData(ClearKeySession* aSession);
+ bool MaybeDeferTillInitialized(std::function<void()>&& aMaybeDefer);
+ void Serialize(const ClearKeySession* aSession,
+ std::vector<uint8_t>& aOutKeyData);
+
+ // Signals the host to perform an output protection check.
+ void QueryOutputProtectionStatusFromHost();
+
+ // Called to notify the result of an output protection status call. The
+ // following arguments are expected, along with their intended use:
+ // - KeyStatus::kUsable indicates that the query was responded to and the
+ // response showed output is protected.
+ // - KeyStatus::kOutputRestricted indicates that the query was responded to
+ // and the response showed output is not protected.
+ // - KeyStatus::kInternalError indicates a query was not repsonded to on
+ // time, or that a query was responded to with a failed cdm::QueryResult.
+ // The status passed to this function will be used to update the status of
+ // the keyId "output-protection", which tests an observe.
+ void NotifyOutputProtectionStatus(cdm::KeyStatus aStatus);
+
+ RefPtr<ClearKeyDecryptionManager> mDecryptionManager;
+ RefPtr<ClearKeyPersistence> mPersistence;
+
+ cdm::Host_10* mHost = nullptr;
+
+ std::set<KeyId> mKeyIds;
+ std::map<std::string, ClearKeySession*> mSessions;
+
+ // The session id of the last session created or loaded from persistent
+ // storage. Used to fire test messages at that session.
+ std::optional<std::string> mLastSessionId;
+
+ std::queue<std::function<void()>> mDeferredInitialize;
+
+ // If there is an inflight query to the host to check the output protection
+ // status. Multiple in flight queries should not be allowed, avoid firing
+ // more if this is true.
+ bool mHasOutstandingOutputProtectionQuery = false;
+ // The last time the manager called QueryOutputProtectionStatus on the host.
+ mozilla::TimeStamp mLastOutputProtectionQueryTime;
+};
+
+#endif // __ClearKeyDecryptor_h__
diff --git a/dom/media/eme/clearkey/ClearKeyStorage.cpp b/dom/media/eme/clearkey/ClearKeyStorage.cpp
new file mode 100644
index 0000000000..1375d33c57
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyStorage.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeyStorage.h"
+
+#include <assert.h>
+// This include is required in order for content_decryption_module to work
+// on Unix systems.
+#include <stddef.h>
+
+#include <vector>
+
+#include "content_decryption_module.h"
+
+#include "ArrayUtils.h"
+#include "ClearKeyUtils.h"
+
+using namespace cdm;
+
+using std::function;
+using std::string;
+using std::vector;
+
+class WriteRecordClient : public FileIOClient {
+ public:
+ /*
+ * This function will take the memory ownership of the parameters and
+ * delete them when done.
+ */
+ static void Write(Host_10* aHost, string& aRecordName,
+ const vector<uint8_t>& aData, function<void()>&& aOnSuccess,
+ function<void()>&& aOnFailure) {
+ WriteRecordClient* client = new WriteRecordClient(
+ aData, std::move(aOnSuccess), std::move(aOnFailure));
+ client->Do(aRecordName, aHost);
+ }
+
+ void OnOpenComplete(Status aStatus) override {
+ // If we hit an error, fail.
+ if (aStatus != Status::kSuccess) {
+ Done(aStatus);
+ } else if (mFileIO) { // Otherwise, write our data to the file.
+ mFileIO->Write(&mData[0], mData.size());
+ }
+ }
+
+ void OnReadComplete(Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ // This function should never be called, we only ever write data with this
+ // client.
+ assert(false);
+ }
+
+ void OnWriteComplete(Status aStatus) override { Done(aStatus); }
+
+ private:
+ explicit WriteRecordClient(const vector<uint8_t>& aData,
+ function<void()>&& aOnSuccess,
+ function<void()>&& aOnFailure)
+ : mFileIO(nullptr),
+ mOnSuccess(std::move(aOnSuccess)),
+ mOnFailure(std::move(aOnFailure)),
+ mData(aData) {}
+
+ void Do(const string& aName, Host_10* aHost) {
+ // Initialize the FileIO.
+ mFileIO = aHost->CreateFileIO(this);
+ mFileIO->Open(aName.c_str(), aName.size());
+ }
+
+ void Done(cdm::FileIOClient::Status aStatus) {
+ // Note: Call Close() before running continuation, in case the
+ // continuation tries to open the same record; if we call Close()
+ // after running the continuation, the Close() call will arrive
+ // just after the Open() call succeeds, immediately closing the
+ // record we just opened.
+ if (mFileIO) {
+ mFileIO->Close();
+ }
+
+ if (IO_SUCCEEDED(aStatus)) {
+ mOnSuccess();
+ } else {
+ mOnFailure();
+ }
+
+ delete this;
+ }
+
+ FileIO* mFileIO = nullptr;
+
+ function<void()> mOnSuccess;
+ function<void()> mOnFailure;
+
+ const vector<uint8_t> mData;
+};
+
+void WriteData(Host_10* aHost, string& aRecordName,
+ const vector<uint8_t>& aData, function<void()>&& aOnSuccess,
+ function<void()>&& aOnFailure) {
+ WriteRecordClient::Write(aHost, aRecordName, aData, std::move(aOnSuccess),
+ std::move(aOnFailure));
+}
+
+class ReadRecordClient : public FileIOClient {
+ public:
+ /*
+ * This function will take the memory ownership of the parameters and
+ * delete them when done.
+ */
+ static void Read(Host_10* aHost, string& aRecordName,
+ function<void(const uint8_t*, uint32_t)>&& aOnSuccess,
+ function<void()>&& aOnFailure) {
+ (new ReadRecordClient(std::move(aOnSuccess), std::move(aOnFailure)))
+ ->Do(aRecordName, aHost);
+ }
+
+ void OnOpenComplete(Status aStatus) override {
+ auto err = aStatus;
+ if (aStatus != Status::kSuccess) {
+ Done(err, nullptr, 0);
+ } else {
+ mFileIO->Read();
+ }
+ }
+
+ void OnReadComplete(Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ Done(aStatus, aData, aDataSize);
+ }
+
+ void OnWriteComplete(Status aStatus) override {
+ // We should never reach here, this client only ever reads data.
+ assert(false);
+ }
+
+ private:
+ explicit ReadRecordClient(
+ function<void(const uint8_t*, uint32_t)>&& aOnSuccess,
+ function<void()>&& aOnFailure)
+ : mFileIO(nullptr),
+ mOnSuccess(std::move(aOnSuccess)),
+ mOnFailure(std::move(aOnFailure)) {}
+
+ void Do(const string& aName, Host_10* aHost) {
+ mFileIO = aHost->CreateFileIO(this);
+ mFileIO->Open(aName.c_str(), aName.size());
+ }
+
+ void Done(cdm::FileIOClient::Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) {
+ // Note: Call Close() before running continuation, in case the
+ // continuation tries to open the same record; if we call Close()
+ // after running the continuation, the Close() call will arrive
+ // just after the Open() call succeeds, immediately closing the
+ // record we just opened.
+ if (mFileIO) {
+ mFileIO->Close();
+ }
+
+ if (IO_SUCCEEDED(aStatus)) {
+ mOnSuccess(aData, aDataSize);
+ } else {
+ mOnFailure();
+ }
+
+ delete this;
+ }
+
+ FileIO* mFileIO = nullptr;
+
+ function<void(const uint8_t*, uint32_t)> mOnSuccess;
+ function<void()> mOnFailure;
+};
+
+void ReadData(Host_10* aHost, string& aRecordName,
+ function<void(const uint8_t*, uint32_t)>&& aOnSuccess,
+ function<void()>&& aOnFailure) {
+ ReadRecordClient::Read(aHost, aRecordName, std::move(aOnSuccess),
+ std::move(aOnFailure));
+}
diff --git a/dom/media/eme/clearkey/ClearKeyStorage.h b/dom/media/eme/clearkey/ClearKeyStorage.h
new file mode 100644
index 0000000000..bd8e99b901
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyStorage.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyStorage_h__
+#define __ClearKeyStorage_h__
+
+#include <stdint.h>
+
+#include <functional>
+#include <string>
+#include <vector>
+
+#include "ClearKeySessionManager.h"
+
+#define IO_SUCCEEDED(x) ((x) == cdm::FileIOClient::Status::kSuccess)
+#define IO_FAILED(x) ((x) != cdm::FileIOClient::Status::kSuccess)
+
+// Writes data to a file and fires the appropriate callback when complete.
+void WriteData(cdm::Host_10* aHost, std::string& aRecordName,
+ const std::vector<uint8_t>& aData,
+ std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure);
+
+// Reads data from a file and fires the appropriate callback when complete.
+void ReadData(cdm::Host_10* aHost, std::string& aRecordName,
+ std::function<void(const uint8_t*, uint32_t)>&& aOnSuccess,
+ std::function<void()>&& aOnFailure);
+
+#endif // __ClearKeyStorage_h__
diff --git a/dom/media/eme/clearkey/ClearKeyUtils.cpp b/dom/media/eme/clearkey/ClearKeyUtils.cpp
new file mode 100644
index 0000000000..3f9588bdf9
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyUtils.cpp
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ClearKeyUtils.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <memory.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <algorithm>
+#include <cctype>
+#include <memory>
+#include <sstream>
+#include <vector>
+
+#include "pk11pub.h"
+#include "prerror.h"
+#include "secmodt.h"
+
+#include "ArrayUtils.h"
+#include "BigEndian.h"
+#include "ClearKeyBase64.h"
+#include "mozilla/Sprintf.h"
+#include "psshparser/PsshParser.h"
+
+using namespace cdm;
+using std::string;
+using std::stringstream;
+using std::vector;
+
+struct DeleteHelper {
+ void operator()(PK11Context* value) { PK11_DestroyContext(value, true); }
+ void operator()(PK11SlotInfo* value) { PK11_FreeSlot(value); }
+ void operator()(PK11SymKey* value) { PK11_FreeSymKey(value); }
+};
+
+template <class T>
+struct MaybeDeleteHelper {
+ void operator()(T* ptr) {
+ if (ptr) {
+ DeleteHelper del;
+ del(ptr);
+ }
+ }
+};
+
+void CK_Log(const char* aFmt, ...) {
+ FILE* out = stdout;
+
+ if (getenv("CLEARKEY_LOG_FILE")) {
+ out = fopen(getenv("CLEARKEY_LOG_FILE"), "a");
+ }
+
+ va_list ap;
+
+ va_start(ap, aFmt);
+ const size_t len = 1024;
+ char buf[len];
+ VsprintfLiteral(buf, aFmt, ap);
+ va_end(ap);
+
+ fprintf(out, "%s\n", buf);
+ fflush(out);
+
+ if (out != stdout) {
+ fclose(out);
+ }
+}
+
+static bool PrintableAsString(const uint8_t* aBytes, uint32_t aLength) {
+ return std::all_of(aBytes, aBytes + aLength,
+ [](uint8_t c) { return isprint(c) == 1; });
+}
+
+void CK_LogArray(const char* prepend, const uint8_t* aData,
+ const uint32_t aDataSize) {
+ // If the data is valid ascii, use that. Otherwise print the hex
+ string data = PrintableAsString(aData, aDataSize)
+ ? string(aData, aData + aDataSize)
+ : ClearKeyUtils::ToHexString(aData, aDataSize);
+
+ CK_LOGD("%s%s", prepend, data.c_str());
+}
+
+/* static */
+bool ClearKeyUtils::DecryptCbcs(const vector<uint8_t>& aKey,
+ const vector<uint8_t>& aIV,
+ mozilla::Span<uint8_t> aSubsample,
+ uint32_t aCryptByteBlock,
+ uint32_t aSkipByteBlock) {
+ if (aKey.size() != CENC_KEY_LEN || aIV.size() != CENC_KEY_LEN) {
+ CK_LOGE("Key and IV size should be 16!");
+ return false;
+ }
+
+ std::unique_ptr<PK11SlotInfo, MaybeDeleteHelper<PK11SlotInfo>> slot(
+ PK11_GetInternalKeySlot());
+
+ if (!slot.get()) {
+ CK_LOGE("Failed to get internal PK11 slot");
+ return false;
+ }
+
+ SECItem keyItem = {siBuffer, (unsigned char*)&aKey[0], CENC_KEY_LEN};
+ SECItem ivItem = {siBuffer, (unsigned char*)&aIV[0], CENC_KEY_LEN};
+
+ std::unique_ptr<PK11SymKey, MaybeDeleteHelper<PK11SymKey>> key(
+ PK11_ImportSymKey(slot.get(), CKM_AES_CBC, PK11_OriginUnwrap, CKA_DECRYPT,
+ &keyItem, nullptr));
+
+ if (!key.get()) {
+ CK_LOGE("Failed to import sym key");
+ return false;
+ }
+
+ std::unique_ptr<PK11Context, MaybeDeleteHelper<PK11Context>> ctx(
+ PK11_CreateContextBySymKey(CKM_AES_CBC, CKA_DECRYPT, key.get(), &ivItem));
+
+ if (!ctx) {
+ CK_LOGE("Failed to get PK11Context!");
+ return false;
+ }
+
+ assert(aCryptByteBlock <= 0xFF);
+ assert(aSkipByteBlock <= 0xFF);
+
+ uint8_t* encryptedSubsample = &aSubsample[0];
+ const uint32_t BLOCK_SIZE = 16;
+ const uint32_t skipBytes = aSkipByteBlock * BLOCK_SIZE;
+ const uint32_t totalBlocks = aSubsample.Length() / BLOCK_SIZE;
+ uint32_t blocksProcessed = 0;
+
+ if (aSkipByteBlock == 0) {
+ // ISO/IEC 23001 - 7 Section 9.6.1
+ // 'When the fields default_crypt_byte_block and default_skip_byte_block in
+ // a version 1 Track Encryption Box('tenc') are non - zero numbers, pattern
+ // encryption SHALL be applied.'
+ // So if both are 0, then everything is encrypted. Similarly, if skip is 0
+ // and crypt is non-0, everything is encrypted.
+ // In this case we can just decrypt all the blocks in one call. This is the
+ // same outcome as decrypting them using smaller steps, as either way the
+ // CBC result should be the same.
+ MOZ_ASSERT(skipBytes == 0);
+ aCryptByteBlock = totalBlocks;
+ }
+
+ while (blocksProcessed < totalBlocks) {
+ uint32_t blocksToDecrypt = aCryptByteBlock <= totalBlocks - blocksProcessed
+ ? aCryptByteBlock
+ : totalBlocks - blocksProcessed;
+ uint32_t bytesToDecrypt = blocksToDecrypt * BLOCK_SIZE;
+ int outLen;
+ SECStatus rv;
+ rv = PK11_CipherOp(ctx.get(), encryptedSubsample, &outLen, bytesToDecrypt,
+ encryptedSubsample, bytesToDecrypt);
+ if (rv != SECSuccess) {
+ CK_LOGE("PK11_CipherOp() failed");
+ return false;
+ }
+
+ encryptedSubsample += skipBytes + bytesToDecrypt;
+ blocksProcessed += aSkipByteBlock + blocksToDecrypt;
+ }
+
+ return true;
+}
+
+/* static */
+bool ClearKeyUtils::DecryptAES(const vector<uint8_t>& aKey,
+ vector<uint8_t>& aData, vector<uint8_t>& aIV) {
+ assert(aIV.size() == CENC_KEY_LEN);
+ assert(aKey.size() == CENC_KEY_LEN);
+
+ PK11SlotInfo* slot = PK11_GetInternalKeySlot();
+ if (!slot) {
+ CK_LOGE("Failed to get internal PK11 slot");
+ return false;
+ }
+
+ SECItem keyItem = {siBuffer, (unsigned char*)&aKey[0], CENC_KEY_LEN};
+ PK11SymKey* key = PK11_ImportSymKey(slot, CKM_AES_CTR, PK11_OriginUnwrap,
+ CKA_ENCRYPT, &keyItem, nullptr);
+ PK11_FreeSlot(slot);
+ if (!key) {
+ CK_LOGE("Failed to import sym key");
+ return false;
+ }
+
+ CK_AES_CTR_PARAMS params;
+ params.ulCounterBits = 32;
+ memcpy(&params.cb, &aIV[0], CENC_KEY_LEN);
+ SECItem paramItem = {siBuffer, (unsigned char*)&params,
+ sizeof(CK_AES_CTR_PARAMS)};
+
+ unsigned int outLen = 0;
+ auto rv = PK11_Decrypt(key, CKM_AES_CTR, &paramItem, &aData[0], &outLen,
+ aData.size(), &aData[0], aData.size());
+
+ aData.resize(outLen);
+ PK11_FreeSymKey(key);
+
+ if (rv != SECSuccess) {
+ CK_LOGE("PK11_Decrypt() failed");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * ClearKey expects all Key IDs to be base64 encoded with non-standard alphabet
+ * and padding.
+ */
+static bool EncodeBase64Web(vector<uint8_t> aBinary, string& aEncoded) {
+ const char sAlphabet[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ const uint8_t sMask = 0x3f;
+
+ aEncoded.resize((aBinary.size() * 8 + 5) / 6);
+
+ // Pad binary data in case there's rubbish past the last byte.
+ aBinary.push_back(0);
+
+ // Number of bytes not consumed in the previous character
+ uint32_t shift = 0;
+
+ auto out = aEncoded.begin();
+ auto data = aBinary.begin();
+ for (string::size_type i = 0; i < aEncoded.length(); i++) {
+ if (shift) {
+ out[i] = (*data << (6 - shift)) & sMask;
+ data++;
+ } else {
+ out[i] = 0;
+ }
+
+ out[i] += (*data >> (shift + 2)) & sMask;
+ shift = (shift + 2) % 8;
+
+ // Cast idx to size_t before using it as an array-index,
+ // to pacify clang 'Wchar-subscripts' warning:
+ size_t idx = static_cast<size_t>(out[i]);
+
+ // out of bounds index for 'sAlphabet'
+ assert(idx < MOZ_ARRAY_LENGTH(sAlphabet));
+ out[i] = sAlphabet[idx];
+ }
+
+ return true;
+}
+
+/* static */
+void ClearKeyUtils::MakeKeyRequest(const vector<KeyId>& aKeyIDs,
+ string& aOutRequest,
+ SessionType aSessionType) {
+ assert(!aKeyIDs.empty() && aOutRequest.empty());
+
+ aOutRequest.append("{\"kids\":[");
+ for (size_t i = 0; i < aKeyIDs.size(); i++) {
+ if (i) {
+ aOutRequest.append(",");
+ }
+ aOutRequest.append("\"");
+
+ string base64key;
+ EncodeBase64Web(aKeyIDs[i], base64key);
+ aOutRequest.append(base64key);
+
+ aOutRequest.append("\"");
+ }
+ aOutRequest.append("],\"type\":");
+
+ aOutRequest.append("\"");
+ aOutRequest.append(SessionTypeToString(aSessionType));
+ aOutRequest.append("\"}");
+}
+
+#define EXPECT_SYMBOL(CTX, X) \
+ do { \
+ if (GetNextSymbol(CTX) != (X)) { \
+ CK_LOGE("Unexpected symbol in JWK parser"); \
+ return false; \
+ } \
+ } while (false)
+
+struct ParserContext {
+ const uint8_t* mIter;
+ const uint8_t* mEnd;
+};
+
+static uint8_t PeekSymbol(ParserContext& aCtx) {
+ for (; aCtx.mIter < aCtx.mEnd; (aCtx.mIter)++) {
+ if (!isspace(*aCtx.mIter)) {
+ return *aCtx.mIter;
+ }
+ }
+
+ return 0;
+}
+
+static uint8_t GetNextSymbol(ParserContext& aCtx) {
+ uint8_t sym = PeekSymbol(aCtx);
+ aCtx.mIter++;
+ return sym;
+}
+
+static bool SkipToken(ParserContext& aCtx);
+
+static bool SkipString(ParserContext& aCtx) {
+ EXPECT_SYMBOL(aCtx, '"');
+ for (uint8_t sym = GetNextSymbol(aCtx); sym; sym = GetNextSymbol(aCtx)) {
+ if (sym == '\\') {
+ sym = GetNextSymbol(aCtx);
+ if (!sym) {
+ return false;
+ }
+ } else if (sym == '"') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Skip whole object and values it contains.
+ */
+static bool SkipObject(ParserContext& aCtx) {
+ EXPECT_SYMBOL(aCtx, '{');
+
+ if (PeekSymbol(aCtx) == '}') {
+ GetNextSymbol(aCtx);
+ return true;
+ }
+
+ while (true) {
+ if (!SkipString(aCtx)) return false;
+ EXPECT_SYMBOL(aCtx, ':');
+ if (!SkipToken(aCtx)) return false;
+
+ if (PeekSymbol(aCtx) == '}') {
+ GetNextSymbol(aCtx);
+ return true;
+ }
+ EXPECT_SYMBOL(aCtx, ',');
+ }
+}
+
+/**
+ * Skip array value and the values it contains.
+ */
+static bool SkipArray(ParserContext& aCtx) {
+ EXPECT_SYMBOL(aCtx, '[');
+
+ if (PeekSymbol(aCtx) == ']') {
+ GetNextSymbol(aCtx);
+ return true;
+ }
+
+ while (SkipToken(aCtx)) {
+ if (PeekSymbol(aCtx) == ']') {
+ GetNextSymbol(aCtx);
+ return true;
+ }
+ EXPECT_SYMBOL(aCtx, ',');
+ }
+
+ return false;
+}
+
+/**
+ * Skip unquoted literals like numbers, |true|, and |null|.
+ * (XXX and anything else that matches /([:alnum:]|[+-.])+/)
+ */
+static bool SkipLiteral(ParserContext& aCtx) {
+ for (; aCtx.mIter < aCtx.mEnd; aCtx.mIter++) {
+ if (!isalnum(*aCtx.mIter) && *aCtx.mIter != '.' && *aCtx.mIter != '-' &&
+ *aCtx.mIter != '+') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool SkipToken(ParserContext& aCtx) {
+ uint8_t startSym = PeekSymbol(aCtx);
+ if (startSym == '"') {
+ CK_LOGD("JWK parser skipping string");
+ return SkipString(aCtx);
+ } else if (startSym == '{') {
+ CK_LOGD("JWK parser skipping object");
+ return SkipObject(aCtx);
+ } else if (startSym == '[') {
+ CK_LOGD("JWK parser skipping array");
+ return SkipArray(aCtx);
+ } else {
+ CK_LOGD("JWK parser skipping literal");
+ return SkipLiteral(aCtx);
+ }
+}
+
+static bool GetNextLabel(ParserContext& aCtx, string& aOutLabel) {
+ EXPECT_SYMBOL(aCtx, '"');
+
+ const uint8_t* start = aCtx.mIter;
+ for (uint8_t sym = GetNextSymbol(aCtx); sym; sym = GetNextSymbol(aCtx)) {
+ if (sym == '\\') {
+ GetNextSymbol(aCtx);
+ continue;
+ }
+
+ if (sym == '"') {
+ aOutLabel.assign(start, aCtx.mIter - 1);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool ParseKeyObject(ParserContext& aCtx, KeyIdPair& aOutKey) {
+ EXPECT_SYMBOL(aCtx, '{');
+
+ // Reject empty objects as invalid licenses.
+ if (PeekSymbol(aCtx) == '}') {
+ GetNextSymbol(aCtx);
+ return false;
+ }
+
+ string keyId;
+ string key;
+
+ while (true) {
+ string label;
+ string value;
+
+ if (!GetNextLabel(aCtx, label)) {
+ return false;
+ }
+
+ EXPECT_SYMBOL(aCtx, ':');
+ if (label == "kty") {
+ if (!GetNextLabel(aCtx, value)) return false;
+ // By spec, type must be "oct".
+ if (value != "oct") return false;
+ } else if (label == "k" && PeekSymbol(aCtx) == '"') {
+ // if this isn't a string we will fall through to the SkipToken() path.
+ if (!GetNextLabel(aCtx, key)) return false;
+ } else if (label == "kid" && PeekSymbol(aCtx) == '"') {
+ if (!GetNextLabel(aCtx, keyId)) return false;
+ } else {
+ if (!SkipToken(aCtx)) return false;
+ }
+
+ uint8_t sym = PeekSymbol(aCtx);
+ if (!sym || sym == '}') {
+ break;
+ }
+ EXPECT_SYMBOL(aCtx, ',');
+ }
+
+ return !key.empty() && !keyId.empty() &&
+ DecodeBase64(keyId, aOutKey.mKeyId) &&
+ DecodeBase64(key, aOutKey.mKey) && GetNextSymbol(aCtx) == '}';
+}
+
+static bool ParseKeys(ParserContext& aCtx, vector<KeyIdPair>& aOutKeys) {
+ // Consume start of array.
+ EXPECT_SYMBOL(aCtx, '[');
+
+ while (true) {
+ KeyIdPair key;
+ if (!ParseKeyObject(aCtx, key)) {
+ CK_LOGE("Failed to parse key object");
+ return false;
+ }
+
+ assert(!key.mKey.empty() && !key.mKeyId.empty());
+ aOutKeys.push_back(key);
+
+ uint8_t sym = PeekSymbol(aCtx);
+ if (!sym || sym == ']') {
+ break;
+ }
+
+ EXPECT_SYMBOL(aCtx, ',');
+ }
+
+ return GetNextSymbol(aCtx) == ']';
+}
+
+/* static */
+bool ClearKeyUtils::ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize,
+ vector<KeyIdPair>& aOutKeys,
+ SessionType aSessionType) {
+ ParserContext ctx;
+ ctx.mIter = aKeyData;
+ ctx.mEnd = aKeyData + aKeyDataSize;
+
+ // Consume '{' from start of object.
+ EXPECT_SYMBOL(ctx, '{');
+
+ while (true) {
+ string label;
+ // Consume member key.
+ if (!GetNextLabel(ctx, label)) return false;
+ EXPECT_SYMBOL(ctx, ':');
+
+ if (label == "keys") {
+ // Parse "keys" array.
+ if (!ParseKeys(ctx, aOutKeys)) return false;
+ } else if (label == "type") {
+ // Consume type string.
+ string type;
+ if (!GetNextLabel(ctx, type)) return false;
+ if (type != SessionTypeToString(aSessionType)) {
+ return false;
+ }
+ } else {
+ SkipToken(ctx);
+ }
+
+ // Check for end of object.
+ if (PeekSymbol(ctx) == '}') {
+ break;
+ }
+
+ // Consume ',' between object members.
+ EXPECT_SYMBOL(ctx, ',');
+ }
+
+ // Consume '}' from end of object.
+ EXPECT_SYMBOL(ctx, '}');
+
+ return true;
+}
+
+static bool ParseKeyIds(ParserContext& aCtx, vector<KeyId>& aOutKeyIds) {
+ // Consume start of array.
+ EXPECT_SYMBOL(aCtx, '[');
+
+ while (true) {
+ string label;
+ vector<uint8_t> keyId;
+ if (!GetNextLabel(aCtx, label) || !DecodeBase64(label, keyId)) {
+ return false;
+ }
+ if (!keyId.empty() && keyId.size() <= kMaxKeyIdsLength) {
+ aOutKeyIds.push_back(keyId);
+ }
+
+ uint8_t sym = PeekSymbol(aCtx);
+ if (!sym || sym == ']') {
+ break;
+ }
+
+ EXPECT_SYMBOL(aCtx, ',');
+ }
+
+ return GetNextSymbol(aCtx) == ']';
+}
+
+/* static */
+bool ClearKeyUtils::ParseKeyIdsInitData(const uint8_t* aInitData,
+ uint32_t aInitDataSize,
+ vector<KeyId>& aOutKeyIds) {
+ ParserContext ctx;
+ ctx.mIter = aInitData;
+ ctx.mEnd = aInitData + aInitDataSize;
+
+ // Consume '{' from start of object.
+ EXPECT_SYMBOL(ctx, '{');
+
+ while (true) {
+ string label;
+ // Consume member kids.
+ if (!GetNextLabel(ctx, label)) return false;
+ EXPECT_SYMBOL(ctx, ':');
+
+ if (label == "kids") {
+ // Parse "kids" array.
+ if (!ParseKeyIds(ctx, aOutKeyIds) || aOutKeyIds.empty()) {
+ return false;
+ }
+ } else {
+ SkipToken(ctx);
+ }
+
+ // Check for end of object.
+ if (PeekSymbol(ctx) == '}') {
+ break;
+ }
+
+ // Consume ',' between object members.
+ EXPECT_SYMBOL(ctx, ',');
+ }
+
+ // Consume '}' from end of object.
+ EXPECT_SYMBOL(ctx, '}');
+
+ return true;
+}
+
+/* static */ const char* ClearKeyUtils::SessionTypeToString(
+ SessionType aSessionType) {
+ switch (aSessionType) {
+ case SessionType::kTemporary:
+ return "temporary";
+ case SessionType::kPersistentLicense:
+ return "persistent-license";
+ default: {
+ // We don't support any other license types.
+ assert(false);
+ return "invalid";
+ }
+ }
+}
+
+/* static */
+bool ClearKeyUtils::IsValidSessionId(const char* aBuff, uint32_t aLength) {
+ if (aLength > 10) {
+ // 10 is the max number of characters in UINT32_MAX when
+ // represented as a string; ClearKey session ids are integers.
+ return false;
+ }
+ for (uint32_t i = 0; i < aLength; i++) {
+ if (!isdigit(aBuff[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+string ClearKeyUtils::ToHexString(const uint8_t* aBytes, uint32_t aLength) {
+ stringstream ss;
+ ss << std::showbase << std::uppercase << std::hex;
+ for (uint32_t i = 0; i < aLength; ++i) {
+ ss << std::hex << static_cast<uint32_t>(aBytes[i]);
+ ss << " ";
+ }
+
+ return ss.str();
+}
diff --git a/dom/media/eme/clearkey/ClearKeyUtils.h b/dom/media/eme/clearkey/ClearKeyUtils.h
new file mode 100644
index 0000000000..04c227c71d
--- /dev/null
+++ b/dom/media/eme/clearkey/ClearKeyUtils.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __ClearKeyUtils_h__
+#define __ClearKeyUtils_h__
+
+#include <assert.h>
+// stdef.h is required for content_decryption_module to work on Unix systems.
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "content_decryption_module.h"
+#include "pk11pub.h"
+
+#include "mozilla/Span.h"
+
+#if 0
+void CK_Log(const char* aFmt, ...);
+# define CK_LOGE(...) CK_Log(__VA_ARGS__)
+# define CK_LOGD(...) CK_Log(__VA_ARGS__)
+# define CK_LOGW(...) CK_Log(__VA_ARGS__)
+# define CK_LOGARRAY(APREPEND, ADATA, ADATA_SIZE) \
+ CK_LogArray(APREPEND, ADATA, ADATA_SIZE)
+#else
+// Note: Enabling logging slows things down a LOT, especially when logging to
+// a file.
+# define CK_LOGE(...)
+# define CK_LOGD(...)
+# define CK_LOGW(...)
+# define CK_LOGARRAY(APREPEND, ADATA, ADATA_SIZE)
+#endif
+
+typedef std::vector<uint8_t> KeyId;
+typedef std::vector<uint8_t> Key;
+
+// The session response size should be within a reasonable limit.
+// The size 64 KB is referenced from web-platform-test.
+static const uint32_t kMaxSessionResponseLength = 65536;
+
+// Provide limitation for KeyIds length and webm initData size.
+static const uint32_t kMaxWebmInitDataSize = 65536;
+static const uint32_t kMaxKeyIdsLength = 512;
+
+void CK_LogArray(const char* aPrepend, const uint8_t* aData,
+ const uint32_t aDataSize);
+
+struct KeyIdPair {
+ KeyId mKeyId;
+ Key mKey;
+};
+
+class ClearKeyUtils {
+ public:
+ static bool DecryptCbcs(const std::vector<uint8_t>& aKey,
+ const std::vector<uint8_t>& aIV,
+ mozilla::Span<uint8_t> aSubsampleEncryptedRange,
+ uint32_t aCryptByteBlocks, uint32_t aSkipByteBlocks);
+
+ static bool DecryptAES(const std::vector<uint8_t>& aKey,
+ std::vector<uint8_t>& aData,
+ std::vector<uint8_t>& aIV);
+
+ static bool ParseKeyIdsInitData(const uint8_t* aInitData,
+ uint32_t aInitDataSize,
+ std::vector<KeyId>& aOutKeyIds);
+
+ static void MakeKeyRequest(const std::vector<KeyId>& aKeyIds,
+ std::string& aOutRequest,
+ cdm::SessionType aSessionType);
+
+ static bool ParseJWK(const uint8_t* aKeyData, uint32_t aKeyDataSize,
+ std::vector<KeyIdPair>& aOutKeys,
+ cdm::SessionType aSessionType);
+ static const char* SessionTypeToString(cdm::SessionType aSessionType);
+
+ static bool IsValidSessionId(const char* aBuff, uint32_t aLength);
+
+ static std::string ToHexString(const uint8_t* aBytes, uint32_t aLength);
+};
+
+template <class Container, class Element>
+inline bool Contains(const Container& aContainer, const Element& aElement) {
+ return aContainer.find(aElement) != aContainer.end();
+}
+
+template <typename T>
+inline void Assign(std::vector<T>& aVec, const T* aData, size_t aLength) {
+ aVec.assign(aData, aData + aLength);
+}
+
+#endif // __ClearKeyUtils_h__
diff --git a/dom/media/eme/clearkey/RefCounted.h b/dom/media/eme/clearkey/RefCounted.h
new file mode 100644
index 0000000000..25f308164c
--- /dev/null
+++ b/dom/media/eme/clearkey/RefCounted.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __RefCount_h__
+#define __RefCount_h__
+
+#include <assert.h>
+#include <stdint.h>
+
+#include <atomic>
+
+#include "ClearKeyUtils.h"
+
+// Note: Thread safe.
+class RefCounted {
+ public:
+ void AddRef() { ++mRefCount; }
+
+ uint32_t Release() {
+ uint32_t newCount = --mRefCount;
+ if (!newCount) {
+ delete this;
+ }
+ return newCount;
+ }
+
+ protected:
+ RefCounted() : mRefCount(0) {}
+ virtual ~RefCounted() { assert(!mRefCount); }
+ std::atomic<uint32_t> mRefCount;
+};
+
+template <class T>
+class RefPtr {
+ public:
+ RefPtr(const RefPtr& src) { Set(src.mPtr); }
+
+ explicit RefPtr(T* aPtr) { Set(aPtr); }
+ RefPtr() { Set(nullptr); }
+
+ ~RefPtr() { Set(nullptr); }
+ T* operator->() const { return mPtr; }
+ T** operator&() { return &mPtr; }
+ T* operator->() { return mPtr; }
+ operator T*() { return mPtr; }
+
+ T* Get() const { return mPtr; }
+
+ RefPtr& operator=(T* aVal) {
+ Set(aVal);
+ return *this;
+ }
+
+ private:
+ T* Set(T* aPtr) {
+ if (mPtr == aPtr) {
+ return aPtr;
+ }
+ if (mPtr) {
+ mPtr->Release();
+ }
+ mPtr = aPtr;
+ if (mPtr) {
+ aPtr->AddRef();
+ }
+ return mPtr;
+ }
+
+ T* mPtr = nullptr;
+};
+
+#endif // __RefCount_h__
diff --git a/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp
new file mode 100644
index 0000000000..bff6d0f356
--- /dev/null
+++ b/dom/media/eme/clearkey/gtest/TestClearKeyUtils.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include <algorithm>
+#include <stdint.h>
+#include <vector>
+
+#include "../ClearKeyBase64.cpp"
+
+using namespace std;
+
+struct B64Test {
+ string b64;
+ vector<uint8_t> raw;
+ bool shouldPass;
+};
+
+const B64Test tests[] = {
+ {"AAAAADk4AU4AAAAAAAAAAA",
+ {0x0, 0x0, 0x0, 0x0, 0x39, 0x38, 0x1, 0x4e, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0},
+ true},
+ {"h2mqp1zAJjDIC34YXEXBxA==",
+ {0x87, 0x69, 0xaa, 0xa7, 0x5c, 0xc0, 0x26, 0x30, 0xc8, 0xb, 0x7e, 0x18,
+ 0x5c, 0x45, 0xc1, 0xc4},
+ true},
+ {"flcdA35XHQN-Vx0DflcdAw",
+ {0x7e, 0x57, 0x1d, 0x3, 0x7e, 0x57, 0x1d, 0x3, 0x7e, 0x57, 0x1d, 0x3, 0x7e,
+ 0x57, 0x1d, 0x3},
+ true},
+ {
+ "flczM35XMzN-VzMzflczMw",
+ {0x7e, 0x57, 0x33, 0x33, 0x7e, 0x57, 0x33, 0x33, 0x7e, 0x57, 0x33, 0x33,
+ 0x7e, 0x57, 0x33, 0x33},
+ true,
+ },
+ {"flcdBH5XHQR-Vx0EflcdBA",
+ {0x7e, 0x57, 0x1d, 0x4, 0x7e, 0x57, 0x1d, 0x4, 0x7e, 0x57, 0x1d, 0x4, 0x7e,
+ 0x57, 0x1d, 0x4},
+ true},
+ {"fldERH5XRER-V0REfldERA",
+ {0x7e, 0x57, 0x44, 0x44, 0x7e, 0x57, 0x44, 0x44, 0x7e, 0x57, 0x44, 0x44,
+ 0x7e, 0x57, 0x44, 0x44},
+ true},
+ {"fuzzbiz=", {0x7e, 0xec, 0xf3, 0x6e, 0x2c}, true},
+ {"fuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbizfuzzbiz",
+ {0x7e, 0xec, 0xf3, 0x6e, 0x2c, 0xdf, 0xbb, 0x3c, 0xdb, 0x8b, 0x37,
+ 0xee, 0xcf, 0x36, 0xe2, 0xcd, 0xfb, 0xb3, 0xcd, 0xb8, 0xb3, 0x7e,
+ 0xec, 0xf3, 0x6e, 0x2c, 0xdf, 0xbb, 0x3c, 0xdb, 0x8b, 0x37, 0xee,
+ 0xcf, 0x36, 0xe2, 0xcd, 0xfb, 0xb3, 0xcd, 0xb8, 0xb3},
+ true},
+ {"", {}, true},
+ {"00", {0xd3}, true},
+ {"000", {0xd3, 0x4d}, true},
+
+ {"invalid", {0x8a, 0x7b, 0xda, 0x96, 0x27}, true},
+ {"invalic", {0x8a, 0x7b, 0xda, 0x96, 0x27}, true},
+
+ // Failure tests
+ {"A", {}, false}, // 1 character is too few.
+ {"_", {}, false}, // 1 character is too few.
+};
+
+TEST(ClearKey, DecodeBase64)
+{
+ for (const B64Test& test : tests) {
+ vector<uint8_t> v;
+ bool rv = DecodeBase64(string(test.b64), v);
+ EXPECT_EQ(test.shouldPass, rv);
+ if (test.shouldPass) {
+ EXPECT_EQ(test.raw.size(), v.size());
+ for (size_t k = 0; k < test.raw.size(); k++) {
+ EXPECT_EQ(test.raw[k], v[k]);
+ }
+ }
+ }
+}
diff --git a/dom/media/eme/clearkey/gtest/moz.build b/dom/media/eme/clearkey/gtest/moz.build
new file mode 100644
index 0000000000..5771c4724c
--- /dev/null
+++ b/dom/media/eme/clearkey/gtest/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestClearKeyUtils.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "..",
+]
diff --git a/dom/media/eme/clearkey/moz.build b/dom/media/eme/clearkey/moz.build
new file mode 100644
index 0000000000..57160624ea
--- /dev/null
+++ b/dom/media/eme/clearkey/moz.build
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "ArrayUtils.h",
+ "BigEndian.h",
+ "ClearKeyBase64.h",
+ "ClearKeyDecryptionManager.h",
+ "ClearKeyPersistence.h",
+ "ClearKeySession.h",
+ "ClearKeySessionManager.h",
+ "ClearKeyStorage.h",
+ "ClearKeyUtils.h",
+ "RefCounted.h",
+]
+
+UNIFIED_SOURCES += [
+ "ClearKeyBase64.cpp",
+ "ClearKeyDecryptionManager.cpp",
+ "ClearKeyPersistence.cpp",
+ "ClearKeySession.cpp",
+ "ClearKeySessionManager.cpp",
+ "ClearKeyStorage.cpp",
+ "ClearKeyUtils.cpp",
+]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+USE_LIBS += [
+ "nss",
+ "psshparser",
+]
+
+Library("gecko-clearkey")
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp
new file mode 100644
index 0000000000..e5431f50fd
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDrmCDMCallbackProxy.h"
+#include "MediaDrmCDMProxy.h"
+#include "nsString.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "nsContentCID.h"
+#include "nsServiceManagerUtils.h"
+#include "MainThreadUtils.h"
+#include "mozilla/EMEUtils.h"
+
+namespace mozilla {
+
+MediaDrmCDMCallbackProxy::MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy)
+ : mProxy(aProxy) {}
+
+MediaDrmCDMCallbackProxy::~MediaDrmCDMCallbackProxy() = default;
+
+void MediaDrmCDMCallbackProxy::SetSessionId(uint32_t aToken,
+ const nsCString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnSetSessionId(aToken, NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnResolveLoadSessionPromise(aPromiseId, aSuccess);
+}
+
+void MediaDrmCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) {
+ // Note: CDMProxy proxies this from non-main threads to main thread.
+ mProxy->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmCDMCallbackProxy::RejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnRejectPromise(aPromiseId, std::move(aException), aMessage);
+}
+
+void MediaDrmCDMCallbackProxy::SessionMessage(
+ const nsCString& aSessionId, dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // For removing constness
+ nsTArray<uint8_t> message(aMessage.Clone());
+ mProxy->OnSessionMessage(NS_ConvertUTF8toUTF16(aSessionId), aMessageType,
+ message);
+}
+
+void MediaDrmCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnExpirationChange(NS_ConvertUTF8toUTF16(aSessionId), aExpiryTime);
+}
+
+void MediaDrmCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ keyStatusesChange =
+ caps->RemoveKeysForSession(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+ mProxy->OnSessionClosed(NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmCDMCallbackProxy::SessionError(const nsCString& aSessionId,
+ nsresult aException,
+ uint32_t aSystemCode,
+ const nsCString& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnSessionError(NS_ConvertUTF8toUTF16(aSessionId), aException,
+ aSystemCode, NS_ConvertUTF8toUTF16(aMessage));
+}
+
+void MediaDrmCDMCallbackProxy::BatchedKeyStatusChanged(
+ const nsCString& aSessionId, const nsTArray<CDMKeyInfo>& aKeyInfos) {
+ MOZ_ASSERT(NS_IsMainThread());
+ BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos);
+}
+
+void MediaDrmCDMCallbackProxy::BatchedKeyStatusChangedInternal(
+ const nsCString& aSessionId, const nsTArray<CDMKeyInfo>& aKeyInfos) {
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ for (size_t i = 0; i < aKeyInfos.Length(); i++) {
+ keyStatusesChange |= caps->SetKeyStatus(aKeyInfos[i].mKeyId,
+ NS_ConvertUTF8toUTF16(aSessionId),
+ aKeyInfos[i].mStatus);
+ }
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+}
+
+void MediaDrmCDMCallbackProxy::Decrypted(
+ uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event");
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h
new file mode 100644
index 0000000000..e963e21e11
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmCDMCallbackProxy_h_
+#define MediaDrmCDMCallbackProxy_h_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/DecryptorProxyCallback.h"
+
+namespace mozilla {
+class CDMProxy;
+class ErrorResult;
+class MediaDrmCDMProxy;
+
+// Proxies call backs from the MediaDrmProxy -> MediaDrmProxySupport back to the
+// MediaKeys object on the main thread. We used annotation calledFrom = "gecko"
+// to ensure running on main thread.
+class MediaDrmCDMCallbackProxy final : public DecryptorProxyCallback {
+ public:
+ void SetSessionId(uint32_t aCreateSessionToken,
+ const nsCString& aSessionId) override;
+
+ void ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override;
+
+ void ResolvePromise(uint32_t aPromiseId) override;
+
+ void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aSessionId) override;
+
+ void SessionMessage(const nsCString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void ExpirationChange(const nsCString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void SessionClosed(const nsCString& aSessionId) override;
+
+ void SessionError(const nsCString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsCString& aMessage) override;
+
+ void Decrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override;
+
+ void BatchedKeyStatusChanged(const nsCString& aSessionId,
+ const nsTArray<CDMKeyInfo>& aKeyInfos) override;
+
+ ~MediaDrmCDMCallbackProxy();
+
+ private:
+ friend class MediaDrmCDMProxy;
+ explicit MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy);
+
+ void BatchedKeyStatusChangedInternal(const nsCString& aSessionId,
+ const nsTArray<CDMKeyInfo>& aKeyInfos);
+ const RefPtr<MediaDrmCDMProxy> mProxy;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp
new file mode 100644
index 0000000000..6ef47a84c3
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/MediaDrmCDMProxy.h"
+#include "MediaDrmCDMCallbackProxy.h"
+
+namespace mozilla {
+
+MediaDrmSessionType ToMediaDrmSessionType(
+ dom::MediaKeySessionType aSessionType) {
+ switch (aSessionType) {
+ case dom::MediaKeySessionType::Temporary:
+ return kKeyStreaming;
+ case dom::MediaKeySessionType::Persistent_license:
+ return kKeyOffline;
+ default:
+ return kKeyStreaming;
+ };
+}
+
+MediaDrmCDMProxy::MediaDrmCDMProxy(dom::MediaKeys* aKeys,
+ const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired,
+ bool aPersistentStateRequired)
+ : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired,
+ aPersistentStateRequired),
+ mCDM(nullptr),
+ mShutdownCalled(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(MediaDrmCDMProxy);
+}
+
+MediaDrmCDMProxy::~MediaDrmCDMProxy() { MOZ_COUNT_DTOR(MediaDrmCDMProxy); }
+
+void MediaDrmCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ EME_LOG("MediaDrmCDMProxy::Init (%s, %s) %s",
+ NS_ConvertUTF16toUTF8(aOrigin).get(),
+ NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(),
+ NS_ConvertUTF16toUTF8(aName).get());
+
+ // Create a thread to work with cdm.
+ if (!mOwnerThread) {
+ nsresult rv =
+ NS_NewNamedThread("MDCDMThread", getter_AddRefs(mOwnerThread));
+ if (NS_FAILED(rv)) {
+ RejectPromiseWithStateError(
+ aPromiseId, nsLiteralCString(
+ "Couldn't create CDM thread MediaDrmCDMProxy::Init"));
+ return;
+ }
+ }
+
+ mCDM = mozilla::MakeUnique<MediaDrmProxySupport>(mKeySystem);
+ nsCOMPtr<nsIRunnable> task(
+ NewRunnableMethod<uint32_t>("MediaDrmCDMProxy::md_Init", this,
+ &MediaDrmCDMProxy::md_Init, aPromiseId));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+
+ UniquePtr<CreateSessionData> data(new CreateSessionData());
+ data->mSessionType = aSessionType;
+ data->mCreateSessionToken = aCreateSessionToken;
+ data->mPromiseId = aPromiseId;
+ data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType);
+ data->mInitData = std::move(aInitData);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<CreateSessionData>&&>(
+ "MediaDrmCDMProxy::md_CreateSession", this,
+ &MediaDrmCDMProxy::md_CreateSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) {
+ // TODO: Implement LoadSession.
+ RejectPromiseWithStateError(
+ aPromiseId, "Currently Fennec does not support LoadSession"_ns);
+}
+
+void MediaDrmCDMProxy::SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+
+ mOwnerThread->Dispatch(NewRunnableMethod<PromiseId, const nsTArray<uint8_t>>(
+ "MediaDrmCDMProxy::md_SetServerCertificate", this,
+ &MediaDrmCDMProxy::md_SetServerCertificate,
+ aPromiseId, std::move(aCert)),
+ NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::UpdateSession(const nsAString& aSessionId,
+ PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ UniquePtr<UpdateSessionData> data(new UpdateSessionData());
+ data->mPromiseId = aPromiseId;
+ data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
+ data->mResponse = std::move(aResponse);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<UpdateSessionData>&&>(
+ "MediaDrmCDMProxy::md_UpdateSession", this,
+ &MediaDrmCDMProxy::md_UpdateSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ UniquePtr<SessionOpData> data(new SessionOpData());
+ data->mPromiseId = aPromiseId;
+ data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<SessionOpData>&&>(
+ "MediaDrmCDMProxy::md_CloseSession", this,
+ &MediaDrmCDMProxy::md_CloseSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ // TODO: Implement RemoveSession.
+ RejectPromiseWithStateError(
+ aPromiseId, "Currently Fennec does not support RemoveSession"_ns);
+}
+
+void MediaDrmCDMProxy::QueryOutputProtectionStatus() {
+ // TODO(bryce): determine if this is needed for Android and implement as
+ // needed. See also `NotifyOutputProtectionStatus`.
+}
+
+void MediaDrmCDMProxy::NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) {
+ // TODO(bryce): determine if this is needed for Android and implement as
+ // needed. See also `QueryOutputProtectionStatus`.
+}
+
+void MediaDrmCDMProxy::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod(
+ "MediaDrmCDMProxy::md_Shutdown", this, &MediaDrmCDMProxy::md_Shutdown));
+
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+ mOwnerThread->Shutdown();
+ mOwnerThread = nullptr;
+ mKeys.Clear();
+}
+
+void MediaDrmCDMProxy::Terminated() {
+ // TODO: Implement Terminated.
+ // Should find a way to handle the case when remote side MediaDrm crashed.
+}
+
+void MediaDrmCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+
+ RefPtr<dom::MediaKeySession> session(
+ mKeys->GetPendingSession(aCreateSessionToken));
+ if (session) {
+ session->SetSessionId(aSessionId);
+ }
+}
+
+void MediaDrmCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ mKeys->OnSessionLoaded(aPromiseId, aSuccess);
+}
+
+void MediaDrmCDMProxy::OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyMessage(aMessageType, aMessage);
+ }
+}
+
+void MediaDrmCDMProxy::OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->SetExpiration(static_cast<double>(aExpiryTime));
+ }
+}
+
+void MediaDrmCDMProxy::OnSessionClosed(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->OnClosed();
+ }
+}
+
+void MediaDrmCDMProxy::OnSessionError(const nsAString& aSessionId,
+ nsresult aException, uint32_t aSystemCode,
+ const nsAString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyError(aSystemCode);
+ }
+}
+
+void MediaDrmCDMProxy::OnRejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromise(aPromiseId, std::move(aException), aMsg);
+}
+
+RefPtr<DecryptPromise> MediaDrmCDMProxy::Decrypt(MediaRawData* aSample) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypting individually");
+ return nullptr;
+}
+
+void MediaDrmCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event");
+}
+
+void MediaDrmCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->RejectPromise(aId, std::move(aException), aReason);
+ }
+ } else {
+ nsCOMPtr<nsIRunnable> task(
+ new RejectPromiseTask(this, aId, std::move(aException), aReason));
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+ }
+}
+
+void MediaDrmCDMProxy::RejectPromiseWithStateError(PromiseId aId,
+ const nsCString& aReason) {
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aReason);
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void MediaDrmCDMProxy::ResolvePromise(PromiseId aId) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromise(aId);
+ } else {
+ NS_WARNING("MediaDrmCDMProxy unable to resolve promise!");
+ }
+ } else {
+ nsCOMPtr<nsIRunnable> task;
+ task =
+ NewRunnableMethod<PromiseId>("MediaDrmCDMProxy::ResolvePromise", this,
+ &MediaDrmCDMProxy::ResolvePromise, aId);
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+ }
+}
+
+template <typename T>
+void MediaDrmCDMProxy::ResolvePromiseWithResult(PromiseId aId,
+ const T& aResult) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromiseWithResult(aId, aResult);
+ } else {
+ NS_WARNING("MediaDrmCDMProxy unable to resolve promise!");
+ }
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> task;
+ task = NewRunnableMethod<PromiseId, T>(
+ "MediaDrmCDMProxy::ResolvePromiseWithResult", this,
+ &MediaDrmCDMProxy::ResolvePromiseWithResult<T>, aId, aResult);
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyStatusesChange();
+ }
+}
+
+void MediaDrmCDMProxy::GetStatusForPolicy(
+ PromiseId aPromiseId, const dom::HDCPVersion& aMinHdcpVersion) {
+ // TODO: Implement GetStatusForPolicy.
+ constexpr auto err =
+ "Currently Fennec does not support GetStatusForPolicy"_ns;
+
+ ErrorResult rv;
+ rv.ThrowNotSupportedError(err);
+ RejectPromise(aPromiseId, std::move(rv), err);
+}
+
+#ifdef DEBUG
+bool MediaDrmCDMProxy::IsOnOwnerThread() {
+ return NS_GetCurrentThread() == mOwnerThread;
+}
+#endif
+
+const nsString& MediaDrmCDMProxy::GetMediaDrmStubId() const {
+ MOZ_ASSERT(mCDM);
+ return mCDM->GetMediaDrmStubId();
+}
+
+void MediaDrmCDMProxy::OnCDMCreated(uint32_t aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+
+ if (mCDM) {
+ mKeys->OnCDMCreated(aPromiseId, 0);
+ return;
+ }
+
+ // No CDM? Just reject the promise.
+ constexpr auto err = "Null CDM in OnCDMCreated()"_ns;
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(err);
+ mKeys->RejectPromise(aPromiseId, std::move(rv), err);
+}
+
+void MediaDrmCDMProxy::md_Init(uint32_t aPromiseId) {
+ MOZ_ASSERT(IsOnOwnerThread());
+ MOZ_ASSERT(mCDM);
+
+ UniquePtr<MediaDrmCDMCallbackProxy> callback(
+ new MediaDrmCDMCallbackProxy(this));
+ mCDM->Init(std::move(callback));
+ nsCOMPtr<nsIRunnable> task(
+ NewRunnableMethod<uint32_t>("MediaDrmCDMProxy::OnCDMCreated", this,
+ &MediaDrmCDMProxy::OnCDMCreated, aPromiseId));
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::md_CreateSession(UniquePtr<CreateSessionData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_CreateSession"_ns);
+ return;
+ }
+
+ mCDM->CreateSession(aData->mCreateSessionToken, aData->mPromiseId,
+ aData->mInitDataType, aData->mInitData,
+ ToMediaDrmSessionType(aData->mSessionType));
+}
+
+void MediaDrmCDMProxy::md_SetServerCertificate(PromiseId aPromiseId,
+ const nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aPromiseId,
+ "Null CDM in md_SetServerCertificate"_ns);
+ return;
+ }
+
+ if (mCDM->SetServerCertificate(aCert)) {
+ ResolvePromiseWithResult(aPromiseId, true);
+ } else {
+ RejectPromiseWithStateError(
+ aPromiseId, "MediaDrmCDMProxy unable to set server certificate"_ns);
+ }
+}
+
+void MediaDrmCDMProxy::md_UpdateSession(UniquePtr<UpdateSessionData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_UpdateSession"_ns);
+ return;
+ }
+ mCDM->UpdateSession(aData->mPromiseId, aData->mSessionId, aData->mResponse);
+}
+
+void MediaDrmCDMProxy::md_CloseSession(UniquePtr<SessionOpData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_CloseSession"_ns);
+ return;
+ }
+ mCDM->CloseSession(aData->mPromiseId, aData->mSessionId);
+}
+
+void MediaDrmCDMProxy::md_Shutdown() {
+ MOZ_ASSERT(IsOnOwnerThread());
+ MOZ_ASSERT(mCDM);
+ if (mShutdownCalled) {
+ return;
+ }
+ mShutdownCalled = true;
+ mCDM->Shutdown();
+ mCDM = nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h
new file mode 100644
index 0000000000..89d562eac0
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmCDMProxy_h_
+#define MediaDrmCDMProxy_h_
+
+#include <jni.h>
+#include "mozilla/jni/Types.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/CDMCaps.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/MediaDrmProxySupport.h"
+#include "mozilla/UniquePtr.h"
+
+#include "MediaCodec.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class MediaDrmCDMCallbackProxy;
+class MediaDrmCDMProxy final : public CDMProxy {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDrmCDMProxy, override)
+
+ MediaDrmCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired,
+ bool aPersistentStateRequired);
+
+ void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName) override;
+
+ void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType, PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) override;
+
+ void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) override;
+
+ void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) override;
+
+ void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) override;
+
+ void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override;
+
+ void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) override;
+
+ void QueryOutputProtectionStatus() override;
+
+ void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) override;
+
+ void Shutdown() override;
+
+ void Terminated() override;
+
+ void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) override;
+
+ void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override;
+
+ void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void OnSessionClosed(const nsAString& aSessionId) override;
+
+ void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) override;
+
+ void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) override;
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override;
+ void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override;
+
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) override;
+ // Reject promise with an InvalidStateError and the given message.
+ void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason);
+
+ // Resolves promise with "undefined".
+ // Can be called from any thread.
+ void ResolvePromise(PromiseId aId) override;
+
+ void OnKeyStatusesChange(const nsAString& aSessionId) override;
+
+ void GetStatusForPolicy(PromiseId aPromiseId,
+ const dom::HDCPVersion& aMinHdcpVersion) override;
+
+#ifdef DEBUG
+ bool IsOnOwnerThread() override;
+#endif
+
+ const nsString& GetMediaDrmStubId() const;
+
+ private:
+ virtual ~MediaDrmCDMProxy();
+
+ void OnCDMCreated(uint32_t aPromiseId);
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult);
+
+ struct CreateSessionData {
+ MediaKeySessionType mSessionType;
+ uint32_t mCreateSessionToken;
+ PromiseId mPromiseId;
+ nsCString mInitDataType;
+ nsTArray<uint8_t> mInitData;
+ };
+
+ struct UpdateSessionData {
+ PromiseId mPromiseId;
+ nsCString mSessionId;
+ nsTArray<uint8_t> mResponse;
+ };
+
+ struct SessionOpData {
+ PromiseId mPromiseId;
+ nsCString mSessionId;
+ };
+
+ class RejectPromiseTask : public Runnable {
+ public:
+ RejectPromiseTask(MediaDrmCDMProxy* aProxy, PromiseId aId,
+ ErrorResult&& aException, const nsCString& aReason)
+ : Runnable("RejectPromiseTask"),
+ mProxy(aProxy),
+ mId(aId),
+ mException(std::move(aException)),
+ mReason(aReason) {}
+ NS_IMETHOD Run() override {
+ // Moving into or out of a non-copyable ErrorResult will assert that both
+ // ErorResults are from our current thread. Avoid the assertion by moving
+ // into a current-thread CopyableErrorResult first. Note that this is
+ // safe, because CopyableErrorResult never holds state that can't move
+ // across threads.
+ CopyableErrorResult rv(std::move(mException));
+ mProxy->RejectPromise(mId, std::move(rv), mReason);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MediaDrmCDMProxy> mProxy;
+ PromiseId mId;
+ // We use a CopyableErrorResult here, because we're going to dispatch to a
+ // different thread and normal ErrorResult doesn't support that.
+ // CopyableErrorResult ensures that it only stores values that are safe to
+ // move across threads.
+ CopyableErrorResult mException;
+ nsCString mReason;
+ };
+
+ nsCString mNodeId;
+ UniquePtr<MediaDrmProxySupport> mCDM;
+ bool mShutdownCalled;
+
+ // =====================================================================
+ // For MediaDrmProxySupport
+ void md_Init(uint32_t aPromiseId);
+ void md_CreateSession(UniquePtr<CreateSessionData>&& aData);
+ void md_SetServerCertificate(PromiseId aPromiseId,
+ const nsTArray<uint8_t>& aCert);
+ void md_UpdateSession(UniquePtr<UpdateSessionData>&& aData);
+ void md_CloseSession(UniquePtr<SessionOpData>&& aData);
+ void md_Shutdown();
+ // =====================================================================
+};
+
+} // namespace mozilla
+
+#endif // MediaDrmCDMProxy_h_
diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp
new file mode 100644
index 0000000000..717a57df35
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDrmProxySupport.h"
+#include "MediaDrmCDMCallbackProxy.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/java/MediaDrmProxyNatives.h"
+#include "mozilla/java/SessionKeyInfoWrappers.h"
+#include "MediaCodec.h" // For MediaDrm::KeyStatus
+
+namespace mozilla {
+
+LogModule* GetMDRMNLog() {
+ static LazyLogModule log("MediaDrmProxySupport");
+ return log;
+}
+
+class MediaDrmJavaCallbacksSupport
+ : public java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<
+ MediaDrmJavaCallbacksSupport> {
+ public:
+ typedef java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<
+ MediaDrmJavaCallbacksSupport>
+ MediaDrmProxyNativeCallbacks;
+ using MediaDrmProxyNativeCallbacks::AttachNative;
+ using MediaDrmProxyNativeCallbacks::DisposeNative;
+
+ explicit MediaDrmJavaCallbacksSupport(
+ UniquePtr<MediaDrmCDMCallbackProxy>&& aDecryptorProxyCallback)
+ : mDecryptorProxyCallback(std::move(aDecryptorProxyCallback)) {
+ MOZ_ASSERT(mDecryptorProxyCallback);
+ }
+ /*
+ * Native implementation, called by Java.
+ */
+ void OnSessionCreated(int aCreateSessionToken, int aPromiseId,
+ jni::ByteArray::Param aSessionId,
+ jni::ByteArray::Param aRequest);
+
+ void OnSessionUpdated(int aPromiseId, jni::ByteArray::Param aSessionId);
+
+ void OnSessionClosed(int aPromiseId, jni::ByteArray::Param aSessionId);
+
+ void OnSessionMessage(
+ jni::ByteArray::Param aSessionId,
+ int /*mozilla::dom::MediaKeyMessageType*/ aSessionMessageType,
+ jni::ByteArray::Param aRequest);
+
+ void OnSessionError(jni::ByteArray::Param aSessionId,
+ jni::String::Param aMessage);
+
+ void OnSessionBatchedKeyChanged(jni::ByteArray::Param,
+ jni::ObjectArray::Param);
+
+ void OnRejectPromise(int aPromiseId, jni::String::Param aMessage);
+
+ private:
+ UniquePtr<MediaDrmCDMCallbackProxy> mDecryptorProxyCallback;
+}; // MediaDrmJavaCallbacksSupport
+
+void MediaDrmJavaCallbacksSupport::OnSessionCreated(
+ int aCreateSessionToken, int aPromiseId, jni::ByteArray::Param aSessionId,
+ jni::ByteArray::Param aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto reqDataArray = aRequest->GetElements();
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ MDRMN_LOG("SessionId(%s) closed", sessionId.get());
+
+ mDecryptorProxyCallback->SetSessionId(aCreateSessionToken, sessionId);
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionUpdated(
+ int aPromiseId, jni::ByteArray::Param aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MDRMN_LOG(
+ "SessionId(%s) closed",
+ nsCString(reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length())
+ .get());
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionClosed(
+ int aPromiseId, jni::ByteArray::Param aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ MDRMN_LOG("SessionId(%s) closed", sessionId.get());
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+ mDecryptorProxyCallback->SessionClosed(sessionId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionMessage(
+ jni::ByteArray::Param aSessionId,
+ int /*mozilla::dom::MediaKeyMessageType*/ aMessageType,
+ jni::ByteArray::Param aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ auto reqDataArray = aRequest->GetElements();
+
+ nsTArray<uint8_t> retRequest;
+ retRequest.AppendElements(reinterpret_cast<uint8_t*>(reqDataArray.Elements()),
+ reqDataArray.Length());
+
+ mDecryptorProxyCallback->SessionMessage(
+ sessionId, static_cast<dom::MediaKeyMessageType>(aMessageType),
+ retRequest);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionError(
+ jni::ByteArray::Param aSessionId, jni::String::Param aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ nsCString errorMessage = aMessage->ToCString();
+ MDRMN_LOG("SessionId(%s)", sessionId.get());
+ // TODO: We cannot get system error code from media drm API.
+ // Currently use -1 as an error code.
+ mDecryptorProxyCallback->SessionError(
+ sessionId, NS_ERROR_DOM_INVALID_STATE_ERR, -1, errorMessage);
+}
+
+// TODO: MediaDrm.KeyStatus defined the status code not included
+// dom::MediaKeyStatus::Released and dom::MediaKeyStatus::Output_downscaled.
+// Should keep tracking for this if it will be changed in the future.
+static dom::MediaKeyStatus MediaDrmKeyStatusToMediaKeyStatus(int aStatusCode) {
+ using mozilla::java::sdk::MediaDrm;
+ switch (aStatusCode) {
+ case MediaDrm::KeyStatus::STATUS_USABLE:
+ return dom::MediaKeyStatus::Usable;
+ case MediaDrm::KeyStatus::STATUS_EXPIRED:
+ return dom::MediaKeyStatus::Expired;
+ case MediaDrm::KeyStatus::STATUS_OUTPUT_NOT_ALLOWED:
+ return dom::MediaKeyStatus::Output_restricted;
+ case MediaDrm::KeyStatus::STATUS_INTERNAL_ERROR:
+ return dom::MediaKeyStatus::Internal_error;
+ case MediaDrm::KeyStatus::STATUS_PENDING:
+ return dom::MediaKeyStatus::Status_pending;
+ default:
+ return dom::MediaKeyStatus::Internal_error;
+ }
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionBatchedKeyChanged(
+ jni::ByteArray::Param aSessionId, jni::ObjectArray::Param aKeyInfos) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ nsTArray<jni::Object::LocalRef> keyInfosObjectArray(aKeyInfos->GetElements());
+
+ nsTArray<CDMKeyInfo> keyInfosArray;
+
+ for (auto&& keyInfoObject : keyInfosObjectArray) {
+ java::SessionKeyInfo::LocalRef keyInfo(std::move(keyInfoObject));
+ mozilla::jni::ByteArray::LocalRef keyIdByteArray = keyInfo->KeyId();
+ nsTArray<int8_t> keyIdInt8Array = keyIdByteArray->GetElements();
+ // Cast nsTArray<int8_t> to nsTArray<uint8_t>
+ nsTArray<uint8_t>* keyId =
+ reinterpret_cast<nsTArray<uint8_t>*>(&keyIdInt8Array);
+ auto keyStatus = keyInfo->Status(); // int32_t
+ keyInfosArray.AppendElement(
+ CDMKeyInfo(*keyId, dom::Optional<dom::MediaKeyStatus>(
+ MediaDrmKeyStatusToMediaKeyStatus(keyStatus))));
+ }
+
+ mDecryptorProxyCallback->BatchedKeyStatusChanged(sessionId, keyInfosArray);
+}
+
+void MediaDrmJavaCallbacksSupport::OnRejectPromise(
+ int aPromiseId, jni::String::Param aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString reason = aMessage->ToCString();
+ MDRMN_LOG("OnRejectPromise aMessage(%s) ", reason.get());
+ // Current implementation assume all the reject from MediaDrm is due to
+ // invalid state. Other cases should be handled before calling into
+ // MediaDrmProxy API.
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(reason);
+ mDecryptorProxyCallback->RejectPromise(aPromiseId, std::move(rv), reason);
+}
+
+MediaDrmProxySupport::MediaDrmProxySupport(const nsAString& aKeySystem)
+ : mKeySystem(aKeySystem), mDestroyed(false) {
+ mJavaCallbacks = java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::New();
+
+ mBridgeProxy = java::MediaDrmProxy::Create(mKeySystem, mJavaCallbacks);
+
+ MOZ_ASSERT(mBridgeProxy, "mBridgeProxy should not be null");
+ mMediaDrmStubId = mBridgeProxy->GetStubId()->ToString();
+}
+
+MediaDrmProxySupport::~MediaDrmProxySupport() {
+ MOZ_ASSERT(mDestroyed, "Shutdown() should be called before !!");
+ MediaDrmJavaCallbacksSupport::DisposeNative(mJavaCallbacks);
+}
+
+nsresult MediaDrmProxySupport::Init(
+ UniquePtr<MediaDrmCDMCallbackProxy>&& aCallback) {
+ MOZ_ASSERT(mJavaCallbacks);
+
+ MediaDrmJavaCallbacksSupport::AttachNative(
+ mJavaCallbacks,
+ mozilla::MakeUnique<MediaDrmJavaCallbacksSupport>(std::move(aCallback)));
+ return mBridgeProxy != nullptr ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void MediaDrmProxySupport::CreateSession(uint32_t aCreateSessionToken,
+ uint32_t aPromiseId,
+ const nsCString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData,
+ MediaDrmSessionType aSessionType) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ auto initDataBytes = mozilla::jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(&aInitData[0]), aInitData.Length());
+ // TODO: aSessionType is not used here.
+ // Refer to
+ // http://androidxref.com/5.1.1_r6/xref/external/chromium_org/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java#420
+ // it is hard code to streaming type.
+ mBridgeProxy->CreateSession(aCreateSessionToken, aPromiseId,
+ NS_ConvertUTF8toUTF16(aInitDataType),
+ initDataBytes);
+}
+
+void MediaDrmProxySupport::UpdateSession(uint32_t aPromiseId,
+ const nsCString& aSessionId,
+ const nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ auto response = mozilla::jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(aResponse.Elements()),
+ aResponse.Length());
+ mBridgeProxy->UpdateSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId),
+ response);
+}
+
+void MediaDrmProxySupport::CloseSession(uint32_t aPromiseId,
+ const nsCString& aSessionId) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ mBridgeProxy->CloseSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmProxySupport::Shutdown() {
+ MOZ_ASSERT(mBridgeProxy);
+
+ if (mDestroyed) {
+ return;
+ }
+ mBridgeProxy->Destroy();
+ mDestroyed = true;
+}
+
+bool MediaDrmProxySupport::SetServerCertificate(
+ const nsTArray<uint8_t>& aCert) {
+ jni::ByteArray::LocalRef cert = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(aCert.Elements()), aCert.Length());
+ return mBridgeProxy->SetServerCertificate(cert);
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.h b/dom/media/eme/mediadrm/MediaDrmProxySupport.h
new file mode 100644
index 0000000000..e994036f69
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmProxySupport_H
+#define MediaDrmProxySupport_H
+
+#include "mozilla/DecryptorProxyCallback.h"
+#include "mozilla/java/MediaDrmProxyWrappers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/UniquePtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+enum MediaDrmSessionType {
+ kKeyStreaming = 1,
+ kKeyOffline = 2,
+ kKeyRelease = 3,
+};
+
+#ifndef MDRMN_LOG
+LogModule* GetMDRMNLog();
+# define MDRMN_LOG(x, ...) \
+ MOZ_LOG(GetMDRMNLog(), mozilla::LogLevel::Debug, \
+ ("[MediaDrmProxySupport][%s]" x, __FUNCTION__, ##__VA_ARGS__))
+#endif
+
+class MediaDrmCDMCallbackProxy;
+
+class MediaDrmProxySupport final {
+ public:
+ explicit MediaDrmProxySupport(const nsAString& aKeySystem);
+ ~MediaDrmProxySupport();
+
+ /*
+ * APIs to act as GMPDecryptorAPI, discarding unnecessary calls.
+ */
+ nsresult Init(UniquePtr<MediaDrmCDMCallbackProxy>&& aCallback);
+
+ void CreateSession(uint32_t aCreateSessionToken, uint32_t aPromiseId,
+ const nsCString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData,
+ MediaDrmSessionType aSessionType);
+
+ void UpdateSession(uint32_t aPromiseId, const nsCString& aSessionId,
+ const nsTArray<uint8_t>& aResponse);
+
+ void CloseSession(uint32_t aPromiseId, const nsCString& aSessionId);
+
+ void Shutdown();
+
+ const nsString& GetMediaDrmStubId() const { return mMediaDrmStubId; }
+
+ bool SetServerCertificate(const nsTArray<uint8_t>& aCert);
+
+ private:
+ const nsString mKeySystem;
+ java::MediaDrmProxy::GlobalRef mBridgeProxy;
+ java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::GlobalRef mJavaCallbacks;
+ bool mDestroyed;
+ nsString mMediaDrmStubId;
+};
+
+} // namespace mozilla
+#endif // MediaDrmProxySupport_H
diff --git a/dom/media/eme/mediadrm/moz.build b/dom/media/eme/mediadrm/moz.build
new file mode 100644
index 0000000000..c8f433f25f
--- /dev/null
+++ b/dom/media/eme/mediadrm/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla += [
+ "MediaDrmCDMCallbackProxy.h",
+ "MediaDrmCDMProxy.h",
+ "MediaDrmProxySupport.h",
+]
+
+UNIFIED_SOURCES += [
+ "MediaDrmCDMCallbackProxy.cpp",
+ "MediaDrmCDMProxy.cpp",
+ "MediaDrmProxySupport.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.cpp b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp
new file mode 100644
index 0000000000..1fe42aa8e2
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMImpl.h"
+
+#include <unordered_map>
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/KeySystemNames.h"
+
+namespace mozilla {
+
+/* static */
+bool WMFCDMImpl::Supports(const nsAString& aKeySystem) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return false;
+ }
+
+ static std::map<nsString, bool> sSupports;
+ static bool sSetRunOnShutdown = false;
+ if (!sSetRunOnShutdown) {
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("WMFCDMImpl::Supports", [&] {
+ RunOnShutdown([&] { sSupports.clear(); },
+ ShutdownPhase::XPCOMShutdown);
+ }));
+ sSetRunOnShutdown = true;
+ }
+
+ nsString key(aKeySystem);
+ if (const auto& s = sSupports.find(key); s != sSupports.end()) {
+ return s->second;
+ }
+
+ RefPtr<WMFCDMImpl> cdm = MakeRefPtr<WMFCDMImpl>(aKeySystem);
+ nsTArray<KeySystemConfig> configs;
+ bool s = cdm->GetCapabilities(configs);
+ sSupports[key] = s;
+ return s;
+}
+
+bool WMFCDMImpl::GetCapabilities(nsTArray<KeySystemConfig>& aOutConfigs) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return false;
+ }
+
+ static std::unordered_map<std::string, nsTArray<KeySystemConfig>>
+ sKeySystemConfigs{};
+ static bool sSetRunOnShutdown = false;
+ if (!sSetRunOnShutdown) {
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("WMFCDMImpl::GetCapabilities", [&] {
+ RunOnShutdown([&] { sKeySystemConfigs.clear(); },
+ ShutdownPhase::XPCOMShutdown);
+ }));
+ sSetRunOnShutdown = true;
+ }
+
+ // Retrieve result from our cached key system
+ auto keySystem = std::string{NS_ConvertUTF16toUTF8(mKeySystem).get()};
+ if (auto rv = sKeySystemConfigs.find(keySystem);
+ rv != sKeySystemConfigs.end()) {
+ EME_LOG("Return cached capabilities for %s", keySystem.c_str());
+ for (const auto& config : rv->second) {
+ aOutConfigs.AppendElement(config);
+ EME_LOG("-- capabilities (%s)",
+ NS_ConvertUTF16toUTF8(config.GetDebugInfo()).get());
+ }
+ return true;
+ }
+
+ // Not cached result, ask the remote process.
+ nsCOMPtr<nsISerialEventTarget> backgroundTaskQueue;
+ NS_CreateBackgroundTaskQueue(__func__, getter_AddRefs(backgroundTaskQueue));
+ if (!mCDM) {
+ mCDM = MakeRefPtr<MFCDMChild>(mKeySystem);
+ }
+ bool ok = false;
+ static const bool sIsHwSecure[2] = {false, true};
+ for (const auto& isHWSecure : sIsHwSecure) {
+ media::Await(
+ do_AddRef(backgroundTaskQueue), mCDM->GetCapabilities(isHWSecure),
+ [&ok, &aOutConfigs, keySystem,
+ isHWSecure](const MFCDMCapabilitiesIPDL& capabilities) {
+ EME_LOG("capabilities: keySystem=%s (hw-secure=%d)",
+ keySystem.c_str(), isHWSecure);
+ for (const auto& v : capabilities.videoCapabilities()) {
+ EME_LOG("capabilities: video=%s",
+ NS_ConvertUTF16toUTF8(v.contentType()).get());
+ }
+ for (const auto& a : capabilities.audioCapabilities()) {
+ EME_LOG("capabilities: audio=%s",
+ NS_ConvertUTF16toUTF8(a.contentType()).get());
+ }
+ for (const auto& v : capabilities.encryptionSchemes()) {
+ EME_LOG("capabilities: encryptionScheme=%s",
+ EncryptionSchemeStr(v));
+ }
+ KeySystemConfig* config = aOutConfigs.AppendElement();
+ MFCDMCapabilitiesIPDLToKeySystemConfig(capabilities, *config);
+ sKeySystemConfigs[keySystem].AppendElement(*config);
+ // This is equal to "com.microsoft.playready.recommendation.3000", so
+ // we can store it directly without asking the remote process again.
+ if (keySystem.compare(kPlayReadyKeySystemName) == 0 && isHWSecure) {
+ config->mKeySystem.AssignLiteral(kPlayReadyKeySystemHardware);
+ sKeySystemConfigs["com.microsoft.playready.recommendation.3000"]
+ .AppendElement(*config);
+ }
+ ok = true;
+ },
+ [](nsresult rv) {
+ EME_LOG("Fail to get key system capabilities. rv=%x", uint32_t(rv));
+ });
+ }
+ return ok;
+}
+
+RefPtr<WMFCDMImpl::InitPromise> WMFCDMImpl::Init(
+ const WMFCDMImpl::InitParams& aParams) {
+ if (!mCDM) {
+ mCDM = MakeRefPtr<MFCDMChild>(mKeySystem);
+ }
+ RefPtr<WMFCDMImpl> self = this;
+ mCDM->Init(aParams.mOrigin, aParams.mInitDataTypes,
+ aParams.mPersistentStateRequired
+ ? KeySystemConfig::Requirement::Required
+ : KeySystemConfig::Requirement::Optional,
+ aParams.mDistinctiveIdentifierRequired
+ ? KeySystemConfig::Requirement::Required
+ : KeySystemConfig::Requirement::Optional,
+ aParams.mAudioCapabilities, aParams.mVideoCapabilities,
+ aParams.mProxyCallback)
+ ->Then(
+ mCDM->ManagerThread(), __func__,
+ [self, this](const MFCDMInitIPDL& init) {
+ mInitPromiseHolder.ResolveIfExists(true, __func__);
+ },
+ [self, this](const nsresult rv) {
+ mInitPromiseHolder.RejectIfExists(rv, __func__);
+ });
+ return mInitPromiseHolder.Ensure(__func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.h b/dom/media/eme/mediafoundation/WMFCDMImpl.h
new file mode 100644
index 0000000000..452629ec84
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMImpl.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
+
+#include "MediaData.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/KeySystemConfig.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/MFCDMChild.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class WMFCDMProxyCallback;
+
+/**
+ * WMFCDMImpl is a helper class for MFCDM protocol clients. It creates, manages,
+ * and calls MFCDMChild object in the content process on behalf of the client,
+ * and performs conversion between EME and MFCDM types and constants. This class
+ * can be used in two ways (1) call Supports/GetCapabilities to know the
+ * information about given key system or config (2) do session-related
+ * operations. In this case, Init() MUST be called first.
+ */
+class WMFCDMImpl final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMImpl);
+
+ explicit WMFCDMImpl(const nsAString& aKeySystem) : mKeySystem(aKeySystem) {}
+
+ static bool Supports(const nsAString& aKeySystem);
+ // TODO: make this async?
+ bool GetCapabilities(nsTArray<KeySystemConfig>& aOutConfigs);
+
+ using InitPromise = GenericPromise;
+ struct InitParams {
+ nsString mOrigin;
+ CopyableTArray<nsString> mInitDataTypes;
+ bool mPersistentStateRequired;
+ bool mDistinctiveIdentifierRequired;
+ WMFCDMProxyCallback* mProxyCallback;
+ CopyableTArray<MFCDMMediaCapability> mAudioCapabilities;
+ CopyableTArray<MFCDMMediaCapability> mVideoCapabilities;
+ };
+
+ RefPtr<InitPromise> Init(const InitParams& aParams);
+
+ // Following functions MUST be called after calling Init().
+ RefPtr<MFCDMChild::SessionPromise> CreateSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aInitDataType, const nsTArray<uint8_t>& aInitData) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->CreateSessionAndGenerateRequest(aPromiseId, aSessionType,
+ aInitDataType, aInitData);
+ }
+
+ RefPtr<GenericPromise> LoadSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aSessionId) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->LoadSession(aPromiseId, aSessionType, aSessionId);
+ }
+
+ RefPtr<GenericPromise> UpdateSession(uint32_t aPromiseId,
+ const nsAString& aSessionId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->UpdateSession(aPromiseId, aSessionId, aResponse);
+ }
+
+ RefPtr<GenericPromise> CloseSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->CloseSession(aPromiseId, aSessionId);
+ }
+
+ RefPtr<GenericPromise> RemoveSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->RemoveSession(aPromiseId, aSessionId);
+ }
+
+ RefPtr<GenericPromise> SetServerCertificate(uint32_t aPromiseId,
+ nsTArray<uint8_t>& aCert) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->SetServerCertificate(aPromiseId, aCert);
+ }
+
+ RefPtr<GenericPromise> GetStatusForPolicy(
+ uint32_t aPromiseId, const dom::HDCPVersion& aMinHdcpVersion) {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->GetStatusForPolicy(aPromiseId, aMinHdcpVersion);
+ }
+
+ uint64_t Id() {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM,
+ "Should be called only after Init() is resolved");
+ MOZ_DIAGNOSTIC_ASSERT(mCDM->Id() != 0,
+ "Should be called only after Init() is resolved");
+ return mCDM->Id();
+ }
+
+ private:
+ ~WMFCDMImpl() {
+ if (mCDM) {
+ mCDM->Shutdown();
+ }
+ };
+
+ const nsString mKeySystem;
+ RefPtr<MFCDMChild> mCDM;
+
+ MozPromiseHolder<InitPromise> mInitPromiseHolder;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.cpp b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp
new file mode 100644
index 0000000000..21207ecc22
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp
@@ -0,0 +1,414 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMProxy.h"
+
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/WMFCDMProxyCallback.h"
+#include "mozilla/WindowsVersion.h"
+#include "WMFCDMImpl.h"
+#include "WMFCDMProxyCallback.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ EME_LOG("WMFCDMProxy[%p]@%s: " msg, this, __func__, ##__VA_ARGS__)
+
+#define RETURN_IF_SHUTDOWN() \
+ do { \
+ MOZ_ASSERT(NS_IsMainThread()); \
+ if (mIsShutdown) { \
+ return; \
+ } \
+ } while (false)
+
+#define PERFORM_ON_CDM(operation, promiseId, ...) \
+ do { \
+ mCDM->operation(promiseId, __VA_ARGS__) \
+ ->Then( \
+ mMainThread, __func__, \
+ [self = RefPtr{this}, this, promiseId]() { \
+ RETURN_IF_SHUTDOWN(); \
+ if (mKeys.IsNull()) { \
+ EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32 \
+ ") : abort the " #operation " due to empty key", \
+ this, promiseId); \
+ return; \
+ } \
+ ResolvePromise(promiseId); \
+ }, \
+ [self = RefPtr{this}, this, promiseId]() { \
+ RETURN_IF_SHUTDOWN(); \
+ RejectPromiseWithStateError( \
+ promiseId, nsLiteralCString("WMFCDMProxy::" #operation ": " \
+ "failed to " #operation)); \
+ }); \
+ } while (false)
+
+WMFCDMProxy::WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ const dom::MediaKeySystemConfiguration& aConfig)
+ : CDMProxy(
+ aKeys, aKeySystem,
+ aConfig.mDistinctiveIdentifier == dom::MediaKeysRequirement::Required,
+ aConfig.mPersistentState == dom::MediaKeysRequirement::Required),
+ mConfig(aConfig) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+WMFCDMProxy::~WMFCDMProxy() {}
+
+void WMFCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ MOZ_ASSERT(!mOwnerThread);
+ if (NS_FAILED(
+ NS_NewNamedThread("WMFCDMThread", getter_AddRefs(mOwnerThread)))) {
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("WMFCDMProxy::Init: couldn't create CDM thread"));
+ return;
+ }
+
+ mCDM = MakeRefPtr<WMFCDMImpl>(mKeySystem);
+ mProxyCallback = new WMFCDMProxyCallback(this);
+ // SWDRM has a PMP process leakage problem on Windows 10 due to the internal
+ // issue in the media foundation. Microsoft only lands the solution on Windows
+ // 11 and doesn't have plan to port it back to Windows 10. Therefore, for
+ // PlayReady, we need to force to covert SL2000 to SL3000 for our underlying
+ // CDM to avoid that issue. In addition, as we only support L1 for Widevine,
+ // this issue won't happen on it.
+ Maybe<nsString> forcedRobustness =
+ IsPlayReadyKeySystemAndSupported(mKeySystem) && !IsWin11OrLater()
+ ? Some(nsString(u"3000"))
+ : Nothing();
+ WMFCDMImpl::InitParams params{
+ nsString(aOrigin),
+ mConfig.mInitDataTypes,
+ mPersistentStateRequired,
+ mDistinctiveIdentifierRequired,
+ mProxyCallback,
+ GenerateMFCDMMediaCapabilities(mConfig.mAudioCapabilities),
+ GenerateMFCDMMediaCapabilities(mConfig.mVideoCapabilities,
+ forcedRobustness)};
+ mCDM->Init(params)->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aPromiseId](const bool) {
+ MOZ_ASSERT(mCDM->Id() > 0);
+ mKeys->OnCDMCreated(aPromiseId, mCDM->Id());
+ },
+ [self = RefPtr{this}, this, aPromiseId](const nsresult rv) {
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("WMFCDMProxy::Init: WMFCDM init error"));
+ });
+}
+
+CopyableTArray<MFCDMMediaCapability>
+WMFCDMProxy::GenerateMFCDMMediaCapabilities(
+ const dom::Sequence<dom::MediaKeySystemMediaCapability>& aCapabilities,
+ const Maybe<nsString>& forcedRobustness) {
+ CopyableTArray<MFCDMMediaCapability> outCapabilites;
+ for (const auto& capabilities : aCapabilities) {
+ if (!forcedRobustness) {
+ EME_LOG("WMFCDMProxy::Init %p, robustness=%s", this,
+ NS_ConvertUTF16toUTF8(capabilities.mRobustness).get());
+ outCapabilites.AppendElement(MFCDMMediaCapability{
+ capabilities.mContentType, capabilities.mRobustness});
+ } else {
+ EME_LOG("WMFCDMProxy::Init %p, force to robustness=%s", this,
+ NS_ConvertUTF16toUTF8(*forcedRobustness).get());
+ outCapabilites.AppendElement(
+ MFCDMMediaCapability{capabilities.mContentType, *forcedRobustness});
+ }
+ }
+ return outCapabilites;
+}
+
+void WMFCDMProxy::ResolvePromise(PromiseId aId) {
+ auto resolve = [self = RefPtr{this}, this, aId]() {
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::ResolvePromise(this=%p, pid=%" PRIu32 ")", this, aId);
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromise(aId);
+ } else {
+ NS_WARNING("WMFCDMProxy unable to resolve promise!");
+ }
+ };
+
+ if (NS_IsMainThread()) {
+ resolve();
+ return;
+ }
+ mMainThread->Dispatch(
+ NS_NewRunnableFunction("WMFCDMProxy::ResolvePromise", resolve));
+}
+
+void WMFCDMProxy::ResolvePromiseWithKeyStatus(
+ const PromiseId& aId, const dom::MediaKeyStatus& aStatus) {
+ auto resolve = [self = RefPtr{this}, this, aId, aStatus]() {
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::ResolvePromiseWithKeyStatus(this=%p, pid=%" PRIu32
+ ", status=%s)",
+ this, aId, ToMediaKeyStatusStr(aStatus));
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromiseWithKeyStatus(aId, aStatus);
+ } else {
+ NS_WARNING("WMFCDMProxy unable to resolve promise!");
+ }
+ };
+
+ if (NS_IsMainThread()) {
+ resolve();
+ return;
+ }
+ mMainThread->Dispatch(NS_NewRunnableFunction(
+ "WMFCDMProxy::ResolvePromiseWithKeyStatus", resolve));
+}
+
+void WMFCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ if (!NS_IsMainThread()) {
+ // Use CopyableErrorResult to store our exception in the runnable,
+ // because ErrorResult is not OK to move across threads.
+ mMainThread->Dispatch(
+ NewRunnableMethod<PromiseId, StoreCopyPassByRRef<CopyableErrorResult>,
+ nsCString>("WMFCDMProxy::RejectPromise", this,
+ &WMFCDMProxy::RejectPromiseOnMainThread,
+ aId, std::move(aException), aReason),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+ EME_LOG("WMFCDMProxy::RejectPromise(this=%p, pid=%" PRIu32
+ ", code=0x%x, "
+ "reason='%s')",
+ this, aId, aException.ErrorCodeAsInt(), aReason.get());
+ if (!mKeys.IsNull()) {
+ mKeys->RejectPromise(aId, std::move(aException), aReason);
+ } else {
+ // We don't have a MediaKeys object to pass the exception to, so silence
+ // the exception to avoid it asserting due to being unused.
+ aException.SuppressException();
+ }
+}
+
+void WMFCDMProxy::RejectPromiseOnMainThread(PromiseId aId,
+ CopyableErrorResult&& aException,
+ const nsCString& aReason) {
+ RETURN_IF_SHUTDOWN();
+ // Moving into or out of a non-copyable ErrorResult will assert that both
+ // ErorResults are from our current thread. Avoid the assertion by moving
+ // into a current-thread CopyableErrorResult first. Note that this is safe,
+ // because CopyableErrorResult never holds state that can't move across
+ // threads.
+ CopyableErrorResult rv(std::move(aException));
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void WMFCDMProxy::RejectPromiseWithStateError(PromiseId aId,
+ const nsCString& aReason) {
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aReason);
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void WMFCDMProxy::CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
+ EME_LOG("WMFCDMProxy::CreateSession(this=%p, pid=%" PRIu32
+ "), sessionType=%s",
+ this, aPromiseId, SessionTypeToStr(sessionType));
+ mCDM->CreateSession(aPromiseId, sessionType, aInitDataType, aInitData)
+ ->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aCreateSessionToken,
+ aPromiseId](nsString sessionID) {
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32
+ ") : abort the create session due to "
+ "empty key",
+ this, aPromiseId);
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session =
+ mKeys->GetPendingSession(aCreateSessionToken)) {
+ session->SetSessionId(std::move(sessionID));
+ }
+ ResolvePromise(aPromiseId);
+ },
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString(
+ "WMFCDMProxy::CreateSession: cannot create session"));
+ });
+}
+
+void WMFCDMProxy::LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
+ EME_LOG("WMFCDMProxy::LoadSession(this=%p, pid=%" PRIu32
+ "), sessionType=%s, sessionId=%s",
+ this, aPromiseId, SessionTypeToStr(sessionType),
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(LoadSession, aPromiseId, sessionType, aSessionId);
+}
+
+void WMFCDMProxy::UpdateSession(const nsAString& aSessionId,
+ PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::UpdateSession(this=%p, pid=%" PRIu32
+ "), sessionId=%s, responseLen=%zu",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get(),
+ aResponse.Length());
+ PERFORM_ON_CDM(UpdateSession, aPromiseId, aSessionId, aResponse);
+}
+
+void WMFCDMProxy::CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::CloseSession(this=%p, pid=%" PRIu32 "), sessionId=%s",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(CloseSession, aPromiseId, aSessionId);
+}
+
+void WMFCDMProxy::RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::RemoveSession(this=%p, pid=%" PRIu32 "), sessionId=%s",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(RemoveSession, aPromiseId, aSessionId);
+}
+
+void WMFCDMProxy::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mIsShutdown);
+ if (mProxyCallback) {
+ mProxyCallback->Shutdown();
+ mProxyCallback = nullptr;
+ }
+ mIsShutdown = true;
+}
+
+void WMFCDMProxy::OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify key message for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->DispatchKeyMessage(aMessageType, aMessage);
+ }
+}
+
+void WMFCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify key statuses for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->DispatchKeyStatusesChange();
+ }
+}
+
+void WMFCDMProxy::OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify expiration for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->SetExpiration(static_cast<double>(aExpiryTime));
+ }
+}
+
+void WMFCDMProxy::SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::SetServerCertificate(this=%p, pid=%" PRIu32 ")", this,
+ aPromiseId);
+ mCDM->SetServerCertificate(aPromiseId, aCert)
+ ->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ ResolvePromise(aPromiseId);
+ },
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("WMFCDMProxy::SetServerCertificate failed!"));
+ });
+}
+
+void WMFCDMProxy::GetStatusForPolicy(PromiseId aPromiseId,
+ const dom::HDCPVersion& aMinHdcpVersion) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::GetStatusForPolicy(this=%p, pid=%" PRIu32
+ ", minHDCP=%s)",
+ this, aPromiseId,
+ dom::HDCPVersionValues::GetString(aMinHdcpVersion).data());
+ mCDM->GetStatusForPolicy(aPromiseId, aMinHdcpVersion)
+ ->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ ResolvePromiseWithKeyStatus(aPromiseId,
+ dom::MediaKeyStatus::Usable);
+ },
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ ResolvePromiseWithKeyStatus(aPromiseId,
+ dom::MediaKeyStatus::Output_restricted);
+ });
+}
+
+bool WMFCDMProxy::IsHardwareDecryptionSupported() const {
+ return mozilla::IsHardwareDecryptionSupported(mConfig);
+}
+
+uint64_t WMFCDMProxy::GetCDMProxyId() const {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->Id();
+}
+
+#undef LOG
+#undef RETURN_IF_SHUTDOWN
+#undef PERFORM_ON_CDM
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.h b/dom/media/eme/mediafoundation/WMFCDMProxy.h
new file mode 100644
index 0000000000..d41d50997b
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxy.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/CDMCaps.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WMFCDMImpl.h"
+
+#include "nsString.h"
+
+namespace dom {
+struct MediaKeySystemMediaCapability;
+};
+
+namespace mozilla {
+
+class WMFCDMProxyCallback;
+
+class WMFCDMProxy : public CDMProxy {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxy, override)
+
+ WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ const dom::MediaKeySystemConfiguration& aConfig);
+
+ // CDMProxy interface
+ void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin, const nsAString& aName) override;
+
+ void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType, PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) override;
+
+ void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) override;
+
+ void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) override;
+
+ void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) override;
+
+ void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override;
+
+ void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) override;
+
+ void QueryOutputProtectionStatus() override {}
+
+ void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) override {}
+
+ void Shutdown() override;
+
+ void Terminated() override {}
+
+ void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) override {}
+
+ void OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) override {}
+
+ void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void OnSessionClosed(const nsAString& aSessionId) override {}
+
+ void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) override {}
+
+ void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) override {}
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override {
+ return nullptr;
+ }
+ void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override {}
+
+ void ResolvePromise(PromiseId aId) override;
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) override;
+
+ void OnKeyStatusesChange(const nsAString& aSessionId) override;
+
+ void GetStatusForPolicy(PromiseId aPromiseId,
+ const dom::HDCPVersion& aMinHdcpVersion) override;
+
+#ifdef DEBUG
+ bool IsOnOwnerThread() override {
+ return NS_GetCurrentThread() == mOwnerThread;
+ }
+#endif
+
+ WMFCDMProxy* AsWMFCDMProxy() override { return this; }
+
+ bool IsHardwareDecryptionSupported() const override;
+
+ // Can only be called after initialization succeeded.
+ uint64_t GetCDMProxyId() const;
+
+ private:
+ virtual ~WMFCDMProxy();
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult) {}
+ void RejectPromiseOnMainThread(PromiseId aId,
+ CopyableErrorResult&& aException,
+ const nsCString& aReason);
+ // Reject promise with an InvalidStateError and the given message.
+ void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason);
+
+ // For HDCP GetStatusForPolicy usage.
+ void ResolvePromiseWithKeyStatus(const PromiseId& aId,
+ const dom::MediaKeyStatus& aStatus);
+
+ CopyableTArray<MFCDMMediaCapability> GenerateMFCDMMediaCapabilities(
+ const dom::Sequence<dom::MediaKeySystemMediaCapability>& aCapabilities,
+ const Maybe<nsString>& forcedRobustness = Nothing());
+
+ RefPtr<WMFCDMImpl> mCDM;
+
+ const dom::MediaKeySystemConfiguration mConfig;
+
+ RefPtr<WMFCDMProxyCallback> mProxyCallback;
+
+ // It can only be used on the main thread.
+ bool mIsShutdown = false;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp
new file mode 100644
index 0000000000..6464cec4c7
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMProxyCallback.h"
+
+#include "mozilla/WMFCDMProxy.h"
+
+#define RETURN_IF_NULL(proxy) \
+ if (!proxy) { \
+ return; \
+ }
+
+namespace mozilla {
+
+WMFCDMProxyCallback::WMFCDMProxyCallback(WMFCDMProxy* aProxy) : mProxy(aProxy) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mProxy);
+}
+
+WMFCDMProxyCallback::~WMFCDMProxyCallback() { MOZ_ASSERT(!mProxy); }
+
+void WMFCDMProxyCallback::OnSessionMessage(const MFCDMKeyMessage& aMessage) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionMessage",
+ [self = RefPtr{this}, this, message = aMessage]() {
+ RETURN_IF_NULL(mProxy);
+ mProxy->OnSessionMessage(message.sessionId(), message.type(),
+ std::move(message.message()));
+ }));
+}
+
+void WMFCDMProxyCallback::OnSessionKeyStatusesChange(
+ const MFCDMKeyStatusChange& aKeyStatuses) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionKeyStatusesChange",
+ [self = RefPtr{this}, this, keyStatuses = aKeyStatuses]() {
+ RETURN_IF_NULL(mProxy);
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ for (const auto& keyInfo : keyStatuses.keyInfo()) {
+ keyStatusesChange |= caps->SetKeyStatus(
+ keyInfo.keyId(), keyStatuses.sessionId(),
+ dom::Optional<dom::MediaKeyStatus>(keyInfo.status()));
+ }
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(keyStatuses.sessionId());
+ }
+ }));
+}
+
+void WMFCDMProxyCallback::OnSessionKeyExpiration(
+ const MFCDMKeyExpiration& aExpiration) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionKeyExpiration",
+ [self = RefPtr{this}, this, expiration = aExpiration]() {
+ RETURN_IF_NULL(mProxy);
+ mProxy->OnExpirationChange(
+ expiration.sessionId(),
+ expiration.expiredTimeMilliSecondsSinceEpoch());
+ }));
+}
+
+void WMFCDMProxyCallback::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy = nullptr;
+}
+
+#undef RETURN_IF_NULL
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h
new file mode 100644
index 0000000000..6e76f63ccc
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
+
+#include "mozilla/PMFCDM.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class WMFCDMProxy;
+
+// This class is used to notify CDM related events called from MFCDMChild, and
+// it will forward the relative calls to WMFCDMProxy on the main thread.
+class WMFCDMProxyCallback final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxyCallback);
+
+ explicit WMFCDMProxyCallback(WMFCDMProxy* aProxy);
+
+ void OnSessionMessage(const MFCDMKeyMessage& aMessage);
+
+ void OnSessionKeyStatusesChange(const MFCDMKeyStatusChange& aKeyStatuses);
+
+ void OnSessionKeyExpiration(const MFCDMKeyExpiration& aExpiration);
+
+ void Shutdown();
+
+ private:
+ ~WMFCDMProxyCallback();
+ RefPtr<WMFCDMProxy> mProxy;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
diff --git a/dom/media/eme/mediafoundation/moz.build b/dom/media/eme/mediafoundation/moz.build
new file mode 100644
index 0000000000..8c54b381ff
--- /dev/null
+++ b/dom/media/eme/mediafoundation/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla += [
+ "WMFCDMImpl.h",
+ "WMFCDMProxy.h",
+ "WMFCDMProxyCallback.h",
+]
+
+UNIFIED_SOURCES += [
+ "WMFCDMImpl.cpp",
+ "WMFCDMProxy.cpp",
+ "WMFCDMProxyCallback.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/eme/moz.build b/dom/media/eme/moz.build
new file mode 100644
index 0000000000..9965e85ae4
--- /dev/null
+++ b/dom/media/eme/moz.build
@@ -0,0 +1,56 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "KeySystemNames.h",
+ "MediaEncryptedEvent.h",
+ "MediaKeyError.h",
+ "MediaKeyMessageEvent.h",
+ "MediaKeys.h",
+ "MediaKeySession.h",
+ "MediaKeyStatusMap.h",
+ "MediaKeySystemAccess.h",
+ "MediaKeySystemAccessManager.h",
+ "MediaKeySystemAccessPermissionRequest.h",
+]
+
+EXPORTS.mozilla += [
+ "CDMCaps.h",
+ "CDMProxy.h",
+ "DecryptorProxyCallback.h",
+ "DetailedPromise.h",
+ "EMEUtils.h",
+ "KeySystemConfig.h",
+]
+
+UNIFIED_SOURCES += [
+ "CDMCaps.cpp",
+ "DetailedPromise.cpp",
+ "EMEUtils.cpp",
+ "KeySystemConfig.cpp",
+ "MediaEncryptedEvent.cpp",
+ "MediaKeyError.cpp",
+ "MediaKeyMessageEvent.cpp",
+ "MediaKeys.cpp",
+ "MediaKeySession.cpp",
+ "MediaKeyStatusMap.cpp",
+ "MediaKeySystemAccess.cpp",
+ "MediaKeySystemAccessManager.cpp",
+ "MediaKeySystemAccessPermissionRequest.cpp",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ DIRS += ["mediadrm"]
+ LOCAL_INCLUDES += ["/dom/media/platforms/android"]
+
+if CONFIG["MOZ_WMF_CDM"]:
+ DIRS += ["mediafoundation"]
+
+DIRS += ["clearkey"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"