diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/eme/MediaKeys.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/eme/MediaKeys.cpp')
-rw-r--r-- | dom/media/eme/MediaKeys.cpp | 844 |
1 files changed, 844 insertions, 0 deletions
diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp new file mode 100644 index 0000000000..0b256650a2 --- /dev/null +++ b/dom/media/eme/MediaKeys.cpp @@ -0,0 +1,844 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/MediaKeys.h" + +#include "ChromiumCDMProxy.h" +#include "GMPCrashHelper.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaKeyError.h" +#include "mozilla/dom/MediaKeyMessageEvent.h" +#include "mozilla/dom/MediaKeySession.h" +#include "mozilla/dom/MediaKeyStatusMap.h" +#include "mozilla/dom/MediaKeySystemAccess.h" +#include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "nsContentCID.h" +#include "nsContentTypeParser.h" +#include "nsContentUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/MediaDrmCDMProxy.h" +#endif +#ifdef XP_WIN +# include "mozilla/WindowsVersion.h" +#endif +#ifdef MOZ_WMF_CDM +# include "mozilla/WMFCDMProxy.h" +#endif + +namespace mozilla::dom { + +// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to +// disconnect our MediaKeys instances from the inner window (mparent) before +// we unlink it. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mKeySessions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingSessions) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement) + tmp->DisconnectInnerWindow(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mKeySessions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromises) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingSessions) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +MediaKeys::MediaKeys(nsPIDOMWindowInner* aParent, const nsAString& aKeySystem, + const MediaKeySystemConfiguration& aConfig) + : mParent(aParent), + mKeySystem(aKeySystem), + mCreatePromiseId(0), + mConfig(aConfig) { + EME_LOG("MediaKeys[%p] constructed keySystem=%s", this, + NS_ConvertUTF16toUTF8(mKeySystem).get()); +} + +MediaKeys::~MediaKeys() { + MOZ_ASSERT(NS_IsMainThread()); + + DisconnectInnerWindow(); + Shutdown(); + EME_LOG("MediaKeys[%p] destroyed", this); +} + +NS_IMETHODIMP MediaKeys::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!strcmp(aTopic, kMediaKeysResponseTopic), + "Should only listen for responses to MediaKey requests"); + EME_LOG("MediaKeys[%p] observing message with aTopic=%s aData=%s", this, + aTopic, NS_ConvertUTF16toUTF8(aData).get()); + if (!strcmp(aTopic, kMediaKeysResponseTopic)) { + if (!mProxy) { + // This may happen if we're notified during shutdown or startup. If this + // is happening outside of those scenarios there's a bug. + EME_LOG( + "MediaKeys[%p] can't notify CDM of observed message as mProxy is " + "unset", + this); + return NS_OK; + } + + if (u"capture-possible"_ns.Equals(aData)) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckSuccessful, + CDMProxy::OutputProtectionCaptureStatus::CapturePossilbe); + } else if (u"capture-not-possible"_ns.Equals(aData)) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckSuccessful, + CDMProxy::OutputProtectionCaptureStatus::CaptureNotPossible); + } else { + MOZ_ASSERT_UNREACHABLE("No code paths should lead to the failure case"); + // This should be unreachable, but gracefully handle in case. + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + } + return NS_OK; +} + +void MediaKeys::ConnectInnerWindow() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsPIDOMWindowInner> innerWindowParent = GetParentObject(); + MOZ_ASSERT(innerWindowParent, + "We should only be connecting when we have an inner window!"); + innerWindowParent->AddMediaKeysInstance(this); +} + +void MediaKeys::DisconnectInnerWindow() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!GetParentObject()) { + // We don't have a parent. We've been cycle collected, or the window + // already notified us of its destruction and we cleared the ref. + return; + } + + GetParentObject()->RemoveMediaKeysInstance(this); +} + +void MediaKeys::OnInnerWindowDestroy() { + MOZ_ASSERT(NS_IsMainThread()); + + EME_LOG("MediaKeys[%p] OnInnerWindowDestroy()", this); + + // The InnerWindow should clear its reference to this object after this call, + // so we don't need to explicitly call DisconnectInnerWindow before nulling. + mParent = nullptr; + + // Don't call shutdown directly because (at time of writing) mProxy can + // spin the event loop when it's shutdown. This can change the world state + // in the middle of window destruction, which we do not want. + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("MediaKeys::Shutdown", this, &MediaKeys::Shutdown)); +} + +void MediaKeys::Terminated() { + EME_LOG("MediaKeys[%p] CDM crashed unexpectedly", this); + + KeySessionHashMap keySessions; + // Remove entries during iteration will screw it. Make a copy first. + for (const RefPtr<MediaKeySession>& session : mKeySessions.Values()) { + // XXX Could the RefPtr still be moved here? + keySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session}); + } + for (const RefPtr<MediaKeySession>& session : keySessions.Values()) { + session->OnClosed(); + } + keySessions.Clear(); + MOZ_ASSERT(mKeySessions.Count() == 0); + + // Notify the element about that CDM has terminated. + if (mElement) { + mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR); + } + + Shutdown(); +} + +void MediaKeys::Shutdown() { + EME_LOG("MediaKeys[%p]::Shutdown()", this); + if (mProxy) { + mProxy->Shutdown(); + mProxy = nullptr; + } + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService && mObserverAdded) { + observerService->RemoveObserver(this, kMediaKeysResponseTopic); + } + + // Hold a self reference to keep us alive after we clear the self reference + // for each promise. This ensures we stay alive until we're done shutting + // down. + RefPtr<MediaKeys> selfReference = this; + + for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) { + promise->MaybeRejectWithInvalidStateError( + "Promise still outstanding at MediaKeys shutdown"); + Release(); + } + mPromises.Clear(); +} + +nsPIDOMWindowInner* MediaKeys::GetParentObject() const { return mParent; } + +JSObject* MediaKeys::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MediaKeys_Binding::Wrap(aCx, this, aGivenProto); +} + +void MediaKeys::GetKeySystem(nsString& aOutKeySystem) const { + aOutKeySystem.Assign(mKeySystem); +} + +already_AddRefed<DetailedPromise> MediaKeys::SetServerCertificate( + const ArrayBufferViewOrArrayBuffer& aCert, ErrorResult& aRv) { + RefPtr<DetailedPromise> promise( + MakePromise(aRv, "MediaKeys.setServerCertificate"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeRejectWithInvalidStateError( + "Null CDM in MediaKeys.setServerCertificate()"); + return promise.forget(); + } + + nsTArray<uint8_t> data; + CopyArrayBufferViewOrArrayBufferData(aCert, data); + if (data.IsEmpty()) { + promise->MaybeRejectWithTypeError( + "Empty certificate passed to MediaKeys.setServerCertificate()"); + return promise.forget(); + } + + mProxy->SetServerCertificate(StorePromise(promise), data); + return promise.forget(); +} + +already_AddRefed<DetailedPromise> MediaKeys::MakePromise( + ErrorResult& aRv, const nsACString& aName) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + if (!global) { + NS_WARNING("Passed non-global to MediaKeys ctor!"); + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + return DetailedPromise::Create(global, aRv, aName); +} + +PromiseId MediaKeys::StorePromise(DetailedPromise* aPromise) { + static uint32_t sEMEPromiseCount = 1; + MOZ_ASSERT(aPromise); + uint32_t id = sEMEPromiseCount++; + + EME_LOG("MediaKeys[%p]::StorePromise() id=%" PRIu32, this, id); + + // Keep MediaKeys alive for the lifetime of its promises. Any still-pending + // promises are rejected in Shutdown(). + EME_LOG("MediaKeys[%p]::StorePromise() calling AddRef()", this); + AddRef(); + +#ifdef DEBUG + // We should not have already stored this promise! + for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) { + MOZ_ASSERT(promise != aPromise); + } +#endif + + mPromises.InsertOrUpdate(id, RefPtr{aPromise}); + return id; +} + +void MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId, + uint32_t aToken) { + // Should only be called from MediaKeySession::GenerateRequest. + mPromiseIdToken.InsertOrUpdate(aId, aToken); + EME_LOG( + "MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)", + this, aId, aToken); +} + +already_AddRefed<DetailedPromise> MediaKeys::RetrievePromise(PromiseId aId) { + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ")", this, aId); + if (!mPromises.Contains(aId)) { + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 + ") tried to retrieve non-existent promise!", + this, aId); + NS_WARNING(nsPrintfCString( + "Tried to retrieve a non-existent promise id=%" PRIu32, aId) + .get()); + return nullptr; + } + RefPtr<DetailedPromise> promise; + mPromises.Remove(aId, getter_AddRefs(promise)); + EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ") calling Release()", + this, aId); + Release(); + return promise.forget(); +} + +void MediaKeys::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + uint32_t errorCodeAsInt = aException.ErrorCodeAsInt(); + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 ")", this, aId, + errorCodeAsInt); + + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 + ") couldn't retrieve promise! Bailing!", + this, aId, errorCodeAsInt); + return; + } + + // This promise could be a createSession or loadSession promise, + // so we might have a pending session waiting to be resolved into + // the promise on success. We've been directed to reject to promise, + // so we can throw away the corresponding session object. + uint32_t token = 0; + if (mPromiseIdToken.Get(aId, &token)) { + MOZ_ASSERT(mPendingSessions.Contains(token)); + mPendingSessions.Remove(token); + mPromiseIdToken.Remove(aId); + } + + MOZ_ASSERT(aException.Failed()); + promise->MaybeReject(std::move(aException), aReason); + + if (mCreatePromiseId == aId) { + // Note: This will probably destroy the MediaKeys object! + EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 + ") calling Release()", + this, aId, errorCodeAsInt); + Release(); + } +} + +void MediaKeys::OnSessionIdReady(MediaKeySession* aSession) { + if (!aSession) { + NS_WARNING("Invalid MediaKeySession passed to OnSessionIdReady()"); + return; + } + if (mKeySessions.Contains(aSession->GetSessionId())) { + NS_WARNING("MediaKeySession's made ready multiple times!"); + return; + } + if (mPendingSessions.Contains(aSession->Token())) { + NS_WARNING( + "MediaKeySession made ready when it wasn't waiting to be ready!"); + return; + } + if (aSession->GetSessionId().IsEmpty()) { + NS_WARNING( + "MediaKeySession with invalid sessionId passed to OnSessionIdReady()"); + return; + } + mKeySessions.InsertOrUpdate(aSession->GetSessionId(), RefPtr{aSession}); +} + +void MediaKeys::ResolvePromise(PromiseId aId) { + EME_LOG("MediaKeys[%p]::ResolvePromise(%" PRIu32 ")", this, aId); + + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + MOZ_ASSERT(!mPromises.Contains(aId)); + if (!promise) { + return; + } + + uint32_t token = 0; + if (!mPromiseIdToken.Get(aId, &token)) { + promise->MaybeResolveWithUndefined(); + return; + } else if (!mPendingSessions.Contains(token)) { + // Pending session for CreateSession() should be removed when sessionId + // is ready. + promise->MaybeResolveWithUndefined(); + mPromiseIdToken.Remove(aId); + return; + } + mPromiseIdToken.Remove(aId); + + // We should only resolve LoadSession calls via this path, + // not CreateSession() promises. + RefPtr<MediaKeySession> session; + mPendingSessions.Remove(token, getter_AddRefs(session)); + if (!session || session->GetSessionId().IsEmpty()) { + NS_WARNING("Received activation for non-existent session!"); + promise->MaybeRejectWithInvalidAccessError( + "CDM LoadSession() returned a different session ID than requested"); + return; + } + mKeySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session}); + promise->MaybeResolve(session); +} + +class MediaKeysGMPCrashHelper : public GMPCrashHelper { + public: + explicit MediaKeysGMPCrashHelper(MediaKeys* aMediaKeys) + : mMediaKeys(aMediaKeys) { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + } + already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + EME_LOG("MediaKeysGMPCrashHelper::GetPluginCrashedEventTarget()"); + return (mMediaKeys && mMediaKeys->GetParentObject()) + ? do_AddRef(mMediaKeys->GetParentObject()) + : nullptr; + } + + private: + WeakPtr<MediaKeys> mMediaKeys; +}; + +already_AddRefed<CDMProxy> MediaKeys::CreateCDMProxy() { + EME_LOG("MediaKeys[%p]::CreateCDMProxy()", this); + RefPtr<CDMProxy> proxy; +#ifdef MOZ_WIDGET_ANDROID + if (IsWidevineKeySystem(mKeySystem)) { + proxy = new MediaDrmCDMProxy( + this, mKeySystem, + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } else +#endif +#ifdef MOZ_WMF_CDM + if (IsPlayReadyKeySystemAndSupported(mKeySystem)) { + proxy = new WMFCDMProxy(this, mKeySystem, mConfig); + } else +#endif + { + proxy = new ChromiumCDMProxy( + this, mKeySystem, new MediaKeysGMPCrashHelper(this), + mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required, + mConfig.mPersistentState == MediaKeysRequirement::Required); + } + return proxy.forget(); +} + +already_AddRefed<DetailedPromise> MediaKeys::Init(ErrorResult& aRv) { + EME_LOG("MediaKeys[%p]::Init()", this); + RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeys::Init()"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // Determine principal (at creation time) of the MediaKeys object. + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject()); + if (!sop) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get script principal in MediaKeys::Init"); + return promise.forget(); + } + mPrincipal = sop->GetPrincipal(); + + // Begin figuring out the top level principal. + nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject(); + + // If we're in a top level document, getting the top level principal is easy. + // However, we're not in a top level doc this becomes more complicated. If + // we're not top level we need to get the top level principal, this can be + // done by reading the principal of the load info, which we can get of a + // document's channel. + // + // There is an edge case we need to watch out for here where this code can be + // run in an about:blank document before it has done its async load. In this + // case the document will not yet have a load info. We address this below by + // walking up a level in the window context chain. See + // https://bugzilla.mozilla.org/show_bug.cgi?id=1675360 + // for more info. + Document* document = window->GetExtantDoc(); + if (!document) { + NS_WARNING("Failed to get document when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get document in MediaKeys::Init"); + return promise.forget(); + } + + WindowGlobalChild* windowGlobalChild = window->GetWindowGlobalChild(); + if (!windowGlobalChild) { + NS_WARNING("Failed to get window global child when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get window global child in MediaKeys::Init"); + return promise.forget(); + } + + if (windowGlobalChild->SameOriginWithTop()) { + // We're in the same origin as the top window context, so our principal + // is also the top principal. + mTopLevelPrincipal = mPrincipal; + } else { + // We have a different origin than the top doc, try and find the top level + // principal by looking it up via load info, which we read off a channel. + nsIChannel* channel = document->GetChannel(); + + WindowContext* windowContext = document->GetWindowContext(); + if (!windowContext) { + NS_WARNING("Failed to get window context when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get window context in MediaKeys::Init"); + return promise.forget(); + } + while (!channel) { + // We don't have a channel, this can happen if we're in an about:blank + // page that hasn't yet had its async load performed. Try and get + // the channel from our parent doc. We should be able to do this because + // an about:blank is considered the same origin as its parent. We do this + // recursively to cover pages do silly things like nesting blank iframes + // and not waiting for loads. + + // Move our window context up a level. + windowContext = windowContext->GetParentWindowContext(); + if (!windowContext || !windowContext->GetExtantDoc()) { + NS_WARNING( + "Failed to get parent window context's document when creating " + "MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get parent window context's document in " + "MediaKeys::Init (likely due to an nested about about:blank frame " + "that hasn't loaded yet)"); + return promise.forget(); + } + + Document* parentDoc = windowContext->GetExtantDoc(); + channel = parentDoc->GetChannel(); + } + + MOZ_RELEASE_ASSERT( + channel, "Should either have a channel or should have returned by now"); + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + MOZ_RELEASE_ASSERT(loadInfo, "Channels should always have LoadInfo"); + mTopLevelPrincipal = loadInfo->GetTopLevelPrincipal(); + if (!mTopLevelPrincipal) { + NS_WARNING("Failed to get top level principal when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get top level principal in MediaKeys::Init"); + return promise.forget(); + } + } + + // We should have figured out our top level principal. + if (!mPrincipal || !mTopLevelPrincipal) { + NS_WARNING("Failed to get principals when creating MediaKeys"); + promise->MaybeRejectWithInvalidStateError( + "Couldn't get principal(s) in MediaKeys::Init"); + return promise.forget(); + } + + nsAutoCString origin; + nsresult rv = mPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get principal origin string in MediaKeys::Init"); + return promise.forget(); + } + nsAutoCString topLevelOrigin; + rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin); + if (NS_FAILED(rv)) { + promise->MaybeRejectWithInvalidStateError( + "Couldn't get top-level principal origin string in MediaKeys::Init"); + return promise.forget(); + } + + EME_LOG("MediaKeys[%p]::Create() (%s, %s)", this, origin.get(), + topLevelOrigin.get()); + + mProxy = CreateCDMProxy(); + + // The CDMProxy's initialization is asynchronous. The MediaKeys is + // refcounted, and its instance is returned to JS by promise once + // it's been initialized. No external refs exist to the MediaKeys while + // we're waiting for the promise to be resolved, so we must hold a + // reference to the new MediaKeys object until it's been created, + // or its creation has failed. Store the id of the promise returned + // here, and hold a self-reference until that promise is resolved or + // rejected. + MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!"); + mCreatePromiseId = StorePromise(promise); + EME_LOG("MediaKeys[%p]::Init() calling AddRef()", this); + AddRef(); + mProxy->Init(mCreatePromiseId, NS_ConvertUTF8toUTF16(origin), + NS_ConvertUTF8toUTF16(topLevelOrigin), + KeySystemToProxyName(mKeySystem)); + + ConnectInnerWindow(); + + return promise.forget(); +} + +void MediaKeys::OnCDMCreated(PromiseId aId, const uint32_t aPluginId) { + EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 ")", + this, aId, aPluginId); + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + RefPtr<MediaKeys> keys(this); + + promise->MaybeResolve(keys); + if (mCreatePromiseId == aId) { + EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 + ") calling Release()", + this, aId, aPluginId); + Release(); + } + + MediaKeySystemAccess::NotifyObservers(mParent, mKeySystem, + MediaKeySystemStatus::Cdm_created); +} + +static bool IsSessionTypeSupported(const MediaKeySessionType aSessionType, + const MediaKeySystemConfiguration& aConfig) { + if (aSessionType == MediaKeySessionType::Temporary) { + // Temporary is always supported. + return true; + } + if (!aConfig.mSessionTypes.WasPassed()) { + // No other session types supported. + return false; + } + return aConfig.mSessionTypes.Value().Contains(ToString(aSessionType)); +} + +already_AddRefed<MediaKeySession> MediaKeys::CreateSession( + MediaKeySessionType aSessionType, ErrorResult& aRv) { + EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 ")", this, + static_cast<uint8_t>(aSessionType)); + if (!IsSessionTypeSupported(aSessionType, mConfig)) { + EME_LOG("MediaKeys[%p]::CreateSession() failed, unsupported session type", + this); + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys which lost its CDM"); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + EME_LOG("MediaKeys[%p] Creating session", this); + + RefPtr<MediaKeySession> session = new MediaKeySession( + GetParentObject(), this, mKeySystem, aSessionType, aRv); + + if (aRv.Failed()) { + return nullptr; + } + DDLINKCHILD("session", session.get()); + + // Add session to the set of sessions awaiting their sessionId being ready. + EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 + ") putting session with token=%" PRIu32 " into mPendingSessions", + this, static_cast<uint8_t>(aSessionType), session->Token()); + mPendingSessions.InsertOrUpdate(session->Token(), RefPtr{session}); + + return session.forget(); +} + +void MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) { + EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%" PRIu32, this, + aId); + + ResolvePromiseWithResult(aId, aSuccess); +} + +void MediaKeys::OnSessionClosed(MediaKeySession* aSession) { + nsAutoString id; + aSession->GetSessionId(id); + mKeySessions.Remove(id); +} + +already_AddRefed<MediaKeySession> MediaKeys::GetSession( + const nsAString& aSessionId) { + RefPtr<MediaKeySession> session; + mKeySessions.Get(aSessionId, getter_AddRefs(session)); + return session.forget(); +} + +already_AddRefed<MediaKeySession> MediaKeys::GetPendingSession( + uint32_t aToken) { + EME_LOG("MediaKeys[%p]::GetPendingSession(aToken=%" PRIu32 ")", this, aToken); + RefPtr<MediaKeySession> session; + mPendingSessions.Get(aToken, getter_AddRefs(session)); + mPendingSessions.Remove(aToken); + return session.forget(); +} + +bool MediaKeys::IsBoundToMediaElement() const { + MOZ_ASSERT(NS_IsMainThread()); + return mElement != nullptr; +} + +nsresult MediaKeys::Bind(HTMLMediaElement* aElement) { + MOZ_ASSERT(NS_IsMainThread()); + if (IsBoundToMediaElement()) { + return NS_ERROR_FAILURE; + } + + mElement = aElement; + + return NS_OK; +} + +void MediaKeys::Unbind() { + MOZ_ASSERT(NS_IsMainThread()); + mElement = nullptr; +} + +void MediaKeys::CheckIsElementCapturePossible() { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("MediaKeys[%p]::IsElementCapturePossible()", this); + // Note, HTMLMediaElement prevents capture of its content via Capture APIs + // on the element if it has a media keys attached (see bug 1071482). So we + // don't need to check those cases here (they are covered by tests). + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + if (!observerService) { + // This can happen if we're in shutdown which means we may be going away + // soon anyway, but respond saying capture is possible since we can't + // forward the check further. + if (mProxy) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + return; + } + if (!mObserverAdded) { + nsresult rv = + observerService->AddObserver(this, kMediaKeysResponseTopic, false); + if (NS_FAILED(rv)) { + if (mProxy) { + mProxy->NotifyOutputProtectionStatus( + CDMProxy::OutputProtectionCheckStatus::CheckFailed, + CDMProxy::OutputProtectionCaptureStatus::Unused); + } + return; + } + mObserverAdded = true; + } + + if (mCaptureCheckRequestJson.IsEmpty()) { + // Lazily populate the JSON the first time we need it. + JSONStringWriteFunc<nsAutoCString> json; + JSONWriter jw{json}; + jw.Start(); + jw.StringProperty("status", "is-capture-possible"); + jw.StringProperty("keySystem", NS_ConvertUTF16toUTF8(mKeySystem)); + jw.End(); + mCaptureCheckRequestJson = NS_ConvertUTF8toUTF16(json.StringCRef()); + } + + MOZ_DIAGNOSTIC_ASSERT(!mCaptureCheckRequestJson.IsEmpty()); + observerService->NotifyObservers(mParent.get(), kMediaKeysRequestTopic, + mCaptureCheckRequestJson.get()); +} + +void MediaKeys::GetSessionsInfo(nsString& sessionsInfo) { + for (const auto& keySession : mKeySessions.Values()) { + nsString sessionID; + keySession->GetSessionId(sessionID); + sessionsInfo.AppendLiteral("(sid="); + sessionsInfo.Append(sessionID); + MediaKeyStatusMap* keyStatusMap = keySession->KeyStatuses(); + for (uint32_t i = 0; i < keyStatusMap->GetIterableLength(); i++) { + nsString keyID = keyStatusMap->GetKeyIDAsHexString(i); + sessionsInfo.AppendLiteral("(kid="); + sessionsInfo.Append(keyID); + sessionsInfo.AppendLiteral(" status="); + sessionsInfo.AppendASCII( + MediaKeyStatusValues::GetString(keyStatusMap->GetValueAtIndex(i))); + sessionsInfo.AppendLiteral(")"); + } + sessionsInfo.AppendLiteral(")"); + } +} + +already_AddRefed<Promise> MediaKeys::GetStatusForPolicy( + const MediaKeysPolicy& aPolicy, ErrorResult& aRv) { + RefPtr<DetailedPromise> promise( + MakePromise(aRv, "MediaKeys::GetStatusForPolicy()"_ns)); + if (aRv.Failed()) { + return nullptr; + } + + // Currently, only widevine CDM supports for this API. + if (!IsWidevineKeySystem(mKeySystem)) { + EME_LOG( + "MediaKeys[%p]::GetStatusForPolicy() HDCP policy check on unsupported " + "keysystem ", + this); + NS_WARNING("Tried to query without a CDM"); + promise->MaybeRejectWithNotSupportedError( + "HDCP policy check on unsupported keysystem"); + return promise.forget(); + } + + if (!mProxy) { + NS_WARNING("Tried to use a MediaKeys without a CDM"); + promise->MaybeRejectWithInvalidStateError( + "Null CDM in MediaKeys.GetStatusForPolicy()"); + return promise.forget(); + } + + EME_LOG("GetStatusForPolicy minHdcpVersion = %s.", + NS_ConvertUTF16toUTF8(aPolicy.mMinHdcpVersion).get()); + mProxy->GetStatusForPolicy(StorePromise(promise), aPolicy.mMinHdcpVersion); + return promise.forget(); +} + +void MediaKeys::ResolvePromiseWithKeyStatus(PromiseId aId, + MediaKeyStatus aMediaKeyStatus) { + RefPtr<DetailedPromise> promise(RetrievePromise(aId)); + if (!promise) { + return; + } + RefPtr<MediaKeys> keys(this); + EME_LOG( + "MediaKeys[%p]::ResolvePromiseWithKeyStatus() resolve promise id=%" PRIu32 + ", keystatus=%" PRIu8, + this, aId, static_cast<uint8_t>(aMediaKeyStatus)); + promise->MaybeResolve(aMediaKeyStatus); +} + +} // namespace mozilla::dom |