/* 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/MediaStreamBinding.h" #include "mozilla/dom/MediaDeviceInfo.h" #include "mozilla/dom/MediaDevicesBinding.h" #include "mozilla/dom/Promise.h" #include "mozilla/MediaManager.h" #include "MediaTrackConstraints.h" #include "nsContentUtils.h" #include "nsIEventTarget.h" #include "nsINamed.h" #include "nsIScriptGlobalObject.h" #include "nsIPermissionManager.h" #include "nsPIDOMWindow.h" #include "nsQueryObject.h" #define DEVICECHANGE_HOLD_TIME_IN_MS 1000 namespace mozilla { namespace dom { class FuzzTimerCallBack final : public nsITimerCallback, public nsINamed { ~FuzzTimerCallBack() {} public: explicit FuzzTimerCallBack(MediaDevices* aMediaDevices) : mMediaDevices(aMediaDevices) {} NS_DECL_ISUPPORTS NS_IMETHOD Notify(nsITimer* aTimer) final override { mMediaDevices->DispatchTrustedEvent(NS_LITERAL_STRING("devicechange")); return NS_OK; } NS_IMETHOD GetName(nsACString& aName) override { aName.AssignLiteral("FuzzTimerCallBack"); return NS_OK; } private: nsCOMPtr mMediaDevices; }; NS_IMPL_ISUPPORTS(FuzzTimerCallBack, nsITimerCallback, nsINamed) class MediaDevices::GumResolver : public nsIDOMGetUserMediaSuccessCallback { public: NS_DECL_ISUPPORTS explicit GumResolver(Promise* aPromise) : mPromise(aPromise) {} NS_IMETHOD OnSuccess(nsISupports* aStream) override { RefPtr stream = do_QueryObject(aStream); if (!stream) { return NS_ERROR_FAILURE; } mPromise->MaybeResolve(stream); return NS_OK; } private: virtual ~GumResolver() {} RefPtr mPromise; }; class MediaDevices::EnumDevResolver : public nsIGetUserMediaDevicesSuccessCallback { public: NS_DECL_ISUPPORTS EnumDevResolver(Promise* aPromise, uint64_t aWindowId) : mPromise(aPromise), mWindowId(aWindowId) {} NS_IMETHOD OnSuccess(nsIVariant* aDevices) override { // Create array for nsIMediaDevice nsTArray> devices; // Contain the fumes { uint16_t vtype; nsresult rv = aDevices->GetDataType(&vtype); NS_ENSURE_SUCCESS(rv, rv); if (vtype != nsIDataType::VTYPE_EMPTY_ARRAY) { nsIID elementIID; uint16_t elementType; void* rawArray; uint32_t arrayLen; rv = aDevices->GetAsArray(&elementType, &elementIID, &arrayLen, &rawArray); NS_ENSURE_SUCCESS(rv, rv); if (elementType != nsIDataType::VTYPE_INTERFACE) { free(rawArray); return NS_ERROR_FAILURE; } nsISupports **supportsArray = reinterpret_cast(rawArray); for (uint32_t i = 0; i < arrayLen; ++i) { nsCOMPtr device(do_QueryInterface(supportsArray[i])); devices.AppendElement(device); NS_IF_RELEASE(supportsArray[i]); // explicitly decrease refcount for rawptr } free(rawArray); // explicitly free memory from nsIVariant::GetAsArray } } nsTArray> infos; for (auto& device : devices) { nsString type; device->GetType(type); bool isVideo = type.EqualsLiteral("video"); bool isAudio = type.EqualsLiteral("audio"); if (isVideo || isAudio) { MediaDeviceKind kind = isVideo ? MediaDeviceKind::Videoinput : MediaDeviceKind::Audioinput; nsString id; nsString name; device->GetId(id); // Include name only if page currently has a gUM stream active or // persistent permissions (audio or video) have been granted if (MediaManager::Get()->IsActivelyCapturingOrHasAPermission(mWindowId) || Preferences::GetBool("media.navigator.permission.disabled", false)) { device->GetName(name); } RefPtr info = new MediaDeviceInfo(id, kind, name); infos.AppendElement(info); } } mPromise->MaybeResolve(infos); return NS_OK; } private: virtual ~EnumDevResolver() {} RefPtr mPromise; uint64_t mWindowId; }; class MediaDevices::GumRejecter : public nsIDOMGetUserMediaErrorCallback { public: NS_DECL_ISUPPORTS explicit GumRejecter(Promise* aPromise) : mPromise(aPromise) {} NS_IMETHOD OnError(nsISupports* aError) override { RefPtr error = do_QueryObject(aError); if (!error) { return NS_ERROR_FAILURE; } mPromise->MaybeReject(error); return NS_OK; } private: virtual ~GumRejecter() {} RefPtr mPromise; }; MediaDevices::~MediaDevices() { MediaManager* mediamanager = MediaManager::GetIfExists(); if (mediamanager) { mediamanager->RemoveDeviceChangeCallback(this); } } NS_IMPL_ISUPPORTS(MediaDevices::GumResolver, nsIDOMGetUserMediaSuccessCallback) NS_IMPL_ISUPPORTS(MediaDevices::EnumDevResolver, nsIGetUserMediaDevicesSuccessCallback) NS_IMPL_ISUPPORTS(MediaDevices::GumRejecter, nsIDOMGetUserMediaErrorCallback) already_AddRefed MediaDevices::GetUserMedia(const MediaStreamConstraints& aConstraints, CallerType aCallerType, ErrorResult &aRv) { nsPIDOMWindowInner* window = GetOwner(); nsCOMPtr go = do_QueryInterface(window); RefPtr p = Promise::Create(go, aRv); NS_ENSURE_TRUE(!aRv.Failed(), nullptr); RefPtr resolver = new GumResolver(p); RefPtr rejecter = new GumRejecter(p); aRv = MediaManager::Get()->GetUserMedia(window, aConstraints, resolver, rejecter, aCallerType); return p.forget(); } already_AddRefed MediaDevices::EnumerateDevices(CallerType aCallerType, ErrorResult &aRv) { nsPIDOMWindowInner* window = GetOwner(); nsCOMPtr go = do_QueryInterface(window); RefPtr p = Promise::Create(go, aRv); NS_ENSURE_TRUE(!aRv.Failed(), nullptr); RefPtr resolver = new EnumDevResolver(p, window->WindowID()); RefPtr rejecter = new GumRejecter(p); aRv = MediaManager::Get()->EnumerateDevices(window, resolver, rejecter, aCallerType); 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()); nsresult rv = CheckInnerWindowCorrectness(); if (NS_FAILED(rv)) { MOZ_ASSERT(false); 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) { mFuzzTimer = NS_NewTimer(); } if (!mFuzzTimer) { MOZ_ASSERT(false); return; } mFuzzTimer->Cancel(); RefPtr cb = new FuzzTimerCallBack(this); mFuzzTimer->InitWithCallback(cb, DEVICECHANGE_HOLD_TIME_IN_MS, nsITimer::TYPE_ONE_SHOT); } mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() { if (NS_IsMainThread()) { return GetEventHandler(nsGkAtoms::ondevicechange, EmptyString()); } return GetEventHandler(nullptr, NS_LITERAL_STRING("devicechange")); } void MediaDevices::SetOndevicechange(mozilla::dom::EventHandlerNonNull* aCallback) { if (NS_IsMainThread()) { SetEventHandler(nsGkAtoms::ondevicechange, EmptyString(), aCallback); } else { SetEventHandler(nullptr, NS_LITERAL_STRING("devicechange"), aCallback); } MediaManager::Get()->AddDeviceChangeCallback(this); } nsresult MediaDevices::AddEventListener(const nsAString& aType, nsIDOMEventListener* aListener, bool aUseCapture, bool aWantsUntrusted, uint8_t optional_argc) { MediaManager::Get()->AddDeviceChangeCallback(this); return mozilla::DOMEventTargetHelper::AddEventListener(aType, aListener, aUseCapture, aWantsUntrusted, optional_argc); } void MediaDevices::AddEventListener(const nsAString& aType, dom::EventListener* aListener, const dom::AddEventListenerOptionsOrBoolean& aOptions, const dom::Nullable& aWantsUntrusted, ErrorResult& aRv) { MediaManager::Get()->AddDeviceChangeCallback(this); return mozilla::DOMEventTargetHelper::AddEventListener(aType, aListener, aOptions, aWantsUntrusted, aRv); } JSObject* MediaDevices::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return MediaDevicesBinding::Wrap(aCx, this, aGivenProto); } } // namespace dom } // namespace mozilla