summaryrefslogtreecommitdiffstats
path: root/dom/media/eme/MediaKeys.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/eme/MediaKeys.cpp')
-rw-r--r--dom/media/eme/MediaKeys.cpp844
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