diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/systemservices/CamerasParent.cpp | 1220 |
1 files changed, 1220 insertions, 0 deletions
diff --git a/dom/media/systemservices/CamerasParent.cpp b/dom/media/systemservices/CamerasParent.cpp new file mode 100644 index 0000000000..1c63f0cee1 --- /dev/null +++ b/dom/media/systemservices/CamerasParent.cpp @@ -0,0 +1,1220 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=cpp : */ +/* 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 "CamerasParent.h" + +#include <atomic> +#include "MediaEngineSource.h" +#include "PerformanceRecorder.h" +#include "VideoFrameUtils.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/Assertions.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/Unused.h" +#include "mozilla/Services.h" +#include "mozilla/Logging.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/media/MediaUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_permissions.h" +#include "nsIPermissionManager.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" + +#include "api/video/video_frame_buffer.h" +#include "common_video/libyuv/include/webrtc_libyuv.h" + +#if defined(_WIN32) +# include <process.h> +# define getpid() _getpid() +#endif + +#undef LOG +#undef LOG_VERBOSE +#undef LOG_ENABLED +mozilla::LazyLogModule gCamerasParentLog("CamerasParent"); +#define LOG(...) \ + MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#define LOG_FUNCTION() \ + MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Debug, \ + ("CamerasParent(%p)::%s", this, __func__)) +#define LOG_VERBOSE(...) \ + MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Verbose, (__VA_ARGS__)) +#define LOG_ENABLED() MOZ_LOG_TEST(gCamerasParentLog, mozilla::LogLevel::Debug) + +namespace mozilla { +using media::ShutdownBlockingTicket; +namespace camera { + +std::map<uint32_t, const char*> sDeviceUniqueIDs; +std::map<uint32_t, webrtc::VideoCaptureCapability> sAllRequestedCapabilities; + +uint32_t ResolutionFeasibilityDistance(int32_t candidate, int32_t requested) { + // The purpose of this function is to find a smallest resolution + // which is larger than all requested capabilities. + // Then we can use down-scaling to fulfill each request. + + MOZ_DIAGNOSTIC_ASSERT(candidate >= 0, "Candidate unexpectedly negative"); + MOZ_DIAGNOSTIC_ASSERT(requested >= 0, "Requested unexpectedly negative"); + + if (candidate == 0) { + // Treat width|height capability of 0 as "can do any". + // This allows for orthogonal capabilities that are not in discrete steps. + return 0; + } + + uint32_t distance = + std::abs(candidate - requested) * 1000 / std::max(candidate, requested); + if (candidate >= requested) { + // This is a good case, the candidate covers the requested resolution. + return distance; + } + + // This is a bad case, the candidate is lower than the requested resolution. + // This is penalized with an added weight of 10000. + return 10000 + distance; +} + +uint32_t FeasibilityDistance(int32_t candidate, int32_t requested) { + MOZ_DIAGNOSTIC_ASSERT(candidate >= 0, "Candidate unexpectedly negative"); + MOZ_DIAGNOSTIC_ASSERT(requested >= 0, "Requested unexpectedly negative"); + + if (candidate == 0) { + // Treat maxFPS capability of 0 as "can do any". + // This allows for orthogonal capabilities that are not in discrete steps. + return 0; + } + + return std::abs(candidate - requested) * 1000 / + std::max(candidate, requested); +} + +class CamerasParent::VideoEngineArray + : public media::Refcountable<nsTArray<RefPtr<VideoEngine>>> {}; + +// Singleton video engines. The sEngines RefPtr is IPC background thread only +// and outlives the CamerasParent instances. The array elements are video +// capture thread only. +using VideoEngineArray = CamerasParent::VideoEngineArray; +static StaticRefPtr<VideoEngineArray> sEngines; +// Number of CamerasParents instances in the current process for which +// mVideoCaptureThread has been set. IPC background thread only. +static int32_t sNumCamerasParents = 0; +// Video processing thread - where webrtc.org capturer code runs. Outlives the +// CamerasParent instances. IPC background thread only. +static StaticRefPtr<nsIThread> sVideoCaptureThread; + +static already_AddRefed<nsISerialEventTarget> +MakeAndAddRefVideoCaptureThreadAndSingletons() { + ipc::AssertIsOnBackgroundThread(); + + MOZ_ASSERT_IF(sVideoCaptureThread, sNumCamerasParents > 0); + MOZ_ASSERT_IF(!sVideoCaptureThread, sNumCamerasParents == 0); + + if (!sVideoCaptureThread) { + LOG("Spinning up WebRTC Cameras Thread"); + nsIThreadManager::ThreadCreationOptions options; +#ifdef XP_WIN + // Windows desktop capture needs a UI thread + options.isUiThread = true; +#endif + nsCOMPtr<nsIThread> videoCaptureThread; + if (NS_FAILED(NS_NewNamedThread("VideoCapture", + getter_AddRefs(videoCaptureThread), nullptr, + options))) { + return nullptr; + } + sVideoCaptureThread = videoCaptureThread.forget(); + + sEngines = MakeRefPtr<VideoEngineArray>(); + sEngines->AppendElements(CaptureEngine::MaxEngine); + } + + ++sNumCamerasParents; + return do_AddRef(sVideoCaptureThread); +} + +static void ReleaseVideoCaptureThreadAndSingletons() { + ipc::AssertIsOnBackgroundThread(); + + if (--sNumCamerasParents > 0) { + // Other CamerasParent instances are using the singleton classes. + return; + } + + MOZ_ASSERT(sNumCamerasParents == 0, "Double release!"); + + // No other CamerasParent instances alive. Clean up. + LOG("Shutting down VideoEngines and the VideoCapture thread"); + MOZ_ALWAYS_SUCCEEDS(sVideoCaptureThread->Dispatch( + NS_NewRunnableFunction(__func__, [engines = RefPtr(sEngines.forget())] { + for (RefPtr<VideoEngine>& engine : *engines) { + if (engine) { + VideoEngine::Delete(engine); + engine = nullptr; + } + } + }))); + + MOZ_ALWAYS_SUCCEEDS(RefPtr(sVideoCaptureThread.forget())->AsyncShutdown()); +} + +// 3 threads are involved in this code: +// - the main thread for some setups, and occassionally for video capture setup +// calls that don't work correctly elsewhere. +// - the IPC thread on which PBackground is running and which receives and +// sends messages +// - a thread which will execute the actual (possibly slow) camera access +// called "VideoCapture". On Windows this is a thread with an event loop +// suitable for UI access. + +void CamerasParent::OnDeviceChange() { + LOG_FUNCTION(); + + mPBackgroundEventTarget->Dispatch( + NS_NewRunnableFunction(__func__, [this, self = RefPtr(this)]() { + if (IsShuttingDown()) { + LOG("OnDeviceChanged failure: parent shutting down."); + return; + } + Unused << SendDeviceChange(); + })); +}; + +class DeliverFrameRunnable : public mozilla::Runnable { + public: + DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine, + uint32_t aStreamId, const TrackingId& aTrackingId, + const webrtc::VideoFrame& aFrame, + const VideoFrameProperties& aProperties) + : Runnable("camera::DeliverFrameRunnable"), + mParent(aParent), + mCapEngine(aEngine), + mStreamId(aStreamId), + mTrackingId(aTrackingId), + mProperties(aProperties), + mResult(0) { + // No ShmemBuffer (of the right size) was available, so make an + // extra buffer here. We have no idea when we are going to run and + // it will be potentially long after the webrtc frame callback has + // returned, so the copy needs to be no later than here. + // We will need to copy this back into a Shmem later on so we prefer + // using ShmemBuffers to avoid the extra copy. + PerformanceRecorder<CopyVideoStage> rec( + "CamerasParent::VideoFrameToAltBuffer"_ns, aTrackingId, aFrame.width(), + aFrame.height()); + mAlternateBuffer.reset(new unsigned char[aProperties.bufferSize()]); + VideoFrameUtils::CopyVideoFrameBuffers(mAlternateBuffer.get(), + aProperties.bufferSize(), aFrame); + rec.Record(); + } + + DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine, + uint32_t aStreamId, const TrackingId& aTrackingId, + ShmemBuffer aBuffer, VideoFrameProperties& aProperties) + : Runnable("camera::DeliverFrameRunnable"), + mParent(aParent), + mCapEngine(aEngine), + mStreamId(aStreamId), + mTrackingId(aTrackingId), + mBuffer(std::move(aBuffer)), + mProperties(aProperties), + mResult(0){}; + + NS_IMETHOD Run() override { + // runs on BackgroundEventTarget + MOZ_ASSERT(GetCurrentSerialEventTarget() == + mParent->mPBackgroundEventTarget); + if (mParent->IsShuttingDown()) { + // Communication channel is being torn down + mResult = 0; + return NS_OK; + } + if (!mParent->DeliverFrameOverIPC(mCapEngine, mStreamId, mTrackingId, + std::move(mBuffer), + mAlternateBuffer.get(), mProperties)) { + mResult = -1; + } else { + mResult = 0; + } + return NS_OK; + } + + int GetResult() { return mResult; } + + private: + const RefPtr<CamerasParent> mParent; + const CaptureEngine mCapEngine; + const uint32_t mStreamId; + const TrackingId mTrackingId; + ShmemBuffer mBuffer; + UniquePtr<unsigned char[]> mAlternateBuffer; + const VideoFrameProperties mProperties; + int mResult; +}; + +int CamerasParent::DeliverFrameOverIPC(CaptureEngine aCapEngine, + uint32_t aStreamId, + const TrackingId& aTrackingId, + ShmemBuffer aBuffer, + unsigned char* aAltBuffer, + const VideoFrameProperties& aProps) { + // No ShmemBuffers were available, so construct one now of the right size + // and copy into it. That is an extra copy, but we expect this to be + // the exceptional case, because we just assured the next call *will* have a + // buffer of the right size. + if (aAltBuffer != nullptr) { + // Get a shared memory buffer from the pool, at least size big + ShmemBuffer shMemBuff = mShmemPool.Get(this, aProps.bufferSize()); + + if (!shMemBuff.Valid()) { + LOG("No usable Video shmem in DeliverFrame (out of buffers?)"); + // We can skip this frame if we run out of buffers, it's not a real error. + return 0; + } + + PerformanceRecorder<CopyVideoStage> rec( + "CamerasParent::AltBufferToShmem"_ns, aTrackingId, aProps.width(), + aProps.height()); + // get() and Size() check for proper alignment of the segment + memcpy(shMemBuff.GetBytes(), aAltBuffer, aProps.bufferSize()); + rec.Record(); + + if (!SendDeliverFrame(aCapEngine, aStreamId, std::move(shMemBuff.Get()), + aProps)) { + return -1; + } + } else { + MOZ_ASSERT(aBuffer.Valid()); + // ShmemBuffer was available, we're all good. A single copy happened + // in the original webrtc callback. + if (!SendDeliverFrame(aCapEngine, aStreamId, std::move(aBuffer.Get()), + aProps)) { + return -1; + } + } + + return 0; +} + +ShmemBuffer CamerasParent::GetBuffer(size_t aSize) { + return mShmemPool.GetIfAvailable(aSize); +} + +void CallbackHelper::OnFrame(const webrtc::VideoFrame& aVideoFrame) { + LOG_VERBOSE("CamerasParent(%p)::%s", mParent, __func__); + if (profiler_thread_is_being_profiled_for_markers()) { + PROFILER_MARKER_UNTYPED( + nsPrintfCString("CaptureVideoFrame %dx%d %s %s", aVideoFrame.width(), + aVideoFrame.height(), + webrtc::VideoFrameBufferTypeToString( + aVideoFrame.video_frame_buffer()->type()), + mTrackingId.ToString().get()), + MEDIA_RT); + } + RefPtr<DeliverFrameRunnable> runnable = nullptr; + // Get frame properties + camera::VideoFrameProperties properties; + VideoFrameUtils::InitFrameBufferProperties(aVideoFrame, properties); + // Get a shared memory buffer to copy the frame data into + ShmemBuffer shMemBuffer = mParent->GetBuffer(properties.bufferSize()); + if (!shMemBuffer.Valid()) { + // Either we ran out of buffers or they're not the right size yet + LOG("Correctly sized Video shmem not available in DeliverFrame"); + // We will do the copy into a(n extra) temporary buffer inside + // the DeliverFrameRunnable constructor. + } else { + // Shared memory buffers of the right size are available, do the copy here. + PerformanceRecorder<CopyVideoStage> rec( + "CamerasParent::VideoFrameToShmem"_ns, mTrackingId, aVideoFrame.width(), + aVideoFrame.height()); + VideoFrameUtils::CopyVideoFrameBuffers( + shMemBuffer.GetBytes(), properties.bufferSize(), aVideoFrame); + rec.Record(); + runnable = + new DeliverFrameRunnable(mParent, mCapEngine, mStreamId, mTrackingId, + std::move(shMemBuffer), properties); + } + if (!runnable) { + runnable = new DeliverFrameRunnable(mParent, mCapEngine, mStreamId, + mTrackingId, aVideoFrame, properties); + } + MOZ_ASSERT(mParent); + nsIEventTarget* target = mParent->GetBackgroundEventTarget(); + MOZ_ASSERT(target != nullptr); + target->Dispatch(runnable, NS_DISPATCH_NORMAL); +} + +ipc::IPCResult CamerasParent::RecvReleaseFrame(ipc::Shmem&& aShmem) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + + mShmemPool.Put(ShmemBuffer(aShmem)); + return IPC_OK(); +} + +void CamerasParent::CloseEngines() { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + LOG_FUNCTION(); + + // Stop the callers + while (!mCallbacks.IsEmpty()) { + auto capEngine = mCallbacks[0]->mCapEngine; + auto streamNum = mCallbacks[0]->mStreamId; + LOG("Forcing shutdown of engine %d, capturer %d", capEngine, streamNum); + StopCapture(capEngine, streamNum); + Unused << ReleaseCapture(capEngine, streamNum); + } + + if (VideoEngine* engine = mEngines->ElementAt(CameraEngine); engine) { + auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo(); + MOZ_ASSERT(device_info); + if (device_info) { + device_info->DeRegisterVideoInputFeedBack(this); + } + } +} + +VideoEngine* CamerasParent::EnsureInitialized(int aEngine) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + LOG_VERBOSE("CamerasParent(%p)::%s", this, __func__); + CaptureEngine capEngine = static_cast<CaptureEngine>(aEngine); + + if (VideoEngine* engine = mEngines->ElementAt(capEngine); engine) { + return engine; + } + + CaptureDeviceType captureDeviceType = CaptureDeviceType::Camera; + switch (capEngine) { + case ScreenEngine: + captureDeviceType = CaptureDeviceType::Screen; + break; + case BrowserEngine: + captureDeviceType = CaptureDeviceType::Browser; + break; + case WinEngine: + captureDeviceType = CaptureDeviceType::Window; + break; + case CameraEngine: + captureDeviceType = CaptureDeviceType::Camera; + break; + default: + LOG("Invalid webrtc Video engine"); + return nullptr; + } + + RefPtr<VideoEngine> engine = VideoEngine::Create(captureDeviceType); + if (!engine) { + LOG("VideoEngine::Create failed"); + return nullptr; + } + + if (capEngine == CameraEngine) { + auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo(); + MOZ_ASSERT(device_info); + if (device_info) { + device_info->RegisterVideoInputFeedBack(this); + } + } + + return mEngines->ElementAt(capEngine) = std::move(engine); +} + +// Dispatch the runnable to do the camera operation on the +// specific Cameras thread, preventing us from blocking, and +// chain a runnable to send back the result on the IPC thread. +// It would be nice to get rid of the code duplication here, +// perhaps via Promises. +ipc::IPCResult CamerasParent::RecvNumberOfCaptureDevices( + const CaptureEngine& aCapEngine) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + LOG("CaptureEngine=%d", aCapEngine); + + using Promise = MozPromise<int, bool, true>; + InvokeAsync( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine] { + int num = -1; + if (auto* engine = EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + num = static_cast<int>(devInfo->NumberOfDevices()); + } + } + return Promise::CreateAndResolve( + num, "CamerasParent::RecvNumberOfCaptureDevices"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + int nrDevices = aValue.ResolveValue(); + + if (mDestroyed) { + LOG("RecvNumberOfCaptureDevices failure: child not alive"); + return; + } + + if (nrDevices < 0) { + LOG("RecvNumberOfCaptureDevices couldn't find devices"); + Unused << SendReplyFailure(); + return; + } + + LOG("RecvNumberOfCaptureDevices: %d", nrDevices); + Unused << SendReplyNumberOfCaptureDevices(nrDevices); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvEnsureInitialized( + const CaptureEngine& aCapEngine) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + + using Promise = MozPromise<bool, bool, true>; + InvokeAsync(mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine] { + return Promise::CreateAndResolve( + EnsureInitialized(aCapEngine), + "CamerasParent::RecvEnsureInitialized"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + bool result = aValue.ResolveValue(); + + if (mDestroyed) { + LOG("RecvEnsureInitialized: child not alive"); + return; + } + + if (!result) { + LOG("RecvEnsureInitialized failed"); + Unused << SendReplyFailure(); + return; + } + + LOG("RecvEnsureInitialized succeeded"); + Unused << SendReplySuccess(); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvNumberOfCapabilities( + const CaptureEngine& aCapEngine, const nsACString& aUniqueId) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + LOG("Getting caps for %s", PromiseFlatCString(aUniqueId).get()); + + using Promise = MozPromise<int, bool, true>; + InvokeAsync( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), id = nsCString(aUniqueId), aCapEngine]() { + int num = -1; + if (auto* engine = EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + num = devInfo->NumberOfCapabilities(id.get()); + } + } + return Promise::CreateAndResolve( + num, "CamerasParent::RecvNumberOfCapabilities"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + int aNrCapabilities = aValue.ResolveValue(); + + if (mDestroyed) { + LOG("RecvNumberOfCapabilities: child not alive"); + return; + } + + if (aNrCapabilities < 0) { + LOG("RecvNumberOfCapabilities couldn't find capabilities"); + Unused << SendReplyFailure(); + return; + } + + LOG("RecvNumberOfCapabilities: %d", aNrCapabilities); + Unused << SendReplyNumberOfCapabilities(aNrCapabilities); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvGetCaptureCapability( + const CaptureEngine& aCapEngine, const nsACString& aUniqueId, + const int& aIndex) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + LOG("RecvGetCaptureCapability: %s %d", PromiseFlatCString(aUniqueId).get(), + aIndex); + + using Promise = MozPromise<webrtc::VideoCaptureCapability, int, true>; + InvokeAsync( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), id = nsCString(aUniqueId), aCapEngine, + aIndex] { + webrtc::VideoCaptureCapability webrtcCaps; + int error = -1; + if (auto* engine = EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + error = devInfo->GetCapability(id.get(), aIndex, webrtcCaps); + } + } + + if (!error && aCapEngine == CameraEngine) { + auto iter = mAllCandidateCapabilities.find(id); + if (iter == mAllCandidateCapabilities.end()) { + std::map<uint32_t, webrtc::VideoCaptureCapability> + candidateCapabilities; + candidateCapabilities.emplace(aIndex, webrtcCaps); + mAllCandidateCapabilities.emplace(id, candidateCapabilities); + } else { + (iter->second).emplace(aIndex, webrtcCaps); + } + } + if (error) { + return Promise::CreateAndReject( + error, "CamerasParent::RecvGetCaptureCapability"); + } + return Promise::CreateAndResolve( + webrtcCaps, "CamerasParent::RecvGetCaptureCapability"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + if (mDestroyed) { + LOG("RecvGetCaptureCapability: child not alive"); + return; + } + + if (aValue.IsReject()) { + LOG("RecvGetCaptureCapability: reply failure"); + Unused << SendReplyFailure(); + return; + } + + auto webrtcCaps = aValue.ResolveValue(); + VideoCaptureCapability capCap( + webrtcCaps.width, webrtcCaps.height, webrtcCaps.maxFPS, + static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced); + LOG("Capability: %u %u %u %d %d", webrtcCaps.width, + webrtcCaps.height, webrtcCaps.maxFPS, + static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced); + Unused << SendReplyGetCaptureCapability(capCap); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvGetCaptureDevice( + const CaptureEngine& aCapEngine, const int& aDeviceIndex) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + + using Data = std::tuple<nsCString, nsCString, pid_t, int>; + using Promise = MozPromise<Data, bool, true>; + InvokeAsync( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, aDeviceIndex] { + char deviceName[MediaEngineSource::kMaxDeviceNameLength]; + char deviceUniqueId[MediaEngineSource::kMaxUniqueIdLength]; + nsCString name; + nsCString uniqueId; + pid_t devicePid = 0; + int error = -1; + if (auto* engine = EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + error = devInfo->GetDeviceName( + aDeviceIndex, deviceName, sizeof(deviceName), deviceUniqueId, + sizeof(deviceUniqueId), nullptr, 0, &devicePid); + } + } + if (error == 0) { + name.Assign(deviceName); + uniqueId.Assign(deviceUniqueId); + } + return Promise::CreateAndResolve( + std::make_tuple(std::move(name), std::move(uniqueId), devicePid, + error), + "CamerasParent::RecvGetCaptureDevice"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + const auto& [name, uniqueId, devicePid, error] = + aValue.ResolveValue(); + if (mDestroyed) { + return; + } + if (error != 0) { + LOG("GetCaptureDevice failed: %d", error); + Unused << SendReplyFailure(); + return; + } + bool scary = (devicePid == getpid()); + + LOG("Returning %s name %s id (pid = %d)%s", name.get(), + uniqueId.get(), devicePid, (scary ? " (scary)" : "")); + Unused << SendReplyGetCaptureDevice(name, uniqueId, scary); + }); + return IPC_OK(); +} + +// Find out whether the given window with id has permission to use the +// camera. If the permission is not persistent, we'll make it a one-shot by +// removing the (session) permission. +static bool HasCameraPermission(const uint64_t& aWindowId) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<dom::WindowGlobalParent> window = + dom::WindowGlobalParent::GetByInnerWindowId(aWindowId); + if (!window) { + // Could not find window by id + return false; + } + + // If we delegate permission from first party, we should use the top level + // window + if (StaticPrefs::permissions_delegation_enabled()) { + RefPtr<dom::BrowsingContext> topBC = window->BrowsingContext()->Top(); + window = topBC->Canonical()->GetCurrentWindowGlobal(); + } + + // Return false if the window is not the currently-active window for its + // BrowsingContext. + if (!window || !window->IsCurrentGlobal()) { + return false; + } + + nsIPrincipal* principal = window->DocumentPrincipal(); + if (principal->GetIsNullPrincipal()) { + return false; + } + + if (principal->IsSystemPrincipal()) { + return true; + } + + MOZ_ASSERT(principal->GetIsContentPrincipal()); + + nsresult rv; + // Name used with nsIPermissionManager + static const nsLiteralCString cameraPermission = "MediaManagerVideo"_ns; + nsCOMPtr<nsIPermissionManager> mgr = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t video = nsIPermissionManager::UNKNOWN_ACTION; + rv = mgr->TestExactPermissionFromPrincipal(principal, cameraPermission, + &video); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + bool allowed = (video == nsIPermissionManager::ALLOW_ACTION); + + // Session permissions are removed after one use. + if (allowed) { + mgr->RemoveFromPrincipal(principal, cameraPermission); + } + + return allowed; +} + +ipc::IPCResult CamerasParent::RecvAllocateCapture( + const CaptureEngine& aCapEngine, const nsACString& aUniqueIdUTF8, + const uint64_t& aWindowID) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG("CamerasParent(%p)::%s: Verifying permissions", this, __func__); + + using Promise1 = MozPromise<bool, bool, true>; + using Data = std::tuple<int, int>; + using Promise2 = MozPromise<Data, bool, true>; + InvokeAsync(GetMainThreadSerialEventTarget(), __func__, + [aWindowID] { + // Verify whether the claimed origin has received permission + // to use the camera, either persistently or this session (one + // shot). + bool allowed = HasCameraPermission(aWindowID); + if (!allowed) { + // Developer preference for turning off permission check. + if (Preferences::GetBool( + "media.navigator.permission.disabled", false)) { + allowed = true; + LOG("No permission but checks are disabled"); + } else { + LOG("No camera permission for this origin"); + } + } + return Promise1::CreateAndResolve( + allowed, "CamerasParent::RecvAllocateCapture"); + }) + ->Then(mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, + unique_id = nsCString(aUniqueIdUTF8)]( + Promise1::ResolveOrRejectValue&& aValue) { + bool allowed = aValue.ResolveValue(); + int captureId = -1; + int error = -1; + if (allowed && EnsureInitialized(aCapEngine)) { + VideoEngine* engine = mEngines->ElementAt(aCapEngine); + captureId = engine->CreateVideoCapture(unique_id.get()); + engine->WithEntry(captureId, + [&error](VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + error = 0; + } + }); + } + return Promise2::CreateAndResolve( + std::make_tuple(captureId, error), + "CamerasParent::RecvAllocateCapture"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise2::ResolveOrRejectValue&& aValue) { + const auto [captureId, error] = aValue.ResolveValue(); + if (mDestroyed) { + LOG("RecvAllocateCapture: child not alive"); + return; + } + + if (error != 0) { + Unused << SendReplyFailure(); + LOG("RecvAllocateCapture: WithEntry error"); + return; + } + + LOG("Allocated device nr %d", captureId); + Unused << SendReplyAllocateCapture(captureId); + }); + return IPC_OK(); +} + +int CamerasParent::ReleaseCapture(const CaptureEngine& aCapEngine, + int aCaptureId) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + int error = -1; + if (auto* engine = EnsureInitialized(aCapEngine)) { + error = engine->ReleaseVideoCapture(aCaptureId); + } + return error; +} + +ipc::IPCResult CamerasParent::RecvReleaseCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + LOG("RecvReleaseCamera device nr %d", aCaptureId); + + using Promise = MozPromise<int, bool, true>; + InvokeAsync(mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, aCaptureId] { + return Promise::CreateAndResolve( + ReleaseCapture(aCapEngine, aCaptureId), + "CamerasParent::RecvReleaseCapture"); + }) + ->Then(mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this), + aCaptureId](Promise::ResolveOrRejectValue&& aValue) { + int error = aValue.ResolveValue(); + + if (mDestroyed) { + LOG("RecvReleaseCapture: child not alive"); + return; + } + + if (error != 0) { + Unused << SendReplyFailure(); + LOG("RecvReleaseCapture: Failed to free device nr %d", + aCaptureId); + return; + } + + Unused << SendReplySuccess(); + LOG("Freed device nr %d", aCaptureId); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvStartCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId, + const VideoCaptureCapability& aIpcCaps) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + + using Promise = MozPromise<int, bool, true>; + InvokeAsync( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, aCaptureId, aIpcCaps] { + LOG_FUNCTION(); + CallbackHelper** cbh; + int error = -1; + + if (!EnsureInitialized(aCapEngine)) { + return Promise::CreateAndResolve(error, + "CamerasParent::RecvStartCapture"); + } + + cbh = mCallbacks.AppendElement(new CallbackHelper( + static_cast<CaptureEngine>(aCapEngine), aCaptureId, this)); + + mEngines->ElementAt(aCapEngine) + ->WithEntry(aCaptureId, [&](VideoEngine::CaptureEntry& cap) { + webrtc::VideoCaptureCapability capability; + capability.width = aIpcCaps.width(); + capability.height = aIpcCaps.height(); + capability.maxFPS = aIpcCaps.maxFPS(); + capability.videoType = + static_cast<webrtc::VideoType>(aIpcCaps.videoType()); + capability.interlaced = aIpcCaps.interlaced(); + +#ifndef FUZZING_SNAPSHOT + MOZ_DIAGNOSTIC_ASSERT(sDeviceUniqueIDs.find(aCaptureId) == + sDeviceUniqueIDs.end()); +#endif + sDeviceUniqueIDs.emplace(aCaptureId, + cap.VideoCapture()->CurrentDeviceName()); + +#ifndef FUZZING_SNAPSHOT + MOZ_DIAGNOSTIC_ASSERT( + sAllRequestedCapabilities.find(aCaptureId) == + sAllRequestedCapabilities.end()); +#endif + sAllRequestedCapabilities.emplace(aCaptureId, capability); + + if (aCapEngine == CameraEngine) { + for (const auto& it : sDeviceUniqueIDs) { + if (strcmp(it.second, + cap.VideoCapture()->CurrentDeviceName()) == 0) { + capability.width = + std::max(capability.width, + sAllRequestedCapabilities[it.first].width); + capability.height = + std::max(capability.height, + sAllRequestedCapabilities[it.first].height); + capability.maxFPS = + std::max(capability.maxFPS, + sAllRequestedCapabilities[it.first].maxFPS); + } + } + + auto candidateCapabilities = mAllCandidateCapabilities.find( + nsCString(cap.VideoCapture()->CurrentDeviceName())); + if ((candidateCapabilities != + mAllCandidateCapabilities.end()) && + (!candidateCapabilities->second.empty())) { + int32_t minIdx = -1; + uint64_t minDistance = UINT64_MAX; + + for (auto& candidateCapability : + candidateCapabilities->second) { + if (candidateCapability.second.videoType != + capability.videoType) { + continue; + } + // The first priority is finding a suitable resolution. + // So here we raise the weight of width and height + uint64_t distance = uint64_t(ResolutionFeasibilityDistance( + candidateCapability.second.width, + capability.width)) + + uint64_t(ResolutionFeasibilityDistance( + candidateCapability.second.height, + capability.height)) + + uint64_t(FeasibilityDistance( + candidateCapability.second.maxFPS, + capability.maxFPS)); + if (distance < minDistance) { + minIdx = static_cast<int32_t>(candidateCapability.first); + minDistance = distance; + } + } + MOZ_ASSERT(minIdx != -1); + capability = candidateCapabilities->second[minIdx]; + } + } else if (aCapEngine == ScreenEngine || + aCapEngine == BrowserEngine || + aCapEngine == WinEngine) { + for (const auto& it : sDeviceUniqueIDs) { + if (strcmp(it.second, + cap.VideoCapture()->CurrentDeviceName()) == 0) { + capability.maxFPS = + std::max(capability.maxFPS, + sAllRequestedCapabilities[it.first].maxFPS); + } + } + } + + cap.VideoCapture()->SetTrackingId( + (*cbh)->mTrackingId.mUniqueInProcId); + error = cap.VideoCapture()->StartCapture(capability); + + if (!error) { + cap.VideoCapture()->RegisterCaptureDataCallback( + static_cast<rtc::VideoSinkInterface<webrtc::VideoFrame>*>( + *cbh)); + } else { + sDeviceUniqueIDs.erase(aCaptureId); + sAllRequestedCapabilities.erase(aCaptureId); + } + }); + + return Promise::CreateAndResolve(error, + "CamerasParent::RecvStartCapture"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + int error = aValue.ResolveValue(); + + if (mDestroyed) { + LOG("RecvStartCapture failure: child is not alive"); + return; + } + + if (error != 0) { + LOG("RecvStartCapture failure: StartCapture failed"); + Unused << SendReplyFailure(); + return; + } + + Unused << SendReplySuccess(); + }); + return IPC_OK(); +} + +ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource( + const CaptureEngine& aCapEngine, const int& aCaptureId) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + + using Promise = MozPromise<bool, bool, true>; + InvokeAsync(mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, aCaptureId] { + bool result = false; + if (auto* engine = EnsureInitialized(aCapEngine)) { + engine->WithEntry( + aCaptureId, [&](VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + result = cap.VideoCapture()->FocusOnSelectedSource(); + } + }); + } + return Promise::CreateAndResolve( + result, "CamerasParent::RecvFocusOnSelectedSource"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + bool result = aValue.ResolveValue(); + if (mDestroyed) { + LOG("RecvFocusOnSelectedSource failure: child is not alive"); + return; + } + + if (!result) { + Unused << SendReplyFailure(); + LOG("RecvFocusOnSelectedSource failure."); + return; + } + + Unused << SendReplySuccess(); + }); + return IPC_OK(); +} + +void CamerasParent::StopCapture(const CaptureEngine& aCapEngine, + int aCaptureId) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + if (auto* engine = EnsureInitialized(aCapEngine)) { + // we're removing elements, iterate backwards + for (size_t i = mCallbacks.Length(); i > 0; i--) { + if (mCallbacks[i - 1]->mCapEngine == aCapEngine && + mCallbacks[i - 1]->mStreamId == (uint32_t)aCaptureId) { + CallbackHelper* cbh = mCallbacks[i - 1]; + engine->WithEntry(aCaptureId, [cbh, &aCaptureId]( + VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + cap.VideoCapture()->DeRegisterCaptureDataCallback( + static_cast<rtc::VideoSinkInterface<webrtc::VideoFrame>*>(cbh)); + cap.VideoCapture()->StopCaptureIfAllClientsClose(); + + sDeviceUniqueIDs.erase(aCaptureId); + sAllRequestedCapabilities.erase(aCaptureId); + } + }); + + delete mCallbacks[i - 1]; + mCallbacks.RemoveElementAt(i - 1); + break; + } + } + } +} + +ipc::IPCResult CamerasParent::RecvStopCapture(const CaptureEngine& aCapEngine, + const int& aCaptureId) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + MOZ_ASSERT(!mDestroyed); + + LOG_FUNCTION(); + + nsresult rv = mVideoCaptureThread->Dispatch(NS_NewRunnableFunction( + __func__, [this, self = RefPtr(this), aCapEngine, aCaptureId] { + StopCapture(aCapEngine, aCaptureId); + })); + + if (mDestroyed) { + if (NS_FAILED(rv)) { + return IPC_FAIL_NO_REASON(this); + } + } else { + if (NS_SUCCEEDED(rv)) { + if (!SendReplySuccess()) { + return IPC_FAIL_NO_REASON(this); + } + } else { + if (!SendReplyFailure()) { + return IPC_FAIL_NO_REASON(this); + } + } + } + return IPC_OK(); +} + +void CamerasParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + LOG_FUNCTION(); + + // Release shared memory now, it's our last chance + mShmemPool.Cleanup(this); + // We don't want to receive callbacks or anything if we can't + // forward them anymore anyway. + mDestroyed = true; + // We don't need to listen for shutdown any longer. Disconnect the request. + // This breaks the reference cycle between CamerasParent and the shutdown + // promise's Then handler. + mShutdownRequest.DisconnectIfExists(); + + if (mVideoCaptureThread) { + // Shut down the WebRTC stack, on the video capture thread. + MOZ_ALWAYS_SUCCEEDS(mVideoCaptureThread->Dispatch( + NewRunnableMethod(__func__, this, &CamerasParent::CloseEngines))); + } +} + +void CamerasParent::OnShutdown() { + ipc::AssertIsOnBackgroundThread(); + LOG("CamerasParent(%p) ShutdownEvent", this); + mShutdownRequest.Complete(); + (void)Send__delete__(this); +} + +CamerasParent::CamerasParent() + : mShutdownBlocker(ShutdownBlockingTicket::Create( + u"CamerasParent"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), + __LINE__)), + mVideoCaptureThread(mShutdownBlocker + ? MakeAndAddRefVideoCaptureThreadAndSingletons() + : nullptr), + mEngines(sEngines), + mShmemPool(CaptureEngine::MaxEngine), + mPBackgroundEventTarget(GetCurrentSerialEventTarget()), + mDestroyed(false) { + MOZ_ASSERT(mPBackgroundEventTarget != nullptr, + "GetCurrentThreadEventTarget failed"); + LOG("CamerasParent: %p", this); + + // Don't dispatch from the constructor a runnable that may toggle the + // reference count, because the IPC thread does not get a reference until + // after the constructor returns. +} + +// RecvPCamerasConstructor() is used because IPC messages, for +// Send__delete__(), cannot be sent from AllocPCamerasParent(). +ipc::IPCResult CamerasParent::RecvPCamerasConstructor() { + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + + // AsyncShutdown barriers are available only for ShutdownPhases as late as + // XPCOMWillShutdown. The IPC background thread shuts down during + // XPCOMShutdownThreads, so actors may be created when AsyncShutdown barriers + // are no longer available. Should shutdown be past XPCOMWillShutdown we end + // up with a null mShutdownBlocker. + + if (!mShutdownBlocker) { + LOG("CamerasParent(%p) Got no ShutdownBlockingTicket. We are already in " + "shutdown. Deleting.", + this); + return Send__delete__(this) ? IPC_OK() : IPC_FAIL(this, "Failed to send"); + } + + if (!mVideoCaptureThread) { + return Send__delete__(this) ? IPC_OK() : IPC_FAIL(this, "Failed to send"); + } + + MOZ_ASSERT(mEngines); + + mShutdownBlocker->ShutdownPromise() + ->Then(mPBackgroundEventTarget, "CamerasParent OnShutdown", + [this, self = RefPtr(this)]( + const ShutdownPromise::ResolveOrRejectValue& aValue) { + MOZ_ASSERT(aValue.IsResolve(), + "ShutdownBlockingTicket must have been destroyed " + "without us disconnecting the shutdown request"); + OnShutdown(); + }) + ->Track(mShutdownRequest); + + return IPC_OK(); +} + +CamerasParent::~CamerasParent() { + ipc::AssertIsOnBackgroundThread(); + LOG_FUNCTION(); + + if (!mVideoCaptureThread) { + // No video engines or video capture thread to shutdown here. + return; + } + + MOZ_ASSERT(mShutdownBlocker, + "A ShutdownBlocker is a prerequisite for mVideoCaptureThread"); + + ReleaseVideoCaptureThreadAndSingletons(); +} + +already_AddRefed<CamerasParent> CamerasParent::Create() { + ipc::AssertIsOnBackgroundThread(); + return MakeAndAddRef<CamerasParent>(); +} + +} // namespace camera +} // namespace mozilla |