diff options
Diffstat (limited to 'dom/media/eme')
32 files changed, 6164 insertions, 0 deletions
diff --git a/dom/media/eme/CDMCaps.cpp b/dom/media/eme/CDMCaps.cpp new file mode 100644 index 000000000..20ff63bad --- /dev/null +++ b/dom/media/eme/CDMCaps.cpp @@ -0,0 +1,170 @@ +/* -*- 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() + : mMonitor("CDMCaps") +{ +} + +CDMCaps::~CDMCaps() +{ +} + +void +CDMCaps::Lock() +{ + mMonitor.Lock(); +} + +void +CDMCaps::Unlock() +{ + mMonitor.Unlock(); +} + +CDMCaps::AutoLock::AutoLock(CDMCaps& aInstance) + : mData(aInstance) +{ + mData.Lock(); +} + +CDMCaps::AutoLock::~AutoLock() +{ + mData.Unlock(); +} + +// 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::AutoLock::IsKeyUsable(const CencKeyId& aKeyId) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mId == aKeyId) { + return IsUsableStatus(keyStatus.mStatus); + } + } + return false; +} + +bool +CDMCaps::AutoLock::SetKeyStatus(const CencKeyId& aKeyId, + const nsString& aSessionId, + const dom::Optional<dom::MediaKeyStatus>& aStatus) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + + if (!aStatus.WasPassed()) { + // Called from ForgetKeyStatus. + // Return true if the element is found to notify key changes. + return mData.mKeyStatuses.RemoveElement(KeyStatus(aKeyId, + aSessionId, + dom::MediaKeyStatus::Internal_error)); + } + + KeyStatus key(aKeyId, aSessionId, aStatus.Value()); + auto index = mData.mKeyStatuses.IndexOf(key); + if (index != mData.mKeyStatuses.NoIndex) { + if (mData.mKeyStatuses[index].mStatus == aStatus.Value()) { + // No change. + return false; + } + auto oldStatus = mData.mKeyStatuses[index].mStatus; + mData.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 { + mData.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 = mData.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::AutoLock::NotifyWhenKeyIdUsable(const CencKeyId& aKey, + SamplesWaitingForKey* aListener) +{ + mData.mMonitor.AssertCurrentThreadOwns(); + MOZ_ASSERT(!IsKeyUsable(aKey)); + MOZ_ASSERT(aListener); + mData.mWaitForKeys.AppendElement(WaitForKeys(aKey, aListener)); +} + +void +CDMCaps::AutoLock::GetKeyStatusesForSession(const nsAString& aSessionId, + nsTArray<KeyStatus>& aOutKeyStatuses) +{ + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mSessionId.Equals(aSessionId)) { + aOutKeyStatuses.AppendElement(keyStatus); + } + } +} + +void +CDMCaps::AutoLock::GetSessionIdsForKeyId(const CencKeyId& aKeyId, + nsTArray<nsCString>& aOutSessionIds) +{ + for (const KeyStatus& keyStatus : mData.mKeyStatuses) { + if (keyStatus.mId == aKeyId) { + aOutSessionIds.AppendElement(NS_ConvertUTF16toUTF8(keyStatus.mSessionId)); + } + } +} + +bool +CDMCaps::AutoLock::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 000000000..7749bdc67 --- /dev/null +++ b/dom/media/eme/CDMCaps.h @@ -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/. */ + +#ifndef CDMCaps_h_ +#define CDMCaps_h_ + +#include "gmp-decryption.h" +#include "nsIThread.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) + , mSessionId(aSessionId) + , mStatus(aStatus) + {} + KeyStatus(const KeyStatus& aOther) + : mId(aOther.mId) + , 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; + }; + + // Locks the CDMCaps. It must be locked to access its shared state. + // Threadsafe when locked. + class MOZ_STACK_CLASS AutoLock { + public: + explicit AutoLock(CDMCaps& aKeyCaps); + ~AutoLock(); + + 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); + + void GetSessionIdsForKeyId(const CencKeyId& aKeyId, + nsTArray<nsCString>& aOutSessionIds); + + // 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: + // Not taking a strong ref, since this should be allocated on the stack. + CDMCaps& mData; + }; + +private: + void Lock(); + void Unlock(); + + struct WaitForKeys { + WaitForKeys(const CencKeyId& aKeyId, + SamplesWaitingForKey* aListener) + : mKeyId(aKeyId) + , mListener(aListener) + {} + CencKeyId mKeyId; + RefPtr<SamplesWaitingForKey> mListener; + }; + + Monitor mMonitor; + + 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 000000000..f3f1add43 --- /dev/null +++ b/dom/media/eme/CDMProxy.h @@ -0,0 +1,279 @@ +/* -*- 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/MozPromise.h" + +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeys.h" + +#include "nsIThread.h" + +namespace mozilla { +class MediaRawData; + +enum DecryptStatus { + Ok = 0, + GenericErr = 1, + NoKeyErr = 2, + AbortedErr = 3, +}; + +struct DecryptResult { + DecryptResult(DecryptStatus aStatus, MediaRawData* aSample) + : mStatus(aStatus) + , mSample(aSample) + {} + DecryptStatus mStatus; + RefPtr<MediaRawData> mSample; +}; + +class CDMKeyInfo { +public: + explicit CDMKeyInfo(const nsTArray<uint8_t>& aKeyId) + : mKeyId(aKeyId) + , mStatus() + {} + + CDMKeyInfo(const nsTArray<uint8_t>& aKeyId, + const dom::Optional<dom::MediaKeyStatus>& aStatus) + : mKeyId(aKeyId) + , 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; + if (aKeyInfo.mStatus.WasPassed()) { + mStatus.Construct(aKeyInfo.mStatus.Value()); + } + } + + nsTArray<uint8_t> mKeyId; + dom::Optional<dom::MediaKeyStatus> mStatus; +}; + +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_IMETHOD_(MozExternalRefCountType) AddRef(void) = 0; + NS_IMETHOD_(MozExternalRefCountType) Release(void) = 0; + + typedef MozPromise<DecryptResult, DecryptResult, /* IsExclusive = */ true> DecryptPromise; + + // Main thread only. + CDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : mKeys(aKeys) + , mKeySystem(aKeySystem) + , mDistinctiveIdentifierRequired(aDistinctiveIdentifierRequired) + , mPersistentStateRequired(aPersistentStateRequired) + {} + + // 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, + bool aInPrivateBrowsing) = 0; + + virtual void OnSetDecryptorId(uint32_t aId) {} + + // Main thread only. + // Uses the CDM to create a key session. + // Calls MediaKeys::OnSessionActivated() when session is created. + // Assumes ownership of (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, + 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 (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 (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. + 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, + 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, + nsresult aDOMException, + 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 DOMException corresponding to aExceptionCode. + // Can be called from any thread. + virtual void RejectPromise(PromiseId aId, + nsresult aExceptionCode, + 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 CDMCaps& Capabilites() = 0; + + // Main thread only. + virtual void OnKeyStatusesChange(const nsAString& aSessionId) = 0; + + virtual void GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) = 0; + +#ifdef DEBUG + virtual bool IsOnOwnerThread() = 0; +#endif + + virtual uint32_t GetDecryptorId() { return 0; } + +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; + + CDMCaps mCapabilites; + + const bool mDistinctiveIdentifierRequired; + const bool mPersistentStateRequired; +}; + + +} // namespace mozilla + +#endif // CDMProxy_h_ diff --git a/dom/media/eme/DecryptorProxyCallback.h b/dom/media/eme/DecryptorProxyCallback.h new file mode 100644 index 000000000..c1fcb49a4 --- /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" + +class DecryptorProxyCallback { +public: + + virtual ~DecryptorProxyCallback() {} + + virtual void SetDecryptorId(uint32_t aId) = 0; + + 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, + nsresult 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 000000000..5893bea2e --- /dev/null +++ b/dom/media/eme/DetailedPromise.cpp @@ -0,0 +1,105 @@ +/* -*- 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 "mozilla/dom/DOMException.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace 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::ID aSuccessLatencyProbe, + Telemetry::ID 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(Failed); +} + +void +DetailedPromise::MaybeReject(nsresult aArg, const nsACString& aReason) +{ + nsPrintfCString msg("%s promise rejected 0x%x '%s'", mName.get(), aArg, + PromiseFlatCString(aReason).get()); + EME_LOG(msg.get()); + + MaybeReportTelemetry(Failed); + + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); + + ErrorResult rv; + rv.ThrowDOMException(aArg, aReason); + Promise::MaybeReject(rv); +} + +void +DetailedPromise::MaybeReject(ErrorResult&, const nsACString& aReason) +{ + NS_NOTREACHED("nsresult expected in MaybeReject()"); +} + +/* static */ already_AddRefed<DetailedPromise> +DetailedPromise::Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv, + const nsACString& aName) +{ + RefPtr<DetailedPromise> promise = new DetailedPromise(aGlobal, aName); + promise->CreateWrapper(nullptr, aRv); + return aRv.Failed() ? nullptr : promise.forget(); +} + +/* static */ already_AddRefed<DetailedPromise> +DetailedPromise::Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv, + const nsACString& aName, + Telemetry::ID aSuccessLatencyProbe, + Telemetry::ID aFailureLatencyProbe) +{ + RefPtr<DetailedPromise> promise = new DetailedPromise(aGlobal, aName, aSuccessLatencyProbe, aFailureLatencyProbe); + promise->CreateWrapper(nullptr, aRv); + return aRv.Failed() ? nullptr : promise.forget(); +} + +void +DetailedPromise::MaybeReportTelemetry(Status 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 == Succeeded) ? "succcess" : "failure"), latency); + Telemetry::ID tid = (aStatus == Succeeded) ? mSuccessLatencyProbe.Value() + : mFailureLatencyProbe.Value(); + Telemetry::Accumulate(tid, latency); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/DetailedPromise.h b/dom/media/eme/DetailedPromise.h new file mode 100644 index 000000000..83e40c022 --- /dev/null +++ b/dom/media/eme/DetailedPromise.h @@ -0,0 +1,73 @@ +/* -*- 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 { +namespace 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); + + static already_AddRefed<DetailedPromise> + Create(nsIGlobalObject* aGlobal, ErrorResult& aRv, + const nsACString& aName, + Telemetry::ID aSuccessLatencyProbe, + Telemetry::ID aFailureLatencyProbe); + + template <typename T> + void MaybeResolve(const T& aArg) + { + EME_LOG("%s promise resolved", mName.get()); + MaybeReportTelemetry(Succeeded); + Promise::MaybeResolve<T>(aArg); + } + + void MaybeReject(nsresult aArg) = delete; + void MaybeReject(nsresult aArg, const nsACString& aReason); + + void MaybeReject(ErrorResult& aArg) = delete; + void MaybeReject(ErrorResult&, const nsACString& aReason); + +private: + explicit DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName); + + explicit DetailedPromise(nsIGlobalObject* aGlobal, + const nsACString& aName, + Telemetry::ID aSuccessLatencyProbe, + Telemetry::ID aFailureLatencyProbe); + virtual ~DetailedPromise(); + + enum Status { Succeeded, Failed }; + void MaybeReportTelemetry(Status aStatus); + + nsCString mName; + bool mResponded; + TimeStamp mStartTime; + Optional<Telemetry::ID> mSuccessLatencyProbe; + Optional<Telemetry::ID> mFailureLatencyProbe; +}; + +} // namespace dom +} // namespace mozilla + +#endif // __DetailedPromise_h__ diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp new file mode 100644 index 000000000..c248b3a24 --- /dev/null +++ b/dom/media/eme/EMEUtils.cpp @@ -0,0 +1,97 @@ +/* -*- 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 "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()); + if (aBufferOrView.IsArrayBuffer()) { + const dom::ArrayBuffer& buffer = aBufferOrView.GetAsArrayBuffer(); + buffer.ComputeLengthAndData(); + return ArrayData(buffer.Data(), buffer.Length()); + } else if (aBufferOrView.IsArrayBufferView()) { + const dom::ArrayBufferView& bufferview = aBufferOrView.GetAsArrayBufferView(); + bufferview.ComputeLengthAndData(); + return ArrayData(bufferview.Data(), bufferview.Length()); + } + return ArrayData(nullptr, 0); +} + +void +CopyArrayBufferViewOrArrayBufferData(const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView, + nsTArray<uint8_t>& aOutData) +{ + ArrayData data = GetArrayBufferViewOrArrayBufferData(aBufferOrView); + aOutData.Clear(); + if (!data.IsValid()) { + return; + } + aOutData.AppendElements(data.mData, data.mLength); +} + +bool +IsClearkeyKeySystem(const nsAString& aKeySystem) +{ + return !CompareUTF8toUTF16(kEMEKeySystemClearkey, aKeySystem); +} + +bool +IsPrimetimeKeySystem(const nsAString& aKeySystem) +{ + return !CompareUTF8toUTF16(kEMEKeySystemPrimetime, aKeySystem); +} + +bool +IsWidevineKeySystem(const nsAString& aKeySystem) +{ + return !CompareUTF8toUTF16(kEMEKeySystemWidevine, aKeySystem); +} + +nsString +KeySystemToGMPName(const nsAString& aKeySystem) +{ + if (IsPrimetimeKeySystem(aKeySystem)) { + return NS_LITERAL_STRING("gmp-eme-adobe"); + } + if (IsClearkeyKeySystem(aKeySystem)) { + return NS_LITERAL_STRING("gmp-clearkey"); + } + if (IsWidevineKeySystem(aKeySystem)) { + return NS_LITERAL_STRING("gmp-widevinecdm"); + } + MOZ_ASSERT(false, "We should only call this for known GMPs"); + return EmptyString(); +} + +CDMType +ToCDMTypeTelemetryEnum(const nsString& aKeySystem) +{ + if (IsWidevineKeySystem(aKeySystem)) { + return CDMType::eWidevine; + } else if (IsClearkeyKeySystem(aKeySystem)) { + return CDMType::eClearKey; + } else if (IsPrimetimeKeySystem(aKeySystem)) { + return CDMType::ePrimetime; + } + return CDMType::eUnknown; +} + +} // namespace mozilla diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h new file mode 100644 index 000000000..1794f8462 --- /dev/null +++ b/dom/media/eme/EMEUtils.h @@ -0,0 +1,107 @@ +/* -*- 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 "VideoUtils.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 +IsPrimetimeKeySystem(const nsAString& aKeySystem); + +bool +IsWidevineKeySystem(const nsAString& aKeySystem); + +enum CDMType { + eClearKey = 0, + ePrimetime = 1, + eWidevine = 2, + eUnknown = 3 +}; + +CDMType +ToCDMTypeTelemetryEnum(const nsString& aKeySystem); + +} // namespace mozilla + +#endif // EME_LOG_H_ diff --git a/dom/media/eme/MediaEncryptedEvent.cpp b/dom/media/eme/MediaEncryptedEvent.cpp new file mode 100644 index 000000000..8e2595fcb --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.cpp @@ -0,0 +1,129 @@ +/* -*- 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 "jsfriendapi.h" +#include "nsINode.h" +#include "mozilla/dom/MediaKeys.h" + +namespace mozilla { +namespace 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) + tmp->mInitData = nullptr; + mozilla::DropJSObjects(this); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaEncryptedEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaEncryptedEvent::MediaEncryptedEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) +{ + mozilla::HoldJSObjects(this); +} + +MediaEncryptedEvent::~MediaEncryptedEvent() +{ + mInitData = nullptr; + mozilla::DropJSObjects(this); +} + +JSObject* +MediaEncryptedEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaEncryptedEventBinding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<MediaEncryptedEvent> +MediaEncryptedEvent::Constructor(EventTarget* aOwner) +{ + RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner); + e->InitEvent(NS_LITERAL_STRING("encrypted"), false, false); + 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(NS_LITERAL_STRING("encrypted"), false, false); + e->mInitDataType = aInitDataType; + e->mRawInitData = aInitData; + 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()) { + const auto& a = aEventInitDict.mInitData.Value(); + a.ComputeLengthAndData(); + e->mInitData = ArrayBuffer::Create(aGlobal.Context(), + a.Length(), + a.Data()); + if (!e->mInitData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + 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.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + mRawInitData.Clear(); + } + aData.set(mInitData); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaEncryptedEvent.h b/dom/media/eme/MediaEncryptedEvent.h new file mode 100644 index 000000000..806fccad6 --- /dev/null +++ b/dom/media/eme/MediaEncryptedEvent.h @@ -0,0 +1,67 @@ +/* -*- 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 "mozilla/dom/MediaEncryptedEventBinding.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingUtils.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace dom { + +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 dom +} // namespace mozilla + +#endif // mozilla_dom_MediaKeyNeededEvent_h__ diff --git a/dom/media/eme/MediaKeyError.cpp b/dom/media/eme/MediaKeyError.cpp new file mode 100644 index 000000000..635c34364 --- /dev/null +++ b/dom/media/eme/MediaKeyError.cpp @@ -0,0 +1,39 @@ +/* -*- 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 { +namespace dom { + +MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode) + : Event(aOwner, nullptr, nullptr) + , mSystemCode(aSystemCode) +{ + InitEvent(NS_LITERAL_STRING("error"), false, false); +} + +MediaKeyError::~MediaKeyError() +{ +} + +uint32_t +MediaKeyError::SystemCode() const +{ + return mSystemCode; +} + +JSObject* +MediaKeyError::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyErrorBinding::Wrap(aCx, this, aGivenProto); +} + + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyError.h b/dom/media/eme/MediaKeyError.h new file mode 100644 index 000000000..df79b2c95 --- /dev/null +++ b/dom/media/eme/MediaKeyError.h @@ -0,0 +1,38 @@ +/* -*- 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 "mozilla/ErrorResult.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/Event.h" +#include "js/TypeDecls.h" + +namespace mozilla { +namespace dom { + +class MediaKeyError final : public Event +{ +public: + NS_FORWARD_TO_EVENT + + 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 dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeyMessageEvent.cpp b/dom/media/eme/MediaKeyMessageEvent.cpp new file mode 100644 index 000000000..37c509e67 --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.cpp @@ -0,0 +1,122 @@ +/* -*- 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/GCAPI.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 { +namespace 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) + tmp->mMessage = nullptr; + mozilla::DropJSObjects(this); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaKeyMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) +{ + mozilla::HoldJSObjects(this); +} + +MediaKeyMessageEvent::~MediaKeyMessageEvent() +{ + mMessage = nullptr; + mozilla::DropJSObjects(this); +} + +MediaKeyMessageEvent* +MediaKeyMessageEvent::AsMediaKeyMessageEvent() +{ + return this; +} + +JSObject* +MediaKeyMessageEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyMessageEventBinding::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(NS_LITERAL_STRING("message"), false, false); + e->mMessageType = aMessageType; + e->mRawMessage = aMessage; + 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); + aEventInitDict.mMessage.ComputeLengthAndData(); + e->mMessage = ArrayBuffer::Create(aGlobal.Context(), + aEventInitDict.mMessage.Length(), + aEventInitDict.mMessage.Data()); + if (!e->mMessage) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + 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.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + mRawMessage.Clear(); + } + aMessage.set(mMessage); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyMessageEvent.h b/dom/media/eme/MediaKeyMessageEvent.h new file mode 100644 index 000000000..b6cc86dfa --- /dev/null +++ b/dom/media/eme/MediaKeyMessageEvent.h @@ -0,0 +1,67 @@ +/* -*- 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 "mozilla/ErrorResult.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 { +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 000000000..d5eff3f77 --- /dev/null +++ b/dom/media/eme/MediaKeySession.cpp @@ -0,0 +1,674 @@ +/* -*- 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/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaEncryptedEvent.h" +#include "mozilla/dom/MediaKeyStatusMap.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "mozilla/dom/KeyIdsInitDataBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/CDMProxy.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Move.h" +#include "nsContentUtils.h" +#include "mozilla/EMEUtils.h" +#include "GMPUtils.h" +#include "nsPrintfCString.h" +#include "psshparser/PsshParser.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, + DOMEventTargetHelper, + mMediaKeyError, + mKeys, + mKeyStatusMap, + mClosed) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(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(JSContext* aCx, + 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, NS_LITERAL_CSTRING("MediaKeys.createSession")); +} + +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() +{ +} + +MediaKeyError* +MediaKeySession::GetError() const +{ + return mMediaKeyError; +} + +void +MediaKeySession::GetKeySystem(nsString& aOutKeySystem) const +{ + aOutKeySystem.Assign(mKeySystem); +} + +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 MediaKeySessionBinding::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; + { + CDMCaps::AutoLock caps(mKeys->GetCDMProxy()->Capabilites()); + 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())); + using IntegerType = typename std::underlying_type<MediaKeyStatus>::type; + for (const CDMCaps::KeyStatus& status : keyStatuses) { + message.Append(nsPrintfCString(" (%s,%s)", ToBase64(status.mId).get(), + MediaKeyStatusValues::strings[static_cast<IntegerType>(status.mStatus)].value)); + } + message.Append(" }"); + EME_LOG(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(nsContentUtils::ConvertStringFromEncoding(NS_LITERAL_CSTRING("UTF-8"), 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, + NS_LITERAL_CSTRING("MediaKeySession.generateRequest"))); + 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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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. + + Telemetry::Accumulate(Telemetry::VIDEO_CDM_GENERATE_REQUEST_CALLED, + ToCDMTypeTelemetryEnum(mKeySystem)); + + // Convert initData to base64 for easier logging. + // Note: CreateSession() Move()s the data out of the array, so we have + // to copy it here. + nsAutoCString base64InitData(ToBase64(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(base64)='%s' initDataType='%s'", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + pid, + base64InitData.get(), + NS_ConvertUTF16toUTF8(aInitDataType).get()); + + return promise.forget(); +} + +already_AddRefed<Promise> +MediaKeySession::Load(const nsAString& aSessionId, ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.load"))); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If this object is closed, return a promise rejected with an InvalidStateError. + if (IsClosed()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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, 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, + NS_LITERAL_CSTRING("MediaKeySession.update"))); + 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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Update() called before sessionId set by CDM")); + return promise.forget(); + } + + nsTArray<uint8_t> data; + if (IsClosed() || !mKeys->GetCDMProxy()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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 base64 for easier logging. + // Note: UpdateSession() Move()s the data out of the array, so we have + // to copy it here. + nsAutoCString base64Response(ToBase64(data)); + + PromiseId pid = mKeys->StorePromise(promise); + mKeys->GetCDMProxy()->UpdateSession(mSessionId, + pid, + data); + + EME_LOG("MediaKeySession[%p,'%s'] Update() sent to CDM, " + "promiseId=%d Response(base64)='%s'", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + pid, + base64Response.get()); + + return promise.forget(); +} + +already_AddRefed<Promise> +MediaKeySession::Close(ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeySession.close"))); + 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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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, + NS_LITERAL_CSTRING("MediaKeySession.remove"))); + 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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySession.Remove() called before sessionId set by CDM")); + return promise.forget(); + } + if (mSessionType != MediaKeySessionType::Persistent_license) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, + NS_LITERAL_CSTRING("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->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("MediaKeySesison.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(base64)='%s'", + this, NS_ConvertUTF16toUTF8(mSessionId).get(), + MediaKeyMessageTypeValues::strings[uint32_t(aMessageType)].value, + ToBase64(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, NS_LITERAL_STRING("keystatuseschange"), false); + asyncDispatcher->PostDOMEvent(); +} + +uint32_t +MediaKeySession::Token() const +{ + return mToken; +} + +already_AddRefed<DetailedPromise> +MediaKeySession::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); +} + +void +MediaKeySession::SetExpiration(double aExpiration) +{ + EME_LOG("MediaKeySession[%p,'%s'] SetExpiry(%lf)", + this, + NS_ConvertUTF16toUTF8(mSessionId).get(), + aExpiration); + mExpiration = aExpiration; +} + +EventHandlerNonNull* +MediaKeySession::GetOnkeystatuseschange() +{ + return GetEventHandler(nsGkAtoms::onkeystatuseschange, EmptyString()); +} + +void +MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) +{ + SetEventHandler(nsGkAtoms::onkeystatuseschange, EmptyString(), aCallback); +} + +EventHandlerNonNull* +MediaKeySession::GetOnmessage() +{ + return GetEventHandler(nsGkAtoms::onmessage, EmptyString()); +} + +void +MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) +{ + SetEventHandler(nsGkAtoms::onmessage, EmptyString(), aCallback); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h new file mode 100644 index 000000000..40481df99 --- /dev/null +++ b/dom/media/eme/MediaKeySession.h @@ -0,0 +1,137 @@ +/* -*- 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 "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/Date.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 { +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeyError; +class MediaKeyStatusMap; + +class MediaKeySession final : public DOMEventTargetHelper +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession, + DOMEventTargetHelper) +public: + MediaKeySession(JSContext* aCx, + 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 GetKeySystem(nsString& aRetval) 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 000000000..5af6ff535 --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.cpp @@ -0,0 +1,121 @@ +/* -*- 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" + +namespace mozilla { +namespace 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() +{ +} + +JSObject* +MediaKeyStatusMap::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeyStatusMapBinding::Wrap(aCx, this, aGivenProto); +} + +nsPIDOMWindowInner* +MediaKeyStatusMap::GetParentObject() const +{ + return mParent; +} + +void +MediaKeyStatusMap::Get(JSContext* aCx, + const ArrayBufferViewOrArrayBuffer& aKey, + JS::MutableHandle<JS::Value> aOutValue, + ErrorResult& aOutRv) const +{ + ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey); + if (!keyId.IsValid()) { + aOutValue.setUndefined(); + return; + } + for (const KeyStatus& status : mStatuses) { + if (keyId == status.mKeyId) { + bool ok = ToJSValue(aCx, status.mStatus, aOutValue);
+ if (!ok) {
+ aOutRv.NoteJSContextException(aCx);
+ }
+ 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); +} + +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 dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeyStatusMap.h b/dom/media/eme/MediaKeyStatusMap.h new file mode 100644 index 000000000..396ae500e --- /dev/null +++ b/dom/media/eme/MediaKeyStatusMap.h @@ -0,0 +1,97 @@ +/* -*- 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/ErrorResult.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 { +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_SCRIPT_HOLDER_CLASS(MediaKeyStatusMap) + +public: + explicit MediaKeyStatusMap(nsPIDOMWindowInner* aParent); + +protected: + ~MediaKeyStatusMap(); + +public: + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + void Get(JSContext* aCx, + const ArrayBufferViewOrArrayBuffer& aKey, + JS::MutableHandle<JS::Value> 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; + 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) + , 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 000000000..7007d3a03 --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.cpp @@ -0,0 +1,1135 @@ +/* -*- 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 "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozilla/Preferences.h" +#include "MediaPrefs.h" +#include "nsContentTypeParser.h" +#ifdef MOZ_FMP4 +#include "MP4Decoder.h" +#endif +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#include "WMFDecoderModule.h" +#endif +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "mozIGeckoMediaPluginService.h" +#include "VideoUtils.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "mozilla/EMEUtils.h" +#include "GMPUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsXULAppAPI.h" +#include "gmp-audio-decode.h" +#include "gmp-video-decode.h" +#include "DecoderDoctorDiagnostics.h" +#include "WebMDecoder.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsUnicharUtils.h" +#include "mozilla/dom/MediaSource.h" +#ifdef MOZ_WIDGET_ANDROID +#include "FennecJNIWrappers.h" +#endif +namespace mozilla { +namespace 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 + +MediaKeySystemAccess::MediaKeySystemAccess(nsPIDOMWindowInner* aParent, + const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent) + , mKeySystem(aKeySystem) + , mConfig(aConfig) +{ +} + +MediaKeySystemAccess::~MediaKeySystemAccess() +{ +} + +JSObject* +MediaKeySystemAccess::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return MediaKeySystemAccessBinding::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) +{ + bool havePlugin = HaveGMPFor(NS_LITERAL_CSTRING(GMP_API_DECRYPTOR), + { 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 = NS_LITERAL_CSTRING("CDM is not installed"); + return MediaKeySystemStatus::Cdm_not_installed; + } + + return MediaKeySystemStatus::Available; +} + +/* static */ +MediaKeySystemStatus +MediaKeySystemAccess::GetKeySystemStatus(const nsAString& aKeySystem, + nsACString& aOutMessage) +{ + MOZ_ASSERT(MediaPrefs::EMEEnabled() || IsClearkeyKeySystem(aKeySystem)); + + if (IsClearkeyKeySystem(aKeySystem)) { + return EnsureCDMInstalled(aKeySystem, aOutMessage); + } + + if (Preferences::GetBool("media.gmp-eme-adobe.visible", false)) { + if (IsPrimetimeKeySystem(aKeySystem)) { + if (!Preferences::GetBool("media.gmp-eme-adobe.enabled", false)) { + aOutMessage = NS_LITERAL_CSTRING("Adobe EME disabled"); + return MediaKeySystemStatus::Cdm_disabled; + } +#ifdef XP_WIN + // Win Vista and later only. + if (!IsVistaOrLater()) { + aOutMessage = NS_LITERAL_CSTRING("Minimum Windows version (Vista) not met for Adobe EME"); + return MediaKeySystemStatus::Cdm_not_supported; + } +#endif + return EnsureCDMInstalled(aKeySystem, aOutMessage); + } + } + + if (IsWidevineKeySystem(aKeySystem)) { + if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) { +#ifdef XP_WIN + // Win Vista and later only. + if (!IsVistaOrLater()) { + aOutMessage = NS_LITERAL_CSTRING("Minimum Windows version (Vista) not met for Widevine EME"); + return MediaKeySystemStatus::Cdm_not_supported; + } +#endif + if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) { + aOutMessage = NS_LITERAL_CSTRING("Widevine EME disabled"); + 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 = NS_LITERAL_CSTRING("Widevine CDM is not available"); + return MediaKeySystemStatus::Cdm_not_installed; + } + return MediaKeySystemStatus::Available; +#endif + } + } + + return MediaKeySystemStatus::Cdm_not_supported; +} + +typedef nsCString EMECodecString; + +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_AAC, "aac"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_OPUS, "opus"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VORBIS, "vorbis"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_H264, "h264"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP8, "vp8"); +static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP9, "vp9"); + +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 (IsH264CodecString(aCodec)) { + return EME_CODEC_H264; + } + if (IsVP8CodecString(aCodec)) { + return EME_CODEC_VP8; + } + if (IsVP9CodecString(aCodec)) { + return EME_CODEC_VP9; + } + return EmptyCString(); +} + +// A codec can be decrypted-and-decoded by the CDM, or only decrypted +// by the CDM and decoded by Gecko. Not both. +struct KeySystemContainerSupport +{ + 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 +{ + nsString mKeySystem; + nsTArray<nsString> mInitDataTypes; + KeySystemFeatureSupport mPersistentState = KeySystemFeatureSupport::Prohibited; + KeySystemFeatureSupport mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + nsTArray<MediaKeySessionType> mSessionTypes; + nsTArray<nsString> mVideoRobustness; + nsTArray<nsString> mAudioRobustness; + KeySystemContainerSupport mMP4; + KeySystemContainerSupport mWebM; +}; + +static nsTArray<KeySystemConfig> +GetSupportedKeySystems() +{ + nsTArray<KeySystemConfig> keySystemConfigs; + + { + if (HavePluginForKeySystem(kEMEKeySystemClearkey)) { + KeySystemConfig clearkey; + clearkey.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemClearkey); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc")); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids")); + clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm")); + clearkey.mPersistentState = KeySystemFeatureSupport::Requestable; + clearkey.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited; + clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); + if (MediaPrefs::ClearKeyPersistentLicenseEnabled()) { + clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Persistent_license); + } +#if defined(XP_WIN) + // Clearkey CDM uses WMF decoders on Windows. + if (WMFDecoderModule::HasAAC()) { + clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_AAC); + } else { + clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC); + } + if (WMFDecoderModule::HasH264()) { + clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + } else { + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); + } +#else + clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC); + clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264); +#endif + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_OPUS); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP8); + clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP9); + keySystemConfigs.AppendElement(Move(clearkey)); + } + } + { + if (HavePluginForKeySystem(kEMEKeySystemWidevine)) { + KeySystemConfig widevine; + widevine.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemWidevine); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc")); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids")); + widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm")); + 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(NS_LITERAL_STRING("SW_SECURE_CRYPTO")); + widevine.mVideoRobustness.AppendElement(NS_LITERAL_STRING("SW_SECURE_DECODE")); +#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 our policy is to prevent + // the Adobe GMP's unencrypted AAC decoding path being used to + // decode content decrypted by the Widevine CDM. + if (WMFDecoderModule::HasAAC()) { + widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC); + } +#elif !defined(MOZ_WIDGET_ANDROID) + widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC); +#endif + +#if defined(MOZ_WIDGET_ANDROID) + using namespace mozilla::java; + // 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, MediaDrmProxy::AVC, &widevine.mMP4 }, + { nsCString("audio/mp4"), EME_CODEC_AAC, MediaDrmProxy::AAC, &widevine.mMP4 }, + { nsCString("video/webm"), EME_CODEC_VP8, MediaDrmProxy::VP8, &widevine.mWebM }, + { nsCString("video/webm"), EME_CODEC_VP9, MediaDrmProxy::VP9, &widevine.mWebM}, + { nsCString("audio/webm"), EME_CODEC_VORBIS, MediaDrmProxy::VORBIS, &widevine.mWebM}, + { nsCString("audio/webm"), EME_CODEC_OPUS, MediaDrmProxy::OPUS, &widevine.mWebM}, + }; + + for (const auto& data: validationList) { + if (MediaDrmProxy::IsCryptoSchemeSupported(kEMEKeySystemWidevine, + data.mMimeType)) { + if (MediaDrmProxy::CanDecode(data.mCodecType)) { + data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType); + } else { + data.mSupportType->SetCanDecrypt(data.mEMECodecType); + } + } + } +#else + widevine.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + 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(Move(widevine)); + } + } + { + if (HavePluginForKeySystem(kEMEKeySystemPrimetime)) { + KeySystemConfig primetime; + primetime.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemPrimetime); + primetime.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc")); + primetime.mPersistentState = KeySystemFeatureSupport::Required; + primetime.mDistinctiveIdentifier = KeySystemFeatureSupport::Required; + primetime.mSessionTypes.AppendElement(MediaKeySessionType::Temporary); + primetime.mMP4.SetCanDecryptAndDecode(EME_CODEC_AAC); + primetime.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + keySystemConfigs.AppendElement(Move(primetime)); + } + } + + return keySystemConfigs; +} + +static bool +GetKeySystemConfig(const nsAString& aKeySystem, KeySystemConfig& aOutKeySystemConfig) +{ + for (auto&& config : GetSupportedKeySystems()) { + if (config.mKeySystem.Equals(aKeySystem)) { + aOutKeySystemConfig = mozilla::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) && + NS_SUCCEEDED(MediaSource::IsTypeSupported(aContentType, aDiagnostics))) { + // 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 our policy is to prevent + // the Adobe GMP's unencrypted AAC decoding path being used to + // decode content decrypted by the Widevine CDM. + if (codec == EME_CODEC_AAC && + IsWidevineKeySystem(aKeySystem) && + !WMFDecoderModule::HasAAC()) { + if (aDiagnostics) { + aDiagnostics->SetKeySystemIssue( + DecoderDoctorDiagnostics::eWidevineWithNoWMF); + } + } +#endif + return false; + } + return true; +} + +static bool +ToSessionType(const nsAString& aSessionType, MediaKeySessionType& aOutType) +{ + using MediaKeySessionTypeValues::strings; + const char* temporary = + strings[static_cast<uint32_t>(MediaKeySessionType::Temporary)].value; + if (aSessionType.EqualsASCII(temporary)) { + aOutType = MediaKeySessionType::Temporary; + return true; + } + const char* persistentLicense = + strings[static_cast<uint32_t>(MediaKeySessionType::Persistent_license)].value; + if (aSessionType.EqualsASCII(persistentLicense)) { + aOutType = MediaKeySessionType::Persistent_license; + return true; + } + return false; +} + +// 5.2.1 Is persistent session type? +static bool +IsPersistentSessionType(MediaKeySessionType aSessionType) +{ + return aSessionType == MediaKeySessionType::Persistent_license; +} + +CodecType +GetMajorType(const nsAString& aContentType) +{ + if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("audio/"), aContentType)) { + return Audio; + } + if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("video/"), aContentType)) { + 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)) { + 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.2.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) +{ + // 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& contentType = capabilities.mContentType; + // Let robustness be requested media capability's robustness member. + const nsString& robustness = capabilities.mRobustness; + // If content type is the empty string, return null. + if (contentType.IsEmpty()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') rejected; " + "audio or video capability has empty contentType.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + return Sequence<MediaKeySystemMediaCapability>(); + } + // If content type is an invalid or unrecognized MIME type, continue + // to the next iteration. + nsAutoString container; + nsTArray<nsString> codecStrings; + if (!ParseMIMETypeString(contentType, container, codecStrings)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "failed to parse contentType as MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + bool invalid = false; + nsTArray<EMECodecString> codecs; + for (const nsString& codecString : codecStrings) { + EMECodecString emeCodec = ToEMEAPICodecString(codecString); + if (emeCodec.IsEmpty()) { + invalid = true; + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "'%s' is an invalid codec string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).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.) + NS_ConvertUTF16toUTF8 container_utf8(container); + const bool isMP4 = DecoderTraits::IsMP4TypeAndEnabled(container_utf8, aDiagnostics); + if (isMP4 && !aKeySystem.mMP4.IsSupported()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MP4 requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + const bool isWebM = DecoderTraits::IsWebMTypeAndEnabled(container_utf8); + if (isWebM && !aKeySystem.mWebM.IsSupported()) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "WebM requested but unsupported.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (!isMP4 && !isWebM) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "Unsupported or unrecognized container requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).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(contentType)) { + 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()) { + // 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 content type is not strictly a audio/video type, continue to the next iteration. + const auto majorType = GetMajorType(container); + if (majorType == Invalid) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MIME type is not an audio or video MIME type.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "MIME type mixes audio codecs in video capabilities " + "or video codecs in audio capabilities.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).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') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + if (majorType == Video && !aKeySystem.mVideoRobustness.Contains(robustness)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "unsupported robustness string.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).get()); + continue; + } + // Note: specified robustness requirements are satisfied. + } + + // 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, + contentType, + majorType, + containerSupport, + codecs, + aDiagnostics)) { + EME_LOG("MediaKeySystemConfiguration (label='%s') " + "MediaKeySystemMediaCapability('%s','%s') unsupported; " + "codec unsupported by CDM requested.", + NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(), + NS_ConvertUTF16toUTF8(contentType).get(), + NS_ConvertUTF16toUTF8(robustness).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 Move(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.2.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 { + using MediaKeySessionTypeValues::strings; + const char* temporary = strings[static_cast<uint32_t>(MediaKeySessionType::Temporary)].value; + // Note: fallible. Results in an empty array. + sessionTypes.AppendElement(NS_ConvertUTF8toUTF16(nsDependentCString(temporary)), mozilla::fallible); + } + return sessionTypes; +} + +// 3.1.2.2 Get Supported Configuration and Consent +static bool +GetSupportedConfig(const KeySystemConfig& aKeySystem, + const MediaKeySystemConfiguration& aCandidate, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics) +{ + // 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; + } + + 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(Move(sessionTypes)); + + // If the videoCapabilities and audioCapabilities members in candidate + // configuration are both empty, return NotSupported. + // 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. + + // 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); + // 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 = 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); + // 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 = 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::HasAAC()) { + 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) +{ + KeySystemConfig implementation; + if (!GetKeySystemConfig(aKeySystem, implementation)) { + return false; + } + for (const MediaKeySystemConfiguration& candidate : aConfigs) { + if (mozilla::dom::GetSupportedConfig(implementation, + candidate, + aOutConfig, + aDiagnostics)) { + 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-request", json.get()); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h new file mode 100644 index 000000000..a166ac22e --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccess.h @@ -0,0 +1,81 @@ +/* -*- 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 "mozilla/ErrorResult.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; + +namespace dom { + +class MediaKeySystemAccess final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_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); + + static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem, + const nsAString& aInitDataType); + +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 000000000..8fefc62ec --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.cpp @@ -0,0 +1,340 @@ +/* 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 "MediaPrefs.h" +#include "mozilla/EMEUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/DetailedPromise.h" +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#endif +#ifdef XP_MACOSX +#include "nsCocoaFeatures.h" +#endif +#include "nsPrintfCString.h" + +namespace mozilla { +namespace dom { + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +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->mRequests.Length(); i++) { + tmp->mRequests[i].RejectPromise(NS_LITERAL_CSTRING("Promise still outstanding at MediaKeySystemAccessManager GC")); + tmp->mRequests[i].CancelTimer(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRequests[i].mPromise) + } + tmp->mRequests.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->mRequests.Length(); i++) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRequests[i].mPromise) + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +MediaKeySystemAccessManager::MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow) + , mAddedObservers(false) +{ +} + +MediaKeySystemAccessManager::~MediaKeySystemAccessManager() +{ + Shutdown(); +} + +void +MediaKeySystemAccessManager::Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs) +{ + Request(aPromise, aKeySystem, aConfigs, RequestType::Initial); +} + +void +MediaKeySystemAccessManager::Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + RequestType aType) +{ + EME_LOG("MediaKeySystemAccessManager::Request %s", NS_ConvertUTF16toUTF8(aKeySystem).get()); + + if (aKeySystem.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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; + } + if (aConfigs.IsEmpty()) { + aPromise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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; + } + + DecoderDoctorDiagnostics diagnostics; + + // Ensure keysystem is supported. + if (!IsWidevineKeySystem(aKeySystem) && + !IsClearkeyKeySystem(aKeySystem) && + !IsPrimetimeKeySystem(aKeySystem)) { + // Not to inform user, because nothing to do if the keySystem is not + // supported. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Key system is unsupported")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); + return; + } + + if (!MediaPrefs::EMEEnabled() && !IsClearkeyKeySystem(aKeySystem)) { + // 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. + MediaKeySystemAccess::NotifyObservers(mWindow, + aKeySystem, + MediaKeySystemStatus::Api_disabled); + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("EME has been preffed off")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); + return; + } + + nsAutoCString message; + MediaKeySystemStatus status = + MediaKeySystemAccess::GetKeySystemStatus(aKeySystem, message); + + nsPrintfCString msg("MediaKeySystemAccess::GetKeySystemStatus(%s) " + "result=%s msg='%s'", + NS_ConvertUTF16toUTF8(aKeySystem).get(), + MediaKeySystemStatusValues::strings[(size_t)status].value, + message.get()); + LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg)); + + if (status == MediaKeySystemStatus::Cdm_not_installed && + (IsPrimetimeKeySystem(aKeySystem) || IsWidevineKeySystem(aKeySystem))) { + // 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 (aType == RequestType::Initial && + AwaitInstall(aPromise, aKeySystem, aConfigs)) { + // Notify chrome that we're going to wait for the CDM to download/update. + // Note: If we're re-trying, we don't re-send the notification, + // as chrome is already displaying the "we can't play, updating" + // notification. + MediaKeySystemAccess::NotifyObservers(mWindow, aKeySystem, status); + } else { + // We waited or can't wait for an update and we still can't service + // the request. Give up. Chrome will still be showing a "I can't play, + // updating" notification. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Gave up while waiting for a CDM update")); + } + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, 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, aKeySystem, status); + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, message); + return; + } + + MediaKeySystemConfiguration config; + if (MediaKeySystemAccess::GetSupportedConfig(aKeySystem, aConfigs, config, &diagnostics)) { + RefPtr<MediaKeySystemAccess> access( + new MediaKeySystemAccess(mWindow, aKeySystem, config)); + aPromise->MaybeResolve(access); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, true, __func__); + return; + } + // Not to inform user, because nothing to do if the corresponding keySystem + // configuration is not supported. + aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR, + NS_LITERAL_CSTRING("Key system configuration is not supported")); + diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), + aKeySystem, false, __func__); +} + +MediaKeySystemAccessManager::PendingRequest::PendingRequest(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs, + nsITimer* aTimer) + : mPromise(aPromise) + , mKeySystem(aKeySystem) + , mConfigs(aConfigs) + , mTimer(aTimer) +{ + MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest); +} + +MediaKeySystemAccessManager::PendingRequest::PendingRequest(const PendingRequest& aOther) + : mPromise(aOther.mPromise) + , mKeySystem(aOther.mKeySystem) + , mConfigs(aOther.mConfigs) + , mTimer(aOther.mTimer) +{ + MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest); +} + +MediaKeySystemAccessManager::PendingRequest::~PendingRequest() +{ + MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest); +} + +void +MediaKeySystemAccessManager::PendingRequest::CancelTimer() +{ + if (mTimer) { + mTimer->Cancel(); + } +} + +void +MediaKeySystemAccessManager::PendingRequest::RejectPromise(const nsCString& aReason) +{ + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, aReason); + } +} + +bool +MediaKeySystemAccessManager::AwaitInstall(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfigs) +{ + EME_LOG("MediaKeySystemAccessManager::AwaitInstall %s", NS_ConvertUTF16toUTF8(aKeySystem).get()); + + if (!EnsureObserversAdded()) { + NS_WARNING("Failed to add pref observer"); + return false; + } + + nsCOMPtr<nsITimer> timer(do_CreateInstance("@mozilla.org/timer;1")); + if (!timer || NS_FAILED(timer->Init(this, 60 * 1000, nsITimer::TYPE_ONE_SHOT))) { + NS_WARNING("Failed to create timer to await CDM install."); + return false; + } + + mRequests.AppendElement(PendingRequest(aPromise, aKeySystem, aConfigs, timer)); + return true; +} + +void +MediaKeySystemAccessManager::RetryRequest(PendingRequest& aRequest) +{ + aRequest.CancelTimer(); + Request(aRequest.mPromise, aRequest.mKeySystem, aRequest.mConfigs, RequestType::Subsequent); +} + +nsresult +MediaKeySystemAccessManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + EME_LOG("MediaKeySystemAccessManager::Observe %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<PendingRequest> requests; + for (size_t i = mRequests.Length(); i-- > 0; ) { + PendingRequest& request = mRequests[i]; + nsAutoCString message; + MediaKeySystemStatus status = + MediaKeySystemAccess::GetKeySystemStatus(request.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(Move(request)); + mRequests.RemoveElementAt(i); + } + // Retry all pending requests, but this time fail if the CDM is not installed. + for (PendingRequest& request : requests) { + RetryRequest(request); + } + } 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 < mRequests.Length(); i++) { + if (mRequests[i].mTimer == timer) { + EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request"); + PendingRequest request = mRequests[i]; + mRequests.RemoveElementAt(i); + RetryRequest(request); + break; + } + } + } + return NS_OK; +} + +bool +MediaKeySystemAccessManager::EnsureObserversAdded() +{ + 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() +{ + EME_LOG("MediaKeySystemAccessManager::Shutdown"); + nsTArray<PendingRequest> requests(Move(mRequests)); + for (PendingRequest& request : requests) { + // Cancel all requests; we're shutting down. + request.CancelTimer(); + request.RejectPromise(NS_LITERAL_CSTRING("Promise still outstanding at MediaKeySystemAccessManager shutdown")); + } + if (mAddedObservers) { + nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService(); + if (obsService) { + obsService->RemoveObserver(this, "gmp-changed"); + mAddedObservers = false; + } + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h new file mode 100644 index 000000000..9c092248e --- /dev/null +++ b/dom/media/eme/MediaKeySystemAccessManager.h @@ -0,0 +1,83 @@ +/* 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_MediaKeySystemAccessManager_h +#define mozilla_dom_MediaKeySystemAccessManager_h + +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "nsIObserver.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" + +namespace mozilla { +namespace dom { + +class DetailedPromise; +class TestGMPVideoDecoder; + +class MediaKeySystemAccessManager final : public nsIObserver +{ +public: + + explicit MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager, nsIObserver) + NS_DECL_NSIOBSERVER + + void Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig); + + void Shutdown(); + + struct PendingRequest { + PendingRequest(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig, + nsITimer* aTimer); + PendingRequest(const PendingRequest& aOther); + ~PendingRequest(); + void CancelTimer(); + void RejectPromise(const nsCString& aReason); + + RefPtr<DetailedPromise> mPromise; + const nsString mKeySystem; + const Sequence<MediaKeySystemConfiguration> mConfigs; + nsCOMPtr<nsITimer> mTimer; + }; + +private: + + enum RequestType { + Initial, + Subsequent + }; + + void Request(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig, + RequestType aType); + + ~MediaKeySystemAccessManager(); + + bool EnsureObserversAdded(); + + bool AwaitInstall(DetailedPromise* aPromise, + const nsAString& aKeySystem, + const Sequence<MediaKeySystemConfiguration>& aConfig); + + void RetryRequest(PendingRequest& aRequest); + + nsTArray<PendingRequest> mRequests; + + nsCOMPtr<nsPIDOMWindowInner> mWindow; + bool mAddedObservers; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp new file mode 100644 index 000000000..eedd675e4 --- /dev/null +++ b/dom/media/eme/MediaKeys.cpp @@ -0,0 +1,593 @@ +/* -*- 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 "GMPService.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/Telemetry.h" +#include "GMPCDMProxy.h" +#ifdef MOZ_WIDGET_ANDROID +#include "mozilla/MediaDrmCDMProxy.h" +#endif +#include "mozilla/EMEUtils.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsContentTypeParser.h" +#ifdef MOZ_FMP4 +#include "MP4Decoder.h" +#endif +#ifdef XP_WIN +#include "mozilla/WindowsVersion.h" +#endif +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "nsPrintfCString.h" + +namespace mozilla { + +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeys, + mElement, + mParent, + mKeySessions, + mPromises, + mPendingSessions); +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_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() +{ + Shutdown(); + EME_LOG("MediaKeys[%p] destroyed", this); +} + +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 (auto iter = mKeySessions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<MediaKeySession>& session = iter.Data(); + keySessions.Put(session->GetSessionId(), session); + } + for (auto iter = keySessions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<MediaKeySession>& session = iter.Data(); + 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() +{ + if (mProxy) { + mProxy->Shutdown(); + mProxy = nullptr; + } + + RefPtr<MediaKeys> kungFuDeathGrip = this; + + for (auto iter = mPromises.Iter(); !iter.Done(); iter.Next()) { + RefPtr<dom::DetailedPromise>& promise = iter.Data(); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("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 MediaKeysBinding::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, + NS_LITERAL_CSTRING("MediaKeys.setServerCertificate"))); + if (aRv.Failed()) { + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in MediaKeys.setServerCertificate()")); + return promise.forget(); + } + + nsTArray<uint8_t> data; + CopyArrayBufferViewOrArrayBufferData(aCert, data); + if (data.IsEmpty()) { + promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR, + NS_LITERAL_CSTRING("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=%d", this, id); + + // Keep MediaKeys alive for the lifetime of its promises. Any still-pending + // promises are rejected in Shutdown(). + AddRef(); + +#ifdef DEBUG + // We should not have already stored this promise! + for (auto iter = mPromises.ConstIter(); !iter.Done(); iter.Next()) { + MOZ_ASSERT(iter.Data() != aPromise); + } +#endif + + mPromises.Put(id, aPromise); + return id; +} + +void +MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken) +{ + // Should only be called from MediaKeySession::GenerateRequest. + mPromiseIdToken.Put(aId, aToken); + EME_LOG("MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)", + this, aId, aToken); +} + +already_AddRefed<DetailedPromise> +MediaKeys::RetrievePromise(PromiseId aId) +{ + if (!mPromises.Contains(aId)) { + NS_WARNING(nsPrintfCString("Tried to retrieve a non-existent promise id=%d", aId).get()); + return nullptr; + } + RefPtr<DetailedPromise> promise; + mPromises.Remove(aId, getter_AddRefs(promise)); + Release(); + return promise.forget(); +} + +void +MediaKeys::RejectPromise(PromiseId aId, nsresult aExceptionCode, + const nsCString& aReason) +{ + EME_LOG("MediaKeys[%p]::RejectPromise(%d, 0x%x)", this, aId, aExceptionCode); + + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + 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(NS_FAILED(aExceptionCode)); + promise->MaybeReject(aExceptionCode, aReason); + + if (mCreatePromiseId == aId) { + // Note: This will probably destroy the MediaKeys object! + 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.Put(aSession->GetSessionId(), aSession); +} + +void +MediaKeys::ResolvePromise(PromiseId aId) +{ + EME_LOG("MediaKeys[%p]::ResolvePromise(%d)", 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->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR, + NS_LITERAL_CSTRING("CDM LoadSession() returned a different session ID than requested")); + return; + } + mKeySessions.Put(session->GetSessionId(), 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() +{ + 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 GMPCDMProxy(this, + mKeySystem, + new MediaKeysGMPCrashHelper(this), + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } + return proxy.forget(); +} + +already_AddRefed<DetailedPromise> +MediaKeys::Init(ErrorResult& aRv) +{ + RefPtr<DetailedPromise> promise(MakePromise(aRv, + NS_LITERAL_CSTRING("MediaKeys::Init()"))); + if (aRv.Failed()) { + return nullptr; + } + + mProxy = CreateCDMProxy(); + + // Determine principal (at creation time) of the MediaKeys object. + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject()); + if (!sop) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get script principal in MediaKeys::Init")); + return promise.forget(); + } + mPrincipal = sop->GetPrincipal(); + + // Determine principal of the "top-level" window; the principal of the + // page that will display in the URL bar. + nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject(); + if (!window) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get top-level window in MediaKeys::Init")); + return promise.forget(); + } + nsCOMPtr<nsPIDOMWindowOuter> top = window->GetOuterWindow()->GetTop(); + if (!top || !top->GetExtantDoc()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get document in MediaKeys::Init")); + return promise.forget(); + } + + mTopLevelPrincipal = top->GetExtantDoc()->NodePrincipal(); + + if (!mPrincipal || !mTopLevelPrincipal) { + NS_WARNING("Failed to get principals when creating MediaKeys"); + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get principal(s) in MediaKeys::Init")); + return promise.forget(); + } + + nsAutoCString origin; + nsresult rv = mPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get principal origin string in MediaKeys::Init")); + return promise.forget(); + } + nsAutoCString topLevelOrigin; + rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin); + if (NS_FAILED(rv)) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't get top-level principal origin string in MediaKeys::Init")); + return promise.forget(); + } + + nsIDocument* doc = window->GetExtantDoc(); + const bool inPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(doc); + + EME_LOG("MediaKeys[%p]::Create() (%s, %s), %s", + this, + origin.get(), + topLevelOrigin.get(), + (inPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); + + // 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); + AddRef(); + mProxy->Init(mCreatePromiseId, + NS_ConvertUTF8toUTF16(origin), + NS_ConvertUTF8toUTF16(topLevelOrigin), + KeySystemToGMPName(mKeySystem), + inPrivateBrowsing); + + return promise.forget(); +} + +void +MediaKeys::OnCDMCreated(PromiseId aId, const nsACString& aNodeId, const uint32_t aPluginId) +{ + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + mNodeId = aNodeId; + RefPtr<MediaKeys> keys(this); + EME_LOG("MediaKeys[%p]::OnCDMCreated() resolve promise id=%d", this, aId); + promise->MaybeResolve(keys); + if (mCreatePromiseId == aId) { + Release(); + } + + MediaKeySystemAccess::NotifyObservers(mParent, + mKeySystem, + MediaKeySystemStatus::Cdm_created); + + Telemetry::Accumulate(Telemetry::VIDEO_CDM_CREATED, ToCDMTypeTelemetryEnum(mKeySystem)); +} + +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; + } + using MediaKeySessionTypeValues::strings; + const char* sessionType = strings[static_cast<uint32_t>(aSessionType)].value; + for (const nsString& s : aConfig.mSessionTypes.Value()) { + if (s.EqualsASCII(sessionType)) { + return true; + } + } + return false; +} + +already_AddRefed<MediaKeySession> +MediaKeys::CreateSession(JSContext* aCx, + MediaKeySessionType aSessionType, + ErrorResult& aRv) +{ + if (!IsSessionTypeSupported(aSessionType, mConfig)) { + EME_LOG("MediaKeys[%p,'%s'] 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(aCx, + GetParentObject(), + this, + mKeySystem, + aSessionType, + aRv); + + if (aRv.Failed()) { + return nullptr; + } + + // Add session to the set of sessions awaiting their sessionId being ready. + mPendingSessions.Put(session->Token(), session); + + return session.forget(); +} + +void +MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) +{ + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%d", this, aId); + + promise->MaybeResolve(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) +{ + RefPtr<MediaKeySession> session; + mPendingSessions.Get(aToken, getter_AddRefs(session)); + mPendingSessions.Remove(aToken); + return session.forget(); +} + +const nsCString& +MediaKeys::GetNodeId() const +{ + MOZ_ASSERT(NS_IsMainThread()); + return mNodeId; +} + +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; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/eme/MediaKeys.h b/dom/media/eme/MediaKeys.h new file mode 100644 index 000000000..a3dbf37df --- /dev/null +++ b/dom/media/eme/MediaKeys.h @@ -0,0 +1,168 @@ +/* -*- 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 "nsWrapperCache.h" +#include "nsISupports.h" +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/DetailedPromise.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla { + +class CDMProxy; + +namespace dom { + +class ArrayBufferViewOrArrayBuffer; +class MediaKeySession; +class HTMLMediaElement; + +typedef nsRefPtrHashtable<nsStringHashKey, MediaKeySession> KeySessionHashMap; +typedef nsRefPtrHashtable<nsUint32HashKey, dom::DetailedPromise> PromiseHashMap; +typedef nsRefPtrHashtable<nsUint32HashKey, MediaKeySession> PendingKeySessionsHashMap; +typedef nsDataHashtable<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 nsISupports, + public nsWrapperCache, + public SupportsWeakPtr<MediaKeys> +{ + ~MediaKeys(); + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaKeys) + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(MediaKeys) + + 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(); + + // Javascript: readonly attribute DOMString keySystem; + void GetKeySystem(nsString& retval) const; + + // JavaScript: MediaKeys.createSession() + already_AddRefed<MediaKeySession> CreateSession(JSContext* aCx, + 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 nsACString& aNodeId, 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 DOMException corresponding to aExceptionCode. + void RejectPromise(PromiseId aId, nsresult aExceptionCode, + const nsCString& aReason); + // Resolves promise with "undefined". + void ResolvePromise(PromiseId aId); + + const nsCString& GetNodeId() const; + + 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; + +private: + + // Instantiate CDMProxy instance. + // It could be MediaDrmCDMProxy (Widevine on Fennec) or GMPCDMProxy (the rest). + already_AddRefed<CDMProxy> CreateCDMProxy(); + + // Removes promise from mPromises, and returns it. + already_AddRefed<DetailedPromise> RetrievePromise(PromiseId aId); + + // 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; + + RefPtr<HTMLMediaElement> mElement; + + nsCOMPtr<nsPIDOMWindowInner> mParent; + const nsString mKeySystem; + nsCString mNodeId; + KeySessionHashMap mKeySessions; + PromiseHashMap mPromises; + PendingKeySessionsHashMap mPendingSessions; + PromiseId mCreatePromiseId; + + RefPtr<nsIPrincipal> mPrincipal; + RefPtr<nsIPrincipal> mTopLevelPrincipal; + + const MediaKeySystemConfiguration mConfig; + + PendingPromiseIdTokenHashMap mPromiseIdToken; +}; + +} // 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 000000000..ee57afae9 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp @@ -0,0 +1,140 @@ +/* -*- 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 "mozIGeckoMediaPluginService.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, + nsresult aException, + const nsCString& aMessage) +{ + MOZ_ASSERT(NS_IsMainThread()); + mProxy->OnRejectPromise(aPromiseId, 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); + 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; + { + CDMCaps::AutoLock caps(mProxy->Capabilites()); + 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; + { + CDMCaps::AutoLock caps(mProxy->Capabilites()); + 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
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h new file mode 100644 index 000000000..29f9c8192 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h @@ -0,0 +1,69 @@ +/* -*- 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; +// 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 SetDecryptorId(uint32_t aId) override {} + + 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, + nsresult 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
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp new file mode 100644 index 000000000..a57d764e7 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp @@ -0,0 +1,467 @@ +/* -*- 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" + +using namespace mozilla::java::sdk; + +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, + bool aInPrivateBrowsing) +{ + 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(), + (aInPrivateBrowsing ? "PrivateBrowsing" : "NonPrivateBrowsing")); + + // Create a thread to work with cdm. + if (!mOwnerThread) { + nsresult rv = NS_NewNamedThread("MDCDMThread", getter_AddRefs(mOwnerThread)); + if (NS_FAILED(rv)) { + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Couldn't create CDM thread MediaDrmCDMProxy::Init")); + return; + } + } + + mCDM = mozilla::MakeUnique<MediaDrmProxySupport>(mKeySystem); + nsCOMPtr<nsIRunnable> task(NewRunnableMethod<uint32_t>(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); + + nsAutoPtr<CreateSessionData> data(new CreateSessionData()); + data->mSessionType = aSessionType; + data->mCreateSessionToken = aCreateSessionToken; + data->mPromiseId = aPromiseId; + data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType); + data->mInitData = Move(aInitData); + + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<CreateSessionData>>(this, + &MediaDrmCDMProxy::md_CreateSession, + data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +MediaDrmCDMProxy::LoadSession(PromiseId aPromiseId, + const nsAString& aSessionId) +{ + // TODO: Implement LoadSession. + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Currently Fennec did not support LoadSession")); +} + +void +MediaDrmCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) +{ + // TODO: Implement SetServerCertificate. + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Currently Fennec did not support SetServerCertificate")); +} + +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()); + + nsAutoPtr<UpdateSessionData> data(new UpdateSessionData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + data->mResponse = Move(aResponse); + + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<UpdateSessionData>>(this, + &MediaDrmCDMProxy::md_UpdateSession, + 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()); + + nsAutoPtr<SessionOpData> data(new SessionOpData()); + data->mPromiseId = aPromiseId; + data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId); + + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<nsAutoPtr<SessionOpData>>(this, + &MediaDrmCDMProxy::md_CloseSession, + data)); + mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL); +} + +void +MediaDrmCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) +{ + // TODO: Implement RemoveSession. + RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Currently Fennec did not support RemoveSession")); +} + +void +MediaDrmCDMProxy::Shutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mOwnerThread); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod(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, + 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, + nsresult aDOMException, + const nsCString& aMsg) +{ + MOZ_ASSERT(NS_IsMainThread()); + RejectPromise(aPromiseId, aDOMException, aMsg); +} + +RefPtr<MediaDrmCDMProxy::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, nsresult aCode, + const nsCString& aReason) +{ + if (NS_IsMainThread()) { + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, aCode, aReason); + } + } else { + nsCOMPtr<nsIRunnable> task(new RejectPromiseTask(this, aId, aCode, + aReason)); + NS_DispatchToMainThread(task); + } +} + +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>(this, + &MediaDrmCDMProxy::ResolvePromise, + aId); + NS_DispatchToMainThread(task); + } +} + +const nsString& +MediaDrmCDMProxy::KeySystem() const +{ + return mKeySystem; +} + +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::GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) +{ + CDMCaps::AutoLock caps(Capabilites()); + caps.GetSessionIdsForKeyId(aKeyId, aSessionIds); +} + +#ifdef DEBUG +bool +MediaDrmCDMProxy::IsOnOwnerThread() +{ + return NS_GetCurrentThread() == mOwnerThread; +} +#endif + +void +MediaDrmCDMProxy::OnCDMCreated(uint32_t aPromiseId) +{ + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + + if (mCDM) { + mKeys->OnCDMCreated(aPromiseId, GetNodeId(), 0); + return; + } + + // No CDM? Just reject the promise. + mKeys->RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in OnCDMCreated()")); +} + +void +MediaDrmCDMProxy::md_Init(uint32_t aPromiseId) +{ + MOZ_ASSERT(IsOnOwnerThread()); + MOZ_ASSERT(mCDM); + + mCallback = new MediaDrmCDMCallbackProxy(this); + mCDM->Init(mCallback); + nsCOMPtr<nsIRunnable> task( + NewRunnableMethod<uint32_t>(this, + &MediaDrmCDMProxy::OnCDMCreated, + aPromiseId)); + NS_DispatchToMainThread(task); +} + +void +MediaDrmCDMProxy::md_CreateSession(nsAutoPtr<CreateSessionData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in md_CreateSession")); + return; + } + + mCDM->CreateSession(aData->mCreateSessionToken, + aData->mPromiseId, + aData->mInitDataType, + aData->mInitData, + ToMediaDrmSessionType(aData->mSessionType)); +} + +void +MediaDrmCDMProxy::md_UpdateSession(nsAutoPtr<UpdateSessionData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in md_UpdateSession")); + return; + } + mCDM->UpdateSession(aData->mPromiseId, + aData->mSessionId, + aData->mResponse); +} + +void +MediaDrmCDMProxy::md_CloseSession(nsAutoPtr<SessionOpData> aData) +{ + MOZ_ASSERT(IsOnOwnerThread()); + + if (!mCDM) { + RejectPromise(aData->mPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR, + NS_LITERAL_CSTRING("Null CDM in md_CloseSession")); + 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
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h new file mode 100644 index 000000000..c7cb3000f --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h @@ -0,0 +1,184 @@ +/* -*- 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 "GeneratedJNINatives.h" + +#include "mozilla/CDMProxy.h" +#include "mozilla/CDMCaps.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/MediaDrmProxySupport.h" +#include "mozilla/UniquePtr.h" + +#include "MediaCodec.h" +#include "nsString.h" +#include "nsAutoPtr.h" + +using namespace mozilla::java; + +namespace mozilla { +class MediaDrmCDMCallbackProxy; +class MediaDrmCDMProxy : public CDMProxy { +public: + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDrmCDMProxy) + + MediaDrmCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired); + + void Init(PromiseId aPromiseId, + const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + bool aInPrivateBrowsing) override; + + void CreateSession(uint32_t aCreateSessionToken, + MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) override; + + void LoadSession(PromiseId aPromiseId, + 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 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, + 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, + nsresult aCode, + 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, nsresult aCode, + const nsCString& aReason) override; + + // Resolves promise with "undefined". + // Can be called from any thread. + void ResolvePromise(PromiseId aId) override; + + // Threadsafe. + const nsString& KeySystem() const override; + + CDMCaps& Capabilites() override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetSessionIdsForKeyId(const nsTArray<uint8_t>& aKeyId, + nsTArray<nsCString>& aSessionIds) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override; +#endif + +private: + virtual ~MediaDrmCDMProxy(); + + void OnCDMCreated(uint32_t aPromiseId); + + 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, + nsresult aCode, + const nsCString& aReason) + : mProxy(aProxy) + , mId(aId) + , mCode(aCode) + , mReason(aReason) + { + } + NS_METHOD Run() { + mProxy->RejectPromise(mId, mCode, mReason); + return NS_OK; + } + private: + RefPtr<MediaDrmCDMProxy> mProxy; + PromiseId mId; + nsresult mCode; + nsCString mReason; + }; + + nsCString mNodeId; + mozilla::UniquePtr<MediaDrmProxySupport> mCDM; + nsAutoPtr<MediaDrmCDMCallbackProxy> mCallback; + bool mShutdownCalled; + +// ===================================================================== +// For MediaDrmProxySupport + void md_Init(uint32_t aPromiseId); + void md_CreateSession(nsAutoPtr<CreateSessionData> aData); + void md_UpdateSession(nsAutoPtr<UpdateSessionData> aData); + void md_CloseSession(nsAutoPtr<SessionOpData> aData); + void md_Shutdown(); +// ===================================================================== +}; + +} // namespace mozilla +#endif // MediaDrmCDMProxy_h_
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp new file mode 100644 index 000000000..192769470 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp @@ -0,0 +1,284 @@ +/* -*- 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 "FennecJNINatives.h" +#include "MediaCodec.h" // For MediaDrm::KeyStatus +#include "MediaPrefs.h" + +using namespace mozilla::java; + +namespace mozilla { + +LogModule* GetMDRMNLog() { + static LazyLogModule log("MediaDrmProxySupport"); + return log; +} + +class MediaDrmJavaCallbacksSupport + : public MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<MediaDrmJavaCallbacksSupport> +{ +public: + typedef MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<MediaDrmJavaCallbacksSupport> MediaDrmProxyNativeCallbacks; + using MediaDrmProxyNativeCallbacks::DisposeNative; + using MediaDrmProxyNativeCallbacks::AttachNative; + + 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::KeyStatus; + switch (aStatusCode) { + case KeyStatus::STATUS_USABLE: return dom::MediaKeyStatus::Usable; + case KeyStatus::STATUS_EXPIRED: return dom::MediaKeyStatus::Expired; + case KeyStatus::STATUS_OUTPUT_NOT_ALLOWED: return dom::MediaKeyStatus::Output_restricted; + case KeyStatus::STATUS_INTERNAL_ERROR: return dom::MediaKeyStatus::Internal_error; + case 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(mozilla::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. + mDecryptorProxyCallback->RejectPromise(aPromiseId, + NS_ERROR_DOM_INVALID_STATE_ERR, + reason); +} + +MediaDrmProxySupport::MediaDrmProxySupport(const nsAString& aKeySystem) + : mKeySystem(aKeySystem), mDestroyed(false) +{ + mJavaCallbacks = MediaDrmProxy::NativeMediaDrmProxyCallbacks::New(); + + mBridgeProxy = + MediaDrmProxy::Create(mKeySystem, + mJavaCallbacks, + MediaPrefs::PDMAndroidRemoteCodecEnabled()); +} + +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; +} + +} // namespace mozilla
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.h b/dom/media/eme/mediadrm/MediaDrmProxySupport.h new file mode 100644 index 000000000..e43b71bc1 --- /dev/null +++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h @@ -0,0 +1,67 @@ +/* -*- 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/Logging.h" +#include "FennecJNIWrappers.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: + + 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(); + +private: + const nsString mKeySystem; + java::MediaDrmProxy::GlobalRef mBridgeProxy; + java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::GlobalRef mJavaCallbacks; + DecryptorProxyCallback* mCallback; + bool mDestroyed; + +}; + +} // namespace mozilla +#endif // MediaDrmProxySupport_H
\ No newline at end of file diff --git a/dom/media/eme/mediadrm/moz.build b/dom/media/eme/mediadrm/moz.build new file mode 100644 index 000000000..c01f7a52e --- /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'
\ No newline at end of file diff --git a/dom/media/eme/moz.build b/dom/media/eme/moz.build new file mode 100644 index 000000000..47e622ae3 --- /dev/null +++ b/dom/media/eme/moz.build @@ -0,0 +1,45 @@ +# -*- 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 += [ + 'MediaEncryptedEvent.h', + 'MediaKeyError.h', + 'MediaKeyMessageEvent.h', + 'MediaKeys.h', + 'MediaKeySession.h', + 'MediaKeyStatusMap.h', + 'MediaKeySystemAccess.h', + 'MediaKeySystemAccessManager.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', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android': + DIRS += ['mediadrm'] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' |