diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/media/eme | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/eme')
35 files changed, 7089 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..f8e8964667 --- /dev/null +++ b/dom/media/eme/CDMProxy.h @@ -0,0 +1,316 @@ +/* -*- 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; + +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. + 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()); + } + + // 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. + virtual const nsCString& GetNodeId() const = 0; + + // 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. + virtual const nsString& KeySystem() const = 0; + + virtual DataMutex<CDMCaps>& Capabilites() = 0; + + // 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 nsAString& aMinHdcpVersion) = 0; + +#ifdef DEBUG + virtual bool IsOnOwnerThread() = 0; +#endif + + virtual ChromiumCDMProxy* AsChromiumCDMProxy() { return nullptr; } + + protected: + 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..2a4d8f4dfe --- /dev/null +++ b/dom/media/eme/EMEUtils.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/dom/UnionTypes.h" + +namespace mozilla { + +LogModule* GetEMELog() { + static LazyLogModule log("EME"); + return log; +} + +LogModule* GetEMEVerboseLog() { + static LazyLogModule log("EMEV"); + return log; +} + +ArrayData GetArrayBufferViewOrArrayBufferData( + const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView) { + MOZ_ASSERT(aBufferOrView.IsArrayBuffer() || + aBufferOrView.IsArrayBufferView()); + JS::AutoCheckCannotGC nogc; + if (aBufferOrView.IsArrayBuffer()) { + const dom::ArrayBuffer& buffer = aBufferOrView.GetAsArrayBuffer(); + buffer.ComputeState(); + return ArrayData(buffer.Data(), buffer.Length()); + } else if (aBufferOrView.IsArrayBufferView()) { + const dom::ArrayBufferView& bufferview = + aBufferOrView.GetAsArrayBufferView(); + bufferview.ComputeState(); + return ArrayData(bufferview.Data(), bufferview.Length()); + } + return ArrayData(nullptr, 0); +} + +void CopyArrayBufferViewOrArrayBufferData( + const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray<uint8_t>& aOutData) { + JS::AutoCheckCannotGC nogc; + ArrayData data = GetArrayBufferViewOrArrayBufferData(aBufferOrView); + aOutData.Clear(); + if (!data.IsValid()) { + return; + } + aOutData.AppendElements(data.mData, data.mLength); +} + +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); +} + +nsString KeySystemToGMPName(const nsAString& aKeySystem) { + if (IsClearkeyKeySystem(aKeySystem)) { + return u"gmp-clearkey"_ns; + } + if (IsWidevineKeySystem(aKeySystem)) { + return u"gmp-widevinecdm"_ns; + } + MOZ_ASSERT(false, "We should only call this for known GMPs"); + return u""_ns; +} + +} // namespace mozilla diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h new file mode 100644 index 0000000000..f4de8b383d --- /dev/null +++ b/dom/media/eme/EMEUtils.h @@ -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/. */ + +#ifndef EME_LOG_H_ +#define EME_LOG_H_ + +#include "mozilla/Logging.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { + +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); + +struct ArrayData { + explicit ArrayData(const uint8_t* aData, size_t aLength) + : mData(aData), mLength(aLength) {} + const uint8_t* mData; + const size_t mLength; + bool IsValid() const { return mData != nullptr && mLength != 0; } + bool operator==(const nsTArray<uint8_t>& aOther) const { + return mLength == aOther.Length() && + memcmp(mData, aOther.Elements(), mLength) == 0; + } +}; + +// Helper function to extract data coming in from JS in an +// (ArrayBuffer or ArrayBufferView) IDL typed function argument. +// +// Be *very* careful with this! +// +// Only use returned ArrayData inside the lifetime of the +// ArrayBufferViewOrArrayBuffer; the ArrayData struct does not contain +// a copy of the data! +// +// And do *not* call out to anything that could call into JavaScript, +// while the ArrayData is live, as then all bets about the data not changing +// are off! No calls into JS, no calls into JS-implemented WebIDL or XPIDL, +// nothing. Beware! +// +// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer. +ArrayData GetArrayBufferViewOrArrayBufferData( + const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView); + +nsString KeySystemToGMPName(const nsAString& aKeySystem); + +bool IsClearkeyKeySystem(const nsAString& aKeySystem); + +bool IsWidevineKeySystem(const nsAString& aKeySystem); + +// 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); + +} // namespace mozilla + +#endif // EME_LOG_H_ diff --git a/dom/media/eme/KeySystemNames.h b/dom/media/eme/KeySystemNames.h new file mode 100644 index 0000000000..8a07bc1bd8 --- /dev/null +++ b/dom/media/eme/KeySystemNames.h @@ -0,0 +1,26 @@ +/* -*- 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"; +} // 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..b011715295 --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.cpp @@ -0,0 +1,108 @@ +/* -*- 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.Length(), + mRawInitData.Elements()); + if (!mInitData) { + aRv.NoteJSContextException(cx); + 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..69ce5499f4 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.cpp @@ -0,0 +1,103 @@ +/* -*- 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.Length(), + mRawMessage.Elements()); + if (!mMessage) { + aRv.NoteJSContextException(cx); + 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..79190be710 --- /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.Append(nsPrintfCString( + " (%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); + asyncDispatcher->PostDOMEvent(); +} + +void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) { + EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", this, + NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode); + + RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode)); + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event); + 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..c53f516423 --- /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; +} + +void MediaKeyStatusMap::Get(const ArrayBufferViewOrArrayBuffer& aKey, + OwningMediaKeyStatusOrUndefined& aOutValue, + ErrorResult& aOutRv) const { + ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey); + if (!keyId.IsValid()) { + aOutValue.SetUndefined(); + return; + } + for (const KeyStatus& status : mStatuses) { + if (keyId == status.mKeyId) { + aOutValue.SetAsMediaKeyStatus() = status.mStatus; + return; + } + } + aOutValue.SetUndefined(); +} + +bool MediaKeyStatusMap::Has(const ArrayBufferViewOrArrayBuffer& aKey) const { + ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey); + if (!keyId.IsValid()) { + return false; + } + + for (const KeyStatus& status : mStatuses) { + if (keyId == status.mKeyId) { + return true; + } + } + + return false; +} + +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..35027b25e2 --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.h @@ -0,0 +1,92 @@ +/* -*- 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; + }; + + 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..ed2a8a8200 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.cpp @@ -0,0 +1,1316 @@ +/* -*- 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 "GMPUtils.h" +#include "MediaContainerType.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaSource.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsDOMString.h" +#include "nsIObserverService.h" +#include "nsMimeTypes.h" +#include "nsReadableUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" +#include "WebMDecoder.h" + +#ifdef XP_WIN +# include "WMFDecoderModule.h" +#endif +#ifdef MOZ_WIDGET_ANDROID +# include "AndroidDecoderModule.h" +# include "mozilla/java/MediaDrmProxyWrappers.h" +#endif + +namespace mozilla::dom { + +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) { + EME_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); +} + +static bool HavePluginForKeySystem(const nsCString& aKeySystem) { + nsCString api = nsLiteralCString(CHROMIUM_CDM_API); + + bool havePlugin = HaveGMPFor(api, {aKeySystem}); +#ifdef MOZ_WIDGET_ANDROID + // Check if we can use MediaDrm for this keysystem. + if (!havePlugin) { + havePlugin = mozilla::java::MediaDrmProxy::IsSchemeSupported(aKeySystem); + } +#endif + return havePlugin; +} + +static MediaKeySystemStatus EnsureCDMInstalled(const nsAString& aKeySystem, + nsACString& aOutMessage) { + if (!HavePluginForKeySystem(NS_ConvertUTF16toUTF8(aKeySystem))) { + aOutMessage = "CDM is not installed"_ns; + return MediaKeySystemStatus::Cdm_not_installed; + } + + return MediaKeySystemStatus::Available; +} + +/* static */ +MediaKeySystemStatus MediaKeySystemAccess::GetKeySystemStatus( + const nsAString& aKeySystem, nsACString& aOutMessage) { + MOZ_ASSERT(StaticPrefs::media_eme_enabled() || + IsClearkeyKeySystem(aKeySystem)); + + if (IsClearkeyKeySystem(aKeySystem)) { + return EnsureCDMInstalled(aKeySystem, aOutMessage); + } + + if (IsWidevineKeySystem(aKeySystem)) { + 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(aKeySystem, aOutMessage); +#ifdef MOZ_WIDGET_ANDROID + } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible", + false)) { + nsCString keySystem = NS_ConvertUTF16toUTF8(aKeySystem); + 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 + } + } + + return MediaKeySystemStatus::Cdm_not_supported; +} + +typedef nsCString EMECodecString; + +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; + +EMECodecString ToEMEAPICodecString(const nsString& aCodec) { + if (IsAACCodecString(aCodec)) { + return EME_CODEC_AAC; + } + if (aCodec.EqualsLiteral("opus")) { + return EME_CODEC_OPUS; + } + if (aCodec.EqualsLiteral("vorbis")) { + return EME_CODEC_VORBIS; + } + if (aCodec.EqualsLiteral("flac")) { + return EME_CODEC_FLAC; + } + if (IsH264CodecString(aCodec)) { + return EME_CODEC_H264; + } + if (IsVP8CodecString(aCodec)) { + return EME_CODEC_VP8; + } + if (IsVP9CodecString(aCodec)) { + return EME_CODEC_VP9; + } + return ""_ns; +} + +// A codec can be decrypted-and-decoded by the CDM, or only decrypted +// by the CDM and decoded by Gecko. Not both. +struct KeySystemContainerSupport { + KeySystemContainerSupport() = default; + ~KeySystemContainerSupport() = default; + KeySystemContainerSupport(const KeySystemContainerSupport& aOther) { + mCodecsDecoded = aOther.mCodecsDecoded.Clone(); + mCodecsDecrypted = aOther.mCodecsDecrypted.Clone(); + } + KeySystemContainerSupport& operator=( + const KeySystemContainerSupport& aOther) { + if (this == &aOther) { + return *this; + } + mCodecsDecoded = aOther.mCodecsDecoded.Clone(); + mCodecsDecrypted = aOther.mCodecsDecrypted.Clone(); + return *this; + } + KeySystemContainerSupport(KeySystemContainerSupport&& aOther) = default; + KeySystemContainerSupport& operator=(KeySystemContainerSupport&&) = 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(EMECodecString aCodec) const { + return mCodecsDecoded.Contains(aCodec); + } + + // CDM decrypts and passes the decrypted samples back to Gecko for decoding. + bool Decrypts(EMECodecString aCodec) const { + return mCodecsDecrypted.Contains(aCodec); + } + + void SetCanDecryptAndDecode(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(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); + } + + private: + nsTArray<EMECodecString> mCodecsDecoded; + nsTArray<EMECodecString> mCodecsDecrypted; +}; + +enum class KeySystemFeatureSupport { + Prohibited = 1, + Requestable = 2, + Required = 3, +}; + +struct KeySystemConfig { + KeySystemConfig() = default; + ~KeySystemConfig() = default; + 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 mKeySystem; + nsTArray<nsString> mInitDataTypes; + KeySystemFeatureSupport mPersistentState = + KeySystemFeatureSupport::Prohibited; + KeySystemFeatureSupport mDistinctiveIdentifier = + KeySystemFeatureSupport::Prohibited; + nsTArray<MediaKeySessionType> mSessionTypes; + nsTArray<nsString> mVideoRobustness; + nsTArray<nsString> mAudioRobustness; + nsTArray<nsString> mEncryptionSchemes; + KeySystemContainerSupport mMP4; + KeySystemContainerSupport mWebM; +}; + +static nsTArray<KeySystemConfig> GetSupportedKeySystems() { + nsTArray<KeySystemConfig> keySystemConfigs; + + { + const nsCString keySystem = nsLiteralCString(kClearKeyKeySystemName); + if (HavePluginForKeySystem(keySystem)) { + KeySystemConfig clearkey; + clearkey.mKeySystem.AssignLiteral(kClearKeyKeySystemName); + clearkey.mInitDataTypes.AppendElement(u"cenc"_ns); + clearkey.mInitDataTypes.AppendElement(u"keyids"_ns); + clearkey.mInitDataTypes.AppendElement(u"webm"_ns); + clearkey.mPersistentState = KeySystemFeatureSupport::Requestable; + clearkey.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); + clearkey.mEncryptionSchemes.AppendElement(u"cenc"_ns); + clearkey.mEncryptionSchemes.AppendElement(u"cbcs"_ns); + clearkey.mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); + if (StaticPrefs::media_clearkey_persistent_license_enabled()) { + clearkey.mSessionTypes.AppendElement( + MediaKeySessionType::Persistent_license); + } +#if defined(XP_WIN) + // Clearkey CDM uses WMF's H.264 decoder on Windows. + if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) { + clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + } else { + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); + } +#else + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); +#endif + clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC); + clearkey.mMP4.SetCanDecrypt(EME_CODEC_FLAC); + clearkey.mMP4.SetCanDecrypt(EME_CODEC_OPUS); + clearkey.mMP4.SetCanDecrypt(EME_CODEC_VP9); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_OPUS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP8); + clearkey.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{clearkey}; + clearkeyWithProtectionQuery.mKeySystem.AssignLiteral( + kClearKeyWithProtectionQueryKeySystemName); + keySystemConfigs.AppendElement(std::move(clearkeyWithProtectionQuery)); + } + + keySystemConfigs.AppendElement(std::move(clearkey)); + } + } + { + const nsCString keySystem = nsLiteralCString(kWidevineKeySystemName); + if (HavePluginForKeySystem(keySystem)) { + KeySystemConfig widevine; + widevine.mKeySystem.AssignLiteral(kWidevineKeySystemName); + widevine.mInitDataTypes.AppendElement(u"cenc"_ns); + widevine.mInitDataTypes.AppendElement(u"keyids"_ns); + widevine.mInitDataTypes.AppendElement(u"webm"_ns); + widevine.mPersistentState = KeySystemFeatureSupport::Requestable; + widevine.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + widevine.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); +#ifdef MOZ_WIDGET_ANDROID + widevine.mSessionTypes.AppendElement( + MediaKeySessionType::Persistent_license); +#endif + widevine.mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + widevine.mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + widevine.mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns); + widevine.mEncryptionSchemes.AppendElement(u"cenc"_ns); + widevine.mEncryptionSchemes.AppendElement(u"cbcs"_ns); + widevine.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; + KeySystemContainerSupport* mSupportType; + } DataForValidation; + + DataForValidation validationList[] = { + {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC, + &widevine.mMP4}, + {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC, + &widevine.mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC, + &widevine.mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC, + &widevine.mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &widevine.mMP4}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8, + &widevine.mWebM}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9, + &widevine.mWebM}, + {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS, + &widevine.mWebM}, + {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &widevine.mWebM}, + }; + + for (const auto& data : validationList) { + if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName, + data.mMimeType)) { + if (AndroidDecoderModule::SupportsMimeType(data.mMimeType) != + media::DecodeSupport::Unsupported) { + 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)) { + widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC); + } +# else + widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC); +# endif + widevine.mMP4.SetCanDecrypt(EME_CODEC_FLAC); + widevine.mMP4.SetCanDecrypt(EME_CODEC_OPUS); + widevine.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + widevine.mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9); + widevine.mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + widevine.mWebM.SetCanDecrypt(EME_CODEC_OPUS); + widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8); + widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9); +#endif + keySystemConfigs.AppendElement(std::move(widevine)); + } + } + + return keySystemConfigs; +} + +static bool GetKeySystemConfig(const nsAString& aKeySystem, + KeySystemConfig& aOutKeySystemConfig) { + for (auto&& config : GetSupportedKeySystems()) { + if (config.mKeySystem.Equals(aKeySystem)) { + aOutKeySystemConfig = std::move(config); + return true; + } + } + // No matching key system found. + return false; +} + +/* static */ +bool MediaKeySystemAccess::KeySystemSupportsInitDataType( + const nsAString& aKeySystem, const nsAString& aInitDataType) { + KeySystemConfig implementation; + return GetKeySystemConfig(aKeySystem, implementation) && + implementation.mInitDataTypes.Contains(aInitDataType); +} + +enum CodecType { Audio, Video, Invalid }; + +static bool CanDecryptAndDecode( + const nsString& aKeySystem, const nsString& aContentType, + CodecType aCodecType, const KeySystemContainerSupport& aContainerSupport, + const nsTArray<EMECodecString>& aCodecs, + DecoderDoctorDiagnostics* aDiagnostics) { + MOZ_ASSERT(aCodecType != Invalid); + for (const 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 == 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; +} + +CodecType GetMajorType(const MediaMIMEType& aMIMEType) { + if (aMIMEType.HasAudioMajorType()) { + return Audio; + } + if (aMIMEType.HasVideoMajorType()) { + return Video; + } + return Invalid; +} + +static CodecType GetCodecType(const EMECodecString& aCodec) { + if (aCodec.Equals(EME_CODEC_AAC) || aCodec.Equals(EME_CODEC_OPUS) || + aCodec.Equals(EME_CODEC_VORBIS) || aCodec.Equals(EME_CODEC_FLAC)) { + return Audio; + } + if (aCodec.Equals(EME_CODEC_H264) || aCodec.Equals(EME_CODEC_VP8) || + aCodec.Equals(EME_CODEC_VP9)) { + return Video; + } + return Invalid; +} + +static bool AllCodecsOfType(const nsTArray<EMECodecString>& aCodecs, + const CodecType aCodecType) { + for (const 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<EMECodecString> codecs; + for (const auto& codecString : + containerType.ExtendedType().Codecs().Range()) { + 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 isMP4 = + DecoderTraits::IsMP4SupportedType(containerType, aDiagnostics); + if (isMP4 && !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 (!isMP4 && !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 (isMP4) { + if (aCodecType == Audio) { + codecs.AppendElement(EME_CODEC_AAC); + } else if (aCodecType == Video) { + codecs.AppendElement(EME_CODEC_H264); + } + } else if (isWebM) { + if (aCodecType == Audio) { + codecs.AppendElement(EME_CODEC_VORBIS); + } else if (aCodecType == Video) { + codecs.AppendElement(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 = isMP4 ? 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 KeySystemFeatureSupport aFeatureSupport, + 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 && + aFeatureSupport == KeySystemFeatureSupport::Prohibited) { + 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 (aFeatureSupport == KeySystemFeatureSupport::Prohibited) { + 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 (aFeatureSupport == KeySystemFeatureSupport::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) { + // 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 (!aKeySystem.mSessionTypes.Contains(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 == + KeySystemFeatureSupport::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 == KeySystemFeatureSupport::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) { + KeySystemConfig implementation; + if (!GetKeySystemConfig(aKeySystem, implementation)) { + return false; + } + 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); +} + +} // namespace mozilla::dom diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h new file mode 100644 index 0000000000..40ce2aab2d --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.h @@ -0,0 +1,85 @@ +/* -*- 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 { + +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 nsAString& aKeySystem, nsACString& aOutExceptionMessage); + + static bool IsSupported(const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + DecoderDoctorDiagnostics* aDiagnostics); + + 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..093b278b84 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.cpp @@ -0,0 +1,681 @@ +/* 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/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 +#ifdef XP_MACOSX +# include "nsCocoaFeatures.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) + : mPromise(aPromise), mKeySystem(aKeySystem), mConfigs(aConfigs) { + 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) && + !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->mKeySystem, 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)); + + // We may need to install the CDM to continue. + if (status == MediaKeySystemStatus::Cdm_not_installed && + IsWidevineKeySystem(aRequest->mKeySystem)) { + // 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; + } + + const nsString keySystem = aRequest->mKeySystem; + if (AwaitInstall(std::move(aRequest))) { + // Notify chrome that we're going to wait for the CDM to download/update. + MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status); + } else { + // Failed to await the install. Log failure and give up trying to service + // this request. + 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. + 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]->mKeySystem, 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..992de2a9e7 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.h @@ -0,0 +1,229 @@ +/* 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 + * + */ + +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 { + 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; + // The KeySystem passed for this request. + const nsString mKeySystem; + // The config(s) passed for this request. + const Sequence<MediaKeySystemConfiguration> mConfigs; + + // 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..7ce3c6d246 --- /dev/null +++ b/dom/media/eme/MediaKeys.cpp @@ -0,0 +1,836 @@ +/* -*- 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 + +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. + GetMainThreadEventTarget()->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() { + EME_LOG("MediaKeys[%p]::CreateCDMProxy()", this); + 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 + { + 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), + KeySystemToGMPName(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(")"); + } +} + +already_AddRefed<Promise> MediaKeys::GetStatusForPolicy( + const MediaKeysPolicy& aPolicy, ErrorResult& aRv) { + RefPtr<DetailedPromise> promise( + MakePromise(aRv, "MediaKeys::GetStatusForPolicy()"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // Currently, only widevine CDM supports for this API. + if (!IsWidevineKeySystem(mKeySystem)) { + EME_LOG( + "MediaKeys[%p]::GetStatusForPolicy() HDCP policy check on unsupported " + "keysystem ", + this); + NS_WARNING("Tried to query without a CDM"); + promise->MaybeRejectWithNotSupportedError( + "HDCP policy check on unsupported keysystem"); + 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.", + NS_ConvertUTF16toUTF8(aPolicy.mMinHdcpVersion).get()); + mProxy->GetStatusForPolicy(StorePromise(promise), aPolicy.mMinHdcpVersion); + 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/mediadrm/MediaDrmCDMCallbackProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp new file mode 100644 index 0000000000..1232c1e6ee --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "mozilla/CDMProxy.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(CDMProxy* aProxy) + : mProxy(aProxy) {} + +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..52a6140f0d --- /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; + +// 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 : 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(CDMProxy* aProxy); + + void BatchedKeyStatusChangedInternal(const nsCString& aSessionId, + const nsTArray<CDMKeyInfo>& aKeyInfos); + // Warning: Weak ref. + CDMProxy* 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..521db07d16 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp @@ -0,0 +1,457 @@ +/* -*- 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; +} + +void MediaDrmCDMProxy::Terminated() { + // TODO: Implement Terminated. + // Should find a way to handle the case when remote side MediaDrm crashed. +} + +const nsCString& MediaDrmCDMProxy::GetNodeId() const { return mNodeId; } + +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); +} + +const nsString& MediaDrmCDMProxy::KeySystem() const { return mKeySystem; } + +DataMutex<CDMCaps>& MediaDrmCDMProxy::Capabilites() { return mCapabilites; } + +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 nsAString& 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); + + mCallback.reset(new MediaDrmCDMCallbackProxy(this)); + mCDM->Init(mCallback.get()); + 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..0d2432f7f2 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h @@ -0,0 +1,194 @@ +/* -*- 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 : 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; + + const nsCString& GetNodeId() const 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; + + // Threadsafe. + const nsString& KeySystem() const override; + + DataMutex<CDMCaps>& Capabilites() override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetStatusForPolicy(PromiseId aPromiseId, + const nsAString& 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; + UniquePtr<MediaDrmCDMCallbackProxy> mCallback; + 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..3ad3c1e038 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp @@ -0,0 +1,271 @@ +/* -*- 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 "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( + DecryptorProxyCallback* aDecryptorProxyCallback) + : mDecryptorProxyCallback(aDecryptorProxyCallback) { + MOZ_ASSERT(aDecryptorProxyCallback); + } + /* + * 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: + DecryptorProxyCallback* 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(DecryptorProxyCallback* aCallback) { + MOZ_ASSERT(mJavaCallbacks); + + mCallback = aCallback; + MediaDrmJavaCallbacksSupport::AttachNative( + mJavaCallbacks, + mozilla::MakeUnique<MediaDrmJavaCallbacksSupport>(mCallback)); + 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..5640189008 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h @@ -0,0 +1,66 @@ +/* -*- 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 "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 MediaDrmProxySupport final { + public: + explicit MediaDrmProxySupport(const nsAString& aKeySystem); + ~MediaDrmProxySupport(); + + /* + * APIs to act as GMPDecryptorAPI, discarding unnecessary calls. + */ + nsresult Init(DecryptorProxyCallback* 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; + DecryptorProxyCallback* mCallback; + 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/moz.build b/dom/media/eme/moz.build new file mode 100644 index 0000000000..364c0a4844 --- /dev/null +++ b/dom/media/eme/moz.build @@ -0,0 +1,49 @@ +# -*- 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", +] + +UNIFIED_SOURCES += [ + "CDMCaps.cpp", + "DetailedPromise.cpp", + "EMEUtils.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"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |