diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/systemservices/CamerasParent.cpp | 1222 |
1 files changed, 1222 insertions, 0 deletions
diff --git a/dom/media/systemservices/CamerasParent.cpp b/dom/media/systemservices/CamerasParent.cpp new file mode 100644 index 0000000000..91185f44e1 --- /dev/null +++ b/dom/media/systemservices/CamerasParent.cpp @@ -0,0 +1,1222 @@ +/* -*- 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 "MediaUtils.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/Preferences.h" +#include "mozilla/StaticPrefs_permissions.h" +#include "nsIPermissionManager.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_VERBOSE(...) \ + MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Verbose, (__VA_ARGS__)) +#define LOG_ENABLED() MOZ_LOG_TEST(gCamerasParentLog, mozilla::LogLevel::Debug) + +namespace mozilla { +using media::MustGetShutdownBarrier; +using media::NewRunnableFrom; +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); +} + +StaticRefPtr<VideoEngine> CamerasParent::sEngines[CaptureEngine::MaxEngine]; +int32_t CamerasParent::sNumOfOpenCamerasParentEngines = 0; +int32_t CamerasParent::sNumOfCamerasParents = 0; +base::Thread* CamerasParent::sVideoCaptureThread = nullptr; +Monitor* CamerasParent::sThreadMonitor = nullptr; +StaticMutex CamerasParent::sMutex; + +// 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. + +// InputObserver is owned by CamerasParent, and it has a ref to CamerasParent +void InputObserver::OnDeviceChange() { + LOG("%s", __PRETTY_FUNCTION__); + MOZ_ASSERT(mParent); + + RefPtr<InputObserver> self(this); + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self]() { + if (self->mParent->IsShuttingDown()) { + LOG("OnDeviceChanged failure: parent shutting down."); + return NS_ERROR_FAILURE; + } + Unused << self->mParent->SendDeviceChange(); + return NS_OK; + }); + + nsIEventTarget* target = mParent->GetBackgroundEventTarget(); + MOZ_ASSERT(target != nullptr); + target->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); +}; + +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; +}; + +NS_IMPL_ISUPPORTS(CamerasParent, nsIAsyncShutdownBlocker) + +nsresult CamerasParent::DispatchToVideoCaptureThread(RefPtr<Runnable> event) { + // Don't try to dispatch if we're already on the right thread. + // There's a potential deadlock because the sThreadMonitor is likely + // to be taken already. + MonitorAutoLock lock(*sThreadMonitor); + if (!sVideoCaptureThread) { + LOG("Can't dispatch to video capture thread: thread not present"); + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(sVideoCaptureThread->thread_id() != PlatformThread::CurrentId()); + + sVideoCaptureThread->message_loop()->PostTask(event.forget()); + return NS_OK; +} + +void CamerasParent::StopVideoCapture() { + LOG("%s", __PRETTY_FUNCTION__); + // Called when the actor is destroyed. + ipc::AssertIsOnBackgroundThread(); + // Shut down the WebRTC stack (on the capture thread) + RefPtr<CamerasParent> self(this); + DebugOnly<nsresult> rv = + DispatchToVideoCaptureThread(NewRunnableFrom([self]() { + MonitorAutoLock lock(*(self->sThreadMonitor)); + self->CloseEngines(); + // After closing the WebRTC stack, clean up the + // VideoCapture thread. + base::Thread* thread = nullptr; + if (sNumOfOpenCamerasParentEngines == 0 && self->sVideoCaptureThread) { + thread = self->sVideoCaptureThread; + self->sVideoCaptureThread = nullptr; + } + nsresult rv = NS_DispatchToMainThread(NewRunnableFrom([self, thread]() { + if (thread) { + thread->Stop(); + delete thread; + } + // May fail if already removed after RecvPCamerasConstructor(). + (void)MustGetShutdownBarrier()->RemoveBlocker(self); + return NS_OK; + })); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv), + "dispatch for video thread shutdown"); + return rv; + })); +#ifdef DEBUG + // It's ok for the dispatch to fail if the cleanup it has to do + // has been done already. + MOZ_ASSERT(NS_SUCCEEDED(rv) || !mWebRTCAlive); +#endif +} + +int CamerasParent::DeliverFrameOverIPC(CaptureEngine capEng, uint32_t aStreamId, + const TrackingId& aTrackingId, + ShmemBuffer buffer, + unsigned char* altbuffer, + 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 (altbuffer != 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(), altbuffer, aProps.bufferSize()); + rec.Record(); + + if (!SendDeliverFrame(capEng, aStreamId, std::move(shMemBuff.Get()), + aProps)) { + return -1; + } + } else { + MOZ_ASSERT(buffer.Valid()); + // ShmemBuffer was available, we're all good. A single copy happened + // in the original webrtc callback. + if (!SendDeliverFrame(capEng, aStreamId, std::move(buffer.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("%s", __PRETTY_FUNCTION__); + 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); +} + +mozilla::ipc::IPCResult CamerasParent::RecvReleaseFrame( + mozilla::ipc::Shmem&& s) { + mShmemPool.Put(ShmemBuffer(s)); + return IPC_OK(); +} + +bool CamerasParent::SetupEngine(CaptureEngine aCapEngine) { + LOG("%s", __PRETTY_FUNCTION__); + StaticRefPtr<VideoEngine>& engine = sEngines[aCapEngine]; + + if (!engine) { + CaptureDeviceType captureDeviceType = CaptureDeviceType::Camera; + switch (aCapEngine) { + 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 false; + } + + engine = VideoEngine::Create(captureDeviceType); + + if (!engine) { + LOG("VideoEngine::Create failed"); + return false; + } + } + + if (aCapEngine == CameraEngine && !mCameraObserver) { + mCameraObserver = new InputObserver(this); + auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo(); + MOZ_ASSERT(device_info); + if (device_info) { + device_info->RegisterVideoInputFeedBack(mCameraObserver); + } + } + + return true; +} + +void CamerasParent::CloseEngines() { + sThreadMonitor->AssertCurrentThreadOwns(); + LOG("%s", __PRETTY_FUNCTION__); + if (!mWebRTCAlive) { + return; + } + MOZ_ASSERT(sVideoCaptureThread->thread_id() == PlatformThread::CurrentId()); + + // Stop the callers + while (mCallbacks.Length()) { + 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); + } + + StaticRefPtr<VideoEngine>& engine = sEngines[CameraEngine]; + if (engine && mCameraObserver) { + auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo(); + MOZ_ASSERT(device_info); + if (device_info) { + device_info->DeRegisterVideoInputFeedBack(mCameraObserver); + } + mCameraObserver = nullptr; + } + + // CloseEngines() is protected by sThreadMonitor + sNumOfOpenCamerasParentEngines--; + if (sNumOfOpenCamerasParentEngines == 0) { + for (StaticRefPtr<VideoEngine>& engine : sEngines) { + if (engine) { + VideoEngine::Delete(engine); + engine = nullptr; + } + } + } + + mWebRTCAlive = false; +} + +VideoEngine* CamerasParent::EnsureInitialized(int aEngine) { + LOG_VERBOSE("%s", __PRETTY_FUNCTION__); + // We're shutting down, don't try to do new WebRTC ops. + if (!mWebRTCAlive) { + return nullptr; + } + CaptureEngine capEngine = static_cast<CaptureEngine>(aEngine); + if (!SetupEngine(capEngine)) { + LOG("CamerasParent failed to initialize engine"); + return nullptr; + } + + return sEngines[aEngine]; +} + +// 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. +mozilla::ipc::IPCResult CamerasParent::RecvNumberOfCaptureDevices( + const CaptureEngine& aCapEngine) { + LOG("%s", __PRETTY_FUNCTION__); + LOG("CaptureEngine=%d", aCapEngine); + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom([self, aCapEngine]() { + int num = -1; + if (auto engine = self->EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + num = devInfo->NumberOfDevices(); + } + } + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, num]() { + if (!self->mChildIsAlive) { + LOG("RecvNumberOfCaptureDevices failure: child not alive"); + return NS_ERROR_FAILURE; + } + + if (num < 0) { + LOG("RecvNumberOfCaptureDevices couldn't find devices"); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + } + + LOG("RecvNumberOfCaptureDevices: %d", num); + Unused << self->SendReplyNumberOfCaptureDevices(num); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvEnsureInitialized( + const CaptureEngine& aCapEngine) { + LOG("%s", __PRETTY_FUNCTION__); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom([self, aCapEngine]() { + bool result = self->EnsureInitialized(aCapEngine); + + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, result]() { + if (!self->mChildIsAlive) { + LOG("RecvEnsureInitialized: child not alive"); + return NS_ERROR_FAILURE; + } + + if (!result) { + LOG("RecvEnsureInitialized failed"); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + } + + LOG("RecvEnsureInitialized succeeded"); + Unused << self->SendReplySuccess(); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvNumberOfCapabilities( + const CaptureEngine& aCapEngine, const nsACString& unique_id) { + LOG("%s", __PRETTY_FUNCTION__); + LOG("Getting caps for %s", PromiseFlatCString(unique_id).get()); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = + NewRunnableFrom([self, unique_id = nsCString(unique_id), aCapEngine]() { + int num = -1; + if (auto engine = self->EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + num = devInfo->NumberOfCapabilities(unique_id.get()); + } + } + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, num]() { + if (!self->mChildIsAlive) { + LOG("RecvNumberOfCapabilities: child not alive"); + return NS_ERROR_FAILURE; + } + + if (num < 0) { + LOG("RecvNumberOfCapabilities couldn't find capabilities"); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + } + + LOG("RecvNumberOfCapabilities: %d", num); + Unused << self->SendReplyNumberOfCapabilities(num); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, + NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvGetCaptureCapability( + const CaptureEngine& aCapEngine, const nsACString& unique_id, + const int& num) { + LOG("%s", __PRETTY_FUNCTION__); + LOG("RecvGetCaptureCapability: %s %d", PromiseFlatCString(unique_id).get(), + num); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom( + [self, unique_id = nsCString(unique_id), aCapEngine, num]() { + webrtc::VideoCaptureCapability webrtcCaps; + int error = -1; + if (auto engine = self->EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + error = devInfo->GetCapability(unique_id.get(), num, webrtcCaps); + } + + if (!error && aCapEngine == CameraEngine) { + auto iter = self->mAllCandidateCapabilities.find(unique_id); + if (iter == self->mAllCandidateCapabilities.end()) { + std::map<uint32_t, webrtc::VideoCaptureCapability> + candidateCapabilities; + candidateCapabilities.emplace(num, webrtcCaps); + self->mAllCandidateCapabilities.emplace(nsCString(unique_id), + candidateCapabilities); + } else { + (iter->second).emplace(num, webrtcCaps); + } + } + } + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, webrtcCaps, + error]() { + if (!self->mChildIsAlive) { + LOG("RecvGetCaptureCapability: child not alive"); + return NS_ERROR_FAILURE; + } + 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); + if (error) { + LOG("RecvGetCaptureCapability: reply failure"); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + } + Unused << self->SendReplyGetCaptureCapability(capCap); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, + NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvGetCaptureDevice( + const CaptureEngine& aCapEngine, const int& aDeviceIndex) { + LOG("%s", __PRETTY_FUNCTION__); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom([self, aCapEngine, + aDeviceIndex]() { + char deviceName[MediaEngineSource::kMaxDeviceNameLength]; + char deviceUniqueId[MediaEngineSource::kMaxUniqueIdLength]; + nsCString name; + nsCString uniqueId; + pid_t devicePid = 0; + int error = -1; + if (auto engine = self->EnsureInitialized(aCapEngine)) { + if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) { + error = devInfo->GetDeviceName( + aDeviceIndex, deviceName, sizeof(deviceName), deviceUniqueId, + sizeof(deviceUniqueId), nullptr, 0, &devicePid); + } + } + if (!error) { + name.Assign(deviceName); + uniqueId.Assign(deviceUniqueId); + } + RefPtr<nsIRunnable> ipc_runnable = + NewRunnableFrom([self, error, name, uniqueId, devicePid]() { + if (!self->mChildIsAlive) { + return NS_ERROR_FAILURE; + } + if (error) { + LOG("GetCaptureDevice failed: %d", error); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + } + bool scary = (devicePid == getpid()); + + LOG("Returning %s name %s id (pid = %d)%s", name.get(), + uniqueId.get(), devicePid, (scary ? " (scary)" : "")); + Unused << self->SendReplyGetCaptureDevice(name, uniqueId, scary); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + 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; +} + +mozilla::ipc::IPCResult CamerasParent::RecvAllocateCapture( + const CaptureEngine& aCapEngine, const nsACString& unique_id, + const uint64_t& aWindowID) { + LOG("%s: Verifying permissions", __PRETTY_FUNCTION__); + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> mainthread_runnable = NewRunnableFrom( + [self, aCapEngine, unique_id = nsCString(unique_id), 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"); + } + } + // After retrieving the permission (or not) on the main thread, + // bounce to the WebRTC thread to allocate the device (or not), + // then bounce back to the IPC thread for the reply to content. + RefPtr<Runnable> webrtc_runnable = + NewRunnableFrom([self, allowed, aCapEngine, unique_id]() { + int captureId = -1; + int error = -1; + if (allowed && self->EnsureInitialized(aCapEngine)) { + StaticRefPtr<VideoEngine>& engine = self->sEngines[aCapEngine]; + captureId = engine->CreateVideoCapture(unique_id.get()); + engine->WithEntry(captureId, + [&error](VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + error = 0; + } + }); + } + RefPtr<nsIRunnable> ipc_runnable = + NewRunnableFrom([self, captureId, error]() { + if (!self->mChildIsAlive) { + LOG("RecvAllocateCapture: child not alive"); + return NS_ERROR_FAILURE; + } + + if (error) { + Unused << self->SendReplyFailure(); + LOG("RecvAllocateCapture: WithEntry error"); + return NS_ERROR_FAILURE; + } + + LOG("Allocated device nr %d", captureId); + Unused << self->SendReplyAllocateCapture(captureId); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, + NS_DISPATCH_NORMAL); + return NS_OK; + }); + self->DispatchToVideoCaptureThread(webrtc_runnable); + return NS_OK; + }); + NS_DispatchToMainThread(mainthread_runnable); + return IPC_OK(); +} + +int CamerasParent::ReleaseCapture(const CaptureEngine& aCapEngine, + int aCaptureId) { + int error = -1; + if (auto engine = EnsureInitialized(aCapEngine)) { + error = engine->ReleaseVideoCapture(aCaptureId); + } + return error; +} + +mozilla::ipc::IPCResult CamerasParent::RecvReleaseCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId) { + LOG("%s", __PRETTY_FUNCTION__); + LOG("RecvReleaseCamera device nr %d", aCaptureId); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom([self, aCapEngine, + aCaptureId]() { + int error = self->ReleaseCapture(aCapEngine, aCaptureId); + RefPtr<nsIRunnable> ipc_runnable = + NewRunnableFrom([self, error, aCaptureId]() { + if (!self->mChildIsAlive) { + LOG("RecvReleaseCapture: child not alive"); + return NS_ERROR_FAILURE; + } + + if (error) { + Unused << self->SendReplyFailure(); + LOG("RecvReleaseCapture: Failed to free device nr %d", aCaptureId); + return NS_ERROR_FAILURE; + } + + Unused << self->SendReplySuccess(); + LOG("Freed device nr %d", aCaptureId); + return NS_OK; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvStartCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId, + const VideoCaptureCapability& ipcCaps) { + LOG("%s", __PRETTY_FUNCTION__); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom([self, aCapEngine, + aCaptureId, ipcCaps]() { + LOG("%s", __PRETTY_FUNCTION__); + CallbackHelper** cbh; + int error = -1; + if (self->EnsureInitialized(aCapEngine)) { + cbh = self->mCallbacks.AppendElement(new CallbackHelper( + static_cast<CaptureEngine>(aCapEngine), aCaptureId, self)); + + self->sEngines[aCapEngine]->WithEntry( + aCaptureId, [&aCaptureId, &aCapEngine, &error, &ipcCaps, &cbh, + self](VideoEngine::CaptureEntry& cap) { + webrtc::VideoCaptureCapability capability; + capability.width = ipcCaps.width(); + capability.height = ipcCaps.height(); + capability.maxFPS = ipcCaps.maxFPS(); + capability.videoType = + static_cast<webrtc::VideoType>(ipcCaps.videoType()); + capability.interlaced = ipcCaps.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 = self->mAllCandidateCapabilities.find( + nsCString(cap.VideoCapture()->CurrentDeviceName())); + if ((candidateCapabilities != + self->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 = 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); + } + }); + } + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, error]() { + if (!self->mChildIsAlive) { + LOG("RecvStartCapture failure: child is not alive"); + return NS_ERROR_FAILURE; + } + + if (!error) { + Unused << self->SendReplySuccess(); + return NS_OK; + } + + LOG("RecvStartCapture failure: StartCapture failed"); + Unused << self->SendReplyFailure(); + return NS_ERROR_FAILURE; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, NS_DISPATCH_NORMAL); + return NS_OK; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource( + const CaptureEngine& aCapEngine, const int& aCaptureId) { + LOG("%s", __PRETTY_FUNCTION__); + RefPtr<Runnable> webrtc_runnable = NewRunnableFrom( + [self = RefPtr<CamerasParent>(this), aCapEngine, aCaptureId]() { + if (auto engine = self->EnsureInitialized(aCapEngine)) { + engine->WithEntry(aCaptureId, [self](VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + bool result = cap.VideoCapture()->FocusOnSelectedSource(); + RefPtr<nsIRunnable> ipc_runnable = NewRunnableFrom([self, + result]() { + if (!self->mChildIsAlive) { + LOG("RecvFocusOnSelectedSource failure: child is not alive"); + return NS_ERROR_FAILURE; + } + + if (result) { + Unused << self->SendReplySuccess(); + return NS_OK; + } + + Unused << self->SendReplyFailure(); + LOG("RecvFocusOnSelectedSource failure."); + return NS_ERROR_FAILURE; + }); + self->mPBackgroundEventTarget->Dispatch(ipc_runnable, + NS_DISPATCH_NORMAL); + } + }); + } + LOG("RecvFocusOnSelectedSource CameraParent not initialized"); + return NS_ERROR_FAILURE; + }); + DispatchToVideoCaptureThread(webrtc_runnable); + return IPC_OK(); +} + +void CamerasParent::StopCapture(const CaptureEngine& aCapEngine, + int aCaptureId) { + 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; + } + } + } +} + +mozilla::ipc::IPCResult CamerasParent::RecvStopCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId) { + LOG("%s", __PRETTY_FUNCTION__); + + RefPtr<CamerasParent> self(this); + RefPtr<Runnable> webrtc_runnable = + NewRunnableFrom([self, aCapEngine, aCaptureId]() { + self->StopCapture(aCapEngine, aCaptureId); + return NS_OK; + }); + nsresult rv = DispatchToVideoCaptureThread(webrtc_runnable); + if (!self->mChildIsAlive) { + 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::StopIPC() { + MOZ_ASSERT(!mDestroyed); + // 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. + mChildIsAlive = false; + mDestroyed = true; +} + +void CamerasParent::ActorDestroy(ActorDestroyReason aWhy) { + // No more IPC from here + LOG("%s", __PRETTY_FUNCTION__); + StopIPC(); + // Shut down WebRTC (if we're not in full shutdown, else this + // will already have happened) + StopVideoCapture(); +} + +nsString CamerasParent::GetNewName() { + static std::atomic<uint64_t> counter{0}; + nsString name(u"CamerasParent "_ns); + name.AppendInt(++counter); + return name; +} + +NS_IMETHODIMP CamerasParent::BlockShutdown(nsIAsyncShutdownClient*) { + mPBackgroundEventTarget->Dispatch( + NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() { + // Send__delete() can return failure if AddBlocker() registered this + // CamerasParent while RecvPCamerasConstructor() called Send__delete() + // because it noticed that AppShutdown had started. + (void)Send__delete__(self); + })); + return NS_OK; +} + +CamerasParent::CamerasParent() + : mName(GetNewName()), + mShmemPool(CaptureEngine::MaxEngine), + mPBackgroundEventTarget(GetCurrentSerialEventTarget()), + mChildIsAlive(true), + mDestroyed(false), + mWebRTCAlive(false) { + MOZ_ASSERT(mPBackgroundEventTarget != nullptr, + "GetCurrentThreadEventTarget failed"); + LOG("CamerasParent: %p", this); + StaticMutexAutoLock slock(sMutex); + + if (sNumOfCamerasParents++ == 0) { + sThreadMonitor = new Monitor("CamerasParent::sThreadMonitor"); + } +} + +// RecvPCamerasConstructor() is used because IPC messages, for +// Send__delete__(), cannot be sent from AllocPCamerasParent(). +ipc::IPCResult CamerasParent::RecvPCamerasConstructor() { + ipc::AssertIsOnBackgroundThread(); + + // A shutdown blocker must be added if sNumOfOpenCamerasParentEngines might + // be incremented to indicate ownership of an sVideoCaptureThread. + // If this task were queued after checking !IsInOrBeyond(AppShutdown), then + // shutdown may have proceeded on the main thread and so the task may run + // too late to add the blocker. + // 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. + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() { + nsresult rv = MustGetShutdownBarrier()->AddBlocker( + self, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns); + LOG("AddBlocker returned 0x%x", static_cast<unsigned>(rv)); + // AddBlocker() will fail if called after all + // AsyncShutdown.profileBeforeChange conditions have resolved or been + // removed. + // + // The success of this AddBlocker() call is expected when an + // sVideoCaptureThread is created based on the assumption that at + // least one condition (e.g. nsIAsyncShutdownBlocker) added with + // AsyncShutdown.profileBeforeChange.addBlocker() will not resolve or + // be removed until it has queued a task and that task has run. + // (AyncShutdown.jsm's Spinner#observe() makes a similar assumption + // when it calls processNextEvent(), assuming that there will be some + // other event generated, before checking whether its Barrier.wait() + // promise has resolved.) + // + // If AppShutdown::IsInOrBeyond(AppShutdown) returned false, + // then this main thread task was queued before AppShutdown's + // sCurrentShutdownPhase is set to AppShutdown, + // which is before profile-before-change is notified, + // which is when AsyncShutdown conditions are run, + // which is when one condition would queue a task to resolve the + // condition or remove the blocker. + // That task runs after this task and before AsyncShutdown prevents + // further conditions being added through AddBlocker(). + MOZ_ASSERT(NS_SUCCEEDED(rv) || !self->mWebRTCAlive); + })); + + // 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. + // IsInOrBeyond() checks sCurrentShutdownPhase, which is atomic. + // ShutdownPhase::AppShutdown corresponds to profileBeforeChange used by + // MustGetShutdownBarrier() in the parent process. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) { + // The usual blocker removal path depends on the existence of the + // `sVideoCaptureThread`, which is not ensured. Queue removal now. + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() { + // May fail if AddBlocker() failed. + (void)MustGetShutdownBarrier()->RemoveBlocker(self); + })); + return Send__delete__(this) ? IPC_OK() : IPC_FAIL(this, "Failed to send"); + } + + LOG("Spinning up WebRTC Cameras Thread"); + MonitorAutoLock lock(*sThreadMonitor); + if (sVideoCaptureThread == nullptr) { + MOZ_ASSERT(sNumOfOpenCamerasParentEngines == 0); + sVideoCaptureThread = new base::Thread("VideoCapture"); + base::Thread::Options options; +#if defined(_WIN32) + options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINUITHREAD; +#else + options.message_loop_type = MessageLoop::TYPE_MOZILLA_NONMAINTHREAD; +#endif + if (!sVideoCaptureThread->StartWithOptions(options)) { + MOZ_CRASH(); + } + } + mWebRTCAlive = true; + sNumOfOpenCamerasParentEngines++; + return IPC_OK(); +} + +CamerasParent::~CamerasParent() { + LOG("~CamerasParent: %p", this); + StaticMutexAutoLock slock(sMutex); + if (--sNumOfCamerasParents == 0) { + delete sThreadMonitor; + sThreadMonitor = nullptr; + } +} + +already_AddRefed<CamerasParent> CamerasParent::Create() { + mozilla::ipc::AssertIsOnBackgroundThread(); + return MakeAndAddRef<CamerasParent>(); +} + +} // namespace camera +} // namespace mozilla |