/* 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/MediaDevices.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/MediaStreamBinding.h" #include "mozilla/dom/MediaDeviceInfo.h" #include "mozilla/dom/MediaDevicesBinding.h" #include "mozilla/dom/NavigatorBinding.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/MediaManager.h" #include "MediaTrackConstraints.h" #include "nsContentUtils.h" #include "nsINamed.h" #include "nsIScriptGlobalObject.h" #include "nsPIDOMWindow.h" #include "nsQueryObject.h" #define DEVICECHANGE_HOLD_TIME_IN_MS 1000 namespace mozilla::dom { MediaDevices::~MediaDevices() { MOZ_ASSERT(NS_IsMainThread()); if (mFuzzTimer) { mFuzzTimer->Cancel(); } mDeviceChangeListener.DisconnectIfExists(); } already_AddRefed MediaDevices::GetUserMedia( const MediaStreamConstraints& aConstraints, CallerType aCallerType, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); // Get the relevant global for the promise from the wrapper cache because // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded. // We know the wrapper exists because it is being used for |this| from JS. // See https://github.com/heycam/webidl/issues/932 for why the relevant // global is used instead of the current global. nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); // global is a window because MediaDevices is exposed only to Window. nsCOMPtr owner = do_QueryInterface(global); if (Document* doc = owner->GetExtantDoc()) { if (!owner->IsSecureContext()) { doc->SetUseCounter(eUseCounter_custom_GetUserMediaInsec); } Document* topDoc = doc->GetTopLevelContentDocument(); IgnoredErrorResult ignored; if (topDoc && !topDoc->HasFocus(ignored)) { doc->SetUseCounter(eUseCounter_custom_GetUserMediaUnfocused); } } RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } /* If requestedMediaTypes is the empty set, return a promise rejected with a * TypeError. */ if (!MediaManager::IsOn(aConstraints.mVideo) && !MediaManager::IsOn(aConstraints.mAudio)) { p->MaybeRejectWithTypeError("audio and/or video is required"); return p.forget(); } /* If the relevant settings object's responsible document is NOT fully * active, return a promise rejected with a DOMException object whose name * attribute has the value "InvalidStateError". */ if (!owner->IsFullyActive()) { p->MaybeRejectWithInvalidStateError("The document is not fully active."); return p.forget(); } RefPtr self(this); MediaManager::Get() ->GetUserMedia(owner, aConstraints, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr&& aStream) { if (!GetWindowIfCurrent()) { return; // Leave Promise pending after navigation by design. } p->MaybeResolve(std::move(aStream)); }, [this, self, p](const RefPtr& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } error->Reject(p); }); return p.forget(); } already_AddRefed MediaDevices::EnumerateDevices(CallerType aCallerType, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); nsCOMPtr owner = do_QueryInterface(global); if (Document* doc = owner->GetExtantDoc()) { if (!owner->IsSecureContext()) { doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesInsec); } Document* topDoc = doc->GetTopLevelContentDocument(); IgnoredErrorResult ignored; if (topDoc && !topDoc->HasFocus(ignored)) { doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesUnfocused); } } RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr self(this); MediaManager::Get() ->EnumerateDevices(owner, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr&& aDevices) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } auto windowId = window->WindowID(); nsTArray> infos; bool allowLabel = aDevices->Length() == 0 || MediaManager::Get()->IsActivelyCapturingOrHasAPermission( windowId); for (auto& device : *aDevices) { MOZ_ASSERT(device->mKind == dom::MediaDeviceKind::Audioinput || device->mKind == dom::MediaDeviceKind::Videoinput || device->mKind == dom::MediaDeviceKind::Audiooutput); // Include name only if page currently has a gUM stream active // or persistent permissions (audio or video) have been granted nsString label; if (allowLabel || Preferences::GetBool("media.navigator.permission.disabled", false)) { label = device->mName; } infos.AppendElement(MakeRefPtr( device->mID, device->mKind, label, device->mGroupID)); } p->MaybeResolve(std::move(infos)); }, [this, self, p](const RefPtr& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } error->Reject(p); }); return p.forget(); } already_AddRefed MediaDevices::GetDisplayMedia( const DisplayMediaStreamConstraints& aConstraints, CallerType aCallerType, ErrorResult& aRv) { nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } nsCOMPtr owner = do_QueryInterface(global); /* If the relevant global object of this does not have transient activation, * return a promise rejected with a DOMException object whose name attribute * has the value InvalidStateError. */ WindowContext* wc = owner->GetWindowContext(); if (!wc || !wc->HasValidTransientUserGestureActivation()) { p->MaybeRejectWithInvalidStateError( "getDisplayMedia must be called from a user gesture handler."); return p.forget(); } /* If constraints.video is false, return a promise rejected with a newly * created TypeError. */ if (!MediaManager::IsOn(aConstraints.mVideo)) { p->MaybeRejectWithTypeError("video is required"); return p.forget(); } MediaStreamConstraints c; auto& vc = c.mVideo.SetAsMediaTrackConstraints(); if (aConstraints.mVideo.IsMediaTrackConstraints()) { vc = aConstraints.mVideo.GetAsMediaTrackConstraints(); /* If CS contains a member named advanced, return a promise rejected with * a newly created TypeError. */ if (vc.mAdvanced.WasPassed()) { p->MaybeRejectWithTypeError("advanced not allowed"); return p.forget(); } auto getCLR = [](const auto& aCon) -> const ConstrainLongRange& { static ConstrainLongRange empty; return (aCon.WasPassed() && !aCon.Value().IsLong()) ? aCon.Value().GetAsConstrainLongRange() : empty; }; auto getCDR = [](auto&& aCon) -> const ConstrainDoubleRange& { static ConstrainDoubleRange empty; return (aCon.WasPassed() && !aCon.Value().IsDouble()) ? aCon.Value().GetAsConstrainDoubleRange() : empty; }; const auto& w = getCLR(vc.mWidth); const auto& h = getCLR(vc.mHeight); const auto& f = getCDR(vc.mFrameRate); /* If CS contains a member whose name specifies a constrainable property * applicable to display surfaces, and whose value in turn is a dictionary * containing a member named either min or exact, return a promise * rejected with a newly created TypeError. */ if (w.mMin.WasPassed() || h.mMin.WasPassed() || f.mMin.WasPassed()) { p->MaybeRejectWithTypeError("min not allowed"); return p.forget(); } if (w.mExact.WasPassed() || h.mExact.WasPassed() || f.mExact.WasPassed()) { p->MaybeRejectWithTypeError("exact not allowed"); return p.forget(); } /* If CS contains a member whose name, failedConstraint specifies a * constrainable property, constraint, applicable to display surfaces, and * whose value in turn is a dictionary containing a member named max, and * that member's value in turn is less than the constrainable property's * floor value, then let failedConstraint be the name of the constraint, * let message be either undefined or an informative human-readable * message, and return a promise rejected with a new OverconstrainedError * created by calling OverconstrainedError(failedConstraint, message). */ // We fail early without incurring a prompt, on known-to-fail constraint // values that don't reveal anything about the user's system. const char* badConstraint = nullptr; if (w.mMax.WasPassed() && w.mMax.Value() < 1) { badConstraint = "width"; } if (h.mMax.WasPassed() && h.mMax.Value() < 1) { badConstraint = "height"; } if (f.mMax.WasPassed() && f.mMax.Value() < 1) { badConstraint = "frameRate"; } if (badConstraint) { p->MaybeReject(MakeRefPtr( owner, *MakeRefPtr( MediaMgrError::Name::OverconstrainedError, "", NS_ConvertASCIItoUTF16(badConstraint)))); return p.forget(); } } /* If the relevant settings object's responsible document is NOT fully * active, return a promise rejected with a DOMException object whose name * attribute has the value "InvalidStateError". */ if (!owner->IsFullyActive()) { p->MaybeRejectWithInvalidStateError("The document is not fully active."); return p.forget(); } // We ask for "screen" sharing. // // If this is a privileged call or permission is disabled, this gives us full // screen sharing by default, which is useful for internal testing. // // If this is a non-priviliged call, GetUserMedia() will change it to "window" // for us. vc.mMediaSource.Reset(); vc.mMediaSource.Construct().AssignASCII( dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Screen)); RefPtr self(this); MediaManager::Get() ->GetUserMedia(owner, c, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr&& aStream) { if (!GetWindowIfCurrent()) { return; // leave promise pending after navigation. } p->MaybeResolve(std::move(aStream)); }, [this, self, p](RefPtr&& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // leave promise pending after navigation. } error->Reject(p); }); return p.forget(); } NS_IMPL_ADDREF_INHERITED(MediaDevices, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(MediaDevices, DOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN(MediaDevices) NS_INTERFACE_MAP_ENTRY(MediaDevices) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) void MediaDevices::OnDeviceChange() { MOZ_ASSERT(NS_IsMainThread()); if (NS_FAILED(CheckCurrentGlobalCorrectness())) { // This is a ghost window, don't do anything. return; } if (!(MediaManager::Get()->IsActivelyCapturingOrHasAPermission( GetOwner()->WindowID()) || Preferences::GetBool("media.navigator.permission.disabled", false))) { return; } // Do not fire event to content script when // privacy.resistFingerprinting is true. if (nsContentUtils::ShouldResistFingerprinting()) { return; } if (mFuzzTimer) { // An event is already in flight. return; } mFuzzTimer = NS_NewTimer(); if (!mFuzzTimer) { MOZ_ASSERT(false); return; } mFuzzTimer->InitWithNamedFuncCallback( [](nsITimer*, void* aClosure) { MediaDevices* md = static_cast(aClosure); md->DispatchTrustedEvent(u"devicechange"_ns); md->mFuzzTimer = nullptr; }, this, DEVICECHANGE_HOLD_TIME_IN_MS, nsITimer::TYPE_ONE_SHOT, "MediaDevices::mFuzzTimer Callback"); } mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() { return GetEventHandler(nsGkAtoms::ondevicechange); } void MediaDevices::SetupDeviceChangeListener() { if (mIsDeviceChangeListenerSetUp) { return; } nsPIDOMWindowInner* window = GetOwner(); if (!window) { return; } nsISerialEventTarget* mainThread = window->EventTargetFor(TaskCategory::Other); if (!mainThread) { return; } mDeviceChangeListener = MediaManager::Get()->DeviceListChangeEvent().Connect( mainThread, this, &MediaDevices::OnDeviceChange); mIsDeviceChangeListenerSetUp = true; } void MediaDevices::SetOndevicechange( mozilla::dom::EventHandlerNonNull* aCallback) { SetEventHandler(nsGkAtoms::ondevicechange, aCallback); SetupDeviceChangeListener(); } void MediaDevices::EventListenerAdded(nsAtom* aType) { DOMEventTargetHelper::EventListenerAdded(aType); SetupDeviceChangeListener(); } JSObject* MediaDevices::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return MediaDevices_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom