diff options
Diffstat (limited to 'dom/media/systemservices')
68 files changed, 11572 insertions, 0 deletions
diff --git a/dom/media/systemservices/CamerasChild.cpp b/dom/media/systemservices/CamerasChild.cpp new file mode 100644 index 0000000000..c7369aad36 --- /dev/null +++ b/dom/media/systemservices/CamerasChild.cpp @@ -0,0 +1,538 @@ +/* -*- 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 "CamerasChild.h" + +#undef FF + +#include "mozilla/Assertions.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Logging.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/Unused.h" +#include "MediaUtils.h" +#include "nsThreadUtils.h" + +#undef LOG +#undef LOG_ENABLED +mozilla::LazyLogModule gCamerasChildLog("CamerasChild"); +#define LOG(args) MOZ_LOG(gCamerasChildLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(gCamerasChildLog, mozilla::LogLevel::Debug) + +namespace mozilla::camera { + +CamerasSingleton::CamerasSingleton() + : mCamerasMutex("CamerasSingleton::mCamerasMutex"), + mCameras(nullptr), + mCamerasChildThread(nullptr) { + LOG(("CamerasSingleton: %p", this)); +} + +CamerasSingleton::~CamerasSingleton() { LOG(("~CamerasSingleton: %p", this)); } + +class InitializeIPCThread : public Runnable { + public: + InitializeIPCThread() + : Runnable("camera::InitializeIPCThread"), mCamerasChild(nullptr) {} + + NS_IMETHOD Run() override { + // Get the PBackground handle + ipc::PBackgroundChild* existingBackgroundChild = + ipc::BackgroundChild::GetOrCreateForCurrentThread(); + LOG(("BackgroundChild: %p", existingBackgroundChild)); + if (!existingBackgroundChild) { + return NS_ERROR_FAILURE; + } + + // Create CamerasChild + // We will be returning the resulting pointer (synchronously) to our caller. + mCamerasChild = static_cast<mozilla::camera::CamerasChild*>( + existingBackgroundChild->SendPCamerasConstructor()); + if (!mCamerasChild) { + return NS_ERROR_FAILURE; + } + return NS_OK; + } + + CamerasChild* GetCamerasChild() { return mCamerasChild; } + + private: + CamerasChild* mCamerasChild; +}; + +CamerasChild* GetCamerasChild() { + CamerasSingleton::Mutex().AssertCurrentThreadOwns(); + if (!CamerasSingleton::Child()) { + MOZ_ASSERT(!NS_IsMainThread(), "Should not be on the main Thread"); + MOZ_ASSERT(!CamerasSingleton::Thread()); + LOG(("No sCameras, setting up IPC Thread")); + nsresult rv = NS_NewNamedThread("Cameras IPC", + getter_AddRefs(CamerasSingleton::Thread())); + if (NS_FAILED(rv)) { + LOG(("Error launching IPC Thread")); + return nullptr; + } + + // At this point we are in the MediaManager thread, and the thread we are + // dispatching to is the specific Cameras IPC thread that was just made + // above, so now we will fire off a runnable to run + // BackgroundChild::GetOrCreateForCurrentThread there, while we + // block in this thread. + // We block until the following happens in the Cameras IPC thread: + // 1) Creation of PBackground finishes + // 2) Creation of PCameras finishes by sending a message to the parent + RefPtr<InitializeIPCThread> runnable = new InitializeIPCThread(); + RefPtr<SyncRunnable> sr = new SyncRunnable(runnable); + sr->DispatchToThread(CamerasSingleton::Thread()); + CamerasSingleton::Child() = runnable->GetCamerasChild(); + } + if (!CamerasSingleton::Child()) { + LOG(("Failed to set up CamerasChild, are we in shutdown?")); + } + return CamerasSingleton::Child(); +} + +CamerasChild* GetCamerasChildIfExists() { + OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex()); + return CamerasSingleton::Child(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyFailure(void) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = false; + monitor.Notify(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplySuccess(void) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + monitor.Notify(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyNumberOfCapabilities( + const int& capabilityCount) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + mReplyInteger = capabilityCount; + monitor.Notify(); + return IPC_OK(); +} + +// Helper function to dispatch calls to the IPC Thread and +// CamerasChild object. Takes the needed locks and dispatches. +// Takes a "failed" value and a reference to the output variable +// as parameters, will return the right one depending on whether +// dispatching succeeded. +// +// The LockAndDispatch object in the caller must stay alive until after any +// reply data has been retreived (mReplyInteger, etc) so that the data is +// protected by the ReplyMonitor/RequestMutex +template <class T = int> +class LockAndDispatch { + public: + LockAndDispatch(CamerasChild* aCamerasChild, const char* aRequestingFunc, + nsIRunnable* aRunnable, T aFailureValue, + const T& aSuccessValue) + : mCamerasChild(aCamerasChild), + mRequestingFunc(aRequestingFunc), + mRunnable(aRunnable), + mReplyLock(aCamerasChild->mReplyMonitor), + mRequestLock(aCamerasChild->mRequestMutex), + mSuccess(true), + mFailureValue(aFailureValue), + mSuccessValue(aSuccessValue) { + Dispatch(); + } + + T ReturnValue() const { + if (mSuccess) { + return mSuccessValue; + } else { + return mFailureValue; + } + } + + const bool& Success() const { return mSuccess; } + + private: + void Dispatch() { + if (!mCamerasChild->DispatchToParent(mRunnable, mReplyLock)) { + LOG(("Cameras dispatch for IPC failed in %s", mRequestingFunc)); + mSuccess = false; + } + } + + CamerasChild* mCamerasChild; + const char* mRequestingFunc; + nsIRunnable* mRunnable; + // Prevent concurrent use of the reply variables by holding + // the mReplyMonitor. Note that this is unlocked while waiting for + // the reply to be filled in, necessitating the additional mRequestLock/Mutex; + MonitorAutoLock mReplyLock; + MutexAutoLock mRequestLock; + bool mSuccess; + const T mFailureValue; + const T& mSuccessValue; +}; + +bool CamerasChild::DispatchToParent(nsIRunnable* aRunnable, + MonitorAutoLock& aMonitor) { + CamerasSingleton::Mutex().AssertCurrentThreadOwns(); + mReplyMonitor.AssertCurrentThreadOwns(); + CamerasSingleton::Thread()->Dispatch(aRunnable, NS_DISPATCH_NORMAL); + // Guard against spurious wakeups. + mReceivedReply = false; + // Wait for a reply + do { + // If the parent has been shut down, then we won't receive a reply. + if (!mIPCIsAlive) { + return false; + } + aMonitor.Wait(); + } while (!mReceivedReply); + return mReplySuccess; +} + +int CamerasChild::NumberOfCapabilities(CaptureEngine aCapEngine, + const char* deviceUniqueIdUTF8) { + LOG(("%s", __PRETTY_FUNCTION__)); + LOG(("NumberOfCapabilities for %s", deviceUniqueIdUTF8)); + nsCString unique_id(deviceUniqueIdUTF8); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, nsCString>( + "camera::PCamerasChild::SendNumberOfCapabilities", this, + &CamerasChild::SendNumberOfCapabilities, aCapEngine, unique_id); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); + LOG(("Capture capability count: %d", dispatcher.ReturnValue())); + return dispatcher.ReturnValue(); +} + +int CamerasChild::NumberOfCaptureDevices(CaptureEngine aCapEngine) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>( + "camera::PCamerasChild::SendNumberOfCaptureDevices", this, + &CamerasChild::SendNumberOfCaptureDevices, aCapEngine); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); + LOG(("Capture Devices: %d", dispatcher.ReturnValue())); + return dispatcher.ReturnValue(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyNumberOfCaptureDevices( + const int& aDeviceCount) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + mReplyInteger = aDeviceCount; + monitor.Notify(); + return IPC_OK(); +} + +int CamerasChild::EnsureInitialized(CaptureEngine aCapEngine) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>( + "camera::PCamerasChild::SendEnsureInitialized", this, + &CamerasChild::SendEnsureInitialized, aCapEngine); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); + LOG(("Capture Devices: %d", dispatcher.ReturnValue())); + return dispatcher.ReturnValue(); +} + +int CamerasChild::GetCaptureCapability( + CaptureEngine aCapEngine, const char* unique_idUTF8, + const unsigned int capability_number, + webrtc::VideoCaptureCapability* capability) { + LOG(("GetCaptureCapability: %s %d", unique_idUTF8, capability_number)); + MOZ_ASSERT(capability); + nsCString unique_id(unique_idUTF8); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, nsCString, unsigned int>( + "camera::PCamerasChild::SendGetCaptureCapability", this, + &CamerasChild::SendGetCaptureCapability, aCapEngine, unique_id, + capability_number); + mReplyCapability = capability; + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + mReplyCapability = nullptr; + return dispatcher.ReturnValue(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyGetCaptureCapability( + const VideoCaptureCapability& ipcCapability) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + mReplyCapability->width = ipcCapability.width(); + mReplyCapability->height = ipcCapability.height(); + mReplyCapability->maxFPS = ipcCapability.maxFPS(); + mReplyCapability->videoType = + static_cast<webrtc::VideoType>(ipcCapability.videoType()); + mReplyCapability->interlaced = ipcCapability.interlaced(); + monitor.Notify(); + return IPC_OK(); +} + +int CamerasChild::GetCaptureDevice(CaptureEngine aCapEngine, + unsigned int list_number, + char* device_nameUTF8, + const unsigned int device_nameUTF8Length, + char* unique_idUTF8, + const unsigned int unique_idUTF8Length, + bool* scary, bool* device_is_placeholder) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, unsigned int>( + "camera::PCamerasChild::SendGetCaptureDevice", this, + &CamerasChild::SendGetCaptureDevice, aCapEngine, list_number); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + if (dispatcher.Success()) { + base::strlcpy(device_nameUTF8, mReplyDeviceName.get(), + device_nameUTF8Length); + base::strlcpy(unique_idUTF8, mReplyDeviceID.get(), unique_idUTF8Length); + if (scary) { + *scary = mReplyScary; + } + if (device_is_placeholder) { + *device_is_placeholder = mReplyDeviceIsPlaceholder; + } + LOG(("Got %s name %s id", device_nameUTF8, unique_idUTF8)); + } + return dispatcher.ReturnValue(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyGetCaptureDevice( + const nsACString& device_name, const nsACString& device_id, + const bool& scary, const bool& device_is_placeholder) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + mReplyDeviceName = device_name; + mReplyDeviceID = device_id; + mReplyScary = scary; + mReplyDeviceIsPlaceholder = device_is_placeholder; + monitor.Notify(); + return IPC_OK(); +} + +int CamerasChild::AllocateCapture(CaptureEngine aCapEngine, + const char* unique_idUTF8, + uint64_t aWindowID) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCString unique_id(unique_idUTF8); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, nsCString, const uint64_t&>( + "camera::PCamerasChild::SendAllocateCapture", this, + &CamerasChild::SendAllocateCapture, aCapEngine, unique_id, aWindowID); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mReplyInteger); + if (dispatcher.Success()) { + LOG(("Capture Device allocated: %d", mReplyInteger)); + } + return dispatcher.ReturnValue(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvReplyAllocateCapture( + const int& aCaptureId) { + LOG(("%s", __PRETTY_FUNCTION__)); + MonitorAutoLock monitor(mReplyMonitor); + mReceivedReply = true; + mReplySuccess = true; + mReplyInteger = aCaptureId; + monitor.Notify(); + return IPC_OK(); +} + +int CamerasChild::ReleaseCapture(CaptureEngine aCapEngine, + const int capture_id) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, int>( + "camera::PCamerasChild::SendReleaseCapture", this, + &CamerasChild::SendReleaseCapture, aCapEngine, capture_id); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + return dispatcher.ReturnValue(); +} + +void CamerasChild::AddCallback(const CaptureEngine aCapEngine, + const int capture_id, FrameRelay* render) { + MutexAutoLock lock(mCallbackMutex); + CapturerElement ce; + ce.engine = aCapEngine; + ce.id = capture_id; + ce.callback = render; + mCallbacks.AppendElement(ce); +} + +void CamerasChild::RemoveCallback(const CaptureEngine aCapEngine, + const int capture_id) { + MutexAutoLock lock(mCallbackMutex); + for (unsigned int i = 0; i < mCallbacks.Length(); i++) { + CapturerElement ce = mCallbacks[i]; + if (ce.engine == aCapEngine && ce.id == capture_id) { + mCallbacks.RemoveElementAt(i); + break; + } + } +} + +int CamerasChild::StartCapture(CaptureEngine aCapEngine, const int capture_id, + const webrtc::VideoCaptureCapability& webrtcCaps, + FrameRelay* cb) { + LOG(("%s", __PRETTY_FUNCTION__)); + AddCallback(aCapEngine, capture_id, cb); + VideoCaptureCapability capCap( + webrtcCaps.width, webrtcCaps.height, webrtcCaps.maxFPS, + static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, int, VideoCaptureCapability>( + "camera::PCamerasChild::SendStartCapture", this, + &CamerasChild::SendStartCapture, aCapEngine, capture_id, capCap); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + return dispatcher.ReturnValue(); +} + +int CamerasChild::FocusOnSelectedSource(CaptureEngine aCapEngine, + const int aCaptureId) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, int>( + "camera::PCamerasChild::SendFocusOnSelectedSource", this, + &CamerasChild::SendFocusOnSelectedSource, aCapEngine, aCaptureId); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + return dispatcher.ReturnValue(); +} + +int CamerasChild::StopCapture(CaptureEngine aCapEngine, const int capture_id) { + LOG(("%s", __PRETTY_FUNCTION__)); + nsCOMPtr<nsIRunnable> runnable = + mozilla::NewRunnableMethod<CaptureEngine, int>( + "camera::PCamerasChild::SendStopCapture", this, + &CamerasChild::SendStopCapture, aCapEngine, capture_id); + LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + if (dispatcher.Success()) { + RemoveCallback(aCapEngine, capture_id); + } + return dispatcher.ReturnValue(); +} + +class ShutdownRunnable : public Runnable { + public: + explicit ShutdownRunnable(already_AddRefed<Runnable>&& aReplyEvent) + : Runnable("camera::ShutdownRunnable"), mReplyEvent(aReplyEvent){}; + + NS_IMETHOD Run() override { + LOG(("Closing BackgroundChild")); + // This will also destroy the CamerasChild. + ipc::BackgroundChild::CloseForCurrentThread(); + + NS_DispatchToMainThread(mReplyEvent.forget()); + + return NS_OK; + } + + private: + RefPtr<Runnable> mReplyEvent; +}; + +void Shutdown(void) { + // Called from both MediaEngineWebRTC::Shutdown() on the MediaManager thread + // and DeallocPCamerasChild() on the dedicated IPC thread. + OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex()); + + CamerasChild* child = CamerasSingleton::Child(); + if (!child) { + // We don't want to cause everything to get fired up if we're + // really already shut down. + LOG(("Shutdown when already shut down")); + return; + } + if (CamerasSingleton::Thread()) { + LOG(("PBackground thread exists, dispatching close")); + // The IPC thread is shut down on the main thread after the + // BackgroundChild is closed. + RefPtr<ShutdownRunnable> runnable = new ShutdownRunnable( + NewRunnableMethod("nsIThread::Shutdown", CamerasSingleton::Thread(), + &nsIThread::Shutdown)); + CamerasSingleton::Thread()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } else { + LOG(("Shutdown called without PBackground thread")); + } + LOG(("Erasing sCameras & thread refs (original thread)")); + CamerasSingleton::Child() = nullptr; + CamerasSingleton::Thread() = nullptr; +} + +mozilla::ipc::IPCResult CamerasChild::RecvDeliverFrame( + const CaptureEngine& capEngine, const int& capId, + mozilla::ipc::Shmem&& shmem, const VideoFrameProperties& prop) { + MutexAutoLock lock(mCallbackMutex); + if (Callback(capEngine, capId)) { + unsigned char* image = shmem.get<unsigned char>(); + Callback(capEngine, capId)->DeliverFrame(image, prop); + } else { + LOG(("DeliverFrame called with dead callback")); + } + SendReleaseFrame(std::move(shmem)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult CamerasChild::RecvDeviceChange() { + mDeviceListChangeEvent.Notify(); + return IPC_OK(); +} + +void CamerasChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("ActorDestroy")); + MonitorAutoLock monitor(mReplyMonitor); + mIPCIsAlive = false; + // Hopefully prevent us from getting stuck + // on replies that'll never come. + monitor.NotifyAll(); +} + +CamerasChild::CamerasChild() + : mCallbackMutex("mozilla::cameras::CamerasChild::mCallbackMutex"), + mIPCIsAlive(true), + mRequestMutex("mozilla::cameras::CamerasChild::mRequestMutex"), + mReplyMonitor("mozilla::cameras::CamerasChild::mReplyMonitor"), + mReceivedReply(false), + mReplySuccess(false), + mZero(0), + mReplyInteger(0), + mReplyScary(false) { + LOG(("CamerasChild: %p", this)); + + MOZ_COUNT_CTOR(CamerasChild); +} + +CamerasChild::~CamerasChild() { + LOG(("~CamerasChild: %p", this)); + CamerasSingleton::AssertNoChild(); + MOZ_COUNT_DTOR(CamerasChild); +} + +FrameRelay* CamerasChild::Callback(CaptureEngine aCapEngine, int capture_id) { + for (unsigned int i = 0; i < mCallbacks.Length(); i++) { + CapturerElement ce = mCallbacks[i]; + if (ce.engine == aCapEngine && ce.id == capture_id) { + return ce.callback; + } + } + + return nullptr; +} + +} // namespace mozilla::camera diff --git a/dom/media/systemservices/CamerasChild.h b/dom/media/systemservices/CamerasChild.h new file mode 100644 index 0000000000..824ed290b4 --- /dev/null +++ b/dom/media/systemservices/CamerasChild.h @@ -0,0 +1,263 @@ +/* -*- 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/. */ + +#ifndef mozilla_CamerasChild_h +#define mozilla_CamerasChild_h + +#include <utility> + +#include "MediaEventSource.h" +#include "mozilla/Mutex.h" +#include "mozilla/camera/PCamerasChild.h" +#include "mozilla/camera/PCamerasParent.h" +#include "nsCOMPtr.h" + +// conflicts with #include of scoped_ptr.h +#undef FF +#include "modules/video_capture/video_capture_defines.h" + +namespace mozilla { + +namespace ipc { +class BackgroundChildImpl; +} // namespace ipc + +namespace camera { + +class FrameRelay { + public: + virtual int DeliverFrame( + uint8_t* buffer, const mozilla::camera::VideoFrameProperties& props) = 0; +}; + +struct CapturerElement { + CaptureEngine engine; + int id; + FrameRelay* callback; +}; + +// Forward declaration so we can work with pointers to it. +class CamerasChild; +// Helper class in impl that we friend. +template <class T> +class LockAndDispatch; + +// We emulate the sync webrtc.org API with the help of singleton +// CamerasSingleton, which manages a pointer to an IPC object, a thread +// where IPC operations should run on, and a mutex. +// The static function Cameras() will use that Singleton to set up, +// if needed, both the thread and the associated IPC objects and return +// a pointer to the IPC object. Users can then do IPC calls on that object +// after dispatching them to aforementioned thread. + +// 2 Threads are involved in this code: +// - the MediaManager thread, which will call the (static, sync API) functions +// through MediaEngineRemoteVideoSource +// - the Cameras IPC thread, which will be doing our IPC to the parent process +// via PBackground + +// Our main complication is that we emulate a sync API while (having to do) +// async messaging. We dispatch the messages to another thread to send them +// async and hold a Monitor to wait for the result to be asynchronously received +// again. The requirement for async messaging originates on the parent side: +// it's not reasonable to block all PBackground IPC there while waiting for +// something like device enumeration to complete. + +class CamerasSingleton { + public: + static OffTheBooksMutex& Mutex() { return singleton().mCamerasMutex; } + + static CamerasChild*& Child() { + Mutex().AssertCurrentThreadOwns(); + return singleton().mCameras; + } + + static nsCOMPtr<nsIThread>& Thread() { + Mutex().AssertCurrentThreadOwns(); + return singleton().mCamerasChildThread; + } + // The mutex is not held because mCameras is known not to be modified + // concurrently when this is asserted. + static void AssertNoChild() { MOZ_ASSERT(!singleton().mCameras); } + + private: + CamerasSingleton(); + ~CamerasSingleton(); + + static CamerasSingleton& singleton() { + static CamerasSingleton camera; + return camera; + } + + // Reinitializing CamerasChild will change the pointers below. + // We don't want this to happen in the middle of preparing IPC. + // We will be alive on destruction, so this needs to be off the books. + mozilla::OffTheBooksMutex mCamerasMutex; + + // This is owned by the IPC code, and the same code controls the lifetime. + // It will set and clear this pointer as appropriate in setup/teardown. + // We'd normally make this a WeakPtr but unfortunately the IPC code already + // uses the WeakPtr mixin in a protected base class of CamerasChild, and in + // any case the object becomes unusable as soon as IPC is tearing down, which + // will be before actual destruction. + CamerasChild* mCameras; + nsCOMPtr<nsIThread> mCamerasChildThread; +}; + +// Get a pointer to a CamerasChild object we can use to do IPC with. +// This does everything needed to set up, including starting the IPC +// channel with PBackground, blocking until thats done, and starting the +// thread to do IPC on. This will fail if we're in shutdown. On success +// it will set up the CamerasSingleton. +CamerasChild* GetCamerasChild(); + +CamerasChild* GetCamerasChildIfExists(); + +// Shut down the IPC channel and everything associated, like WebRTC. +// This is a static call because the CamerasChild object may not even +// be alive when we're called. +void Shutdown(void); + +// Obtain the CamerasChild object (if possible, i.e. not shutting down), +// and maintain a grip on the object for the duration of the call. +template <class MEM_FUN, class... ARGS> +int GetChildAndCall(MEM_FUN&& f, ARGS&&... args) { + OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex()); + CamerasChild* child = GetCamerasChild(); + if (child) { + return (child->*f)(std::forward<ARGS>(args)...); + } else { + return -1; + } +} + +class CamerasChild final : public PCamerasChild { + friend class mozilla::ipc::BackgroundChildImpl; + template <class T> + friend class mozilla::camera::LockAndDispatch; + + public: + // We are owned by the PBackground thread only. CamerasSingleton + // takes a non-owning reference. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CamerasChild) + + // IPC messages recevied, received on the PBackground thread + // these are the actual callbacks with data + mozilla::ipc::IPCResult RecvDeliverFrame( + const CaptureEngine&, const int&, mozilla::ipc::Shmem&&, + const VideoFrameProperties& prop) override; + + mozilla::ipc::IPCResult RecvDeviceChange() override; + + // these are response messages to our outgoing requests + mozilla::ipc::IPCResult RecvReplyNumberOfCaptureDevices(const int&) override; + mozilla::ipc::IPCResult RecvReplyNumberOfCapabilities(const int&) override; + mozilla::ipc::IPCResult RecvReplyAllocateCapture(const int&) override; + mozilla::ipc::IPCResult RecvReplyGetCaptureCapability( + const VideoCaptureCapability& capability) override; + mozilla::ipc::IPCResult RecvReplyGetCaptureDevice( + const nsACString& device_name, const nsACString& device_id, + const bool& scary, const bool& device_is_placeholder) override; + mozilla::ipc::IPCResult RecvReplyFailure(void) override; + mozilla::ipc::IPCResult RecvReplySuccess(void) override; + void ActorDestroy(ActorDestroyReason aWhy) override; + + // the webrtc.org ViECapture calls are mirrored here, but with access + // to a specific PCameras instance to communicate over. These also + // run on the MediaManager thread + int NumberOfCaptureDevices(CaptureEngine aCapEngine); + int NumberOfCapabilities(CaptureEngine aCapEngine, + const char* deviceUniqueIdUTF8); + int ReleaseCapture(CaptureEngine aCapEngine, const int capture_id); + int StartCapture(CaptureEngine aCapEngine, const int capture_id, + const webrtc::VideoCaptureCapability& capability, + FrameRelay* func); + int FocusOnSelectedSource(CaptureEngine aCapEngine, const int capture_id); + int StopCapture(CaptureEngine aCapEngine, const int capture_id); + // Returns a non-negative capture identifier or -1 on failure. + int AllocateCapture(CaptureEngine aCapEngine, const char* unique_idUTF8, + uint64_t aWindowID); + int GetCaptureCapability(CaptureEngine aCapEngine, const char* unique_idUTF8, + const unsigned int capability_number, + webrtc::VideoCaptureCapability* capability); + int GetCaptureDevice(CaptureEngine aCapEngine, unsigned int list_number, + char* device_nameUTF8, + const unsigned int device_nameUTF8Length, + char* unique_idUTF8, + const unsigned int unique_idUTF8Length, bool* scary, + bool* device_is_placeholder); + int EnsureInitialized(CaptureEngine aCapEngine); + + template <typename This> + int ConnectDeviceListChangeListener(MediaEventListener* aListener, + AbstractThread* aTarget, This* aThis, + void (This::*aMethod)()) { + // According to the spec, if the script sets + // navigator.mediaDevices.ondevicechange and the permission state is + // "always granted", the User Agent MUST fires a devicechange event when + // a new media input device is made available, even the script never + // call getusermedia or enumerateDevices. + + // In order to detect the event, we need to init the camera engine. + // Currently EnsureInitialized(aCapEngine) is only called when one of + // CamerasParent api, e.g., RecvNumberOfCaptureDevices(), is called. + + // So here we setup camera engine via EnsureInitialized(aCapEngine) + + EnsureInitialized(CameraEngine); + *aListener = mDeviceListChangeEvent.Connect(aTarget, aThis, aMethod); + return IPC_OK(); + } + + FrameRelay* Callback(CaptureEngine aCapEngine, int capture_id); + + private: + CamerasChild(); + ~CamerasChild(); + // Dispatch a Runnable to the PCamerasParent, by executing it on the + // decidecated Cameras IPC/PBackground thread. + bool DispatchToParent(nsIRunnable* aRunnable, MonitorAutoLock& aMonitor); + void AddCallback(const CaptureEngine aCapEngine, const int capture_id, + FrameRelay* render); + void RemoveCallback(const CaptureEngine aCapEngine, const int capture_id); + + nsTArray<CapturerElement> mCallbacks; + // Protects the callback arrays + Mutex mCallbackMutex MOZ_UNANNOTATED; + + bool mIPCIsAlive; + + // Hold to prevent multiple outstanding requests. We don't use + // request IDs so we only support one at a time. Don't want try + // to use the webrtc.org API from multiple threads simultanously. + // The monitor below isn't sufficient for this, as it will drop + // the lock when Wait-ing for a response, allowing us to send a new + // request. The Notify on receiving the response will then unblock + // both waiters and one will be guaranteed to get the wrong result. + // Take this one before taking mReplyMonitor. + Mutex mRequestMutex MOZ_UNANNOTATED; + // Hold to wait for an async response to our calls *and* until the + // user of LockAndDispatch<> has read the data out. This is done by + // keeping the LockAndDispatch object alive. + Monitor mReplyMonitor MOZ_UNANNOTATED; + // Async response valid? + bool mReceivedReply; + // Async responses data contents; + bool mReplySuccess; + const int mZero; + int mReplyInteger; + webrtc::VideoCaptureCapability* mReplyCapability = nullptr; + nsCString mReplyDeviceName; + nsCString mReplyDeviceID; + bool mReplyScary; + bool mReplyDeviceIsPlaceholder; + MediaEventProducer<void> mDeviceListChangeEvent; +}; + +} // namespace camera +} // namespace mozilla + +#endif // mozilla_CamerasChild_h diff --git a/dom/media/systemservices/CamerasParent.cpp b/dom/media/systemservices/CamerasParent.cpp new file mode 100644 index 0000000000..24bd1cc8ea --- /dev/null +++ b/dom/media/systemservices/CamerasParent.cpp @@ -0,0 +1,1335 @@ +/* -*- 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 "CamerasTypes.h" +#include "MediaEngineSource.h" +#include "PerformanceRecorder.h" +#include "VideoFrameUtils.h" + +#include "common/browser_logging/WebRtcLog.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_media.h" +#include "mozilla/StaticPrefs_permissions.h" +#include "nsIPermissionManager.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "video_engine/video_capture_factory.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; +// Main VideoCaptureFactory used to create and manage all capture related +// objects. IPC background thread only. Outlives the CamerasParent instances. +static StaticRefPtr<VideoCaptureFactory> sVideoCaptureFactory; + +static VideoCaptureFactory* EnsureVideoCaptureFactory() { + ipc::AssertIsOnBackgroundThread(); + + if (sVideoCaptureFactory) { + return sVideoCaptureFactory; + } + + sVideoCaptureFactory = MakeRefPtr<VideoCaptureFactory>(); + NS_DispatchToMainThread( + NS_NewRunnableFunction("CamerasParent::EnsureVideoCaptureFactory", + []() { ClearOnShutdown(&sVideoCaptureFactory); })); + return sVideoCaptureFactory; +} + +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, mVideoCaptureFactory); + 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, bool, 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; + bool placeholder = false; + 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, &placeholder); + } + } + if (error == 0) { + name.Assign(deviceName); + uniqueId.Assign(deviceUniqueId); + } + + return Promise::CreateAndResolve( + std::make_tuple(std::move(name), std::move(uniqueId), devicePid, + placeholder, error), + "CamerasParent::RecvGetCaptureDevice"); + }) + ->Then( + mPBackgroundEventTarget, __func__, + [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) { + const auto& [name, uniqueId, devicePid, placeholder, 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, + placeholder); + }); + 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; + } + + // when we delegate permission from first party, we should use the top level + // window + 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), + mVideoCaptureFactory(EnsureVideoCaptureFactory()), + 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. +} + +/* static */ +auto CamerasParent::RequestCameraAccess(bool aAllowPermissionRequest) + -> RefPtr<CameraAccessRequestPromise> { + ipc::AssertIsOnBackgroundThread(); + + // Special case for PipeWire where we at this point just need to make sure + // we have information about camera availabilty through the camera portal + if (!aAllowPermissionRequest) { + return EnsureVideoCaptureFactory()->UpdateCameraAvailability()->Then( + GetCurrentSerialEventTarget(), + "CamerasParent::RequestCameraAccess update camera availability", + [](const VideoCaptureFactory::UpdateCameraAvailabilityPromise:: + ResolveOrRejectValue& aValue) { + LOG("Camera availability updated to %s", + aValue.IsResolve() + ? aValue.ResolveValue() == + VideoCaptureFactory::CameraAvailability::Available + ? "available" + : "not available" + : "still unknown"); + return CameraAccessRequestPromise::CreateAndResolve( + CamerasAccessStatus::RequestRequired, + "CamerasParent::RequestCameraAccess camera availability updated"); + }); + } + + static StaticRefPtr<CameraAccessRequestPromise> sCameraAccessRequestPromise; + if (!sCameraAccessRequestPromise) { + sCameraAccessRequestPromise = RefPtr<CameraAccessRequestPromise>( + EnsureVideoCaptureFactory()->InitCameraBackend()->Then( + GetCurrentSerialEventTarget(), + "CamerasParent::RequestCameraAccess camera backend init handler", + [](nsresult aRv) mutable { + MOZ_ASSERT(NS_SUCCEEDED(aRv)); + if (sVideoCaptureThread) { + MOZ_ASSERT(sEngines); + MOZ_ALWAYS_SUCCEEDS( + sVideoCaptureThread->Dispatch(NS_NewRunnableFunction( + __func__, [engines = RefPtr(sEngines.get())] { + if (VideoEngine* engine = + engines->ElementAt(CameraEngine)) { + engine->ClearVideoCaptureDeviceInfo(); + } + }))); + } + return CameraAccessRequestPromise::CreateAndResolve( + CamerasAccessStatus::Granted, + "CamerasParent::RequestCameraAccess camera backend init " + "resolve"); + }, + [](nsresult aRv) mutable { + MOZ_ASSERT(NS_FAILED(aRv)); + return CameraAccessRequestPromise::CreateAndResolve( + aRv == NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR + ? CamerasAccessStatus::Rejected + : CamerasAccessStatus::Error, + "CamerasParent::RequestCameraAccess camera backend init " + "reject"); + })); + static nsresult clearingRv = NS_DispatchToMainThread(NS_NewRunnableFunction( + __func__, [] { ClearOnShutdown(&sCameraAccessRequestPromise); })); + Unused << clearingRv; + } + + // If camera acess is granted, all is jolly. But we need to handle rejection. + return sCameraAccessRequestPromise->Then( + GetCurrentSerialEventTarget(), + "CamerasParent::CameraAccessRequestPromise rejection handler", + [](CamerasAccessStatus aStatus) { + return CameraAccessRequestPromise::CreateAndResolve( + aStatus, "CamerasParent::RequestCameraAccess resolve"); + }, + [promise = RefPtr(sCameraAccessRequestPromise.get()), + aAllowPermissionRequest](void_t aRv) { + if (promise == sCameraAccessRequestPromise) { + sCameraAccessRequestPromise = nullptr; + return CameraAccessRequestPromise::CreateAndResolve( + CamerasAccessStatus::Error, + "CamerasParent::RequestCameraAccess reject"); + } + return CamerasParent::RequestCameraAccess(aAllowPermissionRequest); + }); +} + +// 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"); + } + + NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [this, self = RefPtr(this)] { + mLogHandle = new nsMainThreadPtrHolder<WebrtcLogSinkHandle>( + "CamerasParent::mLogHandle", EnsureWebrtcLogging()); + })); + + 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 diff --git a/dom/media/systemservices/CamerasParent.h b/dom/media/systemservices/CamerasParent.h new file mode 100644 index 0000000000..c2ae55ebe6 --- /dev/null +++ b/dom/media/systemservices/CamerasParent.h @@ -0,0 +1,187 @@ +/* -*- 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/. */ + +#ifndef mozilla_CamerasParent_h +#define mozilla_CamerasParent_h + +#include "CamerasChild.h" +#include "VideoEngine.h" +#include "mozilla/Atomics.h" +#include "mozilla/camera/PCamerasParent.h" +#include "mozilla/ipc/Shmem.h" +#include "mozilla/ShmemPool.h" +#include "api/video/video_sink_interface.h" +#include "modules/video_capture/video_capture.h" +#include "modules/video_capture/video_capture_defines.h" +#include "video/render/incoming_video_stream.h" + +class WebrtcLogSinkHandle; +class nsIThread; + +namespace mozilla { +class VideoCaptureFactory; +} + +namespace mozilla::camera { + +class CamerasParent; + +class CallbackHelper : public rtc::VideoSinkInterface<webrtc::VideoFrame> { + public: + CallbackHelper(CaptureEngine aCapEng, uint32_t aStreamId, + CamerasParent* aParent) + : mCapEngine(aCapEng), + mStreamId(aStreamId), + mTrackingId(CaptureEngineToTrackingSourceStr(aCapEng), aStreamId), + mParent(aParent){}; + + // These callbacks end up running on the VideoCapture thread. + // From VideoCaptureCallback + void OnFrame(const webrtc::VideoFrame& aVideoFrame) override; + + friend CamerasParent; + + private: + const CaptureEngine mCapEngine; + const uint32_t mStreamId; + const TrackingId mTrackingId; + CamerasParent* const mParent; +}; + +class DeliverFrameRunnable; + +class CamerasParent final : public PCamerasParent, + private webrtc::VideoInputFeedBack { + public: + using ShutdownMozPromise = media::ShutdownBlockingTicket::ShutdownMozPromise; + + using CameraAccessRequestPromise = MozPromise<CamerasAccessStatus, void_t, + /* IsExclusive = */ false>; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET( + CamerasParent, mPBackgroundEventTarget) + + class VideoEngineArray; + friend DeliverFrameRunnable; + + static already_AddRefed<CamerasParent> Create(); + + /** + * Request camera access + * Currently only used on desktop. If @value + * aAllowPermissionRequest is true, a request for full camera access may be + * made and the returned promise may be blocked on user input on a modal + * dialog. If @value aAllowPermissionRequest is false, only a request to + * check camera device presence will be made. If any camera device is + * present, we will enumerate a single placeholder device until a successful + * RequestCameraAccess with a true aAllowPermissionRequest. + * The returned promise will never be rejected. + */ + static RefPtr<CameraAccessRequestPromise> RequestCameraAccess( + bool aAllowPermissionRequest); + + // Messages received from the child. These run on the IPC/PBackground thread. + mozilla::ipc::IPCResult RecvPCamerasConstructor(); + mozilla::ipc::IPCResult RecvAllocateCapture( + const CaptureEngine& aCapEngine, const nsACString& aUniqueIdUTF8, + const uint64_t& aWindowID) override; + mozilla::ipc::IPCResult RecvReleaseCapture(const CaptureEngine& aCapEngine, + const int& aCaptureId) override; + mozilla::ipc::IPCResult RecvNumberOfCaptureDevices( + const CaptureEngine& aCapEngine) override; + mozilla::ipc::IPCResult RecvNumberOfCapabilities( + const CaptureEngine& aCapEngine, const nsACString& aUniqueId) override; + mozilla::ipc::IPCResult RecvGetCaptureCapability( + const CaptureEngine& aCapEngine, const nsACString& aUniqueId, + const int& aIndex) override; + mozilla::ipc::IPCResult RecvGetCaptureDevice( + const CaptureEngine& aCapEngine, const int& aDeviceIndex) override; + mozilla::ipc::IPCResult RecvStartCapture( + const CaptureEngine& aCapEngine, const int& aCaptureId, + const VideoCaptureCapability& aIpcCaps) override; + mozilla::ipc::IPCResult RecvFocusOnSelectedSource( + const CaptureEngine& aCapEngine, const int& aCaptureId) override; + mozilla::ipc::IPCResult RecvStopCapture(const CaptureEngine& aCapEngine, + const int& aCaptureId) override; + mozilla::ipc::IPCResult RecvReleaseFrame( + mozilla::ipc::Shmem&& aShmem) override; + void ActorDestroy(ActorDestroyReason aWhy) override; + mozilla::ipc::IPCResult RecvEnsureInitialized( + const CaptureEngine& aCapEngine) override; + + nsIEventTarget* GetBackgroundEventTarget() { + return mPBackgroundEventTarget; + }; + bool IsShuttingDown() { + // the first 2 are pBackground only, the last is atomic + MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); + return mDestroyed; + }; + ShmemBuffer GetBuffer(size_t aSize); + + // helper to forward to the PBackground thread + int DeliverFrameOverIPC(CaptureEngine aCapEngine, uint32_t aStreamId, + const TrackingId& aTrackingId, ShmemBuffer aBuffer, + unsigned char* aAltBuffer, + const VideoFrameProperties& aProps); + + CamerasParent(); + + private: + virtual ~CamerasParent(); + + // We use these helpers for shutdown and for the respective IPC commands. + void StopCapture(const CaptureEngine& aCapEngine, int aCaptureId); + int ReleaseCapture(const CaptureEngine& aCapEngine, int aCaptureId); + + // VideoInputFeedBack + void OnDeviceChange() override; + + VideoEngine* EnsureInitialized(int aEngine); + + // Stops any ongoing capturing and releases resources. Called on + // mVideoCaptureThread. Idempotent. + void CloseEngines(); + + void OnShutdown(); + + nsTArray<CallbackHelper*> mCallbacks; + // If existent, blocks xpcom shutdown while alive. + // Note that this makes a reference cycle that gets broken in ActorDestroy(). + const UniquePtr<media::ShutdownBlockingTicket> mShutdownBlocker; + // Tracks the mShutdownBlocker shutdown handler. mPBackgroundEventTarget only. + MozPromiseRequestHolder<ShutdownMozPromise> mShutdownRequest; + + // Local copy of sVideoCaptureThread. Guaranteed alive if non-null. + const nsCOMPtr<nsISerialEventTarget> mVideoCaptureThread; + + // Reference to same VideoEngineArray as sEngines. Video capture thread only. + const RefPtr<VideoEngineArray> mEngines; + + // Reference to same VideoCaptureFactory as sVideoCaptureFactory. Video + // capture thread only. + const RefPtr<VideoCaptureFactory> mVideoCaptureFactory; + + // image buffers + ShmemPool mShmemPool; + + // PBackgroundParent thread + const nsCOMPtr<nsISerialEventTarget> mPBackgroundEventTarget; + + // Set to true in ActorDestroy. PBackground only. + bool mDestroyed; + + std::map<nsCString, std::map<uint32_t, webrtc::VideoCaptureCapability>> + mAllCandidateCapabilities; + + // While alive, ensure webrtc logging is hooked up to MOZ_LOG. Main thread + // only. + nsMainThreadPtrHandle<WebrtcLogSinkHandle> mLogHandle; +}; + +} // namespace mozilla::camera + +#endif // mozilla_CameraParent_h diff --git a/dom/media/systemservices/CamerasTypes.cpp b/dom/media/systemservices/CamerasTypes.cpp new file mode 100644 index 0000000000..7eda2f650b --- /dev/null +++ b/dom/media/systemservices/CamerasTypes.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "CamerasTypes.h" + +namespace mozilla::camera { + +TrackingId::Source CaptureEngineToTrackingSourceStr( + const CaptureEngine& aEngine) { + switch (aEngine) { + case ScreenEngine: + return TrackingId::Source::Screen; + case BrowserEngine: + return TrackingId::Source::Tab; + case WinEngine: + return TrackingId::Source::Window; + case CameraEngine: + return TrackingId::Source::Camera; + default: + return TrackingId::Source::Unimplemented; + } +} +} // namespace mozilla::camera diff --git a/dom/media/systemservices/CamerasTypes.h b/dom/media/systemservices/CamerasTypes.h new file mode 100644 index 0000000000..5135bcee5b --- /dev/null +++ b/dom/media/systemservices/CamerasTypes.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef mozilla_CamerasTypes_h +#define mozilla_CamerasTypes_h + +#include "ipc/EnumSerializer.h" +#include "PerformanceRecorder.h" + +namespace mozilla::camera { + +enum CaptureEngine : int { + InvalidEngine = 0, + ScreenEngine, + BrowserEngine, + WinEngine, + CameraEngine, + MaxEngine +}; + +enum class CamerasAccessStatus { + // We have full access to cameras, either because it was granted, or because + // requesting it from the user was not necessary. + Granted = 1, + // A permission request to the platform is required before we know the + // camera access status. Enumeration will result in a single placeholder + // device, should any cameras be present on the system. The placeholder + // device cannot be captured. + RequestRequired, + // A permission request was made and was rejected by the platform. + Rejected, + // Generic error while doing the request, for instance with pipewire most + // likely the xdg-desktop-portal request failed. + Error, +}; + +TrackingId::Source CaptureEngineToTrackingSourceStr( + const CaptureEngine& aEngine); + +} // namespace mozilla::camera + +namespace IPC { +template <> +struct ParamTraits<mozilla::camera::CaptureEngine> + : public ContiguousEnumSerializer< + mozilla::camera::CaptureEngine, + mozilla::camera::CaptureEngine::InvalidEngine, + mozilla::camera::CaptureEngine::MaxEngine> {}; + +template <> +struct ParamTraits<mozilla::camera::CamerasAccessStatus> + : public ContiguousEnumSerializerInclusive< + mozilla::camera::CamerasAccessStatus, + mozilla::camera::CamerasAccessStatus::Granted, + mozilla::camera::CamerasAccessStatus::Error> {}; +} // namespace IPC + +#endif // mozilla_CamerasTypes_h diff --git a/dom/media/systemservices/MediaChild.cpp b/dom/media/systemservices/MediaChild.cpp new file mode 100644 index 0000000000..ad10498623 --- /dev/null +++ b/dom/media/systemservices/MediaChild.cpp @@ -0,0 +1,93 @@ +/* -*- 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 "MediaChild.h" +#include "MediaParent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/MediaManager.h" +#include "mozilla/Logging.h" +#include "nsQueryObject.h" + +#undef LOG +mozilla::LazyLogModule gMediaChildLog("MediaChild"); +#define LOG(args) MOZ_LOG(gMediaChildLog, mozilla::LogLevel::Debug, args) + +namespace mozilla::media { + +RefPtr<PrincipalKeyPromise> GetPrincipalKey( + const ipc::PrincipalInfo& aPrincipalInfo, bool aPersist) { + RefPtr<MediaManager> mgr = MediaManager::GetInstance(); + MOZ_ASSERT(mgr); + + if (XRE_GetProcessType() == GeckoProcessType_Default) { + auto p = MakeRefPtr<PrincipalKeyPromise::Private>(__func__); + + mgr->GetNonE10sParent()->RecvGetPrincipalKey( + aPrincipalInfo, aPersist, + [p](const nsACString& aKey) { p->Resolve(aKey, __func__); }); + return p; + } + return Child::Get() + ->SendGetPrincipalKey(aPrincipalInfo, aPersist) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [](const Child::GetPrincipalKeyPromise::ResolveOrRejectValue& + aValue) { + if (aValue.IsReject() || aValue.ResolveValue().IsEmpty()) { + return PrincipalKeyPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + return PrincipalKeyPromise::CreateAndResolve( + aValue.ResolveValue(), __func__); + }); +} + +void SanitizeOriginKeys(const uint64_t& aSinceWhen, bool aOnlyPrivateBrowsing) { + LOG(("SanitizeOriginKeys since %" PRIu64 " %s", aSinceWhen, + (aOnlyPrivateBrowsing ? "in Private Browsing." : "."))); + + if (XRE_GetProcessType() == GeckoProcessType_Default) { + // Avoid opening MediaManager in this case, since this is called by + // sanitize.js when cookies are cleared, which can happen on startup. + RefPtr<Parent<NonE10s>> tmpParent = new Parent<NonE10s>(); + tmpParent->RecvSanitizeOriginKeys(aSinceWhen, aOnlyPrivateBrowsing); + } else { + Child::Get()->SendSanitizeOriginKeys(aSinceWhen, aOnlyPrivateBrowsing); + } +} + +static Child* sChild; + +Child* Child::Get() { + MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Content); + MOZ_ASSERT(NS_IsMainThread()); + if (!sChild) { + sChild = static_cast<Child*>( + dom::ContentChild::GetSingleton()->SendPMediaConstructor()); + } + return sChild; +} + +Child::Child() : mActorDestroyed(false) { + LOG(("media::Child: %p", this)); + MOZ_COUNT_CTOR(Child); +} + +Child::~Child() { + LOG(("~media::Child: %p", this)); + sChild = nullptr; + MOZ_COUNT_DTOR(Child); +} + +void Child::ActorDestroy(ActorDestroyReason aWhy) { mActorDestroyed = true; } + +PMediaChild* AllocPMediaChild() { return new Child(); } + +bool DeallocPMediaChild(media::PMediaChild* aActor) { + delete static_cast<Child*>(aActor); + return true; +} + +} // namespace mozilla::media diff --git a/dom/media/systemservices/MediaChild.h b/dom/media/systemservices/MediaChild.h new file mode 100644 index 0000000000..b0ae776cce --- /dev/null +++ b/dom/media/systemservices/MediaChild.h @@ -0,0 +1,60 @@ +/* -*- 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/. */ + +#ifndef mozilla_MediaChild_h +#define mozilla_MediaChild_h + +#include "mozilla/media/PMediaChild.h" +#include "mozilla/media/PMediaParent.h" +#include "MediaUtils.h" + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} + +namespace media { + +typedef MozPromise<nsCString, nsresult, false> PrincipalKeyPromise; + +// media::Child implements proxying to the chrome process for some media-related +// functions, for the moment just: +// +// GetPrincipalKey() - get a cookie-like persisted unique key for a given +// principalInfo. +// +// SanitizeOriginKeys() - reset persisted unique keys. + +// GetPrincipalKey and SanitizeOriginKeys are asynchronous APIs that return +// pledges (promise-like objects) with the future value. Use pledge.Then(func) +// to access. + +RefPtr<PrincipalKeyPromise> GetPrincipalKey( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, bool aPersist); + +void SanitizeOriginKeys(const uint64_t& aSinceWhen, bool aOnlyPrivateBrowsing); + +class Child : public PMediaChild { + public: + static Child* Get(); + + Child(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + virtual ~Child(); + + private: + bool mActorDestroyed; +}; + +PMediaChild* AllocPMediaChild(); +bool DeallocPMediaChild(PMediaChild* aActor); + +} // namespace media +} // namespace mozilla + +#endif // mozilla_MediaChild_h diff --git a/dom/media/systemservices/MediaParent.cpp b/dom/media/systemservices/MediaParent.cpp new file mode 100644 index 0000000000..d2fb06b8ae --- /dev/null +++ b/dom/media/systemservices/MediaParent.cpp @@ -0,0 +1,536 @@ +/* -*- 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 "MediaParent.h" + +#include "mozilla/Base64.h" +#include <mozilla/StaticMutex.h> + +#include "MediaUtils.h" +#include "MediaEngine.h" +#include "VideoUtils.h" +#include "nsClassHashtable.h" +#include "nsThreadUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIInputStream.h" +#include "nsILineInputStream.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsISupportsImpl.h" +#include "mozilla/Logging.h" + +#undef LOG +mozilla::LazyLogModule gMediaParentLog("MediaParent"); +#define LOG(args) MOZ_LOG(gMediaParentLog, mozilla::LogLevel::Debug, args) + +// A file in the profile dir is used to persist mOriginKeys used to anonymize +// deviceIds to be unique per origin, to avoid them being supercookies. + +#define ORIGINKEYS_FILE u"enumerate_devices.txt" +#define ORIGINKEYS_VERSION "1" + +namespace mozilla::media { + +StaticMutex sOriginKeyStoreStsMutex; + +class OriginKeyStore { + NS_INLINE_DECL_REFCOUNTING(OriginKeyStore); + class OriginKey { + public: + static const size_t DecodedLength = 18; + static const size_t EncodedLength = DecodedLength * 4 / 3; + + explicit OriginKey(const nsACString& aKey, + int64_t aSecondsStamp = 0) // 0 = temporal + : mKey(aKey), mSecondsStamp(aSecondsStamp) {} + + nsCString mKey; // Base64 encoded. + int64_t mSecondsStamp; + }; + + class OriginKeysTable { + public: + OriginKeysTable() : mPersistCount(0) {} + + nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo, + nsCString& aResult, bool aPersist = false) { + nsAutoCString principalString; + PrincipalInfoToString(aPrincipalInfo, principalString); + + OriginKey* key; + if (!mKeys.Get(principalString, &key)) { + nsCString salt; // Make a new one + nsresult rv = GenerateRandomName(salt, OriginKey::EncodedLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + key = mKeys.InsertOrUpdate(principalString, MakeUnique<OriginKey>(salt)) + .get(); + } + if (aPersist && !key->mSecondsStamp) { + key->mSecondsStamp = PR_Now() / PR_USEC_PER_SEC; + mPersistCount++; + } + aResult = key->mKey; + return NS_OK; + } + + void Clear(int64_t aSinceWhen) { + // Avoid int64_t* <-> void* casting offset + OriginKey since(nsCString(), aSinceWhen / PR_USEC_PER_SEC); + for (auto iter = mKeys.Iter(); !iter.Done(); iter.Next()) { + auto originKey = iter.UserData(); + LOG((((originKey->mSecondsStamp >= since.mSecondsStamp) + ? "%s: REMOVE %" PRId64 " >= %" PRId64 + : "%s: KEEP %" PRId64 " < %" PRId64), + __FUNCTION__, originKey->mSecondsStamp, since.mSecondsStamp)); + + if (originKey->mSecondsStamp >= since.mSecondsStamp) { + iter.Remove(); + } + } + mPersistCount = 0; + } + + private: + void PrincipalInfoToString(const ipc::PrincipalInfo& aPrincipalInfo, + nsACString& aString) { + switch (aPrincipalInfo.type()) { + case ipc::PrincipalInfo::TSystemPrincipalInfo: + aString.AssignLiteral("[System Principal]"); + return; + + case ipc::PrincipalInfo::TNullPrincipalInfo: { + const ipc::NullPrincipalInfo& info = + aPrincipalInfo.get_NullPrincipalInfo(); + aString.Assign(info.spec()); + return; + } + + case ipc::PrincipalInfo::TContentPrincipalInfo: { + const ipc::ContentPrincipalInfo& info = + aPrincipalInfo.get_ContentPrincipalInfo(); + aString.Assign(info.originNoSuffix()); + + nsAutoCString suffix; + info.attrs().CreateSuffix(suffix); + aString.Append(suffix); + return; + } + + case ipc::PrincipalInfo::TExpandedPrincipalInfo: { + const ipc::ExpandedPrincipalInfo& info = + aPrincipalInfo.get_ExpandedPrincipalInfo(); + + aString.AssignLiteral("[Expanded Principal ["); + + for (uint32_t i = 0; i < info.allowlist().Length(); i++) { + nsAutoCString str; + PrincipalInfoToString(info.allowlist()[i], str); + + if (i != 0) { + aString.AppendLiteral(", "); + } + + aString.Append(str); + } + + aString.AppendLiteral("]]"); + return; + } + + default: + MOZ_CRASH("Unknown PrincipalInfo type!"); + } + } + + protected: + nsClassHashtable<nsCStringHashKey, OriginKey> mKeys; + size_t mPersistCount; + }; + + class OriginKeysLoader : public OriginKeysTable { + public: + OriginKeysLoader() = default; + + nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo, + nsCString& aResult, bool aPersist = false) { + auto before = mPersistCount; + nsresult rv = + OriginKeysTable::GetPrincipalKey(aPrincipalInfo, aResult, aPersist); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mPersistCount != before) { + Save(); + } + return NS_OK; + } + + already_AddRefed<nsIFile> GetFile() { + MOZ_ASSERT(mProfileDir); + nsCOMPtr<nsIFile> file; + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + file->Append(nsLiteralString(ORIGINKEYS_FILE)); + return file.forget(); + } + + // Format of file is key secondsstamp origin (first line is version #): + // + // 1 + // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424733961 http://fiddle.jshell.net + // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424734841 http://mozilla.github.io + // etc. + + nsresult Read() { + nsCOMPtr<nsIFile> file = GetFile(); + if (NS_WARN_IF(!file)) { + return NS_ERROR_UNEXPECTED; + } + bool exists; + nsresult rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!exists) { + return NS_OK; + } + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsILineInputStream> i = do_QueryInterface(stream); + MOZ_ASSERT(i); + MOZ_ASSERT(!mPersistCount); + + nsCString line; + bool hasMoreLines; + rv = i->ReadLine(line, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!line.EqualsLiteral(ORIGINKEYS_VERSION)) { + // If version on disk is newer than we can understand then ignore it. + return NS_OK; + } + + while (hasMoreLines) { + rv = i->ReadLine(line, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + // Read key secondsstamp origin. + // Ignore any lines that don't fit format in the comment above exactly. + int32_t f = line.FindChar(' '); + if (f < 0) { + continue; + } + const nsACString& key = Substring(line, 0, f); + const nsACString& s = Substring(line, f + 1); + f = s.FindChar(' '); + if (f < 0) { + continue; + } + int64_t secondsstamp = Substring(s, 0, f).ToInteger64(&rv); + if (NS_FAILED(rv)) { + continue; + } + const nsACString& origin = Substring(s, f + 1); + + // Validate key + if (key.Length() != OriginKey::EncodedLength) { + continue; + } + nsCString dummy; + rv = Base64Decode(key, dummy); + if (NS_FAILED(rv)) { + continue; + } + mKeys.InsertOrUpdate(origin, MakeUnique<OriginKey>(key, secondsstamp)); + } + mPersistCount = mKeys.Count(); + return NS_OK; + } + + nsresult Write() { + nsCOMPtr<nsIFile> file = GetFile(); + if (NS_WARN_IF(!file)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = + NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString versionBuffer; + versionBuffer.AppendLiteral(ORIGINKEYS_VERSION); + versionBuffer.Append('\n'); + + uint32_t count; + rv = stream->Write(versionBuffer.Data(), versionBuffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (count != versionBuffer.Length()) { + return NS_ERROR_UNEXPECTED; + } + for (const auto& entry : mKeys) { + const nsACString& origin = entry.GetKey(); + OriginKey* originKey = entry.GetWeak(); + + if (!originKey->mSecondsStamp) { + continue; // don't write temporal ones + } + + nsCString originBuffer; + originBuffer.Append(originKey->mKey); + originBuffer.Append(' '); + originBuffer.AppendInt(originKey->mSecondsStamp); + originBuffer.Append(' '); + originBuffer.Append(origin); + originBuffer.Append('\n'); + + rv = stream->Write(originBuffer.Data(), originBuffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv)) || count != originBuffer.Length()) { + break; + } + } + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream); + MOZ_ASSERT(safeStream); + + rv = safeStream->Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + nsresult Load() { + nsresult rv = Read(); + if (NS_WARN_IF(NS_FAILED(rv))) { + Delete(); + } + return rv; + } + + nsresult Save() { + nsresult rv = Write(); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_WARNING("Failed to write data for EnumerateDevices id-persistence."); + Delete(); + } + return rv; + } + + void Clear(int64_t aSinceWhen) { + OriginKeysTable::Clear(aSinceWhen); + Delete(); + Save(); + } + + nsresult Delete() { + nsCOMPtr<nsIFile> file = GetFile(); + if (NS_WARN_IF(!file)) { + return NS_ERROR_UNEXPECTED; + } + nsresult rv = file->Remove(false); + if (rv == NS_ERROR_FILE_NOT_FOUND) { + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + void SetProfileDir(nsIFile* aProfileDir) { + MOZ_ASSERT(!NS_IsMainThread()); + bool first = !mProfileDir; + mProfileDir = aProfileDir; + // Load from disk when we first get a profileDir, but not subsequently. + if (first) { + Load(); + } + } + + private: + nsCOMPtr<nsIFile> mProfileDir; + }; + + private: + static OriginKeyStore* sOriginKeyStore; + + virtual ~OriginKeyStore() { + MOZ_ASSERT(NS_IsMainThread()); + sOriginKeyStore = nullptr; + LOG(("%s", __FUNCTION__)); + } + + public: + static RefPtr<OriginKeyStore> Get() { + MOZ_ASSERT(NS_IsMainThread()); + if (!sOriginKeyStore) { + sOriginKeyStore = new OriginKeyStore(); + } + return RefPtr(sOriginKeyStore); + } + + // Only accessed on StreamTS threads + OriginKeysLoader mOriginKeys MOZ_GUARDED_BY(sOriginKeyStoreStsMutex); + OriginKeysTable mPrivateBrowsingOriginKeys + MOZ_GUARDED_BY(sOriginKeyStoreStsMutex); +}; +OriginKeyStore* OriginKeyStore::sOriginKeyStore = nullptr; + +template <class Super> +mozilla::ipc::IPCResult Parent<Super>::RecvGetPrincipalKey( + const ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist, + PMediaParent::GetPrincipalKeyResolver&& aResolve) { + MOZ_ASSERT(NS_IsMainThread()); + + // First, get profile dir. + + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPCResult(this, false); + } + + // Resolver has to be called in MainThread but the key is discovered + // in a different thread. We wrap the resolver around a MozPromise to make + // it more flexible and pass it to the new task. When this is done the + // resolver is resolved in MainThread. + + // Then over to stream-transport thread (a thread pool) to do the actual + // file io. Stash a promise to hold the answer and get an id for this request. + + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(sts); + auto taskQueue = TaskQueue::Create(sts.forget(), "RecvGetPrincipalKey"); + RefPtr<Parent<Super>> that(this); + + InvokeAsync( + taskQueue, __func__, + [this, that, profileDir, aPrincipalInfo, aPersist]() { + MOZ_ASSERT(!NS_IsMainThread()); + + StaticMutexAutoLock lock(sOriginKeyStoreStsMutex); + mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir); + + nsresult rv; + nsAutoCString result; + if (IsPrincipalInfoPrivate(aPrincipalInfo)) { + rv = mOriginKeyStore->mPrivateBrowsingOriginKeys.GetPrincipalKey( + aPrincipalInfo, result); + } else { + rv = mOriginKeyStore->mOriginKeys.GetPrincipalKey(aPrincipalInfo, + result, aPersist); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return PrincipalKeyPromise::CreateAndReject(rv, __func__); + } + return PrincipalKeyPromise::CreateAndResolve(result, __func__); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [aResolve](const PrincipalKeyPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + aResolve(""_ns); + } else { + aResolve(aValue.ResolveValue()); + } + }); + + return IPC_OK(); +} + +template <class Super> +mozilla::ipc::IPCResult Parent<Super>::RecvSanitizeOriginKeys( + const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPCResult(this, false); + } + // Over to stream-transport thread (a thread pool) to do the file io. + + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(sts); + RefPtr<Parent<Super>> that(this); + + rv = sts->Dispatch( + NewRunnableFrom( + [this, that, profileDir, aSinceWhen, aOnlyPrivateBrowsing]() { + MOZ_ASSERT(!NS_IsMainThread()); + StaticMutexAutoLock lock(sOriginKeyStoreStsMutex); + mOriginKeyStore->mPrivateBrowsingOriginKeys.Clear(aSinceWhen); + if (!aOnlyPrivateBrowsing) { + mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir); + mOriginKeyStore->mOriginKeys.Clear(aSinceWhen); + } + return NS_OK; + }), + NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPCResult(this, false); + } + return IPC_OK(); +} + +template <class Super> +void Parent<Super>::ActorDestroy(ActorDestroyReason aWhy) { + // No more IPC from here + mDestroyed = true; + LOG(("%s", __FUNCTION__)); +} + +template <class Super> +Parent<Super>::Parent() + : mOriginKeyStore(OriginKeyStore::Get()), mDestroyed(false) { + LOG(("media::Parent: %p", this)); +} + +template <class Super> +Parent<Super>::~Parent() { + NS_ReleaseOnMainThread("Parent<Super>::mOriginKeyStore", + mOriginKeyStore.forget()); + LOG(("~media::Parent: %p", this)); +} + +PMediaParent* AllocPMediaParent() { + Parent<PMediaParent>* obj = new Parent<PMediaParent>(); + obj->AddRef(); + return obj; +} + +bool DeallocPMediaParent(media::PMediaParent* aActor) { + static_cast<Parent<PMediaParent>*>(aActor)->Release(); + return true; +} + +} // namespace mozilla::media + +// Instantiate templates to satisfy linker +template class mozilla::media::Parent<mozilla::media::NonE10s>; diff --git a/dom/media/systemservices/MediaParent.h b/dom/media/systemservices/MediaParent.h new file mode 100644 index 0000000000..77cba312f3 --- /dev/null +++ b/dom/media/systemservices/MediaParent.h @@ -0,0 +1,91 @@ +/* -*- 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/. */ + +#ifndef mozilla_MediaParent_h +#define mozilla_MediaParent_h + +#include "MediaChild.h" + +#include "mozilla/media/PMediaParent.h" + +namespace mozilla::media { + +// media::Parent implements the chrome-process side of ipc for media::Child APIs +// A same-process version may also be created to service non-e10s calls. + +class OriginKeyStore; + +class NonE10s { + typedef mozilla::ipc::IProtocol::ActorDestroyReason ActorDestroyReason; + + public: + virtual ~NonE10s() = default; + + protected: + virtual mozilla::ipc::IPCResult RecvGetPrincipalKey( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist, + PMediaParent::GetPrincipalKeyResolver&& aResolve) = 0; + virtual mozilla::ipc::IPCResult RecvSanitizeOriginKeys( + const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) = 0; + virtual void ActorDestroy(ActorDestroyReason aWhy) = 0; +}; + +/** + * Dummy class to avoid a templated class being passed to the refcounting macro + * (see Bug 1334421 for what happens then) + */ +class RefCountedParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedParent) + + protected: + virtual ~RefCountedParent() = default; +}; + +// Super = PMediaParent or NonE10s + +template <class Super> +class Parent : public RefCountedParent, public Super { + typedef mozilla::ipc::IProtocol::ActorDestroyReason ActorDestroyReason; + + public: + virtual mozilla::ipc::IPCResult RecvGetPrincipalKey( + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist, + PMediaParent::GetPrincipalKeyResolver&& aResolve) override; + virtual mozilla::ipc::IPCResult RecvSanitizeOriginKeys( + const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) override; + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + Parent(); + + private: + virtual ~Parent(); + + RefPtr<OriginKeyStore> mOriginKeyStore; + bool mDestroyed; +}; + +template <class Parent> +mozilla::ipc::IPCResult IPCResult(Parent* aSelf, bool aSuccess); + +template <> +inline mozilla::ipc::IPCResult IPCResult(Parent<PMediaParent>* aSelf, + bool aSuccess) { + return aSuccess ? IPC_OK() : IPC_FAIL_NO_REASON(aSelf); +} + +template <> +inline mozilla::ipc::IPCResult IPCResult(Parent<NonE10s>* aSelf, + bool aSuccess) { + return IPC_OK(); +} + +PMediaParent* AllocPMediaParent(); +bool DeallocPMediaParent(PMediaParent* aActor); + +} // namespace mozilla::media + +#endif // mozilla_MediaParent_h diff --git a/dom/media/systemservices/MediaSystemResourceClient.cpp b/dom/media/systemservices/MediaSystemResourceClient.cpp new file mode 100644 index 0000000000..50695fc76c --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceClient.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/Monitor.h" +#include "mozilla/ReentrantMonitor.h" + +#include "MediaSystemResourceClient.h" + +namespace mozilla { + +Atomic<uint32_t> MediaSystemResourceClient::sSerialCounter(0); + +MediaSystemResourceClient::MediaSystemResourceClient( + MediaSystemResourceType aReourceType) + : mResourceType(aReourceType), + mId(++sSerialCounter), + mListener(nullptr), + mResourceState(RESOURCE_STATE_START), + mIsSync(false), + mAcquireSyncWaitMonitor(nullptr), + mAcquireSyncWaitDone(nullptr) { + mManager = MediaSystemResourceManager::Get(); + if (mManager) { + mManager->Register(this); + } +} + +MediaSystemResourceClient::~MediaSystemResourceClient() { + ReleaseResource(); + if (mManager) { + mManager->Unregister(this); + } +} + +bool MediaSystemResourceClient::SetListener( + MediaSystemResourceReservationListener* aListener) { + if (!mManager) { + return false; + } + return mManager->SetListener(this, aListener); +} + +void MediaSystemResourceClient::Acquire() { + if (!mManager) { + return; + } + mManager->Acquire(this); +} + +bool MediaSystemResourceClient::AcquireSyncNoWait() { + if (!mManager) { + return false; + } + return mManager->AcquireSyncNoWait(this); +} + +void MediaSystemResourceClient::ReleaseResource() { + if (!mManager) { + return; + } + mManager->ReleaseResource(this); +} + +} // namespace mozilla diff --git a/dom/media/systemservices/MediaSystemResourceClient.h b/dom/media/systemservices/MediaSystemResourceClient.h new file mode 100644 index 0000000000..52cf5107e9 --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceClient.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(MediaSystemResourceClient_h_) +# define MediaSystemResourceClient_h_ + +# include "MediaSystemResourceManager.h" +# include "MediaSystemResourceTypes.h" +# include "mozilla/Atomics.h" +# include "mozilla/media/MediaSystemResourceTypes.h" +# include "mozilla/Monitor.h" +# include "mozilla/RefPtr.h" + +namespace mozilla { + +class MediaSystemResourceManager; + +/** + * This is a base class for listener callbacks. + * This callback is invoked when the media system resource reservation state + * is changed. + */ +class MediaSystemResourceReservationListener { + public: + virtual void ResourceReserved() = 0; + virtual void ResourceReserveFailed() = 0; +}; + +/** + * MediaSystemResourceClient is used to reserve a media system resource + * like hw decoder. When system has a limitation of a media resource, + * use this class to mediate use rights of the resource. + */ +class MediaSystemResourceClient { + public: + // Enumeration for the valid decoding states + enum ResourceState { + RESOURCE_STATE_START, + RESOURCE_STATE_WAITING, + RESOURCE_STATE_ACQUIRED, + RESOURCE_STATE_NOT_ACQUIRED, + RESOURCE_STATE_END + }; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceClient) + + explicit MediaSystemResourceClient(MediaSystemResourceType aReourceType); + + bool SetListener(MediaSystemResourceReservationListener* aListener); + + // Try to acquire media resource asynchronously. + // If the resource is used by others, wait until acquired. + void Acquire(); + + // Try to acquire media resource synchronously. If the resource is not + // immediately available, fail to acquire it. return false if resource is not + // acquired. return true if resource is acquired. + // + // This function should not be called on ImageBridge thread. + // It should be used only for compatibility with legacy code. + bool AcquireSyncNoWait(); + + void ReleaseResource(); + + private: + ~MediaSystemResourceClient(); + + RefPtr<MediaSystemResourceManager> mManager; + const MediaSystemResourceType mResourceType; + const uint32_t mId; + + // Modified only by MediaSystemResourceManager. + // Accessed and modified with MediaSystemResourceManager::mReentrantMonitor + // held. + MediaSystemResourceReservationListener* mListener; + ResourceState mResourceState; + bool mIsSync; + ReentrantMonitor* mAcquireSyncWaitMonitor; + bool* mAcquireSyncWaitDone; + + static mozilla::Atomic<uint32_t> sSerialCounter; + + friend class MediaSystemResourceManager; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceManager.cpp b/dom/media/systemservices/MediaSystemResourceManager.cpp new file mode 100644 index 0000000000..414ef8e81d --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManager.cpp @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/TaskQueue.h" + +#include "MediaSystemResourceManagerChild.h" +#include "MediaSystemResourceClient.h" + +#include "mozilla/layers/ImageBridgeChild.h" + +#include "MediaSystemResourceManager.h" + +namespace mozilla { + +using namespace mozilla::ipc; +using namespace mozilla::layers; + +/* static */ +StaticRefPtr<MediaSystemResourceManager> MediaSystemResourceManager::sSingleton; + +/* static */ +MediaSystemResourceManager* MediaSystemResourceManager::Get() { + if (sSingleton) { + return sSingleton; + } + MediaSystemResourceManager::Init(); + return sSingleton; +} + +/* static */ +void MediaSystemResourceManager::Shutdown() { + MOZ_ASSERT(InImageBridgeChildThread()); + if (sSingleton) { + sSingleton->CloseIPC(); + sSingleton = nullptr; + } +} + +/* static */ +void MediaSystemResourceManager::Init() { + RefPtr<ImageBridgeChild> imageBridge = ImageBridgeChild::GetSingleton(); + if (!imageBridge) { + NS_WARNING("ImageBridge does not exist"); + return; + } + + if (InImageBridgeChildThread()) { + if (!sSingleton) { +#ifdef DEBUG + static int timesCreated = 0; + timesCreated++; + MOZ_ASSERT(timesCreated == 1); +#endif + sSingleton = new MediaSystemResourceManager(); + } + return; + } + + ReentrantMonitor barrier MOZ_UNANNOTATED("MediaSystemResourceManager::Init"); + ReentrantMonitorAutoEnter mainThreadAutoMon(barrier); + bool done = false; + + RefPtr<Runnable> runnable = + NS_NewRunnableFunction("MediaSystemResourceManager::Init", [&]() { + if (!sSingleton) { + sSingleton = new MediaSystemResourceManager(); + } + ReentrantMonitorAutoEnter childThreadAutoMon(barrier); + done = true; + barrier.NotifyAll(); + }); + + imageBridge->GetThread()->Dispatch(runnable.forget()); + + // should stop the thread until done. + while (!done) { + barrier.Wait(); + } +} + +MediaSystemResourceManager::MediaSystemResourceManager() + : mReentrantMonitor("MediaSystemResourceManager.mReentrantMonitor"), + mShutDown(false), + mChild(nullptr) { + MOZ_ASSERT(InImageBridgeChildThread()); + OpenIPC(); +} + +MediaSystemResourceManager::~MediaSystemResourceManager() { + MOZ_ASSERT(IsIpcClosed()); +} + +void MediaSystemResourceManager::OpenIPC() { + MOZ_ASSERT(InImageBridgeChildThread()); + MOZ_ASSERT(!mChild); + + media::PMediaSystemResourceManagerChild* child = + ImageBridgeChild::GetSingleton() + ->SendPMediaSystemResourceManagerConstructor(); + mChild = static_cast<media::MediaSystemResourceManagerChild*>(child); + mChild->SetManager(this); +} + +void MediaSystemResourceManager::CloseIPC() { + MOZ_ASSERT(InImageBridgeChildThread()); + + if (!mChild) { + return; + } + mChild->Destroy(); + mChild = nullptr; + mShutDown = true; +} + +void MediaSystemResourceManager::OnIpcClosed() { mChild = nullptr; } + +bool MediaSystemResourceManager::IsIpcClosed() { return mChild ? true : false; } + +void MediaSystemResourceManager::Register(MediaSystemResourceClient* aClient) { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MOZ_ASSERT(aClient); + MOZ_ASSERT(!mResourceClients.Contains(aClient->mId)); + + mResourceClients.InsertOrUpdate(aClient->mId, aClient); +} + +void MediaSystemResourceManager::Unregister( + MediaSystemResourceClient* aClient) { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MOZ_ASSERT(aClient); + MOZ_ASSERT(mResourceClients.Contains(aClient->mId)); + MOZ_ASSERT(mResourceClients.Get(aClient->mId) == aClient); + + mResourceClients.Remove(aClient->mId); +} + +bool MediaSystemResourceManager::SetListener( + MediaSystemResourceClient* aClient, + MediaSystemResourceReservationListener* aListener) { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MOZ_ASSERT(aClient); + + MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId); + MOZ_ASSERT(client); + + if (!client) { + return false; + } + // State Check + if (aClient->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_START) { + return false; + } + aClient->mListener = aListener; + return true; +} + +void MediaSystemResourceManager::Acquire(MediaSystemResourceClient* aClient) { + MOZ_ASSERT(aClient); + MOZ_ASSERT(!InImageBridgeChildThread()); + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId); + MOZ_ASSERT(client); + MOZ_ASSERT(client == aClient); + + aClient->mIsSync = false; // async request + + if (!client) { + HandleAcquireResult(aClient->mId, false); + return; + } + // State Check + if (aClient->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_START) { + HandleAcquireResult(aClient->mId, false); + return; + } + aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_WAITING; + ImageBridgeChild::GetSingleton()->GetThread()->Dispatch( + NewRunnableMethod<uint32_t>("MediaSystemResourceManager::DoAcquire", this, + &MediaSystemResourceManager::DoAcquire, + aClient->mId)); +} + +bool MediaSystemResourceManager::AcquireSyncNoWait( + MediaSystemResourceClient* aClient) { + MOZ_ASSERT(aClient); + MOZ_ASSERT(!InImageBridgeChildThread()); + + ReentrantMonitor barrier MOZ_UNANNOTATED( + "MediaSystemResourceManager::AcquireSyncNoWait"); + ReentrantMonitorAutoEnter autoMon(barrier); + bool done = false; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId); + MOZ_ASSERT(client); + MOZ_ASSERT(client == aClient); + + aClient->mIsSync = true; // sync request + + if (InImageBridgeChildThread()) { + HandleAcquireResult(aClient->mId, false); + return false; + } + if (!client || client != aClient) { + HandleAcquireResult(aClient->mId, false); + return false; + } + // State Check + if (aClient->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_START) { + HandleAcquireResult(aClient->mId, false); + return false; + } + // Hold barrier Monitor until acquire task end. + aClient->mAcquireSyncWaitMonitor = &barrier; + aClient->mAcquireSyncWaitDone = &done; + aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_WAITING; + } + + ImageBridgeChild::GetSingleton()->GetThread()->Dispatch( + NewRunnableMethod<uint32_t>("MediaSystemResourceManager::DoAcquire", this, + &MediaSystemResourceManager::DoAcquire, + aClient->mId)); + + // should stop the thread until done. + while (!done) { + barrier.Wait(); + } + + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + if (aClient->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_ACQUIRED) { + return false; + } + return true; + } +} + +void MediaSystemResourceManager::DoAcquire(uint32_t aId) { + MOZ_ASSERT(InImageBridgeChildThread()); + if (mShutDown || !mChild) { + HandleAcquireResult(aId, false); + return; + } + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MediaSystemResourceClient* client = mResourceClients.Get(aId); + MOZ_ASSERT(client); + + if (!client || client->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_WAITING) { + HandleAcquireResult(aId, false); + return; + } + MOZ_ASSERT(aId == client->mId); + bool willWait = !client->mAcquireSyncWaitMonitor ? true : false; + mChild->SendAcquire(client->mId, client->mResourceType, willWait); + } +} + +void MediaSystemResourceManager::ReleaseResource( + MediaSystemResourceClient* aClient) { + MOZ_ASSERT(aClient); + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId); + MOZ_ASSERT(client); + MOZ_ASSERT(client == aClient); + + if (!client || client != aClient || + aClient->mResourceState == + MediaSystemResourceClient::RESOURCE_STATE_START || + aClient->mResourceState == + MediaSystemResourceClient::RESOURCE_STATE_END) { + aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_END; + return; + } + + aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_END; + + ImageBridgeChild::GetSingleton()->GetThread()->Dispatch( + NewRunnableMethod<uint32_t>( + "MediaSystemResourceManager::DoRelease", this, + &MediaSystemResourceManager::DoRelease, aClient->mId)); + } +} + +void MediaSystemResourceManager::DoRelease(uint32_t aId) { + MOZ_ASSERT(InImageBridgeChildThread()); + if (mShutDown || !mChild) { + return; + } + mChild->SendRelease(aId); +} + +void MediaSystemResourceManager::RecvResponse(uint32_t aId, bool aSuccess) { + HandleAcquireResult(aId, aSuccess); +} + +void MediaSystemResourceManager::HandleAcquireResult(uint32_t aId, + bool aSuccess) { + if (!InImageBridgeChildThread()) { + ImageBridgeChild::GetSingleton()->GetThread()->Dispatch( + NewRunnableMethod<uint32_t, bool>( + "MediaSystemResourceManager::HandleAcquireResult", this, + &MediaSystemResourceManager::HandleAcquireResult, aId, aSuccess)); + return; + } + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + MediaSystemResourceClient* client = mResourceClients.Get(aId); + if (!client) { + // Client was already unregistered. + return; + } + if (client->mResourceState != + MediaSystemResourceClient::RESOURCE_STATE_WAITING) { + return; + } + + // Update state + if (aSuccess) { + client->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_ACQUIRED; + } else { + client->mResourceState = + MediaSystemResourceClient::RESOURCE_STATE_NOT_ACQUIRED; + } + + if (client->mIsSync) { + if (client->mAcquireSyncWaitMonitor) { + // Notify AcquireSync() complete + MOZ_ASSERT(client->mAcquireSyncWaitDone); + ReentrantMonitorAutoEnter autoMon(*client->mAcquireSyncWaitMonitor); + *client->mAcquireSyncWaitDone = true; + client->mAcquireSyncWaitMonitor->NotifyAll(); + client->mAcquireSyncWaitMonitor = nullptr; + client->mAcquireSyncWaitDone = nullptr; + } + } else { + // Notify Acquire() result + if (client->mListener) { + if (aSuccess) { + client->mListener->ResourceReserved(); + } else { + client->mListener->ResourceReserveFailed(); + } + } + } +} + +} // namespace mozilla diff --git a/dom/media/systemservices/MediaSystemResourceManager.h b/dom/media/systemservices/MediaSystemResourceManager.h new file mode 100644 index 0000000000..293595ece0 --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManager.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(MediaSystemResourceManager_h_) +# define MediaSystemResourceManager_h_ + +# include <queue> + +# include "MediaSystemResourceTypes.h" +# include "mozilla/ReentrantMonitor.h" +# include "mozilla/StaticPtr.h" +# include "nsTHashMap.h" +# include "nsISupportsImpl.h" + +namespace mozilla { + +namespace media { +class MediaSystemResourceManagerChild; +} // namespace media + +class MediaSystemResourceClient; +class MediaSystemResourceReservationListener; +class ReentrantMonitor; +class TaskQueue; + +/** + * Manage media system resource allocation requests within a process. + */ +class MediaSystemResourceManager { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceManager) + + static MediaSystemResourceManager* Get(); + static void Init(); + static void Shutdown(); + + void OnIpcClosed(); + + void Register(MediaSystemResourceClient* aClient); + void Unregister(MediaSystemResourceClient* aClient); + + bool SetListener(MediaSystemResourceClient* aClient, + MediaSystemResourceReservationListener* aListener); + + void Acquire(MediaSystemResourceClient* aClient); + bool AcquireSyncNoWait(MediaSystemResourceClient* aClient); + void ReleaseResource(MediaSystemResourceClient* aClient); + + void RecvResponse(uint32_t aId, bool aSuccess); + + private: + MediaSystemResourceManager(); + virtual ~MediaSystemResourceManager(); + + void OpenIPC(); + void CloseIPC(); + bool IsIpcClosed(); + + void DoAcquire(uint32_t aId); + + void DoRelease(uint32_t aId); + + void HandleAcquireResult(uint32_t aId, bool aSuccess); + + ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; + + bool mShutDown; + + media::MediaSystemResourceManagerChild* mChild; + + nsTHashMap<nsUint32HashKey, MediaSystemResourceClient*> mResourceClients; + + static StaticRefPtr<MediaSystemResourceManager> sSingleton; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceManagerChild.cpp b/dom/media/systemservices/MediaSystemResourceManagerChild.cpp new file mode 100644 index 0000000000..ff671fdf8d --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManagerChild.cpp @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "MediaSystemResourceManager.h" + +#include "MediaSystemResourceManagerChild.h" + +namespace mozilla::media { + +MediaSystemResourceManagerChild::MediaSystemResourceManagerChild() + : mDestroyed(false), mManager(nullptr) {} + +MediaSystemResourceManagerChild::~MediaSystemResourceManagerChild() = default; + +mozilla::ipc::IPCResult MediaSystemResourceManagerChild::RecvResponse( + const uint32_t& aId, const bool& aSuccess) { + if (mManager) { + mManager->RecvResponse(aId, aSuccess); + } + return IPC_OK(); +} + +void MediaSystemResourceManagerChild::ActorDestroy( + ActorDestroyReason aActorDestroyReason) { + MOZ_ASSERT(!mDestroyed); + if (mManager) { + mManager->OnIpcClosed(); + } + mDestroyed = true; +} + +void MediaSystemResourceManagerChild::Destroy() { + if (mDestroyed) { + return; + } + SendRemoveResourceManager(); + // WARNING: |this| is dead, hands off +} + +} // namespace mozilla::media diff --git a/dom/media/systemservices/MediaSystemResourceManagerChild.h b/dom/media/systemservices/MediaSystemResourceManagerChild.h new file mode 100644 index 0000000000..66bf76cdd3 --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManagerChild.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#if !defined(MediaSystemResourceManagerChild_h_) +# define MediaSystemResourceManagerChild_h_ + +# include "mozilla/media/PMediaSystemResourceManagerChild.h" +# include "nsISupportsImpl.h" + +namespace mozilla { + +class MediaSystemResourceManager; + +namespace ipc { +class BackgroundChildImpl; +} // namespace ipc + +namespace media { + +/** + * Handle MediaSystemResourceManager's IPC + */ +class MediaSystemResourceManagerChild final + : public PMediaSystemResourceManagerChild { + friend class PMediaSystemResourceManagerChild; + + public: + struct ResourceListener { + /* The resource is reserved and can be granted. + * The client can allocate the requested resource. + */ + virtual void resourceReserved() = 0; + /* The resource is not reserved any more. + * The client should release the resource as soon as possible if the + * resource is still being held. + */ + virtual void resourceCanceled() = 0; + }; + + MediaSystemResourceManagerChild(); + virtual ~MediaSystemResourceManagerChild(); + + void Destroy(); + + void SetManager(MediaSystemResourceManager* aManager) { mManager = aManager; } + + protected: + mozilla::ipc::IPCResult RecvResponse(const uint32_t& aId, + const bool& aSuccess); + + private: + void ActorDestroy(ActorDestroyReason aActorDestroyReason) override; + + bool mDestroyed; + MediaSystemResourceManager* mManager; + + friend class mozilla::ipc::BackgroundChildImpl; +}; + +} // namespace media +} // namespace mozilla + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceManagerParent.cpp b/dom/media/systemservices/MediaSystemResourceManagerParent.cpp new file mode 100644 index 0000000000..ec20079abc --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManagerParent.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/Unused.h" +#include "mozilla/layers/PImageBridgeParent.h" + +#include "MediaSystemResourceManagerParent.h" + +namespace mozilla::media { + +using namespace ipc; + +MediaSystemResourceManagerParent::MediaSystemResourceManagerParent() + : mDestroyed(false) { + mMediaSystemResourceService = MediaSystemResourceService::Get(); +} + +MediaSystemResourceManagerParent::~MediaSystemResourceManagerParent() { + MOZ_ASSERT(mDestroyed); +} + +mozilla::ipc::IPCResult MediaSystemResourceManagerParent::RecvAcquire( + const uint32_t& aId, const MediaSystemResourceType& aResourceType, + const bool& aWillWait) { + mResourceRequests.WithEntryHandle(aId, [&](auto&& request) { + MOZ_ASSERT(!request); + if (request) { + // Send fail response + mozilla::Unused << SendResponse(aId, false /* fail */); + return; + } + + request.Insert(MakeUnique<MediaSystemResourceRequest>(aId, aResourceType)); + mMediaSystemResourceService->Acquire(this, aId, aResourceType, aWillWait); + }); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult MediaSystemResourceManagerParent::RecvRelease( + const uint32_t& aId) { + MediaSystemResourceRequest* request = mResourceRequests.Get(aId); + if (!request) { + return IPC_OK(); + } + + mMediaSystemResourceService->ReleaseResource(this, aId, + request->mResourceType); + mResourceRequests.Remove(aId); + return IPC_OK(); +} + +mozilla::ipc::IPCResult +MediaSystemResourceManagerParent::RecvRemoveResourceManager() { + IProtocol* mgr = Manager(); + if (!PMediaSystemResourceManagerParent::Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +void MediaSystemResourceManagerParent::ActorDestroy( + ActorDestroyReason aReason) { + MOZ_ASSERT(!mDestroyed); + + // Release all resource requests of the MediaSystemResourceManagerParent. + // Clears all remaining pointers to this object. + mMediaSystemResourceService->ReleaseResource(this); + + mDestroyed = true; +} + +} // namespace mozilla::media diff --git a/dom/media/systemservices/MediaSystemResourceManagerParent.h b/dom/media/systemservices/MediaSystemResourceManagerParent.h new file mode 100644 index 0000000000..29ed219f2e --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceManagerParent.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#if !defined(MediaSystemResourceManagerParent_h_) +# define MediaSystemResourceManagerParent_h_ + +# include "MediaSystemResourceManager.h" +# include "MediaSystemResourceService.h" +# include "MediaSystemResourceTypes.h" +# include "mozilla/media/PMediaSystemResourceManagerParent.h" + +namespace mozilla::media { + +/** + * Handle MediaSystemResourceManager's IPC + */ +class MediaSystemResourceManagerParent final + : public PMediaSystemResourceManagerParent { + friend class PMediaSystemResourceManagerParent; + + public: + MediaSystemResourceManagerParent(); + virtual ~MediaSystemResourceManagerParent(); + + protected: + mozilla::ipc::IPCResult RecvAcquire( + const uint32_t& aId, const MediaSystemResourceType& aResourceType, + const bool& aWillWait); + + mozilla::ipc::IPCResult RecvRelease(const uint32_t& aId); + + mozilla::ipc::IPCResult RecvRemoveResourceManager(); + + private: + void ActorDestroy(ActorDestroyReason aActorDestroyReason) override; + + struct MediaSystemResourceRequest { + MediaSystemResourceRequest() + : mId(-1), mResourceType(MediaSystemResourceType::INVALID_RESOURCE) {} + MediaSystemResourceRequest(uint32_t aId, + MediaSystemResourceType aResourceType) + : mId(aId), mResourceType(aResourceType) {} + int32_t mId; + MediaSystemResourceType mResourceType; + }; + + bool mDestroyed; + + RefPtr<MediaSystemResourceService> mMediaSystemResourceService; + + nsClassHashtable<nsUint32HashKey, MediaSystemResourceRequest> + mResourceRequests; +}; + +} // namespace mozilla::media + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceMessageUtils.h b/dom/media/systemservices/MediaSystemResourceMessageUtils.h new file mode 100644 index 0000000000..f06da1467d --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceMessageUtils.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(MediaSystemResourceMessageUtils_h_) +# define MediaSystemResourceMessageUtils_h_ + +# include "ipc/EnumSerializer.h" +# include "MediaSystemResourceTypes.h" + +namespace IPC { + +template <> +struct ParamTraits<mozilla::MediaSystemResourceType> + : public ContiguousEnumSerializer< + mozilla::MediaSystemResourceType, + mozilla::MediaSystemResourceType::VIDEO_DECODER, + mozilla::MediaSystemResourceType::INVALID_RESOURCE> {}; + +} // namespace IPC + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceService.cpp b/dom/media/systemservices/MediaSystemResourceService.cpp new file mode 100644 index 0000000000..88c4566e76 --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceService.cpp @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MediaSystemResourceManagerParent.h" +#include "mozilla/layers/CompositorThread.h" +#include "mozilla/Unused.h" + +#include "MediaSystemResourceService.h" + +using namespace mozilla::layers; + +namespace mozilla { + +/* static */ +StaticRefPtr<MediaSystemResourceService> MediaSystemResourceService::sSingleton; + +/* static */ +MediaSystemResourceService* MediaSystemResourceService::Get() { + if (sSingleton) { + return sSingleton; + } + Init(); + return sSingleton; +} + +/* static */ +void MediaSystemResourceService::Init() { + if (!sSingleton) { + sSingleton = new MediaSystemResourceService(); + } +} + +/* static */ +void MediaSystemResourceService::Shutdown() { + if (sSingleton) { + sSingleton->Destroy(); + sSingleton = nullptr; + } +} + +MediaSystemResourceService::MediaSystemResourceService() : mDestroyed(false) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); +} + +MediaSystemResourceService::~MediaSystemResourceService() = default; + +void MediaSystemResourceService::Destroy() { mDestroyed = true; } + +void MediaSystemResourceService::Acquire( + media::MediaSystemResourceManagerParent* aParent, uint32_t aId, + MediaSystemResourceType aResourceType, bool aWillWait) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + MOZ_ASSERT(aParent); + + if (mDestroyed) { + return; + } + + MediaSystemResource* resource = + mResources.Get(static_cast<uint32_t>(aResourceType)); + + if (!resource || resource->mResourceCount == 0) { + // Resource does not exit + // Send fail response + mozilla::Unused << aParent->SendResponse(aId, false /* fail */); + return; + } + + // Try to acquire a resource + if (resource->mAcquiredRequests.size() < resource->mResourceCount) { + // Resource is available + resource->mAcquiredRequests.push_back( + MediaSystemResourceRequest(aParent, aId)); + // Send success response + mozilla::Unused << aParent->SendResponse(aId, true /* success */); + return; + } + + if (!aWillWait) { + // Resource is not available and do not wait. + // Send fail response + mozilla::Unused << aParent->SendResponse(aId, false /* fail */); + return; + } + // Wait until acquire. + resource->mWaitingRequests.push_back( + MediaSystemResourceRequest(aParent, aId)); +} + +void MediaSystemResourceService::ReleaseResource( + media::MediaSystemResourceManagerParent* aParent, uint32_t aId, + MediaSystemResourceType aResourceType) { + MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread()); + MOZ_ASSERT(aParent); + + if (mDestroyed) { + return; + } + + MediaSystemResource* resource = + mResources.Get(static_cast<uint32_t>(aResourceType)); + + if (!resource || resource->mResourceCount == 0) { + // Resource does not exit + return; + } + RemoveRequest(aParent, aId, aResourceType); + UpdateRequests(aResourceType); +} + +void MediaSystemResourceService::ReleaseResource( + media::MediaSystemResourceManagerParent* aParent) { + MOZ_ASSERT(aParent); + + if (mDestroyed) { + return; + } + + for (const uint32_t& key : mResources.Keys()) { + RemoveRequests(aParent, static_cast<MediaSystemResourceType>(key)); + UpdateRequests(static_cast<MediaSystemResourceType>(key)); + } +} + +void MediaSystemResourceService::RemoveRequest( + media::MediaSystemResourceManagerParent* aParent, uint32_t aId, + MediaSystemResourceType aResourceType) { + MOZ_ASSERT(aParent); + + MediaSystemResource* resource = + mResources.Get(static_cast<uint32_t>(aResourceType)); + if (!resource) { + return; + } + + std::deque<MediaSystemResourceRequest>::iterator it; + std::deque<MediaSystemResourceRequest>& acquiredRequests = + resource->mAcquiredRequests; + for (it = acquiredRequests.begin(); it != acquiredRequests.end(); it++) { + if (((*it).mParent == aParent) && ((*it).mId == aId)) { + acquiredRequests.erase(it); + return; + } + } + + std::deque<MediaSystemResourceRequest>& waitingRequests = + resource->mWaitingRequests; + for (it = waitingRequests.begin(); it != waitingRequests.end(); it++) { + if (((*it).mParent == aParent) && ((*it).mId == aId)) { + waitingRequests.erase(it); + return; + } + } +} + +void MediaSystemResourceService::RemoveRequests( + media::MediaSystemResourceManagerParent* aParent, + MediaSystemResourceType aResourceType) { + MOZ_ASSERT(aParent); + + MediaSystemResource* resource = + mResources.Get(static_cast<uint32_t>(aResourceType)); + + if (!resource || resource->mResourceCount == 0) { + // Resource does not exit + return; + } + + std::deque<MediaSystemResourceRequest>::iterator it; + std::deque<MediaSystemResourceRequest>& acquiredRequests = + resource->mAcquiredRequests; + for (it = acquiredRequests.begin(); it != acquiredRequests.end();) { + if ((*it).mParent == aParent) { + it = acquiredRequests.erase(it); + } else { + it++; + } + } + + std::deque<MediaSystemResourceRequest>& waitingRequests = + resource->mWaitingRequests; + for (it = waitingRequests.begin(); it != waitingRequests.end();) { + if ((*it).mParent == aParent) { + it = waitingRequests.erase(it); + } else { + it++; + } + } +} + +void MediaSystemResourceService::UpdateRequests( + MediaSystemResourceType aResourceType) { + MediaSystemResource* resource = + mResources.Get(static_cast<uint32_t>(aResourceType)); + + if (!resource || resource->mResourceCount == 0) { + // Resource does not exit + return; + } + + std::deque<MediaSystemResourceRequest>& acquiredRequests = + resource->mAcquiredRequests; + std::deque<MediaSystemResourceRequest>& waitingRequests = + resource->mWaitingRequests; + + while ((acquiredRequests.size() < resource->mResourceCount) && + (!waitingRequests.empty())) { + MediaSystemResourceRequest& request = waitingRequests.front(); + MOZ_ASSERT(request.mParent); + // Send response + mozilla::Unused << request.mParent->SendResponse(request.mId, + true /* success */); + // Move request to mAcquiredRequests + acquiredRequests.push_back(waitingRequests.front()); + waitingRequests.pop_front(); + } +} + +} // namespace mozilla diff --git a/dom/media/systemservices/MediaSystemResourceService.h b/dom/media/systemservices/MediaSystemResourceService.h new file mode 100644 index 0000000000..8a75a6cafd --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceService.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(MediaSystemResourceService_h_) +# define MediaSystemResourceService_h_ + +# include <deque> + +# include "MediaSystemResourceTypes.h" +# include "mozilla/StaticPtr.h" +# include "nsClassHashtable.h" + +namespace mozilla { + +namespace media { +class MediaSystemResourceManagerParent; +} // namespace media + +/** + * Manage media system resource allocation requests within system. + */ +class MediaSystemResourceService { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceService) + + static MediaSystemResourceService* Get(); + static void Init(); + static void Shutdown(); + + void Acquire(media::MediaSystemResourceManagerParent* aParent, uint32_t aId, + MediaSystemResourceType aResourceType, bool aWillWait); + + void ReleaseResource(media::MediaSystemResourceManagerParent* aParent, + uint32_t aId, MediaSystemResourceType aResourceType); + + void ReleaseResource(media::MediaSystemResourceManagerParent* aParent); + + private: + MediaSystemResourceService(); + ~MediaSystemResourceService(); + + struct MediaSystemResourceRequest { + MediaSystemResourceRequest() : mParent(nullptr), mId(-1) {} + MediaSystemResourceRequest(media::MediaSystemResourceManagerParent* aParent, + uint32_t aId) + : mParent(aParent), mId(aId) {} + media::MediaSystemResourceManagerParent* mParent; + uint32_t mId; + }; + + struct MediaSystemResource { + MediaSystemResource() : mResourceCount(0) {} + explicit MediaSystemResource(uint32_t aResourceCount) + : mResourceCount(aResourceCount) {} + + std::deque<MediaSystemResourceRequest> mWaitingRequests; + std::deque<MediaSystemResourceRequest> mAcquiredRequests; + uint32_t mResourceCount; + }; + + void Destroy(); + + void RemoveRequest(media::MediaSystemResourceManagerParent* aParent, + uint32_t aId, MediaSystemResourceType aResourceType); + + void RemoveRequests(media::MediaSystemResourceManagerParent* aParent, + MediaSystemResourceType aResourceType); + + void UpdateRequests(MediaSystemResourceType aResourceType); + + bool mDestroyed; + + nsClassHashtable<nsUint32HashKey, MediaSystemResource> mResources; + + static StaticRefPtr<MediaSystemResourceService> sSingleton; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/systemservices/MediaSystemResourceTypes.h b/dom/media/systemservices/MediaSystemResourceTypes.h new file mode 100644 index 0000000000..d294c2b364 --- /dev/null +++ b/dom/media/systemservices/MediaSystemResourceTypes.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(MediaSystemResourceTypes_h_) +# define MediaSystemResourceTypes_h_ + +namespace mozilla { + +enum class MediaSystemResourceType : uint32_t { + VIDEO_DECODER = 0, + AUDIO_DECODER, // Not supported currently. + VIDEO_ENCODER, + AUDIO_ENCODER, // Not supported currently. + CAMERA, // Not supported currently. + INVALID_RESOURCE, +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/systemservices/MediaTaskUtils.h b/dom/media/systemservices/MediaTaskUtils.h new file mode 100644 index 0000000000..cbe464e015 --- /dev/null +++ b/dom/media/systemservices/MediaTaskUtils.h @@ -0,0 +1,52 @@ +/* -*- 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/. */ + +#ifndef mozilla_MediaTaskUtils_h +#define mozilla_MediaTaskUtils_h + +#include "nsThreadUtils.h" + +// The main reason this file is separate from MediaUtils.h +#include "base/task.h" + +namespace mozilla { +namespace media { + +/* media::NewTaskFrom() - Create a Task from a lambda. + * + * Similar to media::NewRunnableFrom() - Create an nsRunnable from a lambda, + * but ignore the return value from the lambda. + * + * Prefer NS_NewRunnableFunction(), which provides a specific name, unless the + * lambda really must have a non-void return value that is to be ignored. + */ + +template <typename OnRunType> +class LambdaTask : public Runnable { + public: + explicit LambdaTask(OnRunType&& aOnRun) + : Runnable("media::LambdaTask"), mOnRun(std::move(aOnRun)) {} + + private: + NS_IMETHOD + Run() override { + mOnRun(); + return NS_OK; + } + OnRunType mOnRun; +}; + +template <typename OnRunType> +already_AddRefed<LambdaTask<OnRunType>> NewTaskFrom(OnRunType&& aOnRun) { + typedef LambdaTask<OnRunType> LambdaType; + RefPtr<LambdaType> lambda = new LambdaType(std::forward<OnRunType>(aOnRun)); + return lambda.forget(); +} + +} // namespace media +} // namespace mozilla + +#endif // mozilla_MediaTaskUtils_h diff --git a/dom/media/systemservices/MediaUtils.cpp b/dom/media/systemservices/MediaUtils.cpp new file mode 100644 index 0000000000..fad2fd4e2c --- /dev/null +++ b/dom/media/systemservices/MediaUtils.cpp @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MediaUtils.h" + +#include "mozilla/AppShutdown.h" +#include "mozilla/Services.h" + +namespace mozilla::media { + +nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier() { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + if (!svc) { + // We can fail to get the shutdown service if we're already shutting down. + return nullptr; + } + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier)); + if (!barrier) { + // We are probably in a content process. We need to do cleanup at + // XPCOM shutdown in leakchecking builds. + rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier)); + } + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + MOZ_RELEASE_ASSERT(barrier); + return barrier; +} + +nsCOMPtr<nsIAsyncShutdownClient> MustGetShutdownBarrier() { + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + MOZ_RELEASE_ASSERT(barrier); + return barrier; +} + +NS_IMPL_ISUPPORTS(ShutdownBlocker, nsIAsyncShutdownBlocker) + +namespace { +class TicketBlocker : public ShutdownBlocker { + using ShutdownMozPromise = ShutdownBlockingTicket::ShutdownMozPromise; + + public: + explicit TicketBlocker(const nsAString& aName) + : ShutdownBlocker(aName), mPromise(mHolder.Ensure(__func__)) {} + + NS_IMETHOD + BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override { + mHolder.Resolve(true, __func__); + return NS_OK; + } + + void RejectIfExists() { mHolder.RejectIfExists(false, __func__); } + + ShutdownMozPromise* ShutdownPromise() { return mPromise; } + + private: + ~TicketBlocker() = default; + + MozPromiseHolder<ShutdownMozPromise> mHolder; + const RefPtr<ShutdownMozPromise> mPromise; +}; + +class ShutdownBlockingTicketImpl : public ShutdownBlockingTicket { + private: + RefPtr<TicketBlocker> mBlocker; + + public: + explicit ShutdownBlockingTicketImpl(RefPtr<TicketBlocker> aBlocker) + : mBlocker(std::move(aBlocker)) {} + + static UniquePtr<ShutdownBlockingTicket> Create(const nsAString& aName, + const nsAString& aFileName, + int32_t aLineNr) { + auto blocker = MakeRefPtr<TicketBlocker>(aName); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ShutdownBlockingTicketImpl::AddBlocker", + [blocker, file = nsString(aFileName), aLineNr] { + MustGetShutdownBarrier()->AddBlocker(blocker, file, aLineNr, u""_ns); + })); + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) { + // Adding a blocker is not guaranteed to succeed. Remove the blocker in + // case it succeeded anyway, and bail. + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ShutdownBlockingTicketImpl::RemoveBlocker", [blocker] { + MustGetShutdownBarrier()->RemoveBlocker(blocker); + blocker->RejectIfExists(); + })); + return nullptr; + } + + // Adding a blocker is now guaranteed to succeed: + // - If AppShutdown::IsInOrBeyond(AppShutdown) returned false, + // - then the AddBlocker main thread task was queued before AppShutdown's + // sCurrentShutdownPhase is set to ShutdownPhase::AppShutdown, + // - which is before AppShutdown will drain the (main thread) event queue to + // run the AddBlocker task, if not already run, + // - which is before profile-before-change (the earliest barrier we'd add a + // blocker to, see GetShutdownBarrier()) is notified, + // - which is when AsyncShutdown prevents further conditions (blockers) + // being added to the profile-before-change barrier. + return MakeUnique<ShutdownBlockingTicketImpl>(std::move(blocker)); + } + + ~ShutdownBlockingTicketImpl() { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] { + GetShutdownBarrier()->RemoveBlocker(blocker); + blocker->RejectIfExists(); + }))); + } + + ShutdownMozPromise* ShutdownPromise() override { + return mBlocker->ShutdownPromise(); + } +}; +} // namespace + +UniquePtr<ShutdownBlockingTicket> ShutdownBlockingTicket::Create( + const nsAString& aName, const nsAString& aFileName, int32_t aLineNr) { + return ShutdownBlockingTicketImpl::Create(aName, aFileName, aLineNr); +} + +} // namespace mozilla::media diff --git a/dom/media/systemservices/MediaUtils.h b/dom/media/systemservices/MediaUtils.h new file mode 100644 index 0000000000..90e21ce9aa --- /dev/null +++ b/dom/media/systemservices/MediaUtils.h @@ -0,0 +1,332 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_MediaUtils_h +#define mozilla_MediaUtils_h + +#include <map> + +#include "mozilla/Assertions.h" +#include "mozilla/Monitor.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SharedThreadPool.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/UniquePtr.h" +#include "MediaEventSource.h" +#include "nsCOMPtr.h" +#include "nsIAsyncShutdown.h" +#include "nsISupportsImpl.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" + +class nsIEventTarget; + +namespace mozilla::media { + +/* media::NewRunnableFrom() - Create a Runnable from a lambda. + * + * Passing variables (closures) to an async function is clunky with Runnable: + * + * void Foo() + * { + * class FooRunnable : public Runnable + * { + * public: + * FooRunnable(const Bar &aBar) : mBar(aBar) {} + * NS_IMETHOD Run() override + * { + * // Use mBar + * } + * private: + * RefPtr<Bar> mBar; + * }; + * + * RefPtr<Bar> bar = new Bar(); + * NS_DispatchToMainThread(new FooRunnable(bar); + * } + * + * It's worse with more variables. Lambdas have a leg up with variable capture: + * + * void Foo() + * { + * RefPtr<Bar> bar = new Bar(); + * NS_DispatchToMainThread(media::NewRunnableFrom([bar]() mutable { + * // use bar + * })); + * } + * + * Capture is by-copy by default, so the nsRefPtr 'bar' is safely copied for + * access on the other thread (threadsafe refcounting in bar is assumed). + * + * The 'mutable' keyword is only needed for non-const access to bar. + */ + +template <typename OnRunType> +class LambdaRunnable : public Runnable { + public: + explicit LambdaRunnable(OnRunType&& aOnRun) + : Runnable("media::LambdaRunnable"), mOnRun(std::move(aOnRun)) {} + + private: + NS_IMETHODIMP + Run() override { return mOnRun(); } + OnRunType mOnRun; +}; + +template <typename OnRunType> +already_AddRefed<LambdaRunnable<OnRunType>> NewRunnableFrom( + OnRunType&& aOnRun) { + typedef LambdaRunnable<OnRunType> LambdaType; + RefPtr<LambdaType> lambda = new LambdaType(std::forward<OnRunType>(aOnRun)); + return lambda.forget(); +} + +/* media::Refcountable - Add threadsafe ref-counting to something that isn't. + * + * Often, reference counting is the most practical way to share an object with + * another thread without imposing lifetime restrictions, even if there's + * otherwise no concurrent access happening on the object. For instance, an + * algorithm on another thread may find it more expedient to modify a passed-in + * object, rather than pass expensive copies back and forth. + * + * Lists in particular often aren't ref-countable, yet are expensive to copy, + * e.g. nsTArray<RefPtr<Foo>>. Refcountable can be used to make such objects + * (or owning smart-pointers to such objects) refcountable. + * + * Technical limitation: A template specialization is needed for types that take + * a constructor. Please add below (UniquePtr covers a lot of ground though). + */ + +class RefcountableBase { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefcountableBase) + protected: + virtual ~RefcountableBase() = default; +}; + +template <typename T> +class Refcountable : public T, public RefcountableBase { + public: + Refcountable& operator=(T&& aOther) { + T::operator=(std::move(aOther)); + return *this; + } + + Refcountable& operator=(T& aOther) { + T::operator=(aOther); + return *this; + } +}; + +template <typename T> +class Refcountable<UniquePtr<T>> : public UniquePtr<T>, + public RefcountableBase { + public: + explicit Refcountable(T* aPtr) : UniquePtr<T>(aPtr) {} +}; + +template <> +class Refcountable<bool> : public RefcountableBase { + public: + explicit Refcountable(bool aValue) : mValue(aValue) {} + + Refcountable& operator=(bool aOther) { + mValue = aOther; + return *this; + } + + Refcountable& operator=(const Refcountable& aOther) { + mValue = aOther.mValue; + return *this; + } + + explicit operator bool() const { return mValue; } + + private: + bool mValue; +}; + +/* + * Async shutdown helpers + */ + +nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier(); + +// Like GetShutdownBarrier but will release assert that the result is not null. +nsCOMPtr<nsIAsyncShutdownClient> MustGetShutdownBarrier(); + +class ShutdownBlocker : public nsIAsyncShutdownBlocker { + public: + ShutdownBlocker(const nsAString& aName) : mName(aName) {} + + NS_IMETHOD + BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override = 0; + + NS_IMETHOD GetName(nsAString& aName) override { + aName = mName; + return NS_OK; + } + + NS_IMETHOD GetState(nsIPropertyBag**) override { return NS_OK; } + + NS_DECL_THREADSAFE_ISUPPORTS + protected: + virtual ~ShutdownBlocker() = default; + + private: + const nsString mName; +}; + +/** + * A convenience class representing a "ticket" that keeps the process from + * shutting down until it is destructed. It does this by blocking + * xpcom-will-shutdown. Constructed and destroyed on any thread. + */ +class ShutdownBlockingTicket { + public: + using ShutdownMozPromise = MozPromise<bool, bool, false>; + + /** + * Construct with an arbitrary name, __FILE__ and __LINE__. + * Note that __FILE__ needs to be made wide, typically through + * NS_LITERAL_STRING_FROM_CSTRING(__FILE__). + * Returns nullptr if we are too far in the shutdown sequence to add a + * blocker. Any thread. + */ + static UniquePtr<ShutdownBlockingTicket> Create(const nsAString& aName, + const nsAString& aFileName, + int32_t aLineNr); + + virtual ~ShutdownBlockingTicket() = default; + + /** + * MozPromise that gets resolved upon xpcom-will-shutdown. + * Should the ticket get destroyed before the MozPromise has been resolved, + * the MozPromise will get rejected. + */ + virtual ShutdownMozPromise* ShutdownPromise() = 0; +}; + +/** + * Await convenience methods to block until the promise has been resolved or + * rejected. The Resolve/Reject functions, while called on a different thread, + * would be running just as on the current thread thanks to the memory barrier + * provided by the monitor. + * For now Await can only be used with an exclusive MozPromise if passed a + * Resolve/Reject function. + * Await() can *NOT* be called from a task queue/nsISerialEventTarget used for + * resolving/rejecting aPromise, otherwise things will deadlock. + */ +template <typename ResolveValueType, typename RejectValueType, + typename ResolveFunction, typename RejectFunction> +void Await(already_AddRefed<nsIEventTarget> aPool, + RefPtr<MozPromise<ResolveValueType, RejectValueType, true>> aPromise, + ResolveFunction&& aResolveFunction, + RejectFunction&& aRejectFunction) { + RefPtr<TaskQueue> taskQueue = + TaskQueue::Create(std::move(aPool), "MozPromiseAwait"); + Monitor mon MOZ_UNANNOTATED(__func__); + bool done = false; + + aPromise->Then( + taskQueue, __func__, + [&](ResolveValueType&& aResolveValue) { + MonitorAutoLock lock(mon); + aResolveFunction(std::forward<ResolveValueType>(aResolveValue)); + done = true; + mon.Notify(); + }, + [&](RejectValueType&& aRejectValue) { + MonitorAutoLock lock(mon); + aRejectFunction(std::forward<RejectValueType>(aRejectValue)); + done = true; + mon.Notify(); + }); + + MonitorAutoLock lock(mon); + while (!done) { + mon.Wait(); + } +} + +template <typename ResolveValueType, typename RejectValueType, bool Excl> +typename MozPromise<ResolveValueType, RejectValueType, + Excl>::ResolveOrRejectValue +Await(already_AddRefed<nsIEventTarget> aPool, + RefPtr<MozPromise<ResolveValueType, RejectValueType, Excl>> aPromise) { + RefPtr<TaskQueue> taskQueue = + TaskQueue::Create(std::move(aPool), "MozPromiseAwait"); + Monitor mon MOZ_UNANNOTATED(__func__); + bool done = false; + + typename MozPromise<ResolveValueType, RejectValueType, + Excl>::ResolveOrRejectValue val; + aPromise->Then( + taskQueue, __func__, + [&](ResolveValueType aResolveValue) { + val.SetResolve(std::move(aResolveValue)); + MonitorAutoLock lock(mon); + done = true; + mon.Notify(); + }, + [&](RejectValueType aRejectValue) { + val.SetReject(std::move(aRejectValue)); + MonitorAutoLock lock(mon); + done = true; + mon.Notify(); + }); + + MonitorAutoLock lock(mon); + while (!done) { + mon.Wait(); + } + + return val; +} + +/** + * Similar to Await, takes an array of promises of the same type. + * MozPromise::All is used to handle the resolution/rejection of the promises. + */ +template <typename ResolveValueType, typename RejectValueType, + typename ResolveFunction, typename RejectFunction> +void AwaitAll( + already_AddRefed<nsIEventTarget> aPool, + nsTArray<RefPtr<MozPromise<ResolveValueType, RejectValueType, true>>>& + aPromises, + ResolveFunction&& aResolveFunction, RejectFunction&& aRejectFunction) { + typedef MozPromise<ResolveValueType, RejectValueType, true> Promise; + RefPtr<nsIEventTarget> pool = aPool; + RefPtr<TaskQueue> taskQueue = + TaskQueue::Create(do_AddRef(pool), "MozPromiseAwaitAll"); + RefPtr<typename Promise::AllPromiseType> p = + Promise::All(taskQueue, aPromises); + Await(pool.forget(), p, std::move(aResolveFunction), + std::move(aRejectFunction)); +} + +// Note: only works with exclusive MozPromise, as Promise::All would attempt +// to perform copy of nsTArrays which are disallowed. +template <typename ResolveValueType, typename RejectValueType> +typename MozPromise<ResolveValueType, RejectValueType, + true>::AllPromiseType::ResolveOrRejectValue +AwaitAll(already_AddRefed<nsIEventTarget> aPool, + nsTArray<RefPtr<MozPromise<ResolveValueType, RejectValueType, true>>>& + aPromises) { + typedef MozPromise<ResolveValueType, RejectValueType, true> Promise; + RefPtr<nsIEventTarget> pool = aPool; + RefPtr<TaskQueue> taskQueue = + TaskQueue::Create(do_AddRef(pool), "MozPromiseAwaitAll"); + RefPtr<typename Promise::AllPromiseType> p = + Promise::All(taskQueue, aPromises); + return Await(pool.forget(), p); +} + +} // namespace mozilla::media + +#endif // mozilla_MediaUtils_h diff --git a/dom/media/systemservices/OSXRunLoopSingleton.cpp b/dom/media/systemservices/OSXRunLoopSingleton.cpp new file mode 100644 index 0000000000..6dea084e27 --- /dev/null +++ b/dom/media/systemservices/OSXRunLoopSingleton.cpp @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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 "OSXRunLoopSingleton.h" +#include <mozilla/StaticMutex.h> + +#include <AudioUnit/AudioUnit.h> +#include <CoreAudio/AudioHardware.h> +#include <CoreAudio/HostTime.h> +#include <CoreFoundation/CoreFoundation.h> + +static bool gRunLoopSet = false; +static mozilla::StaticMutex gMutex MOZ_UNANNOTATED; + +void mozilla_set_coreaudio_notification_runloop_if_needed() { + mozilla::StaticMutexAutoLock lock(gMutex); + if (gRunLoopSet) { + return; + } + + /* This is needed so that AudioUnit listeners get called on this thread, and + * not the main thread. If we don't do that, they are not called, or a crash + * occur, depending on the OSX version. */ + AudioObjectPropertyAddress runloop_address = { + kAudioHardwarePropertyRunLoop, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster}; + + CFRunLoopRef run_loop = nullptr; + + OSStatus r; + r = AudioObjectSetPropertyData(kAudioObjectSystemObject, &runloop_address, 0, + NULL, sizeof(CFRunLoopRef), &run_loop); + if (r != noErr) { + NS_WARNING( + "Could not make global CoreAudio notifications use their own thread."); + } + + gRunLoopSet = true; +} diff --git a/dom/media/systemservices/OSXRunLoopSingleton.h b/dom/media/systemservices/OSXRunLoopSingleton.h new file mode 100644 index 0000000000..10e7b0153f --- /dev/null +++ b/dom/media/systemservices/OSXRunLoopSingleton.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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/. */ + +#ifndef OSXRUNLOOPSINGLETON_H_ +#define OSXRUNLOOPSINGLETON_H_ + +#include <mozilla/Types.h> + +#if defined(__cplusplus) +extern "C" { +#endif + +/* This function tells CoreAudio to use its own thread for device change + * notifications, and can be called from any thread without external + * synchronization. */ +void MOZ_EXPORT mozilla_set_coreaudio_notification_runloop_if_needed(); + +#if defined(__cplusplus) +} +#endif + +#endif // OSXRUNLOOPSINGLETON_H_ diff --git a/dom/media/systemservices/PCameras.ipdl b/dom/media/systemservices/PCameras.ipdl new file mode 100644 index 0000000000..59168fdcc1 --- /dev/null +++ b/dom/media/systemservices/PCameras.ipdl @@ -0,0 +1,93 @@ +/* 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 protocol PContent; +include protocol PBackground; + +include PBackgroundSharedTypes; + +using mozilla::camera::CaptureEngine from "mozilla/media/CamerasTypes.h"; + +namespace mozilla { +namespace camera { + +// IPC analog for webrtc::VideoCaptureCapability +struct VideoCaptureCapability +{ + int width; + int height; + int maxFPS; + int videoType; + bool interlaced; +}; + + +// IPC analog for webrtc::VideoFrame +// the described buffer is transported seperately in a Shmem +// See VideoFrameUtils.h +struct VideoFrameProperties +{ + // Size of image data within the ShMem, + // the ShMem is at least this large + uint32_t bufferSize; + // From webrtc::VideoFrame + uint32_t timeStamp; + int64_t ntpTimeMs; + int64_t renderTimeMs; + // See webrtc/**/rotation.h + int rotation; + int yAllocatedSize; + int uAllocatedSize; + int vAllocatedSize; + // From webrtc::VideoFrameBuffer + int width; + int height; + int yStride; + int uStride; + int vStride; +}; + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +async protocol PCameras +{ + manager PBackground; + +child: + // transfers ownership of |buffer| from parent to child + async DeliverFrame(CaptureEngine capEngine, int streamId, + Shmem buffer, VideoFrameProperties props); + async DeviceChange(); + async ReplyNumberOfCaptureDevices(int deviceCount); + async ReplyNumberOfCapabilities(int capabilityCount); + async ReplyAllocateCapture(int captureId); + async ReplyGetCaptureCapability(VideoCaptureCapability cap); + async ReplyGetCaptureDevice(nsCString device_name, nsCString device_id, bool scary, bool placeholder); + async ReplyFailure(); + async ReplySuccess(); + async __delete__(); + +parent: + async NumberOfCaptureDevices(CaptureEngine engine); + async NumberOfCapabilities(CaptureEngine engine, nsCString deviceUniqueIdUTF8); + + async GetCaptureCapability(CaptureEngine engine, nsCString unique_idUTF8, + int capability_number); + async GetCaptureDevice(CaptureEngine engine, int deviceIndex); + + async AllocateCapture(CaptureEngine engine, nsCString unique_idUTF8, + uint64_t windowID); + async ReleaseCapture(CaptureEngine engine, int captureId); + async StartCapture(CaptureEngine engine, int captureId, + VideoCaptureCapability capability); + async FocusOnSelectedSource(CaptureEngine engine, int captureId); + async StopCapture(CaptureEngine engine, int captureId); + // transfers frame back + async ReleaseFrame(Shmem s); + + // setup camera engine + async EnsureInitialized(CaptureEngine engine); +}; + +} // namespace camera +} // namespace mozilla diff --git a/dom/media/systemservices/PMedia.ipdl b/dom/media/systemservices/PMedia.ipdl new file mode 100644 index 0000000000..c3d8476b5f --- /dev/null +++ b/dom/media/systemservices/PMedia.ipdl @@ -0,0 +1,55 @@ +/* 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 protocol PContent; + +include PBackgroundSharedTypes; + +include "mozilla/media/MediaChild.h"; + +namespace mozilla { +namespace media { + +[ManualDealloc, ChildImpl="Child", ParentImpl=virtual] +protocol PMedia +{ + manager PContent; + +parent: + /** + * Requests a potentially persistent unique secret key for each principal. + * Has no expiry, but is cleared by age along with cookies. + * This is needed by mediaDevices.enumerateDevices() to produce persistent + * deviceIds that wont work cross-origin. + * + * If this OriginAttributes dictionary has the privateBrowsing flag set to + * false, a key for this origin is returned from a primary pool of temporal + * in-memory keys and persistent keys read from disk. If no key exists, a + * temporal one is created. + * If aPersist is true and key is temporal, the key is promoted to persistent. + * Once persistent, a key cannot become temporal again. + * + * If the OriginAttributes dictionary has the privateBrowsing flag set to + * true, a different key for this origin is returned from a secondary pool + * that is never persisted to disk, and aPersist is ignored. + */ + async GetPrincipalKey(PrincipalInfo aPrincipal, bool aPersist) returns(nsCString aKey); + + /** + * Clear per-orgin list of persistent deviceIds stored for enumerateDevices + * Fire and forget. + * + * aSinceTime - milliseconds since 1 January 1970 00:00:00 UTC. 0 = clear all + * + * aOnlyPrivateBrowsing - if true then only purge the separate in-memory + * per-origin list used in Private Browsing. + */ + async SanitizeOriginKeys(uint64_t aSinceWhen, bool aOnlyPrivateBrowsing); + +child: + async __delete__(); +}; + +} // namespace media +} // namespace mozilla diff --git a/dom/media/systemservices/PMediaSystemResourceManager.ipdl b/dom/media/systemservices/PMediaSystemResourceManager.ipdl new file mode 100644 index 0000000000..a682de60bf --- /dev/null +++ b/dom/media/systemservices/PMediaSystemResourceManager.ipdl @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 protocol PImageBridge; +include "mozilla/media/MediaSystemResourceMessageUtils.h"; + +using mozilla::MediaSystemResourceType from "mozilla/media/MediaSystemResourceTypes.h"; + +namespace mozilla { +namespace media { + +/* + * The PMediaSystemResourceManager is a sub-protocol in PImageBridge + */ +[ManualDealloc] +sync protocol PMediaSystemResourceManager +{ + manager PImageBridge; + +child: + async Response(uint32_t aId, bool aSuccess); + async __delete__(); + +parent: + async Acquire(uint32_t aId, MediaSystemResourceType aResourceType, bool aWillWait); + async Release(uint32_t aId); + + /** + * Asynchronously tell the parent side to remove the PMediaSystemResourceManager. + */ + async RemoveResourceManager(); +}; + +} // namespace media +} // namespace mozilla + diff --git a/dom/media/systemservices/ShmemPool.cpp b/dom/media/systemservices/ShmemPool.cpp new file mode 100644 index 0000000000..c39ed5e790 --- /dev/null +++ b/dom/media/systemservices/ShmemPool.cpp @@ -0,0 +1,99 @@ +/* -*- 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 "mozilla/ShmemPool.h" + +#include <utility> + +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" + +mozilla::LazyLogModule sShmemPoolLog("ShmemPool"); + +#define SHMEMPOOL_LOG_VERBOSE(args) \ + MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Verbose, args) + +namespace mozilla { + +ShmemPool::ShmemPool(size_t aPoolSize, PoolType aPoolType) + : mPoolType(aPoolType), + mMutex("mozilla::ShmemPool"), + mPoolFree(aPoolSize), + mErrorLogged(false) +#ifdef DEBUG + , + mMaxPoolUse(0) +#endif +{ + mShmemPool.SetLength(aPoolSize); +} + +mozilla::ShmemBuffer ShmemPool::GetIfAvailable(size_t aSize) { + MutexAutoLock lock(mMutex); + + // Pool is empty, don't block caller. + if (mPoolFree == 0) { + if (!mErrorLogged) { + // log "out of pool" once as error to avoid log spam + mErrorLogged = true; + SHMEMPOOL_LOG_ERROR( + ("ShmemPool is empty, future occurrences " + "will be logged as warnings")); + } else { + SHMEMPOOL_LOG_WARN(("ShmemPool is empty")); + } + // This isn't initialized, so will be understood as an error. + return ShmemBuffer(); + } + + ShmemBuffer& res = mShmemPool[mPoolFree - 1]; + + if (!res.mInitialized) { + SHMEMPOOL_LOG(("No free preallocated Shmem")); + return ShmemBuffer(); + } + + MOZ_ASSERT(res.mShmem.IsWritable(), "Pool in Shmem is not writable?"); + + if (res.mShmem.Size<uint8_t>() < aSize) { + SHMEMPOOL_LOG(("Free Shmem but not of the right size")); + return ShmemBuffer(); + } + + mPoolFree--; +#ifdef DEBUG + size_t poolUse = mShmemPool.Length() - mPoolFree; + if (poolUse > mMaxPoolUse) { + mMaxPoolUse = poolUse; + SHMEMPOOL_LOG( + ("Maximum ShmemPool use increased: %zu buffers", mMaxPoolUse)); + } +#endif + return std::move(res); +} + +void ShmemPool::Put(ShmemBuffer&& aShmem) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mPoolFree < mShmemPool.Length()); + mShmemPool[mPoolFree] = std::move(aShmem); + mPoolFree++; +#ifdef DEBUG + size_t poolUse = mShmemPool.Length() - mPoolFree; + if (poolUse > 0) { + SHMEMPOOL_LOG_VERBOSE(("ShmemPool usage reduced to %zu buffers", poolUse)); + } +#endif +} + +ShmemPool::~ShmemPool() { +#ifdef DEBUG + for (size_t i = 0; i < mShmemPool.Length(); i++) { + MOZ_ASSERT(!mShmemPool[i].Valid()); + } +#endif +} + +} // namespace mozilla diff --git a/dom/media/systemservices/ShmemPool.h b/dom/media/systemservices/ShmemPool.h new file mode 100644 index 0000000000..e62ccff24a --- /dev/null +++ b/dom/media/systemservices/ShmemPool.h @@ -0,0 +1,181 @@ +/* -*- 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/. */ + +#ifndef mozilla_ShmemPool_h +#define mozilla_ShmemPool_h + +#include "mozilla/Mutex.h" +#include "mozilla/ipc/Shmem.h" +#include "nsTArray.h" + +extern mozilla::LazyLogModule sShmemPoolLog; +#define SHMEMPOOL_LOG(args) \ + MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Debug, args) +#define SHMEMPOOL_LOG_WARN(args) \ + MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Warning, args) +#define SHMEMPOOL_LOG_ERROR(args) \ + MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Error, args) + +namespace mozilla { + +class ShmemPool; + +class ShmemBuffer { + public: + ShmemBuffer() : mInitialized(false) {} + explicit ShmemBuffer(mozilla::ipc::Shmem aShmem) { + mInitialized = true; + mShmem = aShmem; + } + + ShmemBuffer(ShmemBuffer&& rhs) { + mInitialized = rhs.mInitialized; + mShmem = std::move(rhs.mShmem); + } + + ShmemBuffer& operator=(ShmemBuffer&& rhs) { + MOZ_ASSERT(&rhs != this, "self-moves are prohibited"); + mInitialized = rhs.mInitialized; + mShmem = std::move(rhs.mShmem); + return *this; + } + + // No copies allowed + ShmemBuffer(const ShmemBuffer&) = delete; + ShmemBuffer& operator=(const ShmemBuffer&) = delete; + + bool Valid() { return mInitialized; } + + uint8_t* GetBytes() { return mShmem.get<uint8_t>(); } + + mozilla::ipc::Shmem& Get() { return mShmem; } + + private: + friend class ShmemPool; + + bool mInitialized; + mozilla::ipc::Shmem mShmem; +}; + +class ShmemPool final { + public: + enum class PoolType { StaticPool, DynamicPool }; + explicit ShmemPool(size_t aPoolSize, + PoolType aPoolType = PoolType::StaticPool); + ~ShmemPool(); + // Get/GetIfAvailable differ in what thread they can run on. GetIfAvailable + // can run anywhere but won't allocate if the right size isn't available. + ShmemBuffer GetIfAvailable(size_t aSize); + void Put(ShmemBuffer&& aShmem); + + // We need to use the allocation/deallocation functions + // of a specific IPC child/parent instance. + template <class T> + void Cleanup(T* aInstance) { + MutexAutoLock lock(mMutex); + for (size_t i = 0; i < mShmemPool.Length(); i++) { + if (mShmemPool[i].mInitialized) { + aInstance->DeallocShmem(mShmemPool[i].Get()); + mShmemPool[i].mInitialized = false; + } + } + } + + enum class AllocationPolicy { Default, Unsafe }; + + template <class T> + ShmemBuffer Get(T* aInstance, size_t aSize, + AllocationPolicy aPolicy = AllocationPolicy::Default) { + MutexAutoLock lock(mMutex); + + // Pool is empty, don't block caller. + if (mPoolFree == 0 && mPoolType == PoolType::StaticPool) { + if (!mErrorLogged) { + // log "out of pool" once as error to avoid log spam + mErrorLogged = true; + SHMEMPOOL_LOG_ERROR( + ("ShmemPool is empty, future occurrences " + "will be logged as warnings")); + } else { + SHMEMPOOL_LOG_WARN(("ShmemPool is empty")); + } + // This isn't initialized, so will be understood as an error. + return ShmemBuffer(); + } + if (mPoolFree == 0) { + MOZ_ASSERT(mPoolType == PoolType::DynamicPool); + SHMEMPOOL_LOG(("Dynamic ShmemPool empty, allocating extra Shmem buffer")); + ShmemBuffer newBuffer; + mShmemPool.InsertElementAt(0, std::move(newBuffer)); + mPoolFree++; + } + + ShmemBuffer& res = mShmemPool[mPoolFree - 1]; + + if (!res.mInitialized) { + SHMEMPOOL_LOG(("Initializing new Shmem in pool")); + if (!AllocateShmem(aInstance, aSize, res, aPolicy)) { + SHMEMPOOL_LOG(("Failure allocating new Shmem buffer")); + return ShmemBuffer(); + } + res.mInitialized = true; + } + + MOZ_DIAGNOSTIC_ASSERT(res.mShmem.IsWritable(), + "Shmem in Pool is not writable?"); + + // Prepare buffer, increase size if needed (we never shrink as we don't + // maintain seperate sized pools and we don't want to keep reallocating) + if (res.mShmem.Size<char>() < aSize) { + SHMEMPOOL_LOG(("Size change/increase in Shmem Pool")); + aInstance->DeallocShmem(res.mShmem); + res.mInitialized = false; + // this may fail; always check return value + if (!AllocateShmem(aInstance, aSize, res, aPolicy)) { + SHMEMPOOL_LOG(("Failure allocating resized Shmem buffer")); + return ShmemBuffer(); + } else { + res.mInitialized = true; + } + } + + MOZ_ASSERT(res.mShmem.IsWritable(), + "Shmem in Pool is not writable post resize?"); + + mPoolFree--; +#ifdef DEBUG + size_t poolUse = mShmemPool.Length() - mPoolFree; + if (poolUse > mMaxPoolUse) { + mMaxPoolUse = poolUse; + SHMEMPOOL_LOG( + ("Maximum ShmemPool use increased: %zu buffers", mMaxPoolUse)); + } +#endif + return std::move(res); + } + + private: + template <class T> + bool AllocateShmem(T* aInstance, size_t aSize, ShmemBuffer& aRes, + AllocationPolicy aPolicy) { + return (aPolicy == AllocationPolicy::Default && + aInstance->AllocShmem(aSize, &aRes.mShmem)) || + (aPolicy == AllocationPolicy::Unsafe && + aInstance->AllocUnsafeShmem(aSize, &aRes.mShmem)); + } + const PoolType mPoolType; + Mutex mMutex MOZ_UNANNOTATED; + size_t mPoolFree; + bool mErrorLogged; +#ifdef DEBUG + size_t mMaxPoolUse; +#endif + nsTArray<ShmemBuffer> mShmemPool; +}; + +} // namespace mozilla + +#endif // mozilla_ShmemPool_h diff --git a/dom/media/systemservices/VideoEngine.cpp b/dom/media/systemservices/VideoEngine.cpp new file mode 100644 index 0000000000..5aec39b638 --- /dev/null +++ b/dom/media/systemservices/VideoEngine.cpp @@ -0,0 +1,229 @@ +/* -*- 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 "VideoEngine.h" +#include "libwebrtcglue/SystemTime.h" +#include "system_wrappers/include/clock.h" +#include "video_engine/video_capture_factory.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/jni/Utils.h" +#endif + +#if defined(ANDROID) +namespace webrtc { +int32_t SetCaptureAndroidVM(JavaVM* javaVM); +} +#endif + +namespace mozilla::camera { + +#undef LOG +#undef LOG_ENABLED +mozilla::LazyLogModule gVideoEngineLog("VideoEngine"); +#define LOG(args) MOZ_LOG(gVideoEngineLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(gVideoEngineLog, mozilla::LogLevel::Debug) + +#if defined(ANDROID) +int VideoEngine::SetAndroidObjects() { + LOG(("%s", __PRETTY_FUNCTION__)); + + JavaVM* const javaVM = mozilla::jni::GetVM(); + if (!javaVM || webrtc::SetCaptureAndroidVM(javaVM) != 0) { + LOG(("Could not set capture Android VM")); + return -1; + } +# ifdef WEBRTC_INCLUDE_INTERNAL_VIDEO_RENDER + if (webrtc::SetRenderAndroidVM(javaVM) != 0) { + LOG(("Could not set render Android VM")); + return -1; + } +# endif + return 0; +} +#endif + +int32_t VideoEngine::CreateVideoCapture(const char* aDeviceUniqueIdUTF8) { + LOG(("%s", __PRETTY_FUNCTION__)); + MOZ_ASSERT(aDeviceUniqueIdUTF8); + + int32_t id = GenerateId(); + LOG(("CaptureDeviceInfo.type=%s id=%d", mCaptureDevInfo.TypeName(), id)); + + for (auto& it : mCaps) { + if (it.second.VideoCapture() && + it.second.VideoCapture()->CurrentDeviceName() && + strcmp(it.second.VideoCapture()->CurrentDeviceName(), + aDeviceUniqueIdUTF8) == 0) { + mIdMap.emplace(id, it.first); + return id; + } + } + + CaptureEntry entry = {-1, nullptr}; + + entry = CaptureEntry(id, mVideoCaptureFactory->CreateVideoCapture( + id, aDeviceUniqueIdUTF8, mCaptureDevInfo.type)); + + mCaps.emplace(id, std::move(entry)); + mIdMap.emplace(id, id); + return id; +} + +int VideoEngine::ReleaseVideoCapture(const int32_t aId) { + bool found = false; + +#ifdef DEBUG + { + auto it = mIdMap.find(aId); + MOZ_ASSERT(it != mIdMap.end()); + Unused << it; + } +#endif + + for (auto& it : mIdMap) { + if (it.first != aId && it.second == mIdMap[aId]) { + // There are other tracks still using this hardware. + found = true; + } + } + + if (!found) { + WithEntry(aId, [&found](CaptureEntry& cap) { + cap.mVideoCaptureModule = nullptr; + found = true; + }); + MOZ_ASSERT(found); + if (found) { + auto it = mCaps.find(mIdMap[aId]); + MOZ_ASSERT(it != mCaps.end()); + mCaps.erase(it); + } + } + + mIdMap.erase(aId); + return found ? 0 : (-1); +} + +std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> +VideoEngine::GetOrCreateVideoCaptureDeviceInfo() { + LOG(("%s", __PRETTY_FUNCTION__)); + webrtc::Timestamp currentTime = webrtc::Timestamp::Micros(0); + + const char* capDevTypeName = + CaptureDeviceInfo(mCaptureDevInfo.type).TypeName(); + + if (mDeviceInfo) { + LOG(("Device cache available.")); + // Camera cache is invalidated by HW change detection elsewhere + if (mCaptureDevInfo.type == CaptureDeviceType::Camera) { + LOG(("returning cached CaptureDeviceInfo of type %s", capDevTypeName)); + return mDeviceInfo; + } + // Screen sharing cache is invalidated after the expiration time + currentTime = WebrtcSystemTime(); + LOG(("Checking expiry, fetched current time of: %" PRId64, + currentTime.ms())); + LOG(("device cache expiration is %" PRId64, mExpiryTime.ms())); + if (currentTime <= mExpiryTime) { + LOG(("returning cached CaptureDeviceInfo of type %s", capDevTypeName)); + return mDeviceInfo; + } + } + + if (currentTime.IsZero()) { + currentTime = WebrtcSystemTime(); + LOG(("Fetched current time of: %" PRId64, currentTime.ms())); + } + mExpiryTime = currentTime + webrtc::TimeDelta::Millis(kCacheExpiryPeriodMs); + LOG(("new device cache expiration is %" PRId64, mExpiryTime.ms())); + LOG(("creating a new VideoCaptureDeviceInfo of type %s", capDevTypeName)); + +#ifdef MOZ_WIDGET_ANDROID + if (mCaptureDevInfo.type == CaptureDeviceType::Camera) { + if (SetAndroidObjects()) { + LOG(("VideoEngine::SetAndroidObjects Failed")); + return mDeviceInfo; + } + } +#endif + + mDeviceInfo = + mVideoCaptureFactory->CreateDeviceInfo(mId, mCaptureDevInfo.type); + + LOG(("EXIT %s", __PRETTY_FUNCTION__)); + return mDeviceInfo; +} + +void VideoEngine::ClearVideoCaptureDeviceInfo() { + LOG(("%s", __PRETTY_FUNCTION__)); + mDeviceInfo.reset(); +} + +already_AddRefed<VideoEngine> VideoEngine::Create( + const CaptureDeviceType& aCaptureDeviceType, + RefPtr<VideoCaptureFactory> aVideoCaptureFactory) { + LOG(("%s", __PRETTY_FUNCTION__)); + return do_AddRef( + new VideoEngine(aCaptureDeviceType, std::move(aVideoCaptureFactory))); +} + +VideoEngine::CaptureEntry::CaptureEntry( + int32_t aCapnum, rtc::scoped_refptr<webrtc::VideoCaptureModule> aCapture) + : mCapnum(aCapnum), mVideoCaptureModule(aCapture) {} + +rtc::scoped_refptr<webrtc::VideoCaptureModule> +VideoEngine::CaptureEntry::VideoCapture() { + return mVideoCaptureModule; +} + +int32_t VideoEngine::CaptureEntry::Capnum() const { return mCapnum; } + +bool VideoEngine::WithEntry( + const int32_t entryCapnum, + const std::function<void(CaptureEntry& entry)>&& fn) { +#ifdef DEBUG + { + auto it = mIdMap.find(entryCapnum); + MOZ_ASSERT(it != mIdMap.end()); + Unused << it; + } +#endif + + auto it = mCaps.find(mIdMap[entryCapnum]); + MOZ_ASSERT(it != mCaps.end()); + if (it == mCaps.end()) { + return false; + } + fn(it->second); + return true; +} + +int32_t VideoEngine::GenerateId() { + // XXX Something better than this (a map perhaps, or a simple boolean TArray, + // given the number in-use is O(1) normally!) + static int sId = 0; + return mId = sId++; +} + +VideoEngine::VideoEngine(const CaptureDeviceType& aCaptureDeviceType, + RefPtr<VideoCaptureFactory> aVideoCaptureFactory) + : mId(0), + mCaptureDevInfo(aCaptureDeviceType), + mVideoCaptureFactory(std::move(aVideoCaptureFactory)), + mDeviceInfo(nullptr) { + MOZ_ASSERT(mVideoCaptureFactory); + LOG(("%s", __PRETTY_FUNCTION__)); + LOG(("Creating new VideoEngine with CaptureDeviceType %s", + mCaptureDevInfo.TypeName())); +} + +VideoEngine::~VideoEngine() { + MOZ_ASSERT(mCaps.empty()); + MOZ_ASSERT(mIdMap.empty()); +} + +} // namespace mozilla::camera diff --git a/dom/media/systemservices/VideoEngine.h b/dom/media/systemservices/VideoEngine.h new file mode 100644 index 0000000000..d9d2aebd13 --- /dev/null +++ b/dom/media/systemservices/VideoEngine.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +#ifndef mozilla_VideoEngine_h +#define mozilla_VideoEngine_h + +#include "MediaEngine.h" +#include "VideoFrameUtils.h" +#include "mozilla/media/MediaUtils.h" +#include "modules/video_capture/video_capture.h" +#include <memory> +#include <functional> + +namespace mozilla { +class VideoCaptureFactory; +} + +namespace mozilla::camera { + +enum class CaptureDeviceType { Camera, Screen, Window, Browser }; + +struct CaptureDeviceInfo { + CaptureDeviceType type; + + CaptureDeviceInfo() : type(CaptureDeviceType::Camera) {} + explicit CaptureDeviceInfo(CaptureDeviceType t) : type(t) {} + + const char* TypeName() const { + switch (type) { + case CaptureDeviceType::Camera: { + return "Camera"; + } + case CaptureDeviceType::Screen: { + return "Screen"; + } + case CaptureDeviceType::Window: { + return "Window"; + } + case CaptureDeviceType::Browser: { + return "Browser"; + } + } + assert(false); + return "UNKOWN-CaptureDeviceType!"; + } +}; + +// Historically the video engine was part of webrtc +// it was removed (and reimplemented in Talk) +class VideoEngine { + private: + virtual ~VideoEngine(); + + // Base cache expiration period + // Note because cameras use HW plug event detection, this + // only applies to screen based modes. + static const int64_t kCacheExpiryPeriodMs = 2000; + + public: + NS_INLINE_DECL_REFCOUNTING(VideoEngine) + + static already_AddRefed<VideoEngine> Create( + const CaptureDeviceType& aCaptureDeviceType, + RefPtr<VideoCaptureFactory> aVideoCaptureFactory); +#if defined(ANDROID) + static int SetAndroidObjects(); +#endif + /** Returns a non-negative capture identifier or -1 on failure. + */ + int32_t CreateVideoCapture(const char* aDeviceUniqueIdUTF8); + + int ReleaseVideoCapture(const int32_t aId); + + // VideoEngine is responsible for any cleanup in its modules + static void Delete(VideoEngine* aEngine) {} + + /** Returns an existing or creates a new new DeviceInfo. + * Camera info is cached to prevent repeated lengthy polling for "realness" + * of the hardware devices. Other types of capture, e.g. screen share info, + * are cached for 1 second. This could be handled in a more elegant way in + * the future. + * @return on failure the shared_ptr will be null, otherwise it will contain + * a DeviceInfo. + * @see bug 1305212 https://bugzilla.mozilla.org/show_bug.cgi?id=1305212 + */ + std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> + GetOrCreateVideoCaptureDeviceInfo(); + + /** + * Destroys existing DeviceInfo. + * The DeviceInfo will be recreated the next time it is needed. + */ + void ClearVideoCaptureDeviceInfo(); + + class CaptureEntry { + public: + CaptureEntry(int32_t aCapnum, + rtc::scoped_refptr<webrtc::VideoCaptureModule> aCapture); + int32_t Capnum() const; + rtc::scoped_refptr<webrtc::VideoCaptureModule> VideoCapture(); + + private: + int32_t mCapnum; + rtc::scoped_refptr<webrtc::VideoCaptureModule> mVideoCaptureModule; + friend class VideoEngine; + }; + + // Returns true iff an entry for capnum exists + bool WithEntry(const int32_t entryCapnum, + const std::function<void(CaptureEntry& entry)>&& fn); + + private: + VideoEngine(const CaptureDeviceType& aCaptureDeviceType, + RefPtr<VideoCaptureFactory> aVideoCaptureFactory); + int32_t mId; + const CaptureDeviceInfo mCaptureDevInfo; + RefPtr<VideoCaptureFactory> mVideoCaptureFactory; + std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> mDeviceInfo; + std::map<int32_t, CaptureEntry> mCaps; + std::map<int32_t, int32_t> mIdMap; + // The validity period for non-camera capture device infos` + webrtc::Timestamp mExpiryTime = webrtc::Timestamp::Micros(0); + int32_t GenerateId(); +}; +} // namespace mozilla::camera +#endif diff --git a/dom/media/systemservices/VideoFrameUtils.cpp b/dom/media/systemservices/VideoFrameUtils.cpp new file mode 100644 index 0000000000..00ead56a7b --- /dev/null +++ b/dom/media/systemservices/VideoFrameUtils.cpp @@ -0,0 +1,90 @@ +/* -*- 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 "VideoFrameUtils.h" +#include "api/video/video_frame.h" +#include "mozilla/ShmemPool.h" + +namespace mozilla { + +uint32_t VideoFrameUtils::TotalRequiredBufferSize( + const webrtc::VideoFrame& aVideoFrame) { + auto i420 = aVideoFrame.video_frame_buffer()->ToI420(); + auto height = i420->height(); + size_t size = height * i420->StrideY() + + ((height + 1) / 2) * i420->StrideU() + + ((height + 1) / 2) * i420->StrideV(); + MOZ_RELEASE_ASSERT(size < std::numeric_limits<uint32_t>::max()); + return static_cast<uint32_t>(size); +} + +void VideoFrameUtils::InitFrameBufferProperties( + const webrtc::VideoFrame& aVideoFrame, + camera::VideoFrameProperties& aDestProps) { + // The VideoFrameBuffer image data stored in the accompanying buffer + // the buffer is at least this size of larger. + aDestProps.bufferSize() = TotalRequiredBufferSize(aVideoFrame); + + aDestProps.timeStamp() = aVideoFrame.timestamp(); + aDestProps.ntpTimeMs() = aVideoFrame.ntp_time_ms(); + aDestProps.renderTimeMs() = aVideoFrame.render_time_ms(); + + aDestProps.rotation() = aVideoFrame.rotation(); + + auto i420 = aVideoFrame.video_frame_buffer()->ToI420(); + auto height = i420->height(); + aDestProps.yAllocatedSize() = height * i420->StrideY(); + aDestProps.uAllocatedSize() = ((height + 1) / 2) * i420->StrideU(); + aDestProps.vAllocatedSize() = ((height + 1) / 2) * i420->StrideV(); + + aDestProps.width() = i420->width(); + aDestProps.height() = height; + + aDestProps.yStride() = i420->StrideY(); + aDestProps.uStride() = i420->StrideU(); + aDestProps.vStride() = i420->StrideV(); +} + +void VideoFrameUtils::CopyVideoFrameBuffers(uint8_t* aDestBuffer, + const size_t aDestBufferSize, + const webrtc::VideoFrame& aFrame) { + size_t aggregateSize = TotalRequiredBufferSize(aFrame); + + MOZ_ASSERT(aDestBufferSize >= aggregateSize); + auto i420 = aFrame.video_frame_buffer()->ToI420(); + + // If planes are ordered YUV and contiguous then do a single copy + if ((i420->DataY() != nullptr) && + // Check that the three planes are ordered + (i420->DataY() < i420->DataU()) && (i420->DataU() < i420->DataV()) && + // Check that the last plane ends at firstPlane[totalsize] + (&i420->DataY()[aggregateSize] == + &i420->DataV()[((i420->height() + 1) / 2) * i420->StrideV()])) { + memcpy(aDestBuffer, i420->DataY(), aggregateSize); + return; + } + + // Copy each plane + size_t offset = 0; + size_t size; + auto height = i420->height(); + size = height * i420->StrideY(); + memcpy(&aDestBuffer[offset], i420->DataY(), size); + offset += size; + size = ((height + 1) / 2) * i420->StrideU(); + memcpy(&aDestBuffer[offset], i420->DataU(), size); + offset += size; + size = ((height + 1) / 2) * i420->StrideV(); + memcpy(&aDestBuffer[offset], i420->DataV(), size); +} + +void VideoFrameUtils::CopyVideoFrameBuffers( + ShmemBuffer& aDestShmem, const webrtc::VideoFrame& aVideoFrame) { + CopyVideoFrameBuffers(aDestShmem.Get().get<uint8_t>(), + aDestShmem.Get().Size<uint8_t>(), aVideoFrame); +} + +} // namespace mozilla diff --git a/dom/media/systemservices/VideoFrameUtils.h b/dom/media/systemservices/VideoFrameUtils.h new file mode 100644 index 0000000000..23ebf4f316 --- /dev/null +++ b/dom/media/systemservices/VideoFrameUtils.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +#ifndef mozilla_VideoFrameUtil_h +#define mozilla_VideoFrameUtil_h + +#include "mozilla/camera/PCameras.h" + +namespace webrtc { +class VideoFrame; +} + +namespace mozilla { +class ShmemBuffer; + +// Util methods for working with webrtc::VideoFrame(s) and +// the IPC classes that are used to deliver their contents to the +// MediaEnginge + +class VideoFrameUtils { + public: + // Returns the total number of bytes necessary to copy a VideoFrame's buffer + // across all planes. + static uint32_t TotalRequiredBufferSize(const webrtc::VideoFrame& frame); + + // Initializes a camera::VideoFrameProperties from a VideoFrameBuffer + static void InitFrameBufferProperties( + const webrtc::VideoFrame& aVideoFrame, + camera::VideoFrameProperties& aDestProperties); + + // Copies the buffers out of a VideoFrameBuffer into a buffer. + // Attempts to make as few memcopies as possible. + static void CopyVideoFrameBuffers(uint8_t* aDestBuffer, + const size_t aDestBufferSize, + const webrtc::VideoFrame& aVideoFrame); + + // Copies the buffers in a VideoFrameBuffer into a Shmem + // returns the eno from the underlying memcpy. + static void CopyVideoFrameBuffers(ShmemBuffer& aDestShmem, + const webrtc::VideoFrame& aVideoFrame); +}; + +} /* namespace mozilla */ + +#endif diff --git a/dom/media/systemservices/android_video_capture/device_info_android.cc b/dom/media/systemservices/android_video_capture/device_info_android.cc new file mode 100644 index 0000000000..19c734775e --- /dev/null +++ b/dom/media/systemservices/android_video_capture/device_info_android.cc @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "device_info_android.h" + +#include <algorithm> +#include <string> +#include <sstream> +#include <vector> + +#include "rtc_base/logging.h" +#include "modules/utility/include/helpers_android.h" + +#include "mozilla/jni/Utils.h" + +namespace webrtc { + +namespace videocapturemodule { + +// Helper for storing lists of pairs of ints. Used e.g. for resolutions & FPS +// ranges. +typedef std::pair<int, int> IntPair; +typedef std::vector<IntPair> IntPairs; + +static std::string IntPairsToString(const IntPairs& pairs, char separator) { + std::stringstream stream; + for (size_t i = 0; i < pairs.size(); ++i) { + if (i > 0) { + stream << ", "; + } + stream << "(" << pairs[i].first << separator << pairs[i].second << ")"; + } + return stream.str(); +} + +struct AndroidCameraInfo { + std::string name; + bool front_facing; + int orientation; + IntPairs resolutions; // Pairs are: (width,height). + // Pairs are (min,max) in units of FPS*1000 ("milli-frame-per-second"). + IntPairs mfpsRanges; + + std::string ToString() { + std::stringstream stream; + stream << "Name: [" << name << "], MFPS ranges: [" + << IntPairsToString(mfpsRanges, ':') + << "], front_facing: " << front_facing + << ", orientation: " << orientation << ", resolutions: [" + << IntPairsToString(resolutions, 'x') << "]"; + return stream.str(); + } +}; + +// Camera info; populated during DeviceInfoAndroid::Refresh() +static std::vector<AndroidCameraInfo>* g_camera_info = NULL; + +static JavaVM* g_jvm_dev_info = NULL; + +// Set |*index| to the index of |name| in g_camera_info or return false if no +// match found. +static bool FindCameraIndexByName(const std::string& name, size_t* index) { + for (size_t i = 0; i < g_camera_info->size(); ++i) { + if (g_camera_info->at(i).name == name) { + *index = i; + return true; + } + } + return false; +} + +// Returns a pointer to the named member of g_camera_info, or NULL if no match +// is found. +static AndroidCameraInfo* FindCameraInfoByName(const std::string& name) { + size_t index = 0; + if (FindCameraIndexByName(name, &index)) { + return &g_camera_info->at(index); + } + return NULL; +} + +// static +void DeviceInfoAndroid::Initialize(JavaVM* javaVM) { + // TODO(henrike): this "if" would make a lot more sense as an assert, but + // Java_org_webrtc_videoengineapp_ViEAndroidJavaAPI_GetVideoEngine() and + // Java_org_webrtc_videoengineapp_ViEAndroidJavaAPI_Terminate() conspire to + // prevent this. Once that code is made to only + // VideoEngine::SetAndroidObjects() once per process, this can turn into an + // assert. + if (g_camera_info) { + return; + } + + g_jvm_dev_info = javaVM; + BuildDeviceList(); +} + +void DeviceInfoAndroid::BuildDeviceList() { + if (!g_jvm_dev_info) { + return; + } + + AttachThreadScoped ats(g_jvm_dev_info); + JNIEnv* jni = ats.env(); + + g_camera_info = new std::vector<AndroidCameraInfo>(); + jclass j_info_class = mozilla::jni::GetClassRef( + jni, "org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid"); + jclass j_cap_class = mozilla::jni::GetClassRef( + jni, "org/webrtc/videoengine/CaptureCapabilityAndroid"); + assert(j_info_class); + jmethodID j_get_device_info = jni->GetStaticMethodID( + j_info_class, "getDeviceInfo", + "()[Lorg/webrtc/videoengine/CaptureCapabilityAndroid;"); + jarray j_camera_caps = static_cast<jarray>( + jni->CallStaticObjectMethod(j_info_class, j_get_device_info)); + if (jni->ExceptionCheck()) { + jni->ExceptionClear(); + RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get camera capabilities."; + return; + } + if (j_camera_caps == nullptr) { + RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get camera capabilities."; + return; + } + + const jsize capLength = jni->GetArrayLength(j_camera_caps); + + jfieldID widthField = jni->GetFieldID(j_cap_class, "width", "[I"); + jfieldID heightField = jni->GetFieldID(j_cap_class, "height", "[I"); + jfieldID maxFpsField = jni->GetFieldID(j_cap_class, "maxMilliFPS", "I"); + jfieldID minFpsField = jni->GetFieldID(j_cap_class, "minMilliFPS", "I"); + jfieldID orientationField = jni->GetFieldID(j_cap_class, "orientation", "I"); + jfieldID frontFacingField = jni->GetFieldID(j_cap_class, "frontFacing", "Z"); + jfieldID nameField = + jni->GetFieldID(j_cap_class, "name", "Ljava/lang/String;"); + if (widthField == NULL || heightField == NULL || maxFpsField == NULL || + minFpsField == NULL || orientationField == NULL || + frontFacingField == NULL || nameField == NULL) { + RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get field Id."; + return; + } + + for (jsize i = 0; i < capLength; i++) { + jobject capabilityElement = + jni->GetObjectArrayElement((jobjectArray)j_camera_caps, i); + + AndroidCameraInfo info; + jstring camName = + static_cast<jstring>(jni->GetObjectField(capabilityElement, nameField)); + const char* camChars = jni->GetStringUTFChars(camName, nullptr); + info.name = std::string(camChars); + jni->ReleaseStringUTFChars(camName, camChars); + + info.orientation = jni->GetIntField(capabilityElement, orientationField); + info.front_facing = + jni->GetBooleanField(capabilityElement, frontFacingField); + jint min_mfps = jni->GetIntField(capabilityElement, minFpsField); + jint max_mfps = jni->GetIntField(capabilityElement, maxFpsField); + + jintArray widthResArray = static_cast<jintArray>( + jni->GetObjectField(capabilityElement, widthField)); + jintArray heightResArray = static_cast<jintArray>( + jni->GetObjectField(capabilityElement, heightField)); + + const jsize numRes = jni->GetArrayLength(widthResArray); + + jint* widths = jni->GetIntArrayElements(widthResArray, nullptr); + jint* heights = jni->GetIntArrayElements(heightResArray, nullptr); + + for (jsize j = 0; j < numRes; ++j) { + info.resolutions.push_back(std::make_pair(widths[j], heights[j])); + } + + info.mfpsRanges.push_back(std::make_pair(min_mfps, max_mfps)); + g_camera_info->push_back(info); + + jni->ReleaseIntArrayElements(widthResArray, widths, JNI_ABORT); + jni->ReleaseIntArrayElements(heightResArray, heights, JNI_ABORT); + } + + jni->DeleteLocalRef(j_info_class); + jni->DeleteLocalRef(j_cap_class); +} + +void DeviceInfoAndroid::DeInitialize() { + if (g_camera_info) { + delete g_camera_info; + g_camera_info = NULL; + } +} + +int32_t DeviceInfoAndroid::Refresh() { + if (!g_camera_info || g_camera_info->size() == 0) { + DeviceInfoAndroid::BuildDeviceList(); +#ifdef DEBUG + int frontFacingIndex = -1; + for (uint32_t i = 0; i < g_camera_info->size(); i++) { + if (g_camera_info->at(i).front_facing) { + frontFacingIndex = i; + } + } + // Either there is a front-facing camera, and it's first in the list, or + // there is no front-facing camera. + MOZ_ASSERT(frontFacingIndex == 0 || frontFacingIndex == -1); +#endif + } + return 0; +} + +VideoCaptureModule::DeviceInfo* VideoCaptureImpl::CreateDeviceInfo() { + return new videocapturemodule::DeviceInfoAndroid(); +} + +DeviceInfoAndroid::DeviceInfoAndroid() : DeviceInfoImpl() {} + +DeviceInfoAndroid::~DeviceInfoAndroid() {} + +bool DeviceInfoAndroid::FindCameraIndex(const char* deviceUniqueIdUTF8, + size_t* index) { + return FindCameraIndexByName(deviceUniqueIdUTF8, index); +} + +int32_t DeviceInfoAndroid::Init() { return 0; } + +uint32_t DeviceInfoAndroid::NumberOfDevices() { + Refresh(); + return g_camera_info->size(); +} + +int32_t DeviceInfoAndroid::GetDeviceName( + uint32_t deviceNumber, char* deviceNameUTF8, uint32_t deviceNameLength, + char* deviceUniqueIdUTF8, uint32_t deviceUniqueIdUTF8Length, + char* /*productUniqueIdUTF8*/, uint32_t /*productUniqueIdUTF8Length*/, + pid_t* /*pid*/, bool* /*deviceIsPlaceholder*/) { + if (deviceNumber >= g_camera_info->size()) { + return -1; + } + const AndroidCameraInfo& info = g_camera_info->at(deviceNumber); + if (info.name.length() + 1 > deviceNameLength || + info.name.length() + 1 > deviceUniqueIdUTF8Length) { + return -1; + } + memcpy(deviceNameUTF8, info.name.c_str(), info.name.length() + 1); + memcpy(deviceUniqueIdUTF8, info.name.c_str(), info.name.length() + 1); + return 0; +} + +int32_t DeviceInfoAndroid::CreateCapabilityMap(const char* deviceUniqueIdUTF8) { + _captureCapabilities.clear(); + const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8); + if (info == NULL) { + return -1; + } + + for (size_t i = 0; i < info->resolutions.size(); ++i) { + for (size_t j = 0; j < info->mfpsRanges.size(); ++j) { + const IntPair& size = info->resolutions[i]; + const IntPair& mfpsRange = info->mfpsRanges[j]; + VideoCaptureCapability cap; + cap.width = size.first; + cap.height = size.second; + cap.maxFPS = mfpsRange.second / 1000; + cap.videoType = VideoType::kNV21; + _captureCapabilities.push_back(cap); + } + } + return _captureCapabilities.size(); +} + +int32_t DeviceInfoAndroid::GetOrientation(const char* deviceUniqueIdUTF8, + VideoRotation& orientation) { + const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8); + if (info == NULL || VideoCaptureImpl::RotationFromDegrees( + info->orientation, &orientation) != 0) { + return -1; + } + return 0; +} + +void DeviceInfoAndroid::GetMFpsRange(const char* deviceUniqueIdUTF8, + int max_fps_to_match, int* min_mfps, + int* max_mfps) { + const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8); + if (info == NULL) { + return; + } + int desired_mfps = max_fps_to_match * 1000; + int best_diff_mfps = 0; + RTC_LOG(LS_INFO) << "Search for best target mfps " << desired_mfps; + // Search for best fps range with preference shifted to constant fps modes. + for (size_t i = 0; i < info->mfpsRanges.size(); ++i) { + int diff_mfps = + abs(info->mfpsRanges[i].first - desired_mfps) + + abs(info->mfpsRanges[i].second - desired_mfps) + + (info->mfpsRanges[i].second - info->mfpsRanges[i].first) / 2; + RTC_LOG(LS_INFO) << "Fps range " << info->mfpsRanges[i].first << ":" + << info->mfpsRanges[i].second + << ". Distance: " << diff_mfps; + if (i == 0 || diff_mfps < best_diff_mfps) { + best_diff_mfps = diff_mfps; + *min_mfps = info->mfpsRanges[i].first; + *max_mfps = info->mfpsRanges[i].second; + } + } +} + +} // namespace videocapturemodule +} // namespace webrtc diff --git a/dom/media/systemservices/android_video_capture/device_info_android.h b/dom/media/systemservices/android_video_capture/device_info_android.h new file mode 100644 index 0000000000..88d1e65994 --- /dev/null +++ b/dom/media/systemservices/android_video_capture/device_info_android.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_ +#define WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_ + +#include <jni.h> + +#include "modules/video_capture/device_info_impl.h" +#include "modules/video_capture/video_capture_impl.h" + +#define AndroidJavaCaptureDeviceInfoClass \ + "org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid" +#define AndroidJavaCaptureCapabilityClass \ + "org/webrtc/videoengine/CaptureCapabilityAndroid" + +namespace webrtc { +namespace videocapturemodule { + +class DeviceInfoAndroid : public DeviceInfoImpl { + public: + static void Initialize(JavaVM* javaVM); + static void DeInitialize(); + + DeviceInfoAndroid(); + virtual ~DeviceInfoAndroid(); + + // Set |*index| to the index of the camera matching |deviceUniqueIdUTF8|, or + // return false if no match. + bool FindCameraIndex(const char* deviceUniqueIdUTF8, size_t* index); + + virtual int32_t Init(); + virtual uint32_t NumberOfDevices(); + virtual int32_t Refresh(); + virtual int32_t GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8, + uint32_t deviceNameLength, + char* deviceUniqueIdUTF8, + uint32_t deviceUniqueIdUTF8Length, + char* productUniqueIdUTF8 = 0, + uint32_t productUniqueIdUTF8Length = 0, + pid_t* pid = 0, bool* deviceIsPlaceholder = 0); + virtual int32_t CreateCapabilityMap(const char* deviceUniqueIdUTF8) + RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock); + + virtual int32_t DisplayCaptureSettingsDialogBox( + const char* /*deviceUniqueIdUTF8*/, const char* /*dialogTitleUTF8*/, + void* /*parentWindow*/, uint32_t /*positionX*/, uint32_t /*positionY*/) { + return -1; + } + virtual int32_t GetOrientation(const char* deviceUniqueIdUTF8, + VideoRotation& orientation); + + // Populate |min_mfps| and |max_mfps| with the closest supported range of the + // device to |max_fps_to_match|. + void GetMFpsRange(const char* deviceUniqueIdUTF8, int max_fps_to_match, + int* min_mfps, int* max_mfps); + + private: + enum { kExpectedCaptureDelay = 190 }; + static void BuildDeviceList(); +}; + +} // namespace videocapturemodule +} // namespace webrtc + +#endif // WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_ diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java new file mode 100644 index 0000000000..305fc74804 --- /dev/null +++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc.videoengine; + +import org.mozilla.gecko.annotation.WebRTCJNITarget; + +@WebRTCJNITarget +public class CaptureCapabilityAndroid { + public String name; + public int width[]; + public int height[]; + public int minMilliFPS; + public int maxMilliFPS; + public boolean frontFacing; + public boolean infrared; + public int orientation; +} diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java new file mode 100644 index 0000000000..cc54009a7b --- /dev/null +++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc.videoengine; + +import java.io.IOException; +import java.util.List; + +import android.content.Context; +import android.util.Log; +import android.view.Surface; +import android.view.WindowManager; + +import java.util.concurrent.CountDownLatch; + +import org.mozilla.gecko.annotation.WebRTCJNITarget; + +import org.webrtc.CameraEnumerator; +import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraVideoCapturer; +import org.webrtc.CapturerObserver; +import org.webrtc.EglBase; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoFrame; +import org.webrtc.VideoFrame.I420Buffer; + +public class VideoCaptureAndroid implements CameraVideoCapturer.CameraEventsHandler, CapturerObserver { + private final static String TAG = "WEBRTC-JC"; + + private final String deviceName; + private volatile long native_capturer; // |VideoCaptureAndroid*| in C++. + private Context context; + private CameraVideoCapturer cameraVideoCapturer; + private EglBase eglBase; + private SurfaceTextureHelper surfaceTextureHelper; + + // This class is recreated everytime we start/stop capture, so we + // can safely create the CountDownLatches here. + private final CountDownLatch capturerStarted = new CountDownLatch(1); + private boolean capturerStartedSucceeded = false; + private final CountDownLatch capturerStopped = new CountDownLatch(1); + + @WebRTCJNITarget + public VideoCaptureAndroid(String deviceName) { + // Remove the camera facing information from the name. + String[] parts = deviceName.split("Facing (front|back):"); + if (parts.length == 2) { + this.deviceName = parts[1].replace(" (infrared)", ""); + } else { + Log.e(TAG, "VideoCaptureAndroid: Expected facing mode as part of name: " + deviceName); + this.deviceName = deviceName; + } + this.context = GetContext(); + + CameraEnumerator enumerator; + if (Camera2Enumerator.isSupported(context)) { + enumerator = new Camera2Enumerator(context); + } else { + enumerator = new Camera1Enumerator(); + } + try { + cameraVideoCapturer = enumerator.createCapturer(this.deviceName, this); + eglBase = EglBase.create(); + surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureAndroidSurfaceTextureHelper", eglBase.getEglBaseContext()); + cameraVideoCapturer.initialize(surfaceTextureHelper, context, this); + } catch (java.lang.IllegalArgumentException e) { + Log.e(TAG, "VideoCaptureAndroid: Exception while creating capturer: " + e); + } + } + + // Return the global application context. + @WebRTCJNITarget + private static native Context GetContext(); + + // Called by native code. Returns true if capturer is started. + // + // Note that this actually opens the camera, and Camera callbacks run on the + // thread that calls open(), so this is done on the CameraThread. Since ViE + // API needs a synchronous success return value we wait for the result. + @WebRTCJNITarget + private synchronized boolean startCapture( + final int width, final int height, + final int min_mfps, final int max_mfps, + long native_capturer) { + Log.d(TAG, "startCapture: " + width + "x" + height + "@" + + min_mfps + ":" + max_mfps); + + if (cameraVideoCapturer == null) { + return false; + } + + cameraVideoCapturer.startCapture(width, height, max_mfps); + try { + capturerStarted.await(); + } catch (InterruptedException e) { + return false; + } + if (capturerStartedSucceeded) { + this.native_capturer = native_capturer; + } + return capturerStartedSucceeded; + } + + // Called by native code. Returns true when camera is known to be stopped. + @WebRTCJNITarget + private synchronized boolean stopCapture() { + Log.d(TAG, "stopCapture"); + if (cameraVideoCapturer == null) { + return false; + } + + native_capturer = 0; + try { + cameraVideoCapturer.stopCapture(); + capturerStopped.await(); + } catch (InterruptedException e) { + return false; + } + Log.d(TAG, "stopCapture done"); + return true; + } + + @WebRTCJNITarget + private int getDeviceOrientation() { + int orientation = 0; + if (context != null) { + WindowManager wm = (WindowManager) context.getSystemService( + Context.WINDOW_SERVICE); + switch(wm.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + orientation = 90; + break; + case Surface.ROTATION_180: + orientation = 180; + break; + case Surface.ROTATION_270: + orientation = 270; + break; + case Surface.ROTATION_0: + default: + orientation = 0; + break; + } + } + return orientation; + } + + @WebRTCJNITarget + private native void ProvideCameraFrame( + int width, int height, + java.nio.ByteBuffer dataY, int strideY, + java.nio.ByteBuffer dataU, int strideU, + java.nio.ByteBuffer dataV, int strideV, + int rotation, long timeStamp, long captureObject); + + // + // CameraVideoCapturer.CameraEventsHandler interface + // + + // Camera error handler - invoked when camera can not be opened + // or any camera exception happens on camera thread. + public void onCameraError(String errorDescription) {} + + // Called when camera is disconnected. + public void onCameraDisconnected() {} + + // Invoked when camera stops receiving frames. + public void onCameraFreezed(String errorDescription) {} + + // Callback invoked when camera is opening. + public void onCameraOpening(String cameraName) {} + + // Callback invoked when first camera frame is available after camera is started. + public void onFirstFrameAvailable() {} + + // Callback invoked when camera is closed. + public void onCameraClosed() {} + + // + // CapturerObserver interface + // + + // Notify if the capturer have been started successfully or not. + public void onCapturerStarted(boolean success) { + capturerStartedSucceeded = success; + capturerStarted.countDown(); + } + + // Notify that the capturer has been stopped. + public void onCapturerStopped() { + capturerStopped.countDown(); + } + + // Delivers a captured frame. + public void onFrameCaptured(VideoFrame frame) { + if (native_capturer != 0) { + I420Buffer i420Buffer = frame.getBuffer().toI420(); + ProvideCameraFrame(i420Buffer.getWidth(), i420Buffer.getHeight(), + i420Buffer.getDataY(), i420Buffer.getStrideY(), + i420Buffer.getDataU(), i420Buffer.getStrideU(), + i420Buffer.getDataV(), i420Buffer.getStrideV(), + frame.getRotation(), + frame.getTimestampNs() / 1000000, native_capturer); + + i420Buffer.release(); + } + } +} diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java new file mode 100644 index 0000000000..8ad8453955 --- /dev/null +++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +package org.webrtc.videoengine; + +import java.util.ArrayList; +import java.util.List; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.util.Log; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WebRTCJNITarget; + +import org.webrtc.CameraEnumerator; +import org.webrtc.CameraEnumerationAndroid.CaptureFormat; +import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Enumerator; + +public class VideoCaptureDeviceInfoAndroid { + private final static String TAG = "WEBRTC-JC"; + + // Returns information about all cameras on the device. + // Since this reflects static information about the hardware present, there is + // no need to call this function more than once in a single process. It is + // marked "private" as it is only called by native code. + @WebRTCJNITarget + private static CaptureCapabilityAndroid[] getDeviceInfo() { + final Context context = GeckoAppShell.getApplicationContext(); + + if (Camera2Enumerator.isSupported(context)) { + return createDeviceList(new Camera2Enumerator(context)); + } else { + return createDeviceList(new Camera1Enumerator()); + } + } + + private static CaptureCapabilityAndroid[] createDeviceList(CameraEnumerator enumerator) { + + ArrayList<CaptureCapabilityAndroid> allDevices = new ArrayList<CaptureCapabilityAndroid>(); + ArrayList<CaptureCapabilityAndroid> IRDevices = new ArrayList<CaptureCapabilityAndroid>(); + + for (String camera: enumerator.getDeviceNames()) { + List<CaptureFormat> formats = enumerator.getSupportedFormats(camera); + int numFormats = formats.size(); + if (numFormats <= 0) { + continue; + } + + CaptureCapabilityAndroid device = new CaptureCapabilityAndroid(); + + // The only way to plumb through whether the device is front facing + // or not is by the name, but the name we receive depends upon the + // camera API in use. For the Camera1 API, this information is + // already present, but that is not the case when using Camera2. + // Later on, we look up the camera by name, so we have to use a + // format this is easy to undo. Ideally, libwebrtc would expose + // camera facing in VideoCaptureCapability and none of this would be + // necessary. + device.name = "Facing " + (enumerator.isFrontFacing(camera) ? "front" : "back") + ":" + camera; + + + boolean ir = enumerator.isInfrared(camera); + device.infrared = ir; + if (ir) { + device.name += " (infrared)"; + } + + // This isn't part of the new API, but we don't call + // GetDeviceOrientation() anywhere, so this value is unused. + device.orientation = 0; + + device.width = new int[numFormats]; + device.height = new int[numFormats]; + device.minMilliFPS = formats.get(0).framerate.min; + device.maxMilliFPS = formats.get(0).framerate.max; + int i = 0; + for (CaptureFormat format: formats) { + device.width[i] = format.width; + device.height[i] = format.height; + if (format.framerate.min < device.minMilliFPS) { + device.minMilliFPS = format.framerate.min; + } + if (format.framerate.max > device.maxMilliFPS) { + device.maxMilliFPS = format.framerate.max; + } + i++; + } + device.frontFacing = enumerator.isFrontFacing(camera); + // Infrared devices always last (but front facing ones before + // non-front-facing ones), front-facing non IR first, other in + // the middle. + if (!device.infrared) { + if (device.frontFacing) { + allDevices.add(0, device); + } else { + allDevices.add(device); + } + } else { + if (device.frontFacing) { + IRDevices.add(0, device); + } else { + IRDevices.add(device); + } + } + } + + allDevices.addAll(IRDevices); + + return allDevices.toArray(new CaptureCapabilityAndroid[0]); + } +} diff --git a/dom/media/systemservices/android_video_capture/video_capture_android.cc b/dom/media/systemservices/android_video_capture/video_capture_android.cc new file mode 100644 index 0000000000..da0715db27 --- /dev/null +++ b/dom/media/systemservices/android_video_capture/video_capture_android.cc @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "video_capture_android.h" + +#include "device_info_android.h" +#include "modules/utility/include/helpers_android.h" +#include "rtc_base/logging.h" +#include "rtc_base/ref_counted_object.h" +#include "rtc_base/time_utils.h" + +#include "AndroidBridge.h" + +static JavaVM* g_jvm_capture = NULL; +static jclass g_java_capturer_class = NULL; // VideoCaptureAndroid.class. +static jobject g_context = NULL; // Owned android.content.Context. + +namespace webrtc { + +jobject JniCommon_allocateNativeByteBuffer(JNIEnv* env, jclass, jint size) { + void* new_data = ::operator new(size); + jobject byte_buffer = env->NewDirectByteBuffer(new_data, size); + return byte_buffer; +} + +void JniCommon_freeNativeByteBuffer(JNIEnv* env, jclass, jobject byte_buffer) { + void* data = env->GetDirectBufferAddress(byte_buffer); + ::operator delete(data); +} + +// Called by Java to get the global application context. +jobject JNICALL GetContext(JNIEnv* env, jclass) { + assert(g_context); + return g_context; +} + +// Called by Java when the camera has a new frame to deliver. +void JNICALL ProvideCameraFrame(JNIEnv* env, jobject, jint width, jint height, + jobject javaDataY, jint strideY, + jobject javaDataU, jint strideU, + jobject javaDataV, jint strideV, jint rotation, + jlong timeStamp, jlong context) { + if (!context) { + return; + } + + webrtc::videocapturemodule::VideoCaptureAndroid* captureModule = + reinterpret_cast<webrtc::videocapturemodule::VideoCaptureAndroid*>( + context); + uint8_t* dataY = + reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataY)); + uint8_t* dataU = + reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataU)); + uint8_t* dataV = + reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataV)); + + rtc::scoped_refptr<I420Buffer> i420Buffer = I420Buffer::Copy( + width, height, dataY, strideY, dataU, strideU, dataV, strideV); + + captureModule->OnIncomingFrame(i420Buffer, rotation, timeStamp); +} + +int32_t SetCaptureAndroidVM(JavaVM* javaVM) { + if (g_java_capturer_class) { + return 0; + } + + if (javaVM) { + assert(!g_jvm_capture); + g_jvm_capture = javaVM; + AttachThreadScoped ats(g_jvm_capture); + + g_context = mozilla::AndroidBridge::Bridge()->GetGlobalContextRef(); + + videocapturemodule::DeviceInfoAndroid::Initialize(g_jvm_capture); + + { + jclass clsRef = mozilla::jni::GetClassRef( + ats.env(), "org/webrtc/videoengine/VideoCaptureAndroid"); + g_java_capturer_class = + static_cast<jclass>(ats.env()->NewGlobalRef(clsRef)); + ats.env()->DeleteLocalRef(clsRef); + assert(g_java_capturer_class); + + JNINativeMethod native_methods[] = { + {"GetContext", "()Landroid/content/Context;", + reinterpret_cast<void*>(&GetContext)}, + {"ProvideCameraFrame", + "(IILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;ILjava/nio/" + "ByteBuffer;IIJJ)V", + reinterpret_cast<void*>(&ProvideCameraFrame)}}; + if (ats.env()->RegisterNatives(g_java_capturer_class, native_methods, + 2) != 0) + assert(false); + } + + { + jclass clsRef = + mozilla::jni::GetClassRef(ats.env(), "org/webrtc/JniCommon"); + + JNINativeMethod native_methods[] = { + {"nativeAllocateByteBuffer", "(I)Ljava/nio/ByteBuffer;", + reinterpret_cast<void*>(&JniCommon_allocateNativeByteBuffer)}, + {"nativeFreeByteBuffer", "(Ljava/nio/ByteBuffer;)V", + reinterpret_cast<void*>(&JniCommon_freeNativeByteBuffer)}}; + if (ats.env()->RegisterNatives(clsRef, native_methods, 2) != 0) + assert(false); + } + } else { + if (g_jvm_capture) { + AttachThreadScoped ats(g_jvm_capture); + ats.env()->UnregisterNatives(g_java_capturer_class); + ats.env()->DeleteGlobalRef(g_java_capturer_class); + g_java_capturer_class = NULL; + g_context = NULL; + videocapturemodule::DeviceInfoAndroid::DeInitialize(); + g_jvm_capture = NULL; + } + } + + return 0; +} + +namespace videocapturemodule { + +rtc::scoped_refptr<VideoCaptureModule> VideoCaptureImpl::Create( + const char* deviceUniqueIdUTF8) { + rtc::scoped_refptr<VideoCaptureAndroid> implementation( + new rtc::RefCountedObject<VideoCaptureAndroid>()); + if (implementation->Init(deviceUniqueIdUTF8) != 0) { + implementation = nullptr; + } + return implementation; +} + +void VideoCaptureAndroid::OnIncomingFrame(rtc::scoped_refptr<I420Buffer> buffer, + int32_t degrees, + int64_t captureTime) { + MutexLock lock(&api_lock_); + + VideoRotation rotation = + (degrees <= 45 || degrees > 315) ? kVideoRotation_0 + : (degrees > 45 && degrees <= 135) ? kVideoRotation_90 + : (degrees > 135 && degrees <= 225) ? kVideoRotation_180 + : (degrees > 225 && degrees <= 315) ? kVideoRotation_270 + : kVideoRotation_0; // Impossible. + + // Historically, we have ignored captureTime. Why? + VideoFrame captureFrame(I420Buffer::Rotate(*buffer, rotation), 0, + rtc::TimeMillis(), rotation); + + DeliverCapturedFrame(captureFrame); +} + +VideoCaptureAndroid::VideoCaptureAndroid() + : VideoCaptureImpl(), + _deviceInfo(), + _jCapturer(NULL), + _captureStarted(false) {} + +int32_t VideoCaptureAndroid::Init(const char* deviceUniqueIdUTF8) { + const int nameLength = strlen(deviceUniqueIdUTF8); + if (nameLength >= kVideoCaptureUniqueNameLength) return -1; + + RTC_DCHECK_RUN_ON(&api_checker_); + // Store the device name + RTC_LOG(LS_INFO) << "VideoCaptureAndroid::Init: " << deviceUniqueIdUTF8; + _deviceUniqueId = new char[nameLength + 1]; + memcpy(_deviceUniqueId, deviceUniqueIdUTF8, nameLength + 1); + + AttachThreadScoped ats(g_jvm_capture); + JNIEnv* env = ats.env(); + jmethodID ctor = env->GetMethodID(g_java_capturer_class, "<init>", + "(Ljava/lang/String;)V"); + assert(ctor); + jstring j_deviceName = env->NewStringUTF(_deviceUniqueId); + _jCapturer = env->NewGlobalRef( + env->NewObject(g_java_capturer_class, ctor, j_deviceName)); + assert(_jCapturer); + return 0; +} + +VideoCaptureAndroid::~VideoCaptureAndroid() { + // Ensure Java camera is released even if our caller didn't explicitly Stop. + if (_captureStarted) StopCapture(); + AttachThreadScoped ats(g_jvm_capture); + JNIEnv* env = ats.env(); + env->DeleteGlobalRef(_jCapturer); +} + +int32_t VideoCaptureAndroid::StartCapture( + const VideoCaptureCapability& capability) { + AttachThreadScoped ats(g_jvm_capture); + JNIEnv* env = ats.env(); + int width = 0; + int height = 0; + int min_mfps = 0; + int max_mfps = 0; + { + RTC_DCHECK_RUN_ON(&api_checker_); + MutexLock lock(&api_lock_); + + if (_deviceInfo.GetBestMatchedCapability(_deviceUniqueId, capability, + _captureCapability) < 0) { + RTC_LOG(LS_ERROR) << __FUNCTION__ + << "s: GetBestMatchedCapability failed: " + << capability.width << "x" << capability.height; + return -1; + } + + width = _captureCapability.width; + height = _captureCapability.height; + _deviceInfo.GetMFpsRange(_deviceUniqueId, _captureCapability.maxFPS, + &min_mfps, &max_mfps); + + // Exit critical section to avoid blocking camera thread inside + // onIncomingFrame() call. + } + + jmethodID j_start = + env->GetMethodID(g_java_capturer_class, "startCapture", "(IIIIJ)Z"); + assert(j_start); + jlong j_this = reinterpret_cast<intptr_t>(this); + bool started = env->CallBooleanMethod(_jCapturer, j_start, width, height, + min_mfps, max_mfps, j_this); + if (started) { + RTC_DCHECK_RUN_ON(&api_checker_); + MutexLock lock(&api_lock_); + _requestedCapability = capability; + _captureStarted = true; + } + return started ? 0 : -1; +} + +int32_t VideoCaptureAndroid::StopCapture() { + AttachThreadScoped ats(g_jvm_capture); + JNIEnv* env = ats.env(); + { + RTC_DCHECK_RUN_ON(&api_checker_); + MutexLock lock(&api_lock_); + + memset(&_requestedCapability, 0, sizeof(_requestedCapability)); + memset(&_captureCapability, 0, sizeof(_captureCapability)); + _captureStarted = false; + // Exit critical section to avoid blocking camera thread inside + // onIncomingFrame() call. + } + + // try to stop the capturer. + jmethodID j_stop = + env->GetMethodID(g_java_capturer_class, "stopCapture", "()Z"); + return env->CallBooleanMethod(_jCapturer, j_stop) ? 0 : -1; +} + +bool VideoCaptureAndroid::CaptureStarted() { + MutexLock lock(&api_lock_); + return _captureStarted; +} + +int32_t VideoCaptureAndroid::CaptureSettings(VideoCaptureCapability& settings) { + RTC_DCHECK_RUN_ON(&api_checker_); + MutexLock lock(&api_lock_); + settings = _requestedCapability; + return 0; +} + +} // namespace videocapturemodule +} // namespace webrtc diff --git a/dom/media/systemservices/android_video_capture/video_capture_android.h b/dom/media/systemservices/android_video_capture/video_capture_android.h new file mode 100644 index 0000000000..720c28e70b --- /dev/null +++ b/dom/media/systemservices/android_video_capture/video_capture_android.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_ +#define WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_ + +#include <jni.h> + +#include "device_info_android.h" +#include "api/video/i420_buffer.h" +#include "modules/video_capture/video_capture_impl.h" + +namespace webrtc { +namespace videocapturemodule { + +class VideoCaptureAndroid : public VideoCaptureImpl { + public: + VideoCaptureAndroid(); + virtual int32_t Init(const char* deviceUniqueIdUTF8); + + virtual int32_t StartCapture(const VideoCaptureCapability& capability); + virtual int32_t StopCapture(); + virtual bool CaptureStarted(); + virtual int32_t CaptureSettings(VideoCaptureCapability& settings); + + void OnIncomingFrame(rtc::scoped_refptr<I420Buffer> buffer, int32_t degrees, + int64_t captureTime = 0); + + protected: + virtual ~VideoCaptureAndroid(); + + DeviceInfoAndroid _deviceInfo; + jobject _jCapturer; // Global ref to Java VideoCaptureAndroid object. + VideoCaptureCapability _captureCapability; + bool _captureStarted; +}; + +} // namespace videocapturemodule +} // namespace webrtc +#endif // WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_ diff --git a/dom/media/systemservices/moz.build b/dom/media/systemservices/moz.build new file mode 100644 index 0000000000..fa6566cf6b --- /dev/null +++ b/dom/media/systemservices/moz.build @@ -0,0 +1,114 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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("/dom/media/webrtc/third_party_build/webrtc.mozbuild") + +if CONFIG["MOZ_WEBRTC"]: + EXPORTS += [ + "CamerasChild.h", + "CamerasParent.h", + "VideoEngine.h", + "VideoFrameUtils.h", + ] + UNIFIED_SOURCES += [ + "CamerasChild.cpp", + "CamerasParent.cpp", + "video_engine/video_capture_factory.cc", + "VideoEngine.cpp", + "VideoFrameUtils.cpp", + ] + LOCAL_INCLUDES += [ + "/dom/media/webrtc", + "/media/libyuv/libyuv/include", + "/mfbt", + "/third_party/libwebrtc", + "/third_party/libwebrtc/third_party/abseil-cpp", + "/tools/profiler/public", + ] + + if CONFIG["OS_TARGET"] == "Android": + UNIFIED_SOURCES += [ + "android_video_capture/device_info_android.cc", + "android_video_capture/video_capture_android.cc", + ] + elif CONFIG["OS_TARGET"] == "Darwin": + UNIFIED_SOURCES += [ + "objc_video_capture/device_info.mm", + "objc_video_capture/device_info_avfoundation.mm", + "objc_video_capture/device_info_objc.mm", + "objc_video_capture/rtc_video_capture_objc.mm", + "objc_video_capture/video_capture.mm", + "objc_video_capture/video_capture_avfoundation.mm", + ] + LOCAL_INCLUDES += [ + "/third_party/libwebrtc/sdk/objc", + "/third_party/libwebrtc/sdk/objc/base", + ] + CMMFLAGS += [ + "-fobjc-arc", + ] + + if CONFIG["OS_TARGET"] != "Android": + UNIFIED_SOURCES += [ + "video_engine/desktop_capture_impl.cc", + "video_engine/desktop_device_info.cc", + "video_engine/tab_capturer.cc", + ] + + if "WEBRTC_USE_PIPEWIRE" in DEFINES: + UNIFIED_SOURCES += [ + "video_engine/placeholder_device_info.cc", + ] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +if CONFIG["OS_TARGET"] == "Android": + DEFINES["WEBRTC_ANDROID"] = True + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + UNIFIED_SOURCES += ["OSXRunLoopSingleton.cpp"] + EXPORTS += ["OSXRunLoopSingleton.h"] + +EXPORTS.mozilla += [ + "ShmemPool.h", +] + +EXPORTS.mozilla.media += [ + "CamerasTypes.h", + "MediaChild.h", + "MediaParent.h", + "MediaSystemResourceClient.h", + "MediaSystemResourceManager.h", + "MediaSystemResourceManagerChild.h", + "MediaSystemResourceManagerParent.h", + "MediaSystemResourceMessageUtils.h", + "MediaSystemResourceService.h", + "MediaSystemResourceTypes.h", + "MediaTaskUtils.h", + "MediaUtils.h", +] +UNIFIED_SOURCES += [ + "CamerasTypes.cpp", + "MediaChild.cpp", + "MediaParent.cpp", + "MediaSystemResourceClient.cpp", + "MediaSystemResourceManager.cpp", + "MediaSystemResourceManagerChild.cpp", + "MediaSystemResourceManagerParent.cpp", + "MediaSystemResourceService.cpp", + "MediaUtils.cpp", + "ShmemPool.cpp", +] +IPDL_SOURCES += [ + "PCameras.ipdl", + "PMedia.ipdl", + "PMediaSystemResourceManager.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +with Files("android_video_capture/**"): + SCHEDULES.exclusive = ["android"] diff --git a/dom/media/systemservices/objc_video_capture/device_info.h b/dom/media/systemservices/objc_video_capture/device_info.h new file mode 100644 index 0000000000..d7faece0ea --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_ +#define MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_ + +#include "modules/video_capture/device_info_impl.h" + +#include <map> +#include <string> + +@class DeviceInfoIosObjC; + +namespace webrtc::videocapturemodule { +class DeviceInfoIos : public DeviceInfoImpl { + public: + DeviceInfoIos(); + virtual ~DeviceInfoIos(); + + // Implementation of DeviceInfoImpl. + int32_t Init() override; + uint32_t NumberOfDevices() override; + int32_t GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8, + uint32_t deviceNameLength, char* deviceUniqueIdUTF8, + uint32_t deviceUniqueIdUTF8Length, + char* productUniqueIdUTF8 = 0, + uint32_t productUniqueIdUTF8Length = 0, pid_t* pid = 0, + bool* deviceIsPlaceholder = 0) override; + + int32_t NumberOfCapabilities(const char* deviceUniqueIdUTF8) override; + + int32_t GetCapability(const char* deviceUniqueIdUTF8, + const uint32_t deviceCapabilityNumber, + VideoCaptureCapability& capability) override; + + int32_t DisplayCaptureSettingsDialogBox(const char* deviceUniqueIdUTF8, + const char* dialogTitleUTF8, + void* parentWindow, + uint32_t positionX, + uint32_t positionY) override; + + int32_t GetOrientation(const char* deviceUniqueIdUTF8, + VideoRotation& orientation) override; + + int32_t CreateCapabilityMap(const char* device_unique_id_utf8) override + RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock); + + private: + std::map<std::string, VideoCaptureCapabilities> _capabilitiesMap; + DeviceInfoIosObjC* _captureInfo; +}; + +} // namespace webrtc::videocapturemodule + +#endif // MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_ diff --git a/dom/media/systemservices/objc_video_capture/device_info.mm b/dom/media/systemservices/objc_video_capture/device_info.mm new file mode 100644 index 0000000000..74778da02f --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info.mm @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +# error "This file requires ARC support." +#endif + +#include <AVFoundation/AVFoundation.h> + +#include <string> + +#include "device_info.h" +#include "device_info_objc.h" +#include "modules/video_capture/video_capture_impl.h" +#include "mozilla/StaticPrefs_media.h" +#include "objc_video_capture/device_info_avfoundation.h" +#include "rtc_base/logging.h" + +using namespace mozilla; +using namespace webrtc; +using namespace videocapturemodule; + +static NSArray* camera_presets = @[ + AVCaptureSessionPreset352x288, AVCaptureSessionPreset640x480, + AVCaptureSessionPreset1280x720 +]; + +#define IOS_UNSUPPORTED() \ + RTC_LOG(LS_ERROR) << __FUNCTION__ \ + << " is not supported on the iOS platform."; \ + return -1; + +VideoCaptureModule::DeviceInfo* VideoCaptureImpl::CreateDeviceInfo() { + if (StaticPrefs::media_getusermedia_camera_macavf_enabled_AtStartup()) { + return new DeviceInfoAvFoundation(); + } + return new DeviceInfoIos(); +} + +DeviceInfoIos::DeviceInfoIos() { this->Init(); } + +DeviceInfoIos::~DeviceInfoIos() { [_captureInfo registerOwner:nil]; } + +int32_t DeviceInfoIos::Init() { + _captureInfo = [[DeviceInfoIosObjC alloc] init]; + [_captureInfo registerOwner:this]; + + // Fill in all device capabilities. + int deviceCount = [DeviceInfoIosObjC captureDeviceCount]; + + for (int i = 0; i < deviceCount; i++) { + AVCaptureDevice* avDevice = [DeviceInfoIosObjC captureDeviceForIndex:i]; + VideoCaptureCapabilities capabilityVector; + + for (NSString* preset in camera_presets) { + BOOL support = [avDevice supportsAVCaptureSessionPreset:preset]; + if (support) { + VideoCaptureCapability capability = + [DeviceInfoIosObjC capabilityForPreset:preset]; + capabilityVector.push_back(capability); + } + } + + char deviceNameUTF8[256]; + char deviceId[256]; + int error = this->GetDeviceName(i, deviceNameUTF8, 256, deviceId, 256); + if (error) { + return error; + } + std::string deviceIdCopy(deviceId); + std::pair<std::string, VideoCaptureCapabilities> mapPair = + std::pair<std::string, VideoCaptureCapabilities>(deviceIdCopy, + capabilityVector); + _capabilitiesMap.insert(mapPair); + } + + return 0; +} + +uint32_t DeviceInfoIos::NumberOfDevices() { + return [DeviceInfoIosObjC captureDeviceCount]; +} + +int32_t DeviceInfoIos::GetDeviceName( + uint32_t deviceNumber, char* deviceNameUTF8, uint32_t deviceNameUTF8Length, + char* deviceUniqueIdUTF8, uint32_t deviceUniqueIdUTF8Length, + char* productUniqueIdUTF8, uint32_t productUniqueIdUTF8Length, pid_t* pid, + bool* deviceIsPlaceholder) { + if (deviceNumber >= NumberOfDevices()) { + return -1; + } + + NSString* deviceName = [DeviceInfoIosObjC deviceNameForIndex:deviceNumber]; + + NSString* deviceUniqueId = + [DeviceInfoIosObjC deviceUniqueIdForIndex:deviceNumber]; + + strncpy(deviceNameUTF8, [deviceName UTF8String], deviceNameUTF8Length); + deviceNameUTF8[deviceNameUTF8Length - 1] = '\0'; + + strncpy(deviceUniqueIdUTF8, deviceUniqueId.UTF8String, + deviceUniqueIdUTF8Length); + deviceUniqueIdUTF8[deviceUniqueIdUTF8Length - 1] = '\0'; + + if (productUniqueIdUTF8) { + productUniqueIdUTF8[0] = '\0'; + } + + return 0; +} + +int32_t DeviceInfoIos::NumberOfCapabilities(const char* deviceUniqueIdUTF8) { + int32_t numberOfCapabilities = 0; + std::string deviceUniqueId(deviceUniqueIdUTF8); + std::map<std::string, VideoCaptureCapabilities>::iterator it = + _capabilitiesMap.find(deviceUniqueId); + + if (it != _capabilitiesMap.end()) { + numberOfCapabilities = it->second.size(); + } + return numberOfCapabilities; +} + +int32_t DeviceInfoIos::GetCapability(const char* deviceUniqueIdUTF8, + const uint32_t deviceCapabilityNumber, + VideoCaptureCapability& capability) { + std::string deviceUniqueId(deviceUniqueIdUTF8); + std::map<std::string, VideoCaptureCapabilities>::iterator it = + _capabilitiesMap.find(deviceUniqueId); + + if (it != _capabilitiesMap.end()) { + VideoCaptureCapabilities deviceCapabilities = it->second; + + if (deviceCapabilityNumber < deviceCapabilities.size()) { + VideoCaptureCapability cap; + cap = deviceCapabilities[deviceCapabilityNumber]; + capability = cap; + return 0; + } + } + + return -1; +} + +int32_t DeviceInfoIos::DisplayCaptureSettingsDialogBox( + const char* deviceUniqueIdUTF8, const char* dialogTitleUTF8, + void* parentWindow, uint32_t positionX, uint32_t positionY) { + IOS_UNSUPPORTED(); +} + +int32_t DeviceInfoIos::GetOrientation(const char* deviceUniqueIdUTF8, + VideoRotation& orientation) { + if (strcmp(deviceUniqueIdUTF8, "Front Camera") == 0) { + orientation = kVideoRotation_0; + } else { + orientation = kVideoRotation_90; + } + return orientation; +} + +int32_t DeviceInfoIos::CreateCapabilityMap(const char* deviceUniqueIdUTF8) { + std::string deviceName(deviceUniqueIdUTF8); + std::map<std::string, std::vector<VideoCaptureCapability>>::iterator it = + _capabilitiesMap.find(deviceName); + VideoCaptureCapabilities deviceCapabilities; + if (it != _capabilitiesMap.end()) { + _captureCapabilities = it->second; + return 0; + } + + return -1; +} diff --git a/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h new file mode 100644 index 0000000000..0daa663e21 --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_DEVICE_INFO_AVFOUNDATION_H_ +#define DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_DEVICE_INFO_AVFOUNDATION_H_ + +#include <map> +#include <string> + +#include "api/sequence_checker.h" +#include "device_info_objc.h" +#include "modules/video_capture/device_info_impl.h" + +namespace webrtc::videocapturemodule { + +/** + * DeviceInfo implementation for the libwebrtc ios/mac sdk camera backend. + * Single threaded except for DeviceChange() that happens on a platform callback + * thread. + */ +class DeviceInfoAvFoundation : public DeviceInfoImpl { + public: + static int32_t ConvertAVFrameRateToCapabilityFPS(Float64 aRate); + static webrtc::VideoType ConvertFourCCToVideoType(FourCharCode aCode); + + DeviceInfoAvFoundation(); + virtual ~DeviceInfoAvFoundation(); + + // Implementation of DeviceInfoImpl. + int32_t Init() override { return 0; } + void DeviceChange() override; + uint32_t NumberOfDevices() override; + int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8, + uint32_t aDeviceNameLength, char* aDeviceUniqueIdUTF8, + uint32_t aDeviceUniqueIdUTF8Length, + char* aProductUniqueIdUTF8 = nullptr, + uint32_t aProductUniqueIdUTF8Length = 0, + pid_t* aPid = nullptr, + bool* deviceIsPlaceholder = 0) override; + int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8) override; + int32_t GetCapability(const char* aDeviceUniqueIdUTF8, + const uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability) override; + int32_t DisplayCaptureSettingsDialogBox(const char* aDeviceUniqueIdUTF8, + const char* aDialogTitleUTF8, + void* aParentWindow, + uint32_t aPositionX, + uint32_t aPositionY) override { + return -1; + } + int32_t CreateCapabilityMap(const char* aDeviceUniqueIdUTF8) override + RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock); + + private: + const std::tuple<std::string, std::string, VideoCaptureCapabilities>* + FindDeviceAndCapabilities(const std::string& aDeviceUniqueId) const; + void EnsureCapabilitiesMap(); + + SequenceChecker mChecker; + std::atomic<bool> mInvalidateCapabilities; + // [{uniqueId, name, capabilities}] + std::vector<std::tuple<std::string, std::string, VideoCaptureCapabilities>> + mDevicesAndCapabilities RTC_GUARDED_BY(mChecker); + const DeviceInfoIosObjC* mDeviceChangeCaptureInfo RTC_GUARDED_BY(mChecker); +}; + +} // namespace webrtc::videocapturemodule + +#endif diff --git a/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm new file mode 100644 index 0000000000..c477f95c1e --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm @@ -0,0 +1,225 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "device_info_avfoundation.h" +#include <CoreVideo/CVPixelBuffer.h> + +#include <string> + +#include "components/capturer/RTCCameraVideoCapturer.h" +#import "helpers/NSString+StdString.h" +#include "media/base/video_common.h" +#include "modules/video_capture/video_capture_defines.h" +#include "rtc_base/logging.h" + +namespace webrtc::videocapturemodule { +/* static */ +int32_t DeviceInfoAvFoundation::ConvertAVFrameRateToCapabilityFPS( + Float64 aRate) { + return static_cast<int32_t>(aRate); +} + +/* static */ +webrtc::VideoType DeviceInfoAvFoundation::ConvertFourCCToVideoType( + FourCharCode aCode) { + switch (aCode) { + case kCVPixelFormatType_420YpCbCr8Planar: + case kCVPixelFormatType_420YpCbCr8PlanarFullRange: + return webrtc::VideoType::kI420; + case kCVPixelFormatType_24BGR: + return webrtc::VideoType::kRGB24; + case kCVPixelFormatType_32ABGR: + return webrtc::VideoType::kABGR; + case kCMPixelFormat_32ARGB: + return webrtc::VideoType::kBGRA; + case kCMPixelFormat_32BGRA: + return webrtc::VideoType::kARGB; + case kCMPixelFormat_16LE565: + return webrtc::VideoType::kRGB565; + case kCMPixelFormat_16LE555: + case kCMPixelFormat_16LE5551: + return webrtc::VideoType::kARGB1555; + case kCMPixelFormat_422YpCbCr8_yuvs: + return webrtc::VideoType::kYUY2; + case kCMPixelFormat_422YpCbCr8: + return webrtc::VideoType::kUYVY; + case kCMVideoCodecType_JPEG: + case kCMVideoCodecType_JPEG_OpenDML: + return webrtc::VideoType::kMJPEG; + case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: + case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange: + return webrtc::VideoType::kNV12; + default: + RTC_LOG(LS_WARNING) << "Unhandled FourCharCode" << aCode; + return webrtc::VideoType::kUnknown; + } +} + +DeviceInfoAvFoundation::DeviceInfoAvFoundation() + : mInvalidateCapabilities(false), + mDeviceChangeCaptureInfo([[DeviceInfoIosObjC alloc] init]) { + [mDeviceChangeCaptureInfo registerOwner:this]; +} + +DeviceInfoAvFoundation::~DeviceInfoAvFoundation() { + [mDeviceChangeCaptureInfo registerOwner:nil]; +} + +void DeviceInfoAvFoundation::DeviceChange() { + mInvalidateCapabilities = true; + DeviceInfo::DeviceChange(); +} + +uint32_t DeviceInfoAvFoundation::NumberOfDevices() { + RTC_DCHECK_RUN_ON(&mChecker); + EnsureCapabilitiesMap(); + return mDevicesAndCapabilities.size(); +} + +int32_t DeviceInfoAvFoundation::GetDeviceName( + uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameLength, + char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Length, + char* /* aProductUniqueIdUTF8 */, uint32_t /* aProductUniqueIdUTF8Length */, + pid_t* /* aPid */, bool* /*deviceIsPlaceholder*/) { + RTC_DCHECK_RUN_ON(&mChecker); + // Don't EnsureCapabilitiesMap() here, since: + // 1) That might invalidate the capabilities map + // 2) This function depends on the device index + + if (aDeviceNumber >= mDevicesAndCapabilities.size()) { + return -1; + } + + const auto& [uniqueId, name, _] = mDevicesAndCapabilities[aDeviceNumber]; + + strncpy(aDeviceUniqueIdUTF8, uniqueId.c_str(), aDeviceUniqueIdUTF8Length); + aDeviceUniqueIdUTF8[aDeviceUniqueIdUTF8Length - 1] = '\0'; + + strncpy(aDeviceNameUTF8, name.c_str(), aDeviceNameLength); + aDeviceNameUTF8[aDeviceNameLength - 1] = '\0'; + + return 0; +} + +int32_t DeviceInfoAvFoundation::NumberOfCapabilities( + const char* aDeviceUniqueIdUTF8) { + RTC_DCHECK_RUN_ON(&mChecker); + + std::string deviceUniqueId(aDeviceUniqueIdUTF8); + const auto* tup = FindDeviceAndCapabilities(deviceUniqueId); + if (!tup) { + return 0; + } + + const auto& [_, __, capabilities] = *tup; + return static_cast<int32_t>(capabilities.size()); +} + +int32_t DeviceInfoAvFoundation::GetCapability( + const char* aDeviceUniqueIdUTF8, const uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability) { + RTC_DCHECK_RUN_ON(&mChecker); + + std::string deviceUniqueId(aDeviceUniqueIdUTF8); + const auto* tup = FindDeviceAndCapabilities(deviceUniqueId); + if (!tup) { + return -1; + } + + const auto& [_, __, capabilities] = *tup; + if (aDeviceCapabilityNumber >= capabilities.size()) { + return -1; + } + + aCapability = capabilities[aDeviceCapabilityNumber]; + return 0; +} + +int32_t DeviceInfoAvFoundation::CreateCapabilityMap( + const char* aDeviceUniqueIdUTF8) { + RTC_DCHECK_RUN_ON(&mChecker); + + const size_t deviceUniqueIdUTF8Length = strlen(aDeviceUniqueIdUTF8); + if (deviceUniqueIdUTF8Length > kVideoCaptureUniqueNameLength) { + RTC_LOG(LS_INFO) << "Device name too long"; + return -1; + } + RTC_LOG(LS_INFO) << "CreateCapabilityMap called for device " + << aDeviceUniqueIdUTF8; + std::string deviceUniqueId(aDeviceUniqueIdUTF8); + const auto* tup = FindDeviceAndCapabilities(deviceUniqueId); + if (!tup) { + RTC_LOG(LS_INFO) << "no matching device found"; + return -1; + } + + // Store the new used device name + _lastUsedDeviceNameLength = deviceUniqueIdUTF8Length; + _lastUsedDeviceName = static_cast<char*>( + realloc(_lastUsedDeviceName, _lastUsedDeviceNameLength + 1)); + memcpy(_lastUsedDeviceName, aDeviceUniqueIdUTF8, + _lastUsedDeviceNameLength + 1); + + const auto& [_, __, capabilities] = *tup; + _captureCapabilities = capabilities; + return static_cast<int32_t>(_captureCapabilities.size()); +} + +auto DeviceInfoAvFoundation::FindDeviceAndCapabilities( + const std::string& aDeviceUniqueId) const + -> const std::tuple<std::string, std::string, VideoCaptureCapabilities>* { + RTC_DCHECK_RUN_ON(&mChecker); + for (const auto& tup : mDevicesAndCapabilities) { + if (std::get<0>(tup) == aDeviceUniqueId) { + return &tup; + } + } + return nullptr; +} + +void DeviceInfoAvFoundation::EnsureCapabilitiesMap() { + RTC_DCHECK_RUN_ON(&mChecker); + + if (mInvalidateCapabilities.exchange(false)) { + mDevicesAndCapabilities.clear(); + } + + if (!mDevicesAndCapabilities.empty()) { + return; + } + + for (AVCaptureDevice* device in [RTCCameraVideoCapturer + captureDevicesWithDeviceTypes:[RTCCameraVideoCapturer + defaultCaptureDeviceTypes]]) { + std::string uniqueId = [NSString stdStringForString:device.uniqueID]; + std::string name = [NSString stdStringForString:device.localizedName]; + auto& [_, __, capabilities] = mDevicesAndCapabilities.emplace_back( + uniqueId, name, VideoCaptureCapabilities()); + + for (AVCaptureDeviceFormat* format in + [RTCCameraVideoCapturer supportedFormatsForDevice:device]) { + VideoCaptureCapability cap; + FourCharCode fourcc = + CMFormatDescriptionGetMediaSubType(format.formatDescription); + cap.videoType = ConvertFourCCToVideoType(fourcc); + CMVideoDimensions dimensions = + CMVideoFormatDescriptionGetDimensions(format.formatDescription); + cap.width = dimensions.width; + cap.height = dimensions.height; + + for (AVFrameRateRange* range in format.videoSupportedFrameRateRanges) { + cap.maxFPS = ConvertAVFrameRateToCapabilityFPS(range.maxFrameRate); + capabilities.push_back(cap); + } + + if (capabilities.empty()) { + cap.maxFPS = 30; + capabilities.push_back(cap); + } + } + } +} +} // namespace webrtc::videocapturemodule diff --git a/dom/media/systemservices/objc_video_capture/device_info_objc.h b/dom/media/systemservices/objc_video_capture/device_info_objc.h new file mode 100644 index 0000000000..1ddedb471e --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info_objc.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_ +#define MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_ + +#import <AVFoundation/AVFoundation.h> + +#include "modules/video_capture/video_capture_defines.h" +#include "device_info.h" + +@interface DeviceInfoIosObjC : NSObject { + NSArray* _observers; + NSLock* _lock; + webrtc::VideoCaptureModule::DeviceInfo* _owner; +} + ++ (int)captureDeviceCount; ++ (AVCaptureDevice*)captureDeviceForIndex:(int)index; ++ (AVCaptureDevice*)captureDeviceForUniqueId:(NSString*)uniqueId; ++ (NSString*)deviceNameForIndex:(int)index; ++ (NSString*)deviceUniqueIdForIndex:(int)index; ++ (NSString*)deviceNameForUniqueId:(NSString*)uniqueId; ++ (webrtc::VideoCaptureCapability)capabilityForPreset:(NSString*)preset; + +- (void)registerOwner:(webrtc::VideoCaptureModule::DeviceInfo*)owner; +- (void)configureObservers; + +@end + +#endif // MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_ diff --git a/dom/media/systemservices/objc_video_capture/device_info_objc.mm b/dom/media/systemservices/objc_video_capture/device_info_objc.mm new file mode 100644 index 0000000000..048565e04c --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/device_info_objc.mm @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +# error "This file requires ARC support." +#endif + +#import <AVFoundation/AVFoundation.h> + +#import "device_info_objc.h" + +@implementation DeviceInfoIosObjC + +- (id)init { + self = [super init]; + if (nil != self) { + _lock = [[NSLock alloc] init]; + } + return self; +} + +- (void)dealloc { +} + +- (void)registerOwner:(webrtc::VideoCaptureModule::DeviceInfo*)owner { + [_lock lock]; + if (!_owner && owner) { + [self configureObservers]; + } else if (_owner && !owner) { + NSNotificationCenter* notificationCenter = + [NSNotificationCenter defaultCenter]; + for (id observer in _observers) { + [notificationCenter removeObserver:observer]; + } + _observers = nil; + } + _owner = owner; + [_lock unlock]; +} + ++ (int)captureDeviceCount { + int cnt = 0; + @try { + for (AVCaptureDevice* device in + [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { + if ([device isSuspended]) { + continue; + } + cnt++; + } + } @catch (NSException* exception) { + cnt = 0; + } + return cnt; +} + ++ (AVCaptureDevice*)captureDeviceForIndex:(int)index { + int cnt = 0; + @try { + for (AVCaptureDevice* device in + [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { + if ([device isSuspended]) { + continue; + } + if (cnt == index) { + return device; + } + cnt++; + } + } @catch (NSException* exception) { + cnt = 0; + } + + return nil; +} + ++ (AVCaptureDevice*)captureDeviceForUniqueId:(NSString*)uniqueId { + for (AVCaptureDevice* device in + [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { + if ([device isSuspended]) { + continue; + } + if ([uniqueId isEqual:device.uniqueID]) { + return device; + } + } + + return nil; +} + ++ (NSString*)deviceNameForIndex:(int)index { + return [DeviceInfoIosObjC captureDeviceForIndex:index].localizedName; +} + ++ (NSString*)deviceUniqueIdForIndex:(int)index { + return [DeviceInfoIosObjC captureDeviceForIndex:index].uniqueID; +} + ++ (NSString*)deviceNameForUniqueId:(NSString*)uniqueId { + return [[AVCaptureDevice deviceWithUniqueID:uniqueId] localizedName]; +} + ++ (webrtc::VideoCaptureCapability)capabilityForPreset:(NSString*)preset { + webrtc::VideoCaptureCapability capability; + + // TODO(tkchin): Maybe query AVCaptureDevice for supported formats, and + // then get the dimensions / frame rate from each supported format + if ([preset isEqualToString:AVCaptureSessionPreset352x288]) { + capability.width = 352; + capability.height = 288; + capability.maxFPS = 30; + capability.videoType = webrtc::VideoType::kNV12; + capability.interlaced = false; + } else if ([preset isEqualToString:AVCaptureSessionPreset640x480]) { + capability.width = 640; + capability.height = 480; + capability.maxFPS = 30; + capability.videoType = webrtc::VideoType::kNV12; + capability.interlaced = false; + } else if ([preset isEqualToString:AVCaptureSessionPreset1280x720]) { + capability.width = 1280; + capability.height = 720; + capability.maxFPS = 30; + capability.videoType = webrtc::VideoType::kNV12; + capability.interlaced = false; + } + + return capability; +} + +- (void)configureObservers { + // register device connected / disconnected event + NSNotificationCenter* notificationCenter = + [NSNotificationCenter defaultCenter]; + + id deviceWasConnectedObserver = [notificationCenter + addObserverForName:AVCaptureDeviceWasConnectedNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* note) { + [_lock lock]; + AVCaptureDevice* device = [note object]; + BOOL isVideoDevice = [device hasMediaType:AVMediaTypeVideo]; + if (isVideoDevice && _owner) _owner->DeviceChange(); + [_lock unlock]; + }]; + + id deviceWasDisconnectedObserver = [notificationCenter + addObserverForName:AVCaptureDeviceWasDisconnectedNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* note) { + [_lock lock]; + AVCaptureDevice* device = [note object]; + BOOL isVideoDevice = [device hasMediaType:AVMediaTypeVideo]; + if (isVideoDevice && _owner) _owner->DeviceChange(); + [_lock unlock]; + }]; + + _observers = + [[NSArray alloc] initWithObjects:deviceWasConnectedObserver, + deviceWasDisconnectedObserver, nil]; +} + +@end diff --git a/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h new file mode 100644 index 0000000000..5714000e9e --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_ +#define MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_ + +#import <AVFoundation/AVFoundation.h> +#import <Foundation/Foundation.h> +#ifdef WEBRTC_IOS +# import <UIKit/UIKit.h> +#endif + +#include "video_capture.h" + +// The following class listens to a notification with name: +// 'StatusBarOrientationDidChange'. +// This notification must be posted in order for the capturer to reflect the +// orientation change in video w.r.t. the application orientation. +@interface RTCVideoCaptureIosObjC + : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate> + +@property webrtc::VideoRotation frameRotation; + +// custom initializer. Instance of VideoCaptureIos is needed +// for callback purposes. +// default init methods have been overridden to return nil. +- (id)initWithOwner:(webrtc::videocapturemodule::VideoCaptureIos*)owner; +- (BOOL)setCaptureDeviceByUniqueId:(NSString*)uniqueId; +- (BOOL)startCaptureWithCapability: + (const webrtc::VideoCaptureCapability&)capability; +- (BOOL)stopCapture; + +@end +#endif // MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_ diff --git a/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm new file mode 100644 index 0000000000..7253ad1c73 --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +# error "This file requires ARC support." +#endif + +#import <AVFoundation/AVFoundation.h> +#ifdef WEBRTC_IOS +# import <UIKit/UIKit.h> +#endif + +#import "device_info_objc.h" +#import "rtc_video_capture_objc.h" + +#include "rtc_base/logging.h" + +using namespace webrtc; +using namespace webrtc::videocapturemodule; + +@interface RTCVideoCaptureIosObjC (hidden) +- (int)changeCaptureInputWithName:(NSString*)captureDeviceName; +@end + +@implementation RTCVideoCaptureIosObjC { + webrtc::videocapturemodule::VideoCaptureIos* _owner; + webrtc::VideoCaptureCapability _capability; + AVCaptureSession* _captureSession; + BOOL _orientationHasChanged; + AVCaptureConnection* _connection; + BOOL _captureChanging; // Guarded by _captureChangingCondition. + NSCondition* _captureChangingCondition; + dispatch_queue_t _frameQueue; +} + +@synthesize frameRotation = _framRotation; + +- (id)initWithOwner:(VideoCaptureIos*)owner { + if (self == [super init]) { + _owner = owner; + _captureSession = [[AVCaptureSession alloc] init]; +#if defined(WEBRTC_IOS) + _captureSession.usesApplicationAudioSession = NO; +#endif + _captureChanging = NO; + _captureChangingCondition = [[NSCondition alloc] init]; + + if (!_captureSession || !_captureChangingCondition) { + return nil; + } + + // create and configure a new output (using callbacks) + AVCaptureVideoDataOutput* captureOutput = + [[AVCaptureVideoDataOutput alloc] init]; + NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey; + + NSNumber* val = + [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8]; + NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:val + forKey:key]; + captureOutput.videoSettings = videoSettings; + + // add new output + if ([_captureSession canAddOutput:captureOutput]) { + [_captureSession addOutput:captureOutput]; + } else { + RTC_LOG(LS_ERROR) << __FUNCTION__ + << ": Could not add output to AVCaptureSession"; + } + +#ifdef WEBRTC_IOS + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + + NSNotificationCenter* notify = [NSNotificationCenter defaultCenter]; + [notify addObserver:self + selector:@selector(onVideoError:) + name:AVCaptureSessionRuntimeErrorNotification + object:_captureSession]; + [notify addObserver:self + selector:@selector(deviceOrientationDidChange:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; +#endif + } + + // Create a serial queue on which video capture will run. By setting the + // target, blocks should still run on DISPATH_QUEUE_PRIORITY_DEFAULT rather + // than creating a new thread. + _frameQueue = + dispatch_queue_create("org.webrtc.videocapture", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue( + _frameQueue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + + return self; +} + +- (void)directOutputToSelf { + [[self currentOutput] setSampleBufferDelegate:self queue:_frameQueue]; +} + +- (void)directOutputToNil { + [[self currentOutput] setSampleBufferDelegate:nil queue:NULL]; +} + +- (void)deviceOrientationDidChange:(NSNotification*)notification { + _orientationHasChanged = YES; + [self setRelativeVideoOrientation]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (BOOL)setCaptureDeviceByUniqueId:(NSString*)uniqueId { + [self waitForCaptureChangeToFinish]; + // check to see if the camera is already set + if (_captureSession) { + NSArray* currentInputs = [NSArray arrayWithArray:[_captureSession inputs]]; + if ([currentInputs count] > 0) { + AVCaptureDeviceInput* currentInput = [currentInputs objectAtIndex:0]; + if ([uniqueId isEqualToString:[currentInput.device localizedName]]) { + return YES; + } + } + } + + return [self changeCaptureInputByUniqueId:uniqueId]; +} + +- (BOOL)startCaptureWithCapability:(const VideoCaptureCapability&)capability { + [self waitForCaptureChangeToFinish]; + if (!_captureSession) { + return NO; + } + + // check limits of the resolution + if (capability.maxFPS < 0 || capability.maxFPS > 60) { + return NO; + } + + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { + if (capability.width > 1280 || capability.height > 720) { + return NO; + } + } else if ([_captureSession + canSetSessionPreset:AVCaptureSessionPreset640x480]) { + if (capability.width > 640 || capability.height > 480) { + return NO; + } + } else if ([_captureSession + canSetSessionPreset:AVCaptureSessionPreset352x288]) { + if (capability.width > 352 || capability.height > 288) { + return NO; + } + } else if (capability.width < 0 || capability.height < 0) { + return NO; + } + + _capability = capability; + + AVCaptureVideoDataOutput* currentOutput = [self currentOutput]; + if (!currentOutput) return NO; + + [self directOutputToSelf]; + + _orientationHasChanged = NO; + _captureChanging = YES; + dispatch_async(_frameQueue, ^{ + [self startCaptureInBackgroundWithOutput:currentOutput]; + }); + return YES; +} + +- (AVCaptureVideoDataOutput*)currentOutput { + return [[_captureSession outputs] firstObject]; +} + +- (void)startCaptureInBackgroundWithOutput: + (AVCaptureVideoDataOutput*)currentOutput { + NSString* captureQuality = + [NSString stringWithString:AVCaptureSessionPresetLow]; + if (_capability.width >= 1280 || _capability.height >= 720) { + captureQuality = [NSString stringWithString:AVCaptureSessionPreset1280x720]; + } else if (_capability.width >= 640 || _capability.height >= 480) { + captureQuality = [NSString stringWithString:AVCaptureSessionPreset640x480]; + } else if (_capability.width >= 352 || _capability.height >= 288) { + captureQuality = [NSString stringWithString:AVCaptureSessionPreset352x288]; + } + + // begin configuration for the AVCaptureSession + [_captureSession beginConfiguration]; + + // picture resolution + [_captureSession setSessionPreset:captureQuality]; + + _connection = [currentOutput connectionWithMediaType:AVMediaTypeVideo]; + [self setRelativeVideoOrientation]; + + // finished configuring, commit settings to AVCaptureSession. + [_captureSession commitConfiguration]; + + [_captureSession startRunning]; + [self signalCaptureChangeEnd]; +} + +- (void)setRelativeVideoOrientation { + if (!_connection.supportsVideoOrientation) { + return; + } +#ifndef WEBRTC_IOS + _connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight; + return; +#else + switch ([UIDevice currentDevice].orientation) { + case UIDeviceOrientationPortrait: + _connection.videoOrientation = AVCaptureVideoOrientationPortrait; + break; + case UIDeviceOrientationPortraitUpsideDown: + _connection.videoOrientation = + AVCaptureVideoOrientationPortraitUpsideDown; + break; + case UIDeviceOrientationLandscapeLeft: + _connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight; + break; + case UIDeviceOrientationLandscapeRight: + _connection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft; + break; + case UIDeviceOrientationFaceUp: + case UIDeviceOrientationFaceDown: + case UIDeviceOrientationUnknown: + if (!_orientationHasChanged) { + _connection.videoOrientation = AVCaptureVideoOrientationPortrait; + } + break; + } +#endif +} + +- (void)onVideoError:(NSNotification*)notification { + NSLog(@"onVideoError: %@", notification); + // TODO(sjlee): make the specific error handling with this notification. + RTC_LOG(LS_ERROR) << __FUNCTION__ + << ": [AVCaptureSession startRunning] error."; +} + +- (BOOL)stopCapture { +#ifdef WEBRTC_IOS + [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; +#endif + _orientationHasChanged = NO; + [self waitForCaptureChangeToFinish]; + [self directOutputToNil]; + + if (!_captureSession) { + return NO; + } + + _captureChanging = YES; + [_captureSession stopRunning]; + + dispatch_sync(_frameQueue, ^{ + [self signalCaptureChangeEnd]; + }); + return YES; +} + +- (BOOL)changeCaptureInputByUniqueId:(NSString*)uniqueId { + [self waitForCaptureChangeToFinish]; + NSArray* currentInputs = [_captureSession inputs]; + // remove current input + if ([currentInputs count] > 0) { + AVCaptureInput* currentInput = + (AVCaptureInput*)[currentInputs objectAtIndex:0]; + + [_captureSession removeInput:currentInput]; + } + + // Look for input device with the name requested (as our input param) + // get list of available capture devices + int captureDeviceCount = [DeviceInfoIosObjC captureDeviceCount]; + if (captureDeviceCount <= 0) { + return NO; + } + + AVCaptureDevice* captureDevice = + [DeviceInfoIosObjC captureDeviceForUniqueId:uniqueId]; + + if (!captureDevice) { + return NO; + } + + // now create capture session input out of AVCaptureDevice + NSError* deviceError = nil; + AVCaptureDeviceInput* newCaptureInput = + [AVCaptureDeviceInput deviceInputWithDevice:captureDevice + error:&deviceError]; + + if (!newCaptureInput) { + const char* errorMessage = [[deviceError localizedDescription] UTF8String]; + + RTC_LOG(LS_ERROR) << __FUNCTION__ + << ": deviceInputWithDevice error:" << errorMessage; + + return NO; + } + + // try to add our new capture device to the capture session + [_captureSession beginConfiguration]; + + BOOL addedCaptureInput = NO; + if ([_captureSession canAddInput:newCaptureInput]) { + [_captureSession addInput:newCaptureInput]; + addedCaptureInput = YES; + } else { + addedCaptureInput = NO; + } + + [_captureSession commitConfiguration]; + + return addedCaptureInput; +} + +- (void)captureOutput:(AVCaptureOutput*)captureOutput + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection*)connection { + const int kFlags = 0; + CVImageBufferRef videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer); + + if (CVPixelBufferLockBaseAddress(videoFrame, kFlags) != kCVReturnSuccess) { + return; + } + + uint8_t* baseAddress = (uint8_t*)CVPixelBufferGetBaseAddress(videoFrame); + const size_t width = CVPixelBufferGetWidth(videoFrame); + const size_t height = CVPixelBufferGetHeight(videoFrame); + const size_t frameSize = width * height * 2; + + VideoCaptureCapability tempCaptureCapability; + tempCaptureCapability.width = width; + tempCaptureCapability.height = height; + tempCaptureCapability.maxFPS = _capability.maxFPS; + tempCaptureCapability.videoType = VideoType::kUYVY; + + _owner->IncomingFrame(baseAddress, frameSize, tempCaptureCapability, 0); + + CVPixelBufferUnlockBaseAddress(videoFrame, kFlags); +} + +- (void)signalCaptureChangeEnd { + [_captureChangingCondition lock]; + _captureChanging = NO; + [_captureChangingCondition signal]; + [_captureChangingCondition unlock]; +} + +- (void)waitForCaptureChangeToFinish { + [_captureChangingCondition lock]; + while (_captureChanging) { + [_captureChangingCondition wait]; + } + [_captureChangingCondition unlock]; +} +@end diff --git a/dom/media/systemservices/objc_video_capture/video_capture.h b/dom/media/systemservices/objc_video_capture/video_capture.h new file mode 100644 index 0000000000..dbc3bdaa79 --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/video_capture.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_ +#define MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_ + +#include "modules/video_capture/video_capture_impl.h" +#include "api/scoped_refptr.h" + +@class RTCVideoCaptureIosObjC; + +namespace webrtc::videocapturemodule { +class VideoCaptureIos : public VideoCaptureImpl { + public: + VideoCaptureIos(); + virtual ~VideoCaptureIos(); + + static rtc::scoped_refptr<VideoCaptureModule> Create( + const char* device_unique_id_utf8); + + // Implementation of VideoCaptureImpl. + int32_t StartCapture(const VideoCaptureCapability& capability) override; + int32_t StopCapture() override; + bool CaptureStarted() override; + int32_t CaptureSettings(VideoCaptureCapability& settings) override; + + private: + RTCVideoCaptureIosObjC* capture_device_; + bool is_capturing_; + VideoCaptureCapability capability_; +}; + +} // namespace webrtc::videocapturemodule + +#endif // MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_ diff --git a/dom/media/systemservices/objc_video_capture/video_capture.mm b/dom/media/systemservices/objc_video_capture/video_capture.mm new file mode 100644 index 0000000000..ac1964b08a --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/video_capture.mm @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#if !defined(__has_feature) || !__has_feature(objc_arc) +# error "This file requires ARC support." +#endif + +#include "device_info_objc.h" +#include "rtc_video_capture_objc.h" +#include "rtc_base/ref_counted_object.h" +#include "api/scoped_refptr.h" +#include "video_capture_avfoundation.h" +#include "mozilla/StaticPrefs_media.h" + +using namespace mozilla; +using namespace webrtc; +using namespace videocapturemodule; + +rtc::scoped_refptr<VideoCaptureModule> VideoCaptureImpl::Create( + const char* deviceUniqueIdUTF8) { + if (StaticPrefs::media_getusermedia_camera_macavf_enabled_AtStartup()) { + return VideoCaptureAvFoundation::Create(deviceUniqueIdUTF8); + } + return VideoCaptureIos::Create(deviceUniqueIdUTF8); +} + +VideoCaptureIos::VideoCaptureIos() : is_capturing_(false) { + capability_.width = kDefaultWidth; + capability_.height = kDefaultHeight; + capability_.maxFPS = kDefaultFrameRate; + capture_device_ = nil; +} + +VideoCaptureIos::~VideoCaptureIos() { + if (is_capturing_) { + [capture_device_ stopCapture]; + capture_device_ = nil; + } +} + +rtc::scoped_refptr<VideoCaptureModule> VideoCaptureIos::Create( + const char* deviceUniqueIdUTF8) { + if (!deviceUniqueIdUTF8[0]) { + return NULL; + } + + rtc::scoped_refptr<VideoCaptureIos> capture_module( + new rtc::RefCountedObject<VideoCaptureIos>()); + + const int32_t name_length = strlen(deviceUniqueIdUTF8); + if (name_length >= kVideoCaptureUniqueNameLength) return nullptr; + + RTC_DCHECK_RUN_ON(&capture_module->api_checker_); + capture_module->_deviceUniqueId = new char[name_length + 1]; + strncpy(capture_module->_deviceUniqueId, deviceUniqueIdUTF8, name_length + 1); + capture_module->_deviceUniqueId[name_length] = '\0'; + + capture_module->capture_device_ = + [[RTCVideoCaptureIosObjC alloc] initWithOwner:capture_module.get()]; + if (!capture_module->capture_device_) { + return nullptr; + } + + if (![capture_module->capture_device_ + setCaptureDeviceByUniqueId: + [[NSString alloc] initWithCString:deviceUniqueIdUTF8 + encoding:NSUTF8StringEncoding]]) { + return nullptr; + } + return capture_module; +} + +int32_t VideoCaptureIos::StartCapture( + const VideoCaptureCapability& capability) { + capability_ = capability; + + if (![capture_device_ startCaptureWithCapability:capability]) { + return -1; + } + + is_capturing_ = true; + + return 0; +} + +int32_t VideoCaptureIos::StopCapture() { + if (![capture_device_ stopCapture]) { + return -1; + } + + is_capturing_ = false; + return 0; +} + +bool VideoCaptureIos::CaptureStarted() { return is_capturing_; } + +int32_t VideoCaptureIos::CaptureSettings(VideoCaptureCapability& settings) { + settings = capability_; + settings.videoType = VideoType::kNV12; + return 0; +} diff --git a/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h new file mode 100644 index 0000000000..e47fdb5faa --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_VIDEO_CAPTURE2_H_ +#define DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_VIDEO_CAPTURE2_H_ + +#import "components/capturer/RTCCameraVideoCapturer.h" + +#include "api/scoped_refptr.h" +#include "api/sequence_checker.h" +#include "modules/video_capture/video_capture_impl.h" +#include "mozilla/Maybe.h" +#include "PerformanceRecorder.h" + +@class VideoCaptureAdapter; + +namespace webrtc::videocapturemodule { + +/** + * VideoCaptureImpl implementation of the libwebrtc ios/mac sdk camera backend. + * Single threaded except for OnFrame() that happens on a platform callback + * thread. + */ +class VideoCaptureAvFoundation : public VideoCaptureImpl { + public: + VideoCaptureAvFoundation(AVCaptureDevice* _Nonnull aDevice); + virtual ~VideoCaptureAvFoundation(); + + static rtc::scoped_refptr<VideoCaptureModule> Create( + const char* _Nullable aDeviceUniqueIdUTF8); + + // Implementation of VideoCaptureImpl. Single threaded. + + // Starts capturing synchronously. Idempotent. If an existing capture is live + // and another capability is requested we'll restart the underlying backend + // with the new capability. + int32_t StartCapture(const VideoCaptureCapability& aCapability) + MOZ_EXCLUDES(api_lock_) override; + // Stops capturing synchronously. Idempotent. + int32_t StopCapture() MOZ_EXCLUDES(api_lock_) override; + bool CaptureStarted() MOZ_EXCLUDES(api_lock_) override; + int32_t CaptureSettings(VideoCaptureCapability& aSettings) override; + + // Callback. This can be called on any thread. + int32_t OnFrame(__strong RTCVideoFrame* _Nonnull aFrame) + MOZ_EXCLUDES(api_lock_); + + void SetTrackingId(uint32_t aTrackingIdProcId) + MOZ_EXCLUDES(api_lock_) override; + + // Registers the current thread with the profiler if not already registered. + void MaybeRegisterCallbackThread(); + + private: + // Control thread checker. + SequenceChecker mChecker; + AVCaptureDevice* _Nonnull const mDevice RTC_GUARDED_BY(mChecker); + VideoCaptureAdapter* _Nonnull const mAdapter RTC_GUARDED_BY(mChecker); + RTCCameraVideoCapturer* _Nonnull const mCapturer RTC_GUARDED_BY(mChecker); + // If capture has started, this is the capability it was started for. Written + // on the mChecker thread only. + mozilla::Maybe<VideoCaptureCapability> mCapability MOZ_GUARDED_BY(api_lock_); + // The image type that mCapability maps to. Set in lockstep with mCapability. + mozilla::Maybe<mozilla::CaptureStage::ImageType> mImageType + MOZ_GUARDED_BY(api_lock_); + // Id string uniquely identifying this capture source. Written on the mChecker + // thread only. + mozilla::Maybe<mozilla::TrackingId> mTrackingId MOZ_GUARDED_BY(api_lock_); + // Adds frame specific markers to the profiler while mTrackingId is set. + // Callback thread only. + mozilla::PerformanceRecorderMulti<mozilla::CaptureStage> mCaptureRecorder; + mozilla::PerformanceRecorderMulti<mozilla::CopyVideoStage> + mConversionRecorder; + std::atomic<ProfilerThreadId> mCallbackThreadId; +}; + +} // namespace webrtc::videocapturemodule + +@interface VideoCaptureAdapter : NSObject <RTCVideoCapturerDelegate> { + webrtc::Mutex _mutex; + webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable _capturer + RTC_GUARDED_BY(_mutex); +} +- (void)setCapturer: + (webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable)capturer; +@end + +#endif diff --git a/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm new file mode 100644 index 0000000000..36d5f56b16 --- /dev/null +++ b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm @@ -0,0 +1,327 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "video_capture_avfoundation.h" + +#import "api/video_frame_buffer/RTCNativeI420Buffer+Private.h" +#import "base/RTCI420Buffer.h" +#import "base/RTCVideoFrame.h" +#import "base/RTCVideoFrameBuffer.h" +#import "components/capturer/RTCCameraVideoCapturer.h" +#import "helpers/NSString+StdString.h" + +#include "api/scoped_refptr.h" +#include "api/video/video_rotation.h" +#include "CallbackThreadRegistry.h" +#include "device_info_avfoundation.h" +#include "modules/video_capture/video_capture_defines.h" +#include "mozilla/Assertions.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" +#include "rtc_base/time_utils.h" + +using namespace mozilla; +using namespace webrtc::videocapturemodule; + +namespace { +webrtc::VideoRotation ToNativeRotation(RTCVideoRotation aRotation) { + switch (aRotation) { + case RTCVideoRotation_0: + return webrtc::kVideoRotation_0; + case RTCVideoRotation_90: + return webrtc::kVideoRotation_90; + case RTCVideoRotation_180: + return webrtc::kVideoRotation_180; + case RTCVideoRotation_270: + return webrtc::kVideoRotation_270; + default: + MOZ_CRASH_UNSAFE_PRINTF("Unexpected rotation %d", + static_cast<int>(aRotation)); + return webrtc::kVideoRotation_0; + } +} + +AVCaptureDeviceFormat* _Nullable FindFormat( + AVCaptureDevice* _Nonnull aDevice, + webrtc::VideoCaptureCapability aCapability) { + for (AVCaptureDeviceFormat* format in [aDevice formats]) { + CMVideoDimensions dimensions = + CMVideoFormatDescriptionGetDimensions(format.formatDescription); + if (dimensions.width != aCapability.width) { + continue; + } + if (dimensions.height != aCapability.height) { + continue; + } + FourCharCode fourcc = + CMFormatDescriptionGetMediaSubType(format.formatDescription); + if (aCapability.videoType != + DeviceInfoAvFoundation::ConvertFourCCToVideoType(fourcc)) { + continue; + } + if ([format.videoSupportedFrameRateRanges + indexOfObjectPassingTest:^BOOL(AVFrameRateRange* _Nonnull obj, + NSUInteger idx, + BOOL* _Nonnull stop) { + return static_cast<BOOL>( + DeviceInfoAvFoundation::ConvertAVFrameRateToCapabilityFPS( + obj.maxFrameRate) == aCapability.maxFPS); + }] == NSNotFound) { + continue; + } + + return format; + } + return nullptr; +} +} // namespace + +@implementation VideoCaptureAdapter +- (void)setCapturer: + (webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable)capturer { + webrtc::MutexLock lock(&_mutex); + _capturer = capturer; +} + +- (void)capturer:(RTCVideoCapturer* _Nonnull)capturer + didCaptureVideoFrame:(RTCVideoFrame* _Nonnull)frame { + rtc::scoped_refptr<webrtc::videocapturemodule::VideoCaptureAvFoundation> cap; + { + webrtc::MutexLock lock(&_mutex); + cap = rtc::scoped_refptr(_capturer); + } + if (!cap) return; + cap->OnFrame(frame); +} +@end + +namespace webrtc::videocapturemodule { +VideoCaptureAvFoundation::VideoCaptureAvFoundation( + AVCaptureDevice* _Nonnull aDevice) + : mDevice(aDevice), + mAdapter([[VideoCaptureAdapter alloc] init]), + mCapturer([[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] + initWithDelegate:mAdapter]), + mCallbackThreadId() { + const char* uniqueId = [[aDevice uniqueID] UTF8String]; + size_t len = strlen(uniqueId); + _deviceUniqueId = new (std::nothrow) char[len + 1]; + if (_deviceUniqueId) { + memcpy(_deviceUniqueId, uniqueId, len + 1); + } +} + +VideoCaptureAvFoundation::~VideoCaptureAvFoundation() { + // Must block until capture has fully stopped, including async operations. + StopCapture(); +} + +/* static */ +rtc::scoped_refptr<VideoCaptureModule> VideoCaptureAvFoundation::Create( + const char* _Nullable aDeviceUniqueIdUTF8) { + std::string uniqueId(aDeviceUniqueIdUTF8); + + for (AVCaptureDevice* device in [RTCCameraVideoCapturer + captureDevicesWithDeviceTypes:[RTCCameraVideoCapturer + defaultCaptureDeviceTypes]]) { + if ([NSString stdStringForString:device.uniqueID] == uniqueId) { + rtc::scoped_refptr<VideoCaptureModule> module( + new rtc::RefCountedObject<VideoCaptureAvFoundation>(device)); + return module; + } + } + return nullptr; +} + +int32_t VideoCaptureAvFoundation::StartCapture( + const VideoCaptureCapability& aCapability) { + RTC_DCHECK_RUN_ON(&mChecker); + AVCaptureDeviceFormat* format = FindFormat(mDevice, aCapability); + if (!format) { + return -1; + } + + { + MutexLock lock(&api_lock_); + if (mCapability) { + if (mCapability->width == aCapability.width && + mCapability->height == aCapability.height && + mCapability->maxFPS == aCapability.maxFPS && + mCapability->videoType == aCapability.videoType) { + return 0; + } + + api_lock_.Unlock(); + int32_t rv = StopCapture(); + api_lock_.Lock(); + + if (rv != 0) { + return rv; + } + } + } + + [mAdapter setCapturer:this]; + + { + Monitor monitor("VideoCaptureAVFoundation::StartCapture"); + Monitor* copyableMonitor = &monitor; + MonitorAutoLock lock(monitor); + __block Maybe<int32_t> rv; + + [mCapturer startCaptureWithDevice:mDevice + format:format + fps:aCapability.maxFPS + completionHandler:^(NSError* error) { + MonitorAutoLock lock2(*copyableMonitor); + MOZ_RELEASE_ASSERT(!rv); + rv = Some(error ? -1 : 0); + copyableMonitor->Notify(); + }]; + + while (!rv) { + monitor.Wait(); + } + + if (*rv != 0) { + return *rv; + } + } + + MutexLock lock(&api_lock_); + mCapability = Some(aCapability); + mImageType = Some([type = aCapability.videoType] { + switch (type) { + case webrtc::VideoType::kI420: + return CaptureStage::ImageType::I420; + case webrtc::VideoType::kYUY2: + return CaptureStage::ImageType::YUY2; + case webrtc::VideoType::kYV12: + case webrtc::VideoType::kIYUV: + return CaptureStage::ImageType::YV12; + case webrtc::VideoType::kUYVY: + return CaptureStage::ImageType::UYVY; + case webrtc::VideoType::kNV12: + return CaptureStage::ImageType::NV12; + case webrtc::VideoType::kNV21: + return CaptureStage::ImageType::NV21; + case webrtc::VideoType::kMJPEG: + return CaptureStage::ImageType::MJPEG; + case webrtc::VideoType::kRGB24: + case webrtc::VideoType::kBGR24: + case webrtc::VideoType::kABGR: + case webrtc::VideoType::kARGB: + case webrtc::VideoType::kARGB4444: + case webrtc::VideoType::kRGB565: + case webrtc::VideoType::kARGB1555: + case webrtc::VideoType::kBGRA: + case webrtc::VideoType::kUnknown: + // Unlikely, and not represented by CaptureStage::ImageType. + return CaptureStage::ImageType::Unknown; + } + return CaptureStage::ImageType::Unknown; + }()); + + return 0; +} + +int32_t VideoCaptureAvFoundation::StopCapture() { + RTC_DCHECK_RUN_ON(&mChecker); + { + MutexLock lock(&api_lock_); + if (!mCapability) { + return 0; + } + mCapability = Nothing(); + } + + Monitor monitor("VideoCaptureAVFoundation::StopCapture"); + Monitor* copyableMonitor = &monitor; + MonitorAutoLock lock(monitor); + __block bool done = false; + + [mCapturer stopCaptureWithCompletionHandler:^(void) { + MonitorAutoLock lock2(*copyableMonitor); + MOZ_RELEASE_ASSERT(!done); + done = true; + copyableMonitor->Notify(); + }]; + + while (!done) { + monitor.Wait(); + } + + [mAdapter setCapturer:nil]; + + return 0; +} + +bool VideoCaptureAvFoundation::CaptureStarted() { + RTC_DCHECK_RUN_ON(&mChecker); + MutexLock lock(&api_lock_); + return mCapability.isSome(); +} + +int32_t VideoCaptureAvFoundation::CaptureSettings( + VideoCaptureCapability& aSettings) { + MOZ_CRASH("Unexpected call"); + return -1; +} + +int32_t VideoCaptureAvFoundation::OnFrame( + __strong RTCVideoFrame* _Nonnull aFrame) { + MaybeRegisterCallbackThread(); + if (MutexLock lock(&api_lock_); MOZ_LIKELY(mTrackingId)) { + mCaptureRecorder.Start( + 0, "VideoCaptureAVFoundation"_ns, *mTrackingId, aFrame.width, + aFrame.height, mImageType.valueOr(CaptureStage::ImageType::Unknown)); + if (mCapability && mCapability->videoType != webrtc::VideoType::kI420) { + mConversionRecorder.Start(0, "VideoCaptureAVFoundation"_ns, *mTrackingId, + aFrame.width, aFrame.height); + } + } + + const int64_t timestamp_us = + aFrame.timeStampNs / rtc::kNumNanosecsPerMicrosec; + RTCI420Buffer* buffer = [aFrame.buffer toI420]; + mConversionRecorder.Record(0); + // Accessing the (intended-to-be-private) native buffer directly is hacky but + // lets us skip two copies + rtc::scoped_refptr<webrtc::I420BufferInterface> nativeBuffer = + buffer.nativeI420Buffer; + auto frame = webrtc::VideoFrame::Builder() + .set_video_frame_buffer(nativeBuffer) + .set_rotation(ToNativeRotation(aFrame.rotation)) + .set_timestamp_us(timestamp_us) + .build(); + + MutexLock lock(&api_lock_); + int32_t rv = DeliverCapturedFrame(frame); + mCaptureRecorder.Record(0); + return rv; +} + +void VideoCaptureAvFoundation::SetTrackingId(uint32_t aTrackingIdProcId) { + RTC_DCHECK_RUN_ON(&mChecker); + MutexLock lock(&api_lock_); + if (NS_WARN_IF(mTrackingId.isSome())) { + // This capture instance must be shared across multiple camera requests. For + // now ignore other requests than the first. + return; + } + mTrackingId.emplace(TrackingId::Source::Camera, aTrackingIdProcId); +} + +void VideoCaptureAvFoundation::MaybeRegisterCallbackThread() { + ProfilerThreadId id = profiler_current_thread_id(); + if (MOZ_LIKELY(id == mCallbackThreadId)) { + return; + } + mCallbackThreadId = id; + CallbackThreadRegistry::Get()->Register(mCallbackThreadId, + "VideoCaptureAVFoundationCallback"); +} +} // namespace webrtc::videocapturemodule diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.cc b/dom/media/systemservices/video_engine/desktop_capture_impl.cc new file mode 100644 index 0000000000..a966ff06d4 --- /dev/null +++ b/dom/media/systemservices/video_engine/desktop_capture_impl.cc @@ -0,0 +1,782 @@ +/* + * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "video_engine/desktop_capture_impl.h" + +#include <cstdlib> +#include <memory> +#include <string> + +#include "CamerasTypes.h" +#include "VideoEngine.h" +#include "VideoUtils.h" +#include "api/video/i420_buffer.h" +#include "common_video/libyuv/include/webrtc_libyuv.h" +#include "libyuv.h" // NOLINT +#include "modules/include/module_common_types.h" +#include "modules/video_capture/video_capture_config.h" +#include "modules/video_capture/video_capture_impl.h" +#include "system_wrappers/include/clock.h" +#include "rtc_base/logging.h" +#include "rtc_base/ref_counted_object.h" +#include "rtc_base/time_utils.h" +#include "rtc_base/trace_event.h" +#include "modules/desktop_capture/desktop_and_cursor_composer.h" +#include "modules/desktop_capture/desktop_frame.h" +#include "modules/desktop_capture/desktop_capture_options.h" +#include "modules/desktop_capture/desktop_capturer_differ_wrapper.h" +#include "modules/video_capture/video_capture.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/TimeStamp.h" +#include "nsThreadUtils.h" +#include "tab_capturer.h" + +using mozilla::NewRunnableMethod; +using mozilla::TabCapturerWebrtc; +using mozilla::TimeDuration; +using mozilla::camera::CaptureDeviceType; +using mozilla::camera::CaptureEngine; + +static void CaptureFrameOnThread(nsITimer* aTimer, void* aClosure) { + static_cast<webrtc::DesktopCaptureImpl*>(aClosure)->CaptureFrameOnThread(); +} + +namespace webrtc { + +int32_t ScreenDeviceInfoImpl::Init() { + mDesktopDeviceInfo = + std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create()); + return 0; +} + +int32_t ScreenDeviceInfoImpl::Refresh() { + mDesktopDeviceInfo->Refresh(); + return 0; +} + +uint32_t ScreenDeviceInfoImpl::NumberOfDevices() { + return mDesktopDeviceInfo->getDisplayDeviceCount(); +} + +int32_t ScreenDeviceInfoImpl::GetDeviceName( + uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder) { + DesktopDisplayDevice desktopDisplayDevice; + + // always initialize output + if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) { + memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size); + } + + if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) { + memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size); + } + if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) { + memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size); + } + + if (mDesktopDeviceInfo->getDesktopDisplayDeviceInfo( + aDeviceNumber, desktopDisplayDevice) == 0) { + size_t len; + + const char* deviceName = desktopDisplayDevice.getDeviceName(); + len = deviceName ? strlen(deviceName) : 0; + if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) { + memcpy(aDeviceNameUTF8, deviceName, len); + } + + const char* deviceUniqueId = desktopDisplayDevice.getUniqueIdName(); + len = deviceUniqueId ? strlen(deviceUniqueId) : 0; + if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) { + memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len); + } + } + + return 0; +} + +int32_t ScreenDeviceInfoImpl::DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) { + // no device properties to change + return 0; +} + +int32_t ScreenDeviceInfoImpl::NumberOfCapabilities( + const char* aDeviceUniqueIdUTF8) { + return 0; +} + +int32_t ScreenDeviceInfoImpl::GetCapability( + const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability) { + return 0; +} + +int32_t ScreenDeviceInfoImpl::GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting) { + return 0; +} + +int32_t ScreenDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation) { + return 0; +} + +VideoCaptureModule* DesktopCaptureImpl::Create(const int32_t aModuleId, + const char* aUniqueId, + const CaptureDeviceType aType) { + return new rtc::RefCountedObject<DesktopCaptureImpl>(aModuleId, aUniqueId, + aType); +} + +int32_t WindowDeviceInfoImpl::Init() { + mDesktopDeviceInfo = + std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create()); + return 0; +} + +int32_t WindowDeviceInfoImpl::Refresh() { + mDesktopDeviceInfo->Refresh(); + return 0; +} + +uint32_t WindowDeviceInfoImpl::NumberOfDevices() { + return mDesktopDeviceInfo->getWindowCount(); +} + +int32_t WindowDeviceInfoImpl::GetDeviceName( + uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder) { + DesktopDisplayDevice desktopDisplayDevice; + + // always initialize output + if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) { + memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size); + } + if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) { + memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size); + } + if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) { + memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size); + } + + if (mDesktopDeviceInfo->getWindowInfo(aDeviceNumber, desktopDisplayDevice) == + 0) { + size_t len; + + const char* deviceName = desktopDisplayDevice.getDeviceName(); + len = deviceName ? strlen(deviceName) : 0; + if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) { + memcpy(aDeviceNameUTF8, deviceName, len); + } + + const char* deviceUniqueId = desktopDisplayDevice.getUniqueIdName(); + len = deviceUniqueId ? strlen(deviceUniqueId) : 0; + if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) { + memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len); + } + if (aPid) { + *aPid = desktopDisplayDevice.getPid(); + } + } + + return 0; +} + +int32_t WindowDeviceInfoImpl::DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) { + // no device properties to change + return 0; +} + +int32_t WindowDeviceInfoImpl::NumberOfCapabilities( + const char* aDeviceUniqueIdUTF8) { + return 0; +} + +int32_t WindowDeviceInfoImpl::GetCapability( + const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability) { + return 0; +} + +int32_t WindowDeviceInfoImpl::GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting) { + return 0; +} + +int32_t WindowDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation) { + return 0; +} + +int32_t BrowserDeviceInfoImpl::Init() { + mDesktopDeviceInfo = + std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create()); + return 0; +} + +int32_t BrowserDeviceInfoImpl::Refresh() { + mDesktopDeviceInfo->Refresh(); + return 0; +} + +uint32_t BrowserDeviceInfoImpl::NumberOfDevices() { + return mDesktopDeviceInfo->getTabCount(); +} + +int32_t BrowserDeviceInfoImpl::GetDeviceName( + uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder) { + DesktopTab desktopTab; + + // always initialize output + if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) { + memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size); + } + if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) { + memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size); + } + if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) { + memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size); + } + + if (mDesktopDeviceInfo->getTabInfo(aDeviceNumber, desktopTab) == 0) { + size_t len; + + const char* deviceName = desktopTab.getTabName(); + len = deviceName ? strlen(deviceName) : 0; + if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) { + memcpy(aDeviceNameUTF8, deviceName, len); + } + + const char* deviceUniqueId = desktopTab.getUniqueIdName(); + len = deviceUniqueId ? strlen(deviceUniqueId) : 0; + if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) { + memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len); + } + } + + return 0; +} + +int32_t BrowserDeviceInfoImpl::DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) { + // no device properties to change + return 0; +} + +int32_t BrowserDeviceInfoImpl::NumberOfCapabilities( + const char* aDeviceUniqueIdUTF8) { + return 0; +} + +int32_t BrowserDeviceInfoImpl::GetCapability( + const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability) { + return 0; +} + +int32_t BrowserDeviceInfoImpl::GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting) { + return 0; +} + +int32_t BrowserDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation) { + return 0; +} + +std::shared_ptr<VideoCaptureModule::DeviceInfo> +DesktopCaptureImpl::CreateDeviceInfo(const int32_t aId, + const CaptureDeviceType aType) { + if (aType == CaptureDeviceType::Screen) { + auto screenInfo = std::make_shared<ScreenDeviceInfoImpl>(aId); + if (!screenInfo || screenInfo->Init() != 0) { + return nullptr; + } + return screenInfo; + } + if (aType == CaptureDeviceType::Window) { + auto windowInfo = std::make_shared<WindowDeviceInfoImpl>(aId); + if (!windowInfo || windowInfo->Init() != 0) { + return nullptr; + } + return windowInfo; + } + if (aType == CaptureDeviceType::Browser) { + auto browserInfo = std::make_shared<BrowserDeviceInfoImpl>(aId); + if (!browserInfo || browserInfo->Init() != 0) { + return nullptr; + } + return browserInfo; + } + return nullptr; +} + +const char* DesktopCaptureImpl::CurrentDeviceName() const { + return mDeviceUniqueId.c_str(); +} + +static DesktopCaptureOptions CreateDesktopCaptureOptions() { + DesktopCaptureOptions options; +// Help avoid an X11 deadlock, see bug 1456101. +#ifdef MOZ_X11 + MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), + NS_NewRunnableFunction(__func__, [&] { + options = DesktopCaptureOptions::CreateDefault(); + }))); +#else + options = DesktopCaptureOptions::CreateDefault(); +#endif + + // Leave desktop effects enabled during WebRTC captures. + options.set_disable_effects(false); + +#if defined(WEBRTC_WIN) + if (mozilla::StaticPrefs::media_webrtc_capture_allow_directx()) { + options.set_allow_directx_capturer(true); + } + options.set_allow_cropping_window_capturer(true); +# if defined(RTC_ENABLE_WIN_WGC) + if (mozilla::StaticPrefs::media_webrtc_capture_screen_allow_wgc()) { + options.set_allow_wgc_screen_capturer(true); + options.set_allow_wgc_zero_hertz( + mozilla::StaticPrefs::media_webrtc_capture_wgc_allow_zero_hertz()); + } + if (mozilla::StaticPrefs::media_webrtc_capture_window_allow_wgc()) { + options.set_allow_wgc_window_capturer(true); + options.set_allow_wgc_zero_hertz( + mozilla::StaticPrefs::media_webrtc_capture_wgc_allow_zero_hertz()); + } +# endif +#endif + +#if defined(WEBRTC_MAC) + if (mozilla::StaticPrefs::media_webrtc_capture_allow_iosurface()) { + options.set_allow_iosurface(true); + } +#endif + +#if defined(WEBRTC_USE_PIPEWIRE) + if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire()) { + options.set_allow_pipewire(true); + } +#endif + + return options; +} + +static std::unique_ptr<DesktopCapturer> CreateTabCapturer( + const DesktopCaptureOptions& options, DesktopCapturer::SourceId aSourceId, + nsCOMPtr<nsISerialEventTarget> aCaptureThread) { + std::unique_ptr<DesktopCapturer> capturer = + TabCapturerWebrtc::Create(aSourceId, std::move(aCaptureThread)); + if (capturer && options.detect_updated_region()) { + capturer.reset(new DesktopCapturerDifferWrapper(std::move(capturer))); + } + + return capturer; +} + +static bool UsePipewire() { +#if defined(WEBRTC_USE_PIPEWIRE) + return mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() && + webrtc::DesktopCapturer::IsRunningUnderWayland(); +#else + return false; +#endif +} + +static std::unique_ptr<DesktopCapturer> CreateDesktopCapturerAndThread( + CaptureDeviceType aDeviceType, DesktopCapturer::SourceId aSourceId, + nsIThread** aOutThread) { + DesktopCaptureOptions options = CreateDesktopCaptureOptions(); + std::unique_ptr<DesktopCapturer> capturer; + + auto ensureThread = [&]() { + if (*aOutThread) { + return *aOutThread; + } + + nsIThreadManager::ThreadCreationOptions threadOptions; +#if defined(XP_WIN) || defined(XP_MACOSX) + // Windows desktop capture needs a UI thread. + // Mac screen capture needs a thread with a CFRunLoop. + threadOptions.isUiThread = true; +#endif + NS_NewNamedThread("DesktopCapture", aOutThread, nullptr, threadOptions); + return *aOutThread; + }; + + if ((aDeviceType == CaptureDeviceType::Screen || + aDeviceType == CaptureDeviceType::Window) && + UsePipewire()) { + capturer = DesktopCapturer::CreateGenericCapturer(options); + if (!capturer) { + return capturer; + } + + capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer), + options); + } else if (aDeviceType == CaptureDeviceType::Screen) { + capturer = DesktopCapturer::CreateScreenCapturer(options); + if (!capturer) { + return capturer; + } + + capturer->SelectSource(aSourceId); + + capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer), + options); + } else if (aDeviceType == CaptureDeviceType::Window) { +#if defined(RTC_ENABLE_WIN_WGC) + options.set_allow_wgc_capturer_fallback(true); +#endif + capturer = DesktopCapturer::CreateWindowCapturer(options); + if (!capturer) { + return capturer; + } + + capturer->SelectSource(aSourceId); + + capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer), + options); + } else if (aDeviceType == CaptureDeviceType::Browser) { + // XXX We don't capture cursors, so avoid the extra indirection layer. We + // could also pass null for the pMouseCursorMonitor. + capturer = CreateTabCapturer(options, aSourceId, ensureThread()); + } else { + MOZ_ASSERT(!capturer); + return capturer; + } + + MOZ_ASSERT(capturer); + ensureThread(); + + return capturer; +} + +DesktopCaptureImpl::DesktopCaptureImpl(const int32_t aId, const char* aUniqueId, + const CaptureDeviceType aType) + : mModuleId(aId), + mTrackingId(mozilla::TrackingId(CaptureEngineToTrackingSourceStr([&] { + switch (aType) { + case CaptureDeviceType::Screen: + return CaptureEngine::ScreenEngine; + case CaptureDeviceType::Window: + return CaptureEngine::WinEngine; + case CaptureDeviceType::Browser: + return CaptureEngine::BrowserEngine; + default: + return CaptureEngine::InvalidEngine; + } + }()), + aId)), + mDeviceUniqueId(aUniqueId), + mDeviceType(aType), + mControlThread(mozilla::GetCurrentSerialEventTarget()), + mNextFrameMinimumTime(Timestamp::Zero()), + mCallbacks("DesktopCaptureImpl::mCallbacks") {} + +DesktopCaptureImpl::~DesktopCaptureImpl() { + MOZ_ASSERT(!mCaptureThread); + MOZ_ASSERT(!mRequestedCapability); +} + +void DesktopCaptureImpl::RegisterCaptureDataCallback( + rtc::VideoSinkInterface<VideoFrame>* aDataCallback) { + auto callbacks = mCallbacks.Lock(); + callbacks->insert(aDataCallback); +} + +void DesktopCaptureImpl::DeRegisterCaptureDataCallback( + rtc::VideoSinkInterface<VideoFrame>* aDataCallback) { + auto callbacks = mCallbacks.Lock(); + auto it = callbacks->find(aDataCallback); + if (it != callbacks->end()) { + callbacks->erase(it); + } +} + +int32_t DesktopCaptureImpl::StopCaptureIfAllClientsClose() { + { + auto callbacks = mCallbacks.Lock(); + if (!callbacks->empty()) { + return 0; + } + } + return StopCapture(); +} + +int32_t DesktopCaptureImpl::SetCaptureRotation(VideoRotation aRotation) { + MOZ_ASSERT_UNREACHABLE("Unused"); + return -1; +} + +bool DesktopCaptureImpl::SetApplyRotation(bool aEnable) { return true; } + +int32_t DesktopCaptureImpl::StartCapture( + const VideoCaptureCapability& aCapability) { + RTC_DCHECK_RUN_ON(&mControlThreadChecker); + + if (mRequestedCapability) { + // Already initialized + MOZ_ASSERT(*mRequestedCapability == aCapability); + + return 0; + } + + MOZ_ASSERT(!mCaptureThread); + + DesktopCapturer::SourceId sourceId = std::stoi(mDeviceUniqueId); + std::unique_ptr capturer = CreateDesktopCapturerAndThread( + mDeviceType, sourceId, getter_AddRefs(mCaptureThread)); + + MOZ_ASSERT(!capturer == !mCaptureThread); + if (!capturer) { + return -1; + } + + mRequestedCapability = mozilla::Some(aCapability); + mCaptureThreadChecker.Detach(); + + MOZ_ALWAYS_SUCCEEDS(mCaptureThread->Dispatch(NS_NewRunnableFunction( + "DesktopCaptureImpl::InitOnThread", + [this, self = RefPtr(this), capturer = std::move(capturer), + maxFps = std::max(aCapability.maxFPS, 1)]() mutable { + InitOnThread(std::move(capturer), maxFps); + }))); + + return 0; +} + +bool DesktopCaptureImpl::FocusOnSelectedSource() { + RTC_DCHECK_RUN_ON(&mControlThreadChecker); + if (!mCaptureThread) { + MOZ_ASSERT_UNREACHABLE( + "FocusOnSelectedSource must be called after StartCapture"); + return false; + } + + bool success = false; + MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread( + mCaptureThread, NS_NewRunnableFunction(__func__, [&] { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + MOZ_ASSERT(mCapturer); + success = mCapturer && mCapturer->FocusOnSelectedSource(); + }))); + return success; +} + +int32_t DesktopCaptureImpl::StopCapture() { + RTC_DCHECK_RUN_ON(&mControlThreadChecker); + if (mRequestedCapability) { + // Sync-cancel the capture timer so no CaptureFrame calls will come in after + // we return. + MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread( + mCaptureThread, + NewRunnableMethod(__func__, this, + &DesktopCaptureImpl::ShutdownOnThread))); + + mRequestedCapability = mozilla::Nothing(); + } + + if (mCaptureThread) { + // CaptureThread shutdown. + mCaptureThread->AsyncShutdown(); + mCaptureThread = nullptr; + } + + return 0; +} + +bool DesktopCaptureImpl::CaptureStarted() { + MOZ_ASSERT_UNREACHABLE("Unused"); + return true; +} + +int32_t DesktopCaptureImpl::CaptureSettings(VideoCaptureCapability& aSettings) { + MOZ_ASSERT_UNREACHABLE("Unused"); + return -1; +} + +void DesktopCaptureImpl::OnCaptureResult(DesktopCapturer::Result aResult, + std::unique_ptr<DesktopFrame> aFrame) { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + if (!aFrame) { + return; + } + + const auto startProcessTime = Timestamp::Micros(rtc::TimeMicros()); + auto frameTime = startProcessTime; + if (auto diff = startProcessTime - mNextFrameMinimumTime; + diff < TimeDelta::Zero()) { + if (diff > TimeDelta::Millis(-1)) { + // Two consecutive frames within a millisecond is OK. It could happen due + // to timing. + frameTime = mNextFrameMinimumTime; + } else { + // Three consecutive frames within two milliseconds seems too much, drop + // one. + MOZ_ASSERT(diff >= TimeDelta::Millis(-2)); + RTC_LOG(LS_WARNING) << "DesktopCapture render time is getting too far " + "ahead. Framerate is unexpectedly high."; + return; + } + } + + uint8_t* videoFrame = aFrame->data(); + VideoCaptureCapability frameInfo; + frameInfo.width = aFrame->size().width(); + frameInfo.height = aFrame->size().height(); + frameInfo.videoType = VideoType::kARGB; + + size_t videoFrameLength = + frameInfo.width * frameInfo.height * DesktopFrame::kBytesPerPixel; + + const int32_t width = frameInfo.width; + const int32_t height = frameInfo.height; + + // Not encoded, convert to I420. + if (frameInfo.videoType != VideoType::kMJPEG && + CalcBufferSize(frameInfo.videoType, width, abs(height)) != + videoFrameLength) { + RTC_LOG(LS_ERROR) << "Wrong incoming frame length."; + return; + } + + int stride_y = width; + int stride_uv = (width + 1) / 2; + + // Setting absolute height (in case it was negative). + // In Windows, the image starts bottom left, instead of top left. + // Setting a negative source height, inverts the image (within LibYuv). + + mozilla::PerformanceRecorder<mozilla::CopyVideoStage> rec( + "DesktopCaptureImpl::ConvertToI420"_ns, mTrackingId, width, abs(height)); + // TODO(nisse): Use a pool? + rtc::scoped_refptr<I420Buffer> buffer = + I420Buffer::Create(width, abs(height), stride_y, stride_uv, stride_uv); + + const int conversionResult = libyuv::ConvertToI420( + videoFrame, videoFrameLength, buffer->MutableDataY(), buffer->StrideY(), + buffer->MutableDataU(), buffer->StrideU(), buffer->MutableDataV(), + buffer->StrideV(), 0, 0, // No Cropping + aFrame->stride() / DesktopFrame::kBytesPerPixel, height, width, height, + libyuv::kRotate0, ConvertVideoType(frameInfo.videoType)); + if (conversionResult != 0) { + RTC_LOG(LS_ERROR) << "Failed to convert capture frame from type " + << static_cast<int>(frameInfo.videoType) << "to I420."; + return; + } + rec.Record(); + + NotifyOnFrame(VideoFrame::Builder() + .set_video_frame_buffer(buffer) + .set_timestamp_us(frameTime.us()) + .build()); + + const TimeDelta processTime = + Timestamp::Micros(rtc::TimeMicros()) - startProcessTime; + + if (processTime > TimeDelta::Millis(10)) { + RTC_LOG(LS_WARNING) + << "Too long processing time of incoming frame with dimensions " + << width << "x" << height << ": " << processTime.ms() << " ms"; + } +} + +void DesktopCaptureImpl::NotifyOnFrame(const VideoFrame& aFrame) { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + // Set the next frame's minimum time to ensure two consecutive frames don't + // have an identical render time (which is in milliseconds). + Timestamp nextFrameMinimumTime = + Timestamp::Millis(aFrame.render_time_ms()) + TimeDelta::Millis(1); + + MOZ_ASSERT(nextFrameMinimumTime >= mNextFrameMinimumTime); + + mNextFrameMinimumTime = nextFrameMinimumTime; + auto callbacks = mCallbacks.Lock(); + for (auto* cb : *callbacks) { + cb->OnFrame(aFrame); + } +} + +void DesktopCaptureImpl::InitOnThread( + std::unique_ptr<DesktopCapturer> aCapturer, int aFramerate) { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + + mCapturer = std::move(aCapturer); + + // We need to call Start on the same thread we call CaptureFrame on. + mCapturer->Start(this); + + mCaptureTimer = NS_NewTimer(); + mRequestedCaptureInterval = mozilla::Some( + TimeDuration::FromSeconds(1. / static_cast<double>(aFramerate))); + + CaptureFrameOnThread(); +} + +void DesktopCaptureImpl::ShutdownOnThread() { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + if (mCaptureTimer) { + mCaptureTimer->Cancel(); + mCaptureTimer = nullptr; + } + + // DesktopCapturer dtor blocks until fully shut down. TabCapturerWebrtc needs + // the capture thread to be alive. + mCapturer = nullptr; + + mRequestedCaptureInterval = mozilla::Nothing(); +} + +void DesktopCaptureImpl::CaptureFrameOnThread() { + RTC_DCHECK_RUN_ON(&mCaptureThreadChecker); + + auto start = mozilla::TimeStamp::Now(); + mCapturer->CaptureFrame(); + auto end = mozilla::TimeStamp::Now(); + + // Calculate next capture time. + const auto duration = end - start; + const auto timeUntilRequestedCapture = *mRequestedCaptureInterval - duration; + + // Use at most x% CPU or limit framerate + constexpr float sleepTimeFactor = + (100.0f / kMaxDesktopCaptureCpuUsage) - 1.0f; + static_assert(sleepTimeFactor >= 0.0); + static_assert(sleepTimeFactor < 100.0); + const auto sleepTime = duration.MultDouble(sleepTimeFactor); + + mCaptureTimer->InitHighResolutionWithNamedFuncCallback( + &::CaptureFrameOnThread, this, + std::max(timeUntilRequestedCapture, sleepTime), nsITimer::TYPE_ONE_SHOT, + "DesktopCaptureImpl::mCaptureTimer"); +} + +} // namespace webrtc diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.h b/dom/media/systemservices/video_engine/desktop_capture_impl.h new file mode 100644 index 0000000000..7292f6c8a7 --- /dev/null +++ b/dom/media/systemservices/video_engine/desktop_capture_impl.h @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_ +#define WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_ + +/* + * video_capture_impl.h + */ + +#include <memory> +#include <set> +#include <string> + +#include "api/sequence_checker.h" +#include "api/video/video_frame.h" +#include "api/video/video_sink_interface.h" +#include "modules/desktop_capture/desktop_capturer.h" +#include "modules/video_capture/video_capture.h" + +#include "desktop_device_info.h" +#include "mozilla/DataMutex.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" +#include "PerformanceRecorder.h" + +class nsIThread; +class nsITimer; + +namespace mozilla::camera { +enum class CaptureDeviceType; +} + +namespace webrtc { + +class VideoCaptureEncodeInterface; + +// simulate deviceInfo interface for video engine, bridge screen/application and +// real screen/application device info + +class ScreenDeviceInfoImpl : public VideoCaptureModule::DeviceInfo { + public: + ScreenDeviceInfoImpl(int32_t aId) : mId(aId) {} + virtual ~ScreenDeviceInfoImpl() = default; + + int32_t Init(); + int32_t Refresh(); + + virtual uint32_t NumberOfDevices(); + virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8, + uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, + uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, + uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder = nullptr); + + virtual int32_t DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY); + virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8); + virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8, + uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability); + + virtual int32_t GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting); + virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation); + + protected: + int32_t mId; + std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo; +}; + +class WindowDeviceInfoImpl : public VideoCaptureModule::DeviceInfo { + public: + WindowDeviceInfoImpl(int32_t aId) : mId(aId){}; + virtual ~WindowDeviceInfoImpl() = default; + + int32_t Init(); + int32_t Refresh(); + + virtual uint32_t NumberOfDevices(); + virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8, + uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, + uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, + uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder = nullptr); + + virtual int32_t DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY); + virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8); + virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8, + uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability); + + virtual int32_t GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting); + virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation); + + protected: + int32_t mId; + std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo; +}; + +class BrowserDeviceInfoImpl : public VideoCaptureModule::DeviceInfo { + public: + BrowserDeviceInfoImpl(int32_t aId) : mId(aId){}; + virtual ~BrowserDeviceInfoImpl() = default; + + int32_t Init(); + int32_t Refresh(); + + virtual uint32_t NumberOfDevices(); + virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8, + uint32_t aDeviceNameUTF8Size, + char* aDeviceUniqueIdUTF8, + uint32_t aDeviceUniqueIdUTF8Size, + char* aProductUniqueIdUTF8, + uint32_t aProductUniqueIdUTF8Size, pid_t* aPid, + bool* aDeviceIsPlaceholder = nullptr); + + virtual int32_t DisplayCaptureSettingsDialogBox( + const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8, + void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY); + virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8); + virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8, + uint32_t aDeviceCapabilityNumber, + VideoCaptureCapability& aCapability); + + virtual int32_t GetBestMatchedCapability( + const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested, + VideoCaptureCapability& aResulting); + virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8, + VideoRotation& aOrientation); + + protected: + int32_t mId; + std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo; +}; + +// Reuses the video engine pipeline for screen sharing. +// As with video, DesktopCaptureImpl is a proxy for screen sharing +// and follows the video pipeline design +class DesktopCaptureImpl : public DesktopCapturer::Callback, + public VideoCaptureModule { + public: + /* Create a screen capture modules object + */ + static VideoCaptureModule* Create( + const int32_t aModuleId, const char* aUniqueId, + const mozilla::camera::CaptureDeviceType aType); + + [[nodiscard]] static std::shared_ptr<VideoCaptureModule::DeviceInfo> + CreateDeviceInfo(const int32_t aId, + const mozilla::camera::CaptureDeviceType aType); + + // mControlThread only. + void RegisterCaptureDataCallback( + rtc::VideoSinkInterface<VideoFrame>* aCallback) override; + void RegisterCaptureDataCallback( + RawVideoSinkInterface* dataCallback) override {} + void DeRegisterCaptureDataCallback( + rtc::VideoSinkInterface<VideoFrame>* aCallback) override; + int32_t StopCaptureIfAllClientsClose() override; + + int32_t SetCaptureRotation(VideoRotation aRotation) override; + bool SetApplyRotation(bool aEnable) override; + bool GetApplyRotation() override { return true; } + + const char* CurrentDeviceName() const override; + + int32_t StartCapture(const VideoCaptureCapability& aCapability) override; + virtual bool FocusOnSelectedSource() override; + int32_t StopCapture() override; + bool CaptureStarted() override; + int32_t CaptureSettings(VideoCaptureCapability& aSettings) override; + + void CaptureFrameOnThread(); + + const int32_t mModuleId; + const mozilla::TrackingId mTrackingId; + const std::string mDeviceUniqueId; + const mozilla::camera::CaptureDeviceType mDeviceType; + + protected: + DesktopCaptureImpl(const int32_t aId, const char* aUniqueId, + const mozilla::camera::CaptureDeviceType aType); + virtual ~DesktopCaptureImpl(); + + private: + // Maximum CPU usage in %. + static constexpr uint32_t kMaxDesktopCaptureCpuUsage = 50; + void InitOnThread(std::unique_ptr<DesktopCapturer> aCapturer, int aFramerate); + void ShutdownOnThread(); + // DesktopCapturer::Callback interface. + void OnCaptureResult(DesktopCapturer::Result aResult, + std::unique_ptr<DesktopFrame> aFrame) override; + + // Notifies all mCallbacks of OnFrame(). mCaptureThread only. + void NotifyOnFrame(const VideoFrame& aFrame); + + // Control thread on which the public API is called. + const nsCOMPtr<nsISerialEventTarget> mControlThread; + // Set in StartCapture. + mozilla::Maybe<VideoCaptureCapability> mRequestedCapability + RTC_GUARDED_BY(mControlThreadChecker); + // The DesktopCapturer is created on mControlThread but assigned and accessed + // only on mCaptureThread. + std::unique_ptr<DesktopCapturer> mCapturer + RTC_GUARDED_BY(mCaptureThreadChecker); + // Dedicated thread that does the capturing. + nsCOMPtr<nsIThread> mCaptureThread RTC_GUARDED_BY(mControlThreadChecker); + // Checks that API methods are called on mControlThread. + webrtc::SequenceChecker mControlThreadChecker; + // Checks that frame delivery only happens on mCaptureThread. + webrtc::SequenceChecker mCaptureThreadChecker; + // Timer that triggers frame captures. Only used on mCaptureThread. + // TODO(Bug 1806646): Drive capture with vsync instead. + nsCOMPtr<nsITimer> mCaptureTimer RTC_GUARDED_BY(mCaptureThreadChecker); + // Interval between captured frames, based on the framerate in + // mRequestedCapability. mCaptureThread only. + mozilla::Maybe<mozilla::TimeDuration> mRequestedCaptureInterval + RTC_GUARDED_BY(mCaptureThreadChecker); + // Used to make sure incoming timestamp is increasing for every frame. + webrtc::Timestamp mNextFrameMinimumTime RTC_GUARDED_BY(mCaptureThreadChecker); + // Callbacks for captured frames. Mutated on mControlThread, callbacks happen + // on mCaptureThread. + mozilla::DataMutex<std::set<rtc::VideoSinkInterface<VideoFrame>*>> mCallbacks; +}; + +} // namespace webrtc + +#endif // WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_ diff --git a/dom/media/systemservices/video_engine/desktop_device_info.cc b/dom/media/systemservices/video_engine/desktop_device_info.cc new file mode 100644 index 0000000000..185bfe6254 --- /dev/null +++ b/dom/media/systemservices/video_engine/desktop_device_info.cc @@ -0,0 +1,488 @@ +/* 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 "desktop_device_info.h" +#include "modules/desktop_capture/desktop_capture_options.h" +#include "modules/desktop_capture/desktop_capturer.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/UniquePtr.h" +#include "nsIBrowserWindowTracker.h" +#include "nsImportModule.h" + +#include <cstddef> +#include <cstdlib> +#include <cstdio> +#include <cstring> +#include <memory> + +namespace webrtc { + +static inline void SetStringMember(char** aMember, const char* aValue) { + if (!aValue) { + return; + } + + if (*aMember) { + delete[] *aMember; + *aMember = nullptr; + } + + size_t nBufLen = strlen(aValue) + 1; + char* buffer = new char[nBufLen]; + memcpy(buffer, aValue, nBufLen - 1); + buffer[nBufLen - 1] = '\0'; + *aMember = buffer; +} + +DesktopDisplayDevice::DesktopDisplayDevice() { + mScreenId = kInvalidScreenId; + mDeviceUniqueIdUTF8 = nullptr; + mDeviceNameUTF8 = nullptr; + mPid = 0; +} + +DesktopDisplayDevice::~DesktopDisplayDevice() { + mScreenId = kInvalidScreenId; + + delete[] mDeviceUniqueIdUTF8; + delete[] mDeviceNameUTF8; + + mDeviceUniqueIdUTF8 = nullptr; + mDeviceNameUTF8 = nullptr; +} + +void DesktopDisplayDevice::setScreenId(const ScreenId aScreenId) { + mScreenId = aScreenId; +} + +void DesktopDisplayDevice::setDeviceName(const char* aDeviceNameUTF8) { + SetStringMember(&mDeviceNameUTF8, aDeviceNameUTF8); +} + +void DesktopDisplayDevice::setUniqueIdName(const char* aDeviceUniqueIdUTF8) { + SetStringMember(&mDeviceUniqueIdUTF8, aDeviceUniqueIdUTF8); +} + +void DesktopDisplayDevice::setPid(const int aPid) { mPid = aPid; } + +ScreenId DesktopDisplayDevice::getScreenId() { return mScreenId; } + +const char* DesktopDisplayDevice::getDeviceName() { return mDeviceNameUTF8; } + +const char* DesktopDisplayDevice::getUniqueIdName() { + return mDeviceUniqueIdUTF8; +} + +pid_t DesktopDisplayDevice::getPid() { return mPid; } + +DesktopDisplayDevice& DesktopDisplayDevice::operator=( + DesktopDisplayDevice& aOther) { + if (&aOther == this) { + return *this; + } + mScreenId = aOther.getScreenId(); + setUniqueIdName(aOther.getUniqueIdName()); + setDeviceName(aOther.getDeviceName()); + mPid = aOther.getPid(); + + return *this; +} + +DesktopTab::DesktopTab() { + mTabBrowserId = 0; + mTabNameUTF8 = nullptr; + mTabUniqueIdUTF8 = nullptr; + mTabCount = 0; +} + +DesktopTab::~DesktopTab() { + delete[] mTabNameUTF8; + delete[] mTabUniqueIdUTF8; + + mTabNameUTF8 = nullptr; + mTabUniqueIdUTF8 = nullptr; +} + +void DesktopTab::setTabBrowserId(uint64_t aTabBrowserId) { + mTabBrowserId = aTabBrowserId; +} + +void DesktopTab::setUniqueIdName(const char* aTabUniqueIdUTF8) { + SetStringMember(&mTabUniqueIdUTF8, aTabUniqueIdUTF8); +} + +void DesktopTab::setTabName(const char* aTabNameUTF8) { + SetStringMember(&mTabNameUTF8, aTabNameUTF8); +} + +void DesktopTab::setTabCount(const uint32_t aCount) { mTabCount = aCount; } + +uint64_t DesktopTab::getTabBrowserId() { return mTabBrowserId; } + +const char* DesktopTab::getUniqueIdName() { return mTabUniqueIdUTF8; } + +const char* DesktopTab::getTabName() { return mTabNameUTF8; } + +uint32_t DesktopTab::getTabCount() { return mTabCount; } + +DesktopTab& DesktopTab::operator=(DesktopTab& aOther) { + mTabBrowserId = aOther.getTabBrowserId(); + setUniqueIdName(aOther.getUniqueIdName()); + setTabName(aOther.getTabName()); + + return *this; +} + +class DesktopDeviceInfoImpl : public DesktopDeviceInfo { + public: + DesktopDeviceInfoImpl(); + ~DesktopDeviceInfoImpl(); + + int32_t Init() override; + int32_t Refresh() override; + int32_t getDisplayDeviceCount() override; + int32_t getDesktopDisplayDeviceInfo( + uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) override; + int32_t getWindowCount() override; + int32_t getWindowInfo(uint32_t aIndex, + DesktopDisplayDevice& aWindowDevice) override; + uint32_t getTabCount() override; + int32_t getTabInfo(uint32_t aIndex, DesktopTab& aDesktopTab) override; + + protected: + DesktopDisplayDeviceList mDesktopDisplayList; + DesktopDisplayDeviceList mDesktopWindowList; + DesktopTabList mDesktopTabList; + + void CleanUp(); + void CleanUpWindowList(); + void CleanUpTabList(); + void CleanUpScreenList(); + + void InitializeWindowList(); + virtual void InitializeTabList(); + void InitializeScreenList(); + + void RefreshWindowList(); + void RefreshTabList(); + void RefreshScreenList(); + + void DummyTabList(DesktopTabList& aList); +}; + +DesktopDeviceInfoImpl::DesktopDeviceInfoImpl() = default; + +DesktopDeviceInfoImpl::~DesktopDeviceInfoImpl() { CleanUp(); } + +int32_t DesktopDeviceInfoImpl::getDisplayDeviceCount() { + return static_cast<int32_t>(mDesktopDisplayList.size()); +} + +int32_t DesktopDeviceInfoImpl::getDesktopDisplayDeviceInfo( + uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) { + if (aIndex >= mDesktopDisplayList.size()) { + return -1; + } + + std::map<intptr_t, DesktopDisplayDevice*>::iterator iter = + mDesktopDisplayList.begin(); + std::advance(iter, aIndex); + DesktopDisplayDevice* desktopDisplayDevice = iter->second; + if (desktopDisplayDevice) { + aDesktopDisplayDevice = (*desktopDisplayDevice); + } + + return 0; +} + +int32_t DesktopDeviceInfoImpl::getWindowCount() { + return static_cast<int32_t>(mDesktopWindowList.size()); +} + +int32_t DesktopDeviceInfoImpl::getWindowInfo( + uint32_t aIndex, DesktopDisplayDevice& aWindowDevice) { + if (aIndex >= mDesktopWindowList.size()) { + return -1; + } + + std::map<intptr_t, DesktopDisplayDevice*>::iterator itr = + mDesktopWindowList.begin(); + std::advance(itr, aIndex); + DesktopDisplayDevice* window = itr->second; + if (!window) { + return -1; + } + + aWindowDevice = (*window); + return 0; +} + +uint32_t DesktopDeviceInfoImpl::getTabCount() { return mDesktopTabList.size(); } + +int32_t DesktopDeviceInfoImpl::getTabInfo(uint32_t aIndex, + DesktopTab& aDesktopTab) { + if (aIndex >= mDesktopTabList.size()) { + return -1; + } + + std::map<intptr_t, DesktopTab*>::iterator iter = mDesktopTabList.begin(); + std::advance(iter, aIndex); + DesktopTab* desktopTab = iter->second; + if (desktopTab) { + aDesktopTab = (*desktopTab); + } + + return 0; +} + +void DesktopDeviceInfoImpl::CleanUp() { + CleanUpScreenList(); + CleanUpWindowList(); + CleanUpTabList(); +} +int32_t DesktopDeviceInfoImpl::Init() { + InitializeScreenList(); + InitializeWindowList(); + InitializeTabList(); + + return 0; +} +int32_t DesktopDeviceInfoImpl::Refresh() { + RefreshScreenList(); + RefreshWindowList(); + RefreshTabList(); + + return 0; +} + +void DesktopDeviceInfoImpl::CleanUpWindowList() { + std::map<intptr_t, DesktopDisplayDevice*>::iterator iterWindow; + for (iterWindow = mDesktopWindowList.begin(); + iterWindow != mDesktopWindowList.end(); iterWindow++) { + DesktopDisplayDevice* aWindow = iterWindow->second; + delete aWindow; + iterWindow->second = nullptr; + } + mDesktopWindowList.clear(); +} + +void DesktopDeviceInfoImpl::InitializeWindowList() { + DesktopCaptureOptions options; + +// Wayland is special and we will not get any information about windows +// without going through xdg-desktop-portal. We will already have +// a screen placeholder so there is no reason to build windows list. +#if defined(WEBRTC_USE_PIPEWIRE) + if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() && + webrtc::DesktopCapturer::IsRunningUnderWayland()) { + return; + } +#endif + +// Help avoid an X11 deadlock, see bug 1456101. +#ifdef MOZ_X11 + MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), + NS_NewRunnableFunction(__func__, [&] { + options = DesktopCaptureOptions::CreateDefault(); + }))); +#else + options = DesktopCaptureOptions::CreateDefault(); +#endif + std::unique_ptr<DesktopCapturer> winCap = + DesktopCapturer::CreateWindowCapturer(options); + DesktopCapturer::SourceList list; + if (winCap && winCap->GetSourceList(&list)) { + DesktopCapturer::SourceList::iterator itr; + for (itr = list.begin(); itr != list.end(); itr++) { + DesktopDisplayDevice* winDevice = new DesktopDisplayDevice; + if (!winDevice) { + continue; + } + + winDevice->setScreenId(itr->id); + winDevice->setDeviceName(itr->title.c_str()); + winDevice->setPid(itr->pid); + + char idStr[BUFSIZ]; +#if WEBRTC_WIN + _snprintf_s(idStr, sizeof(idStr), sizeof(idStr) - 1, "%ld", + static_cast<long>(winDevice->getScreenId())); +#else + SprintfLiteral(idStr, "%ld", static_cast<long>(winDevice->getScreenId())); +#endif + winDevice->setUniqueIdName(idStr); + mDesktopWindowList[winDevice->getScreenId()] = winDevice; + } + } +} + +void DesktopDeviceInfoImpl::RefreshWindowList() { + CleanUpWindowList(); + InitializeWindowList(); +} + +void DesktopDeviceInfoImpl::CleanUpTabList() { + for (auto& iterTab : mDesktopTabList) { + DesktopTab* desktopTab = iterTab.second; + delete desktopTab; + iterTab.second = nullptr; + } + mDesktopTabList.clear(); +} + +void webrtc::DesktopDeviceInfoImpl::InitializeTabList() { + if (!mozilla::StaticPrefs::media_getusermedia_browser_enabled()) { + return; + } + + // This is a sync dispatch to main thread, which is unfortunate. To + // call JavaScript we have to be on main thread, but the remaining + // DesktopCapturer very much wants to be off main thread. This might + // be solvable by calling this method earlier on while we're still on + // main thread and plumbing the information down to here. + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(__func__, [&] { + nsresult rv; + nsCOMPtr<nsIBrowserWindowTracker> bwt = + do_ImportESModule("resource:///modules/BrowserWindowTracker.sys.mjs", + "BrowserWindowTracker", &rv); + if (NS_FAILED(rv)) { + return; + } + + nsTArray<RefPtr<nsIVisibleTab>> tabArray; + rv = bwt->GetAllVisibleTabs(tabArray); + if (NS_FAILED(rv)) { + return; + } + + for (const auto& browserTab : tabArray) { + nsString contentTitle; + browserTab->GetContentTitle(contentTitle); + int64_t browserId; + browserTab->GetBrowserId(&browserId); + + DesktopTab* desktopTab = new DesktopTab; + if (desktopTab) { + char* contentTitleUTF8 = ToNewUTF8String(contentTitle); + desktopTab->setTabBrowserId(browserId); + desktopTab->setTabName(contentTitleUTF8); + std::ostringstream uniqueId; + uniqueId << browserId; + desktopTab->setUniqueIdName(uniqueId.str().c_str()); + mDesktopTabList[static_cast<intptr_t>(desktopTab->getTabBrowserId())] = + desktopTab; + free(contentTitleUTF8); + } + } + }); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), runnable); +} + +void DesktopDeviceInfoImpl::RefreshTabList() { + CleanUpTabList(); + InitializeTabList(); +} + +void DesktopDeviceInfoImpl::CleanUpScreenList() { + std::map<intptr_t, DesktopDisplayDevice*>::iterator iterDevice; + for (iterDevice = mDesktopDisplayList.begin(); + iterDevice != mDesktopDisplayList.end(); iterDevice++) { + DesktopDisplayDevice* desktopDisplayDevice = iterDevice->second; + delete desktopDisplayDevice; + iterDevice->second = nullptr; + } + mDesktopDisplayList.clear(); +} + +// With PipeWire we can't select which system resource is shared so +// we don't create a window/screen list. Instead we place these constants +// as window name/id so frontend code can identify PipeWire backend +// and does not try to create screen/window preview. + +#define PIPEWIRE_ID 0xaffffff +#define PIPEWIRE_NAME "####_PIPEWIRE_PORTAL_####" + +void DesktopDeviceInfoImpl::InitializeScreenList() { + DesktopCaptureOptions options; + +// Wayland is special and we will not get any information about screens +// without going through xdg-desktop-portal so we just need a screen +// placeholder. +#if defined(WEBRTC_USE_PIPEWIRE) + if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() && + webrtc::DesktopCapturer::IsRunningUnderWayland()) { + DesktopDisplayDevice* screenDevice = new DesktopDisplayDevice; + if (!screenDevice) { + return; + } + + screenDevice->setScreenId(PIPEWIRE_ID); + screenDevice->setDeviceName(PIPEWIRE_NAME); + + char idStr[BUFSIZ]; + SprintfLiteral(idStr, "%ld", + static_cast<long>(screenDevice->getScreenId())); + screenDevice->setUniqueIdName(idStr); + mDesktopDisplayList[screenDevice->getScreenId()] = screenDevice; + return; + } +#endif + +// Help avoid an X11 deadlock, see bug 1456101. +#ifdef MOZ_X11 + MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), + NS_NewRunnableFunction(__func__, [&] { + options = DesktopCaptureOptions::CreateDefault(); + }))); +#else + options = DesktopCaptureOptions::CreateDefault(); +#endif + std::unique_ptr<DesktopCapturer> screenCapturer = + DesktopCapturer::CreateScreenCapturer(options); + DesktopCapturer::SourceList list; + if (screenCapturer && screenCapturer->GetSourceList(&list)) { + DesktopCapturer::SourceList::iterator itr; + for (itr = list.begin(); itr != list.end(); itr++) { + DesktopDisplayDevice* screenDevice = new DesktopDisplayDevice; + screenDevice->setScreenId(itr->id); + if (list.size() == 1) { + screenDevice->setDeviceName("Primary Monitor"); + } else { + screenDevice->setDeviceName(itr->title.c_str()); + } + screenDevice->setPid(itr->pid); + + char idStr[BUFSIZ]; +#if WEBRTC_WIN + _snprintf_s(idStr, sizeof(idStr), sizeof(idStr) - 1, "%ld", + static_cast<long>(screenDevice->getScreenId())); +#else + SprintfLiteral(idStr, "%ld", + static_cast<long>(screenDevice->getScreenId())); +#endif + screenDevice->setUniqueIdName(idStr); + mDesktopDisplayList[screenDevice->getScreenId()] = screenDevice; + } + } +} + +void DesktopDeviceInfoImpl::RefreshScreenList() { + CleanUpScreenList(); + InitializeScreenList(); +} + +/* static */ +DesktopDeviceInfo* DesktopDeviceInfo::Create() { + auto info = mozilla::MakeUnique<DesktopDeviceInfoImpl>(); + if (info->Init() != 0) { + return nullptr; + } + return info.release(); +} +} // namespace webrtc diff --git a/dom/media/systemservices/video_engine/desktop_device_info.h b/dom/media/systemservices/video_engine/desktop_device_info.h new file mode 100644 index 0000000000..824792b3c0 --- /dev/null +++ b/dom/media/systemservices/video_engine/desktop_device_info.h @@ -0,0 +1,84 @@ +/* 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/. */ + +#ifndef WEBRTC_MODULES_DESKTOP_CAPTURE_DEVICE_INFO_H_ +#define WEBRTC_MODULES_DESKTOP_CAPTURE_DEVICE_INFO_H_ + +#include <map> +#include "modules/desktop_capture/desktop_capture_types.h" + +namespace webrtc { + +class DesktopDisplayDevice { + public: + DesktopDisplayDevice(); + ~DesktopDisplayDevice(); + + void setScreenId(const ScreenId aScreenId); + void setDeviceName(const char* aDeviceNameUTF8); + void setUniqueIdName(const char* aDeviceUniqueIdUTF8); + void setPid(pid_t aPid); + + ScreenId getScreenId(); + const char* getDeviceName(); + const char* getUniqueIdName(); + pid_t getPid(); + + DesktopDisplayDevice& operator=(DesktopDisplayDevice& aOther); + + protected: + ScreenId mScreenId; + char* mDeviceNameUTF8; + char* mDeviceUniqueIdUTF8; + pid_t mPid; +}; + +using DesktopDisplayDeviceList = std::map<intptr_t, DesktopDisplayDevice*>; + +class DesktopTab { + public: + DesktopTab(); + ~DesktopTab(); + + void setTabBrowserId(uint64_t aTabBrowserId); + void setUniqueIdName(const char* aTabUniqueIdUTF8); + void setTabName(const char* aTabNameUTF8); + void setTabCount(const uint32_t aCount); + + uint64_t getTabBrowserId(); + const char* getUniqueIdName(); + const char* getTabName(); + uint32_t getTabCount(); + + DesktopTab& operator=(DesktopTab& aOther); + + protected: + uint64_t mTabBrowserId; + char* mTabNameUTF8; + char* mTabUniqueIdUTF8; + uint32_t mTabCount; +}; + +using DesktopTabList = std::map<intptr_t, DesktopTab*>; + +class DesktopDeviceInfo { + public: + virtual ~DesktopDeviceInfo() = default; + + virtual int32_t Init() = 0; + virtual int32_t Refresh() = 0; + virtual int32_t getDisplayDeviceCount() = 0; + virtual int32_t getDesktopDisplayDeviceInfo( + uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) = 0; + virtual int32_t getWindowCount() = 0; + virtual int32_t getWindowInfo(uint32_t aIndex, + DesktopDisplayDevice& aWindowDevice) = 0; + virtual uint32_t getTabCount() = 0; + virtual int32_t getTabInfo(uint32_t aIndex, DesktopTab& aDesktopTab) = 0; + + static DesktopDeviceInfo* Create(); +}; +}; // namespace webrtc + +#endif diff --git a/dom/media/systemservices/video_engine/placeholder_device_info.cc b/dom/media/systemservices/video_engine/placeholder_device_info.cc new file mode 100644 index 0000000000..62496b1b93 --- /dev/null +++ b/dom/media/systemservices/video_engine/placeholder_device_info.cc @@ -0,0 +1,60 @@ +/* -*- 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 "placeholder_device_info.h" +#include "modules/video_capture/video_capture_factory.h" + +namespace mozilla { + +PlaceholderDeviceInfo::PlaceholderDeviceInfo(bool aCameraPresent) + : mCameraPresent(aCameraPresent) {} + +PlaceholderDeviceInfo::~PlaceholderDeviceInfo() = default; + +uint32_t PlaceholderDeviceInfo::NumberOfDevices() { return mCameraPresent; } + +int32_t PlaceholderDeviceInfo::Init() { return 0; } + +int32_t PlaceholderDeviceInfo::GetDeviceName( + uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameLength, + char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Length, + char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Length, + pid_t* aPid, bool* aDeviceIsPlaceholder) { + // Check whether there is camera device reported by the Camera portal + // When the promise is resolved, it means there is a camera available + // but we have to use a placeholder device. + if (!mCameraPresent) { + return -1; + } + + // Making these empty to follow the specs for non-legacy enumeration: + // https://w3c.github.io/mediacapture-main/#access-control-model + memset(aDeviceNameUTF8, 0, aDeviceNameLength); + memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Length); + + if (aProductUniqueIdUTF8) { + memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Length); + } + + if (aDeviceIsPlaceholder) { + *aDeviceIsPlaceholder = true; + } + + return 0; +} + +int32_t PlaceholderDeviceInfo::CreateCapabilityMap( + const char* /*aDeviceUniqueIdUTF8*/) { + return -1; +} + +int32_t PlaceholderDeviceInfo::DisplayCaptureSettingsDialogBox( + const char* /*deviceUniqueIdUTF8*/, const char* /*dialogTitleUTF8*/, + void* /*parentWindow*/, uint32_t /*positionX*/, uint32_t /*positionY*/) { + return -1; +} + +} // namespace mozilla diff --git a/dom/media/systemservices/video_engine/placeholder_device_info.h b/dom/media/systemservices/video_engine/placeholder_device_info.h new file mode 100644 index 0000000000..5632c1f0cd --- /dev/null +++ b/dom/media/systemservices/video_engine/placeholder_device_info.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_SYSTEMSERVICES_VIDEO_ENGINE_PLACEHOLDER_DEVICE_INFO_H_ +#define DOM_MEDIA_SYSTEMSERVICES_VIDEO_ENGINE_PLACEHOLDER_DEVICE_INFO_H_ + +#include "modules/video_capture/device_info_impl.h" +#include "modules/video_capture/video_capture.h" +#include "modules/video_capture/video_capture_impl.h" + +namespace mozilla { + +class PlaceholderDeviceInfo + : public webrtc::videocapturemodule::DeviceInfoImpl { + public: + explicit PlaceholderDeviceInfo(bool aCameraPresent); + ~PlaceholderDeviceInfo() override; + + uint32_t NumberOfDevices() override; + int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8, + uint32_t aDeviceNameLength, char* aDeviceUniqueIdUTF8, + uint32_t aDeviceUniqueIdUTF8Length, + char* aProductUniqueIdUTF8 = nullptr, + uint32_t aProductUniqueIdUTF8Length = 0, + pid_t* aPid = nullptr, + bool* aDeviceIsPlaceholder = nullptr) override; + + int32_t CreateCapabilityMap(const char* aDeviceUniqueIdUTF8) override; + int32_t DisplayCaptureSettingsDialogBox(const char* aDeviceUniqueIdUTF8, + const char* aDialogTitleUTF8, + void* aParentWindow, + uint32_t aPositionX, + uint32_t aPositionY) override; + int32_t Init() override; + + private: + const bool mCameraPresent; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_SYSTEMSERVICES_VIDEO_ENGINE_PLACEHOLDER_DEVICE_INFO_H_ diff --git a/dom/media/systemservices/video_engine/platform_uithread.cc b/dom/media/systemservices/video_engine/platform_uithread.cc new file mode 100644 index 0000000000..701a989a18 --- /dev/null +++ b/dom/media/systemservices/video_engine/platform_uithread.cc @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#if defined(WEBRTC_WIN) + +# include "platform_uithread.h" + +namespace rtc { + +// timer id used in delayed callbacks +static const UINT_PTR kTimerId = 1; +static const wchar_t kThisProperty[] = L"ThreadWindowsUIPtr"; +static const wchar_t kThreadWindow[] = L"WebrtcWindowsUIThread"; + +PlatformUIThread::~PlatformUIThread() { + CritScope scoped_lock(&cs_); + switch (state_) { + case State::STARTED: { + MOZ_DIAGNOSTIC_ASSERT( + false, "PlatformUIThread must be stopped before destruction"); + break; + } + case State::STOPPED: + break; + case State::UNSTARTED: + break; + } +} + +bool PlatformUIThread::InternalInit() { + // Create an event window for use in generating callbacks to capture + // objects. + CritScope scoped_lock(&cs_); + switch (state_) { + // We have already started there is nothing todo. Should this be assert? + case State::STARTED: + break; + // Stop() has already been called so there is likewise nothing to do. + case State::STOPPED: + break; + // Stop() has not been called yet, setup the UI thread, and set our + // state to STARTED. + case State::UNSTARTED: { + WNDCLASSW wc; + HMODULE hModule = GetModuleHandle(NULL); + if (!GetClassInfoW(hModule, kThreadWindow, &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hModule; + wc.lpfnWndProc = EventWindowProc; + wc.lpszClassName = kThreadWindow; + RegisterClassW(&wc); + } + hwnd_ = CreateWindowW(kThreadWindow, L"", 0, 0, 0, 0, 0, NULL, NULL, + hModule, NULL); + // Added in review of bug 1760843, follow up to remove 1767861 + MOZ_RELEASE_ASSERT(hwnd_); + // Expected to always work but if it doesn't we should still fulfill the + // contract of always running the process loop at least a single + // iteration. + // This could be rexamined in the future. + if (hwnd_) { + SetPropW(hwnd_, kThisProperty, this); + // state_ needs to be STARTED before we request the initial timer + state_ = State::STARTED; + if (timeout_) { + // if someone set the timer before we started + RequestCallbackTimer(timeout_); + } + } + break; + } + }; + return state_ == State::STARTED; +} + +bool PlatformUIThread::RequestCallbackTimer(unsigned int milliseconds) { + CritScope scoped_lock(&cs_); + + switch (state_) { + // InternalInit() has yet to run so we do not have a UI thread to use as a + // target of the timer. We should just remember what timer interval was + // requested and let InternalInit() call this function again when it is + // ready. + case State::UNSTARTED: { + timeout_ = milliseconds; + return false; + } + // We have already stopped, do not schedule a new timer. + case State::STOPPED: + return false; + case State::STARTED: { + if (timerid_) { + KillTimer(hwnd_, timerid_); + } + timeout_ = milliseconds; + timerid_ = SetTimer(hwnd_, kTimerId, milliseconds, NULL); + return !!timerid_; + } + } + // UNREACHABLE +} + +void PlatformUIThread::Stop() { + { + RTC_DCHECK_RUN_ON(&thread_checker_); + CritScope scoped_lock(&cs_); + // Shut down the dispatch loop and let the background thread exit. + if (timerid_) { + MOZ_ASSERT(hwnd_); + KillTimer(hwnd_, timerid_); + timerid_ = 0; + } + switch (state_) { + // If we haven't started yet there is nothing to do, we will go into + // the STOPPED state at the end of the function and InternalInit() + // will not move us to STARTED. + case State::UNSTARTED: + break; + // If we have started, that means that InternalInit() has run and the + // message wait loop has or will run. We need to signal it to stop. wich + // will allow PlatformThread::Stop to join that thread. + case State::STARTED: { + MOZ_ASSERT(hwnd_); + PostMessage(hwnd_, WM_CLOSE, 0, 0); + break; + } + // We have already stopped. There is nothing to do. + case State::STOPPED: + break; + } + // Always set our state to STOPPED + state_ = State::STOPPED; + } + monitor_thread_.Finalize(); +} + +void PlatformUIThread::Run() { + // InternalInit() will return false when the thread is already in shutdown. + // otherwise we must run until we get a Windows WM_QUIT msg. + const bool runUntilQuitMsg = InternalInit(); + // The interface contract of Start/Stop is that for a successful call to + // Start, there should be at least one call to the run function. + NativeEventCallback(); + while (runUntilQuitMsg) { + // Alertable sleep to receive WM_QUIT (following a WM_CLOSE triggering a + // WM_DESTROY) + if (MsgWaitForMultipleObjectsEx(0, nullptr, INFINITE, QS_ALLINPUT, + MWMO_ALERTABLE | MWMO_INPUTAVAILABLE) == + WAIT_OBJECT_0) { + MSG msg; + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + // THE ONLY WAY to exit the thread loop + break; + } + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + } +} + +void PlatformUIThread::NativeEventCallback() { native_event_callback_(); } + +/* static */ +LRESULT CALLBACK PlatformUIThread::EventWindowProc(HWND hwnd, UINT uMsg, + WPARAM wParam, + LPARAM lParam) { + if (uMsg == WM_DESTROY) { + RemovePropW(hwnd, kThisProperty); + PostQuitMessage(0); + return 0; + } + + PlatformUIThread* twui = + static_cast<PlatformUIThread*>(GetPropW(hwnd, kThisProperty)); + if (!twui) { + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + if (uMsg == WM_TIMER && wParam == kTimerId) { + twui->NativeEventCallback(); + return 0; + } + + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +} // namespace rtc + +#endif diff --git a/dom/media/systemservices/video_engine/platform_uithread.h b/dom/media/systemservices/video_engine/platform_uithread.h new file mode 100644 index 0000000000..9c213ca933 --- /dev/null +++ b/dom/media/systemservices/video_engine/platform_uithread.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef RTC_BASE_PLATFORM_UITHREAD_H_ +#define RTC_BASE_PLATFORM_UITHREAD_H_ + +#if defined(WEBRTC_WIN) +# include "Assertions.h" +# include "rtc_base/deprecated/recursive_critical_section.h" +# include "rtc_base/platform_thread.h" +# include "api/sequence_checker.h" +# include "ThreadSafety.h" + +namespace rtc { +/* + * Windows UI thread for screen capture + * Launches a thread which enters a message wait loop after calling the + * provided ThreadRunFunction once. A repeating timer event might be registered + * with a callback through the Win32 API. If so, that timer will cause WM_TIMER + * messages to appear in the threads message queue. This will wake the thread + * which will then first look to see if it received the WM_QUIT message, then + * it will pass any non WM_QUIT messages on to the registered message handlers + * (synchronously on the current thread). In the case oF WM_TIMER the + * registered handler calls the NativeEventCallback which is simply the + * ThreadRunFunction which was passed to the constructor. + * + * Shutdown of the message wait loop is triggered by sending a WM_CLOSE which + * will start tearing down the "window" which hosts the UI thread. This will + * cause a WM_DESTROY message to be received. Upon reception a WM_QUIT message + * is enqueued. When the message wait loop receives a WM_QUIT message it stops, + * thus allowing the thread to be joined. + * + * Note: that the only source of a WM_CLOSE should be PlatformUIThread::Stop. + * Note: because PlatformUIThread::Stop is called from a different thread than + * PlatformUIThread::Run, it is possible that Stop can race Run. + * + * After being stopped PlatformUIThread can not be started again. + * + */ + +class PlatformUIThread { + public: + PlatformUIThread(std::function<void()> func, const char* name, + ThreadAttributes attributes) + : name_(name), + native_event_callback_(std::move(func)), + monitor_thread_(PlatformThread::SpawnJoinable([this]() { Run(); }, name, + attributes)) {} + + virtual ~PlatformUIThread(); + + void Stop(); + + /** + * Request a recurring callback. + */ + bool RequestCallbackTimer(unsigned int milliseconds); + + protected: + void Run(); + + private: + static LRESULT CALLBACK EventWindowProc(HWND, UINT, WPARAM, LPARAM); + void NativeEventCallback(); + // Initialize the UI thread that is servicing the timer events + bool InternalInit(); + + // Needs to be initialized before monitor_thread_ as it takes a string view to + // name_ + std::string name_; + RecursiveCriticalSection cs_; + std::function<void()> native_event_callback_; + webrtc::SequenceChecker thread_checker_; + PlatformThread monitor_thread_; + HWND hwnd_ MOZ_GUARDED_BY(cs_) = nullptr; + UINT_PTR timerid_ MOZ_GUARDED_BY(cs_) = 0; + unsigned int timeout_ MOZ_GUARDED_BY(cs_) = 0; + enum class State { + UNSTARTED, + STARTED, + STOPPED, + }; + State state_ MOZ_GUARDED_BY(cs_) = State::UNSTARTED; +}; + +} // namespace rtc + +#endif +#endif // RTC_BASE_PLATFORM_UITHREAD_H_ diff --git a/dom/media/systemservices/video_engine/tab_capturer.cc b/dom/media/systemservices/video_engine/tab_capturer.cc new file mode 100644 index 0000000000..793d965028 --- /dev/null +++ b/dom/media/systemservices/video_engine/tab_capturer.cc @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "tab_capturer.h" + +#include "desktop_device_info.h" +#include "modules/desktop_capture/desktop_capture_options.h" +#include "modules/desktop_capture/desktop_frame.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/TaskQueue.h" +#include "nsThreadUtils.h" +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" + +mozilla::LazyLogModule gTabShareLog("TabShare"); +#define LOG_FUNC_IMPL(level) \ + MOZ_LOG( \ + gTabShareLog, level, \ + ("TabCapturerWebrtc %p: %s id=%" PRIu64, this, __func__, mBrowserId)) +#define LOG_FUNC() LOG_FUNC_IMPL(LogLevel::Debug) +#define LOG_FUNCV() LOG_FUNC_IMPL(LogLevel::Verbose) + +using namespace mozilla::dom; + +namespace mozilla { + +class CaptureFrameRequest { + using CapturePromise = TabCapturerWebrtc::CapturePromise; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CaptureFrameRequest) + + CaptureFrameRequest() : mCaptureTime(TimeStamp::Now()) {} + + operator MozPromiseRequestHolder<CapturePromise>&() { return mRequest; } + + void Complete() { mRequest.Complete(); } + void Disconnect() { mRequest.Disconnect(); } + bool Exists() { return mRequest.Exists(); } + + protected: + virtual ~CaptureFrameRequest() { MOZ_RELEASE_ASSERT(!Exists()); } + + public: + const TimeStamp mCaptureTime; + + private: + MozPromiseRequestHolder<CapturePromise> mRequest; +}; + +TabCapturerWebrtc::TabCapturerWebrtc( + SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread) + : mBrowserId(aSourceId), + mMainThreadWorker( + TaskQueue::Create(do_AddRef(GetMainThreadSerialEventTarget()), + "TabCapturerWebrtc::mMainThreadWorker")), + mCallbackWorker(TaskQueue::Create(aCaptureThread.forget(), + "TabCapturerWebrtc::mCallbackWorker")) { + RTC_DCHECK_RUN_ON(&mControlChecker); + MOZ_ASSERT(aSourceId != 0); + mCallbackChecker.Detach(); + + LOG_FUNC(); +} + +// static +std::unique_ptr<webrtc::DesktopCapturer> TabCapturerWebrtc::Create( + SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread) { + return std::unique_ptr<webrtc::DesktopCapturer>( + new TabCapturerWebrtc(aSourceId, std::move(aCaptureThread))); +} + +TabCapturerWebrtc::~TabCapturerWebrtc() { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + LOG_FUNC(); + + // mMainThreadWorker handles frame capture requests async. Since we're in the + // dtor, no more frame capture requests can be made through CaptureFrame(). It + // can be shut down now. + mMainThreadWorker->BeginShutdown(); + + // There may still be async frame capture requests in flight, waiting to be + // reported to mCallback on mCallbackWorker. Disconnect them (must be done on + // mCallbackWorker) and shut down mCallbackWorker to ensure nothing more can + // get queued to it. + MOZ_ALWAYS_SUCCEEDS( + mCallbackWorker->Dispatch(NS_NewRunnableFunction(__func__, [this] { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + for (const auto& req : mRequests) { + DisconnectRequest(req); + } + mCallbackWorker->BeginShutdown(); + }))); + + // Block until the workers have run all pending tasks. We must do this for two + // reasons: + // - All runnables dispatched to mMainThreadWorker and mCallbackWorker capture + // the raw pointer `this` as they rely on `this` outliving the worker + // TaskQueues. + // - mCallback is only guaranteed to outlive `this`. No calls can be made to + // it after the dtor is finished. + + // Spin the underlying thread of mCallbackWorker, which we are currently on, + // until it is empty. We have no other way of waiting for mCallbackWorker to + // become empty while blocking the current call. + SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( + "~TabCapturerWebrtc"_ns, [&] { return mCallbackWorker->IsEmpty(); }); + + // No need to await shutdown since it was shut down synchronously above. + mMainThreadWorker->AwaitIdle(); +} + +bool TabCapturerWebrtc::GetSourceList( + webrtc::DesktopCapturer::SourceList* aSources) { + MOZ_LOG(gTabShareLog, LogLevel::Debug, + ("TabShare: GetSourceList, result %zu", aSources->size())); + // XXX UI + return true; +} + +bool TabCapturerWebrtc::SelectSource(webrtc::DesktopCapturer::SourceId) { + MOZ_ASSERT_UNREACHABLE("Source is passed through ctor for constness"); + return true; +} + +bool TabCapturerWebrtc::FocusOnSelectedSource() { return true; } + +void TabCapturerWebrtc::Start(webrtc::DesktopCapturer::Callback* aCallback) { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + RTC_DCHECK(!mCallback); + RTC_DCHECK(aCallback); + + LOG_FUNC(); + + mCallback = aCallback; +} + +void TabCapturerWebrtc::CaptureFrame() { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + LOG_FUNCV(); + if (mRequests.GetSize() > 2) { + // Allow two async capture requests in flight + OnCaptureFrameFailure(); + return; + } + + auto request = MakeRefPtr<CaptureFrameRequest>(); + InvokeAsync(mMainThreadWorker, __func__, [this] { return CaptureFrameNow(); }) + ->Then(mCallbackWorker, __func__, + [this, request](CapturePromise::ResolveOrRejectValue&& aValue) { + if (!CompleteRequest(request)) { + // Request was disconnected or overrun. Failure has already + // been reported to the callback elsewhere. + return; + } + + if (aValue.IsReject()) { + OnCaptureFrameFailure(); + return; + } + + OnCaptureFrameSuccess(std::move(aValue.ResolveValue())); + }) + ->Track(*request); + mRequests.PushFront(request.forget()); +} + +void TabCapturerWebrtc::OnCaptureFrameSuccess( + UniquePtr<dom::ImageBitmapCloneData> aData) { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + MOZ_DIAGNOSTIC_ASSERT(aData); + LOG_FUNCV(); + webrtc::DesktopSize size(aData->mPictureRect.Width(), + aData->mPictureRect.Height()); + webrtc::DesktopRect rect = webrtc::DesktopRect::MakeSize(size); + std::unique_ptr<webrtc::DesktopFrame> frame( + new webrtc::BasicDesktopFrame(size)); + + gfx::DataSourceSurface::ScopedMap map(aData->mSurface, + gfx::DataSourceSurface::READ); + if (!map.IsMapped()) { + OnCaptureFrameFailure(); + return; + } + frame->CopyPixelsFrom(map.GetData(), map.GetStride(), rect); + + mCallback->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS, + std::move(frame)); +} + +void TabCapturerWebrtc::OnCaptureFrameFailure() { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + LOG_FUNC(); + mCallback->OnCaptureResult(webrtc::DesktopCapturer::Result::ERROR_TEMPORARY, + nullptr); +} + +bool TabCapturerWebrtc::IsOccluded(const webrtc::DesktopVector& aPos) { + return false; +} + +class TabCapturedHandler final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + using CapturePromise = TabCapturerWebrtc::CapturePromise; + + static void Create(Promise* aPromise, + MozPromiseHolder<CapturePromise> aHolder) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<TabCapturedHandler> handler = + new TabCapturedHandler(std::move(aHolder)); + aPromise->AppendNativeHandler(handler); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + MOZ_ASSERT(NS_IsMainThread()); + if (NS_WARN_IF(!aValue.isObject())) { + mHolder.Reject(NS_ERROR_UNEXPECTED, __func__); + return; + } + + RefPtr<ImageBitmap> bitmap; + if (NS_WARN_IF(NS_FAILED( + UNWRAP_OBJECT(ImageBitmap, &aValue.toObject(), bitmap)))) { + mHolder.Reject(NS_ERROR_UNEXPECTED, __func__); + return; + } + + UniquePtr<ImageBitmapCloneData> data = bitmap->ToCloneData(); + if (!data) { + mHolder.Reject(NS_ERROR_UNEXPECTED, __func__); + return; + } + + mHolder.Resolve(std::move(data), __func__); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + MOZ_ASSERT(NS_IsMainThread()); + mHolder.Reject(aRv.StealNSResult(), __func__); + } + + private: + explicit TabCapturedHandler(MozPromiseHolder<CapturePromise> aHolder) + : mHolder(std::move(aHolder)) {} + + ~TabCapturedHandler() = default; + + MozPromiseHolder<CapturePromise> mHolder; +}; + +NS_IMPL_ISUPPORTS0(TabCapturedHandler) + +bool TabCapturerWebrtc::CompleteRequest(CaptureFrameRequest* aRequest) { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + if (!aRequest->Exists()) { + // Request was disconnected or overrun. mCallback has already been notified. + return false; + } + while (CaptureFrameRequest* req = mRequests.Peek()) { + if (req->mCaptureTime > aRequest->mCaptureTime) { + break; + } + // Pop the request before calling the callback, in case it could mutate + // mRequests, now or in the future. + RefPtr<CaptureFrameRequest> dropMe = mRequests.Pop(); + req->Complete(); + if (req->mCaptureTime < aRequest->mCaptureTime) { + OnCaptureFrameFailure(); + } + } + MOZ_DIAGNOSTIC_ASSERT(!aRequest->Exists()); + return true; +} + +void TabCapturerWebrtc::DisconnectRequest(CaptureFrameRequest* aRequest) { + RTC_DCHECK_RUN_ON(&mCallbackChecker); + LOG_FUNCV(); + aRequest->Disconnect(); + OnCaptureFrameFailure(); +} + +auto TabCapturerWebrtc::CaptureFrameNow() -> RefPtr<CapturePromise> { + MOZ_ASSERT(mMainThreadWorker->IsOnCurrentThread()); + LOG_FUNCV(); + + WindowGlobalParent* wgp = nullptr; + RefPtr<BrowsingContext> context = + BrowsingContext::GetCurrentTopByBrowserId(mBrowserId); + if (context) { + wgp = context->Canonical()->GetCurrentWindowGlobal(); + } + if (!wgp) { + // If we can't access the window, we just won't capture anything + return CapturePromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); + } + + // XXX This would be more efficient if we used CrossProcessPaint directly and + // returned a surface. + RefPtr<Promise> promise = + wgp->DrawSnapshot(nullptr, 1.0, "white"_ns, false, IgnoreErrors()); + if (!promise) { + return CapturePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + MozPromiseHolder<CapturePromise> holder; + RefPtr<CapturePromise> p = holder.Ensure(__func__); + TabCapturedHandler::Create(promise, std::move(holder)); + return p; +} + +} // namespace mozilla diff --git a/dom/media/systemservices/video_engine/tab_capturer.h b/dom/media/systemservices/video_engine/tab_capturer.h new file mode 100644 index 0000000000..92c4fa2ad1 --- /dev/null +++ b/dom/media/systemservices/video_engine/tab_capturer.h @@ -0,0 +1,88 @@ +/* + * Copyright 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_ +#define MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_ + +#include "api/sequence_checker.h" +#include "modules/desktop_capture/desktop_capturer.h" +#include "mozilla/MozPromise.h" +#include "nsDeque.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace dom { +struct ImageBitmapCloneData; +} // namespace dom + +class CaptureFrameRequest; +class TabCapturedHandler; +class TaskQueue; + +class TabCapturerWebrtc : public webrtc::DesktopCapturer { + protected: + TabCapturerWebrtc(SourceId aSourceId, + nsCOMPtr<nsISerialEventTarget> aCaptureThread); + ~TabCapturerWebrtc(); + + public: + friend class CaptureFrameRequest; + friend class TabCapturedHandler; + + static std::unique_ptr<webrtc::DesktopCapturer> Create( + SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread); + + TabCapturerWebrtc(const TabCapturerWebrtc&) = delete; + TabCapturerWebrtc& operator=(const TabCapturerWebrtc&) = delete; + + // DesktopCapturer interface. + void Start(Callback* aCallback) override; + void CaptureFrame() override; + bool GetSourceList(SourceList* aSources) override; + bool SelectSource(SourceId) override; + bool FocusOnSelectedSource() override; + bool IsOccluded(const webrtc::DesktopVector& aPos) override; + + private: + // Capture code + using CapturePromise = + MozPromise<UniquePtr<dom::ImageBitmapCloneData>, nsresult, true>; + RefPtr<CapturePromise> CaptureFrameNow(); + + // Helper that checks for overrun requests. Returns true if aRequest had not + // been dropped due to disconnection or overrun. + // Note that if this returns true, the caller takes the responsibility to call + // mCallback with a capture result for aRequest. + bool CompleteRequest(CaptureFrameRequest* aRequest); + + // Helper that disconnects the request, and notifies mCallback of a temporary + // failure. + void DisconnectRequest(CaptureFrameRequest* aRequest); + + // Handle the result from the async callback from CaptureFrameNow. + void OnCaptureFrameSuccess(UniquePtr<dom::ImageBitmapCloneData> aData); + void OnCaptureFrameFailure(); + + const uint64_t mBrowserId; + const RefPtr<TaskQueue> mMainThreadWorker; + const RefPtr<TaskQueue> mCallbackWorker; + webrtc::SequenceChecker mControlChecker; + webrtc::SequenceChecker mCallbackChecker; + // Set in Start() and guaranteed by the owner of this class to outlive us. + webrtc::DesktopCapturer::Callback* mCallback + RTC_GUARDED_BY(mCallbackChecker) = nullptr; + + // mCallbackWorker only + nsRefPtrDeque<CaptureFrameRequest> mRequests RTC_GUARDED_BY(mCallbackChecker); +}; + +} // namespace mozilla + +#endif // MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_ diff --git a/dom/media/systemservices/video_engine/video_capture_factory.cc b/dom/media/systemservices/video_engine/video_capture_factory.cc new file mode 100644 index 0000000000..e4ca505fa6 --- /dev/null +++ b/dom/media/systemservices/video_engine/video_capture_factory.cc @@ -0,0 +1,230 @@ +/* -*- 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 "video_capture_factory.h" + +#include "mozilla/StaticPrefs_media.h" +#include "desktop_capture_impl.h" +#include "VideoEngine.h" + +#if defined(WEBRTC_USE_PIPEWIRE) +# include "video_engine/placeholder_device_info.h" +#endif + +#if defined(WEBRTC_USE_PIPEWIRE) && defined(MOZ_ENABLE_DBUS) +# include "mozilla/widget/AsyncDBus.h" +#endif + +#include <memory> + +namespace mozilla { + +VideoCaptureFactory::VideoCaptureFactory() { +#if (defined(WEBRTC_LINUX) || defined(WEBRTC_BSD)) && !defined(WEBRTC_ANDROID) + mVideoCaptureOptions = std::make_unique<webrtc::VideoCaptureOptions>(); + // In case pipewire is enabled, this acts as a fallback and can be always + // enabled. + mVideoCaptureOptions->set_allow_v4l2(true); + bool allowPipeWire = false; +# if defined(WEBRTC_USE_PIPEWIRE) + allowPipeWire = + mozilla::StaticPrefs::media_webrtc_camera_allow_pipewire_AtStartup(); + mVideoCaptureOptions->set_allow_pipewire(allowPipeWire); +# endif + if (!allowPipeWire) { + // V4L2 backend can and should be initialized right away since there are no + // permissions involved + InitCameraBackend(); + } +#endif +} + +std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> +VideoCaptureFactory::CreateDeviceInfo( + int32_t aId, mozilla::camera::CaptureDeviceType aType) { + if (aType == mozilla::camera::CaptureDeviceType::Camera) { + std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> deviceInfo; +#if (defined(WEBRTC_LINUX) || defined(WEBRTC_BSD)) && !defined(WEBRTC_ANDROID) +# if defined(WEBRTC_USE_PIPEWIRE) + // Special case when PipeWire is not initialized yet and we need to insert + // a camera device placeholder based on camera device availability we get + // from the camera portal + if (!mCameraBackendInitialized && mVideoCaptureOptions->allow_pipewire()) { + MOZ_ASSERT(mCameraAvailability != Unknown); + deviceInfo.reset( + new PlaceholderDeviceInfo(mCameraAvailability == Available)); + return deviceInfo; + } +# endif + + deviceInfo.reset(webrtc::VideoCaptureFactory::CreateDeviceInfo( + mVideoCaptureOptions.get())); +#else + deviceInfo.reset(webrtc::VideoCaptureFactory::CreateDeviceInfo()); +#endif + return deviceInfo; + } + +#if defined(WEBRTC_ANDROID) || defined(WEBRTC_IOS) + MOZ_ASSERT("CreateDeviceInfo NO DESKTOP CAPTURE IMPL ON ANDROID" == nullptr); + return nullptr; +#else + return webrtc::DesktopCaptureImpl::CreateDeviceInfo(aId, aType); +#endif +} + +rtc::scoped_refptr<webrtc::VideoCaptureModule> +VideoCaptureFactory::CreateVideoCapture( + int32_t aModuleId, const char* aUniqueId, + mozilla::camera::CaptureDeviceType aType) { + if (aType == mozilla::camera::CaptureDeviceType::Camera) { +#if (defined(WEBRTC_LINUX) || defined(WEBRTC_BSD)) && !defined(WEBRTC_ANDROID) + return webrtc::VideoCaptureFactory::Create(mVideoCaptureOptions.get(), + aUniqueId); +#else + return webrtc::VideoCaptureFactory::Create(aUniqueId); +#endif + } + +#if defined(WEBRTC_ANDROID) || defined(WEBRTC_IOS) + MOZ_ASSERT("CreateVideoCapture NO DESKTOP CAPTURE IMPL ON ANDROID" == + nullptr); + return nullptr; +#else + return rtc::scoped_refptr<webrtc::VideoCaptureModule>( + webrtc::DesktopCaptureImpl::Create(aModuleId, aUniqueId, aType)); +#endif +} + +auto VideoCaptureFactory::InitCameraBackend() + -> RefPtr<CameraBackendInitPromise> { + if (!mPromise) { + mPromise = mPromiseHolder.Ensure(__func__); +#if (defined(WEBRTC_LINUX) || defined(WEBRTC_BSD)) && !defined(WEBRTC_ANDROID) + MOZ_ASSERT(mVideoCaptureOptions); + mVideoCaptureOptions->Init(this); +# if defined(WEBRTC_USE_PIPEWIRE) + mPromise = mPromise->Then( + GetCurrentSerialEventTarget(), __func__, + [this, self = RefPtr(this)]( + const CameraBackendInitPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsReject() && + aValue.RejectValue() != NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR) { + // Fallback to V4L2 in case of PipeWire or camera portal failure. + // There is nothing we need to do in order to initialize V4L2 so + // consider the backend initialized and ready to be used. + mVideoCaptureOptions->set_allow_pipewire(false); + mCameraBackendInitialized = true; + + return CameraBackendInitPromise::CreateAndResolve( + NS_OK, + "VideoCaptureFactory::InitCameraBackend Resolve with " + "fallback to V4L2"); + } + + return CameraBackendInitPromise::CreateAndResolveOrReject( + aValue, + "VideoCaptureFactory::InitCameraBackend Resolve or Reject"); + }); +# endif +#else + mCameraBackendInitialized = true; + mPromiseHolder.Resolve(NS_OK, + "VideoCaptureFactory::InitCameraBackend Resolve"); +#endif + } + + return mPromise; +} + +auto VideoCaptureFactory::HasCameraDevice() + -> RefPtr<VideoCaptureFactory::HasCameraDevicePromise> { +#if defined(WEBRTC_USE_PIPEWIRE) && defined(MOZ_ENABLE_DBUS) + if (mVideoCaptureOptions && mVideoCaptureOptions->allow_pipewire()) { + return widget::CreateDBusProxyForBus( + G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, + /* aInterfaceInfo = */ nullptr, "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Camera") + ->Then( + GetCurrentSerialEventTarget(), __func__, + [](RefPtr<GDBusProxy>&& aProxy) { + GVariant* variant = + g_dbus_proxy_get_cached_property(aProxy, "IsCameraPresent"); + if (!variant) { + return HasCameraDevicePromise::CreateAndReject( + NS_ERROR_NO_INTERFACE, + "VideoCaptureFactory::HasCameraDevice Reject"); + } + + if (!g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN)) { + return HasCameraDevicePromise::CreateAndReject( + NS_ERROR_UNEXPECTED, + "VideoCaptureFactory::HasCameraDevice Reject"); + } + + const bool hasCamera = g_variant_get_boolean(variant); + g_variant_unref(variant); + return HasCameraDevicePromise::CreateAndResolve( + hasCamera ? Available : NotAvailable, + "VideoCaptureFactory::HasCameraDevice Resolve"); + }, + [](GUniquePtr<GError>&& aError) { + return HasCameraDevicePromise::CreateAndReject( + NS_ERROR_NO_INTERFACE, + "VideoCaptureFactory::HasCameraDevice Reject"); + }); + } +#endif + return HasCameraDevicePromise::CreateAndReject( + NS_ERROR_NOT_IMPLEMENTED, "VideoCaptureFactory::HasCameraDevice Reject"); +} + +auto VideoCaptureFactory::UpdateCameraAvailability() + -> RefPtr<UpdateCameraAvailabilityPromise> { + return VideoCaptureFactory::HasCameraDevice()->Then( + GetCurrentSerialEventTarget(), __func__, + [this, self = RefPtr(this)]( + const HasCameraDevicePromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + mCameraAvailability = aValue.ResolveValue(); + + return HasCameraDevicePromise::CreateAndResolve( + mCameraAvailability, + "VideoCaptureFactory::UpdateCameraAvailability Resolve"); + } + + // We want to fallback to V4L2 at this point, therefore make sure a + // camera device is announced so we can proceed with a gUM request, + // where we can fallback to V4L2 backend. + mCameraAvailability = Available; + + return HasCameraDevicePromise::CreateAndReject( + aValue.RejectValue(), + "VideoCaptureFactory::UpdateCameraAvailability Reject"); + }); +} + +void VideoCaptureFactory::OnInitialized( + webrtc::VideoCaptureOptions::Status status) { + switch (status) { + case webrtc::VideoCaptureOptions::Status::SUCCESS: + mCameraBackendInitialized = true; + mPromiseHolder.Resolve(NS_OK, __func__); + return; + case webrtc::VideoCaptureOptions::Status::UNAVAILABLE: + mPromiseHolder.Reject(NS_ERROR_NOT_AVAILABLE, __func__); + return; + case webrtc::VideoCaptureOptions::Status::DENIED: + mPromiseHolder.Reject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__); + return; + default: + mPromiseHolder.Reject(NS_ERROR_FAILURE, __func__); + return; + } +} + +} // namespace mozilla diff --git a/dom/media/systemservices/video_engine/video_capture_factory.h b/dom/media/systemservices/video_engine/video_capture_factory.h new file mode 100644 index 0000000000..70505e572a --- /dev/null +++ b/dom/media/systemservices/video_engine/video_capture_factory.h @@ -0,0 +1,89 @@ +/* -*- 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/. */ + +#ifndef MOZILLA_VIDEO_CAPTURE_FACTORY_H_ +#define MOZILLA_VIDEO_CAPTURE_FACTORY_H_ + +#include "modules/video_capture/video_capture_factory.h" +#include "modules/video_capture/video_capture_options.h" +#include "modules/video_capture/video_capture.h" + +#include "mozilla/MozPromise.h" + +namespace mozilla::camera { +enum class CaptureDeviceType; +} + +namespace mozilla { +/** + * NOTE: This class must be accessed only on a single SerialEventTarget + */ +class VideoCaptureFactory : webrtc::VideoCaptureOptions::Callback { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoCaptureFactory); + + enum CameraAvailability { Unknown, Available, NotAvailable }; + + VideoCaptureFactory(); + + std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> CreateDeviceInfo( + int32_t aId, mozilla::camera::CaptureDeviceType aType); + + rtc::scoped_refptr<webrtc::VideoCaptureModule> CreateVideoCapture( + int32_t aModuleId, const char* aUniqueId, + mozilla::camera::CaptureDeviceType aType); + + using CameraBackendInitPromise = MozPromise<nsresult, nsresult, false>; + /** + * Request to initialize webrtc::VideoCaptureOptions + * + * Resolves with NS_OK when VideoCaptureOptions has been properly initialized + * or rejects with one of the possible errors. Since this is only now + * supported by PipeWire, all the errors are PipeWire specific: + * 1) NS_ERROR_NOT_AVAILABLE - PipeWire libraries are not available on + * the system + * 2) NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR - camera access has been rejected + * 3) NS_ERROR_FAILURE - generic error, usually a PipeWire failure + */ + RefPtr<CameraBackendInitPromise> InitCameraBackend(); + + /** + * Updates information about camera availability + */ + using UpdateCameraAvailabilityPromise = + MozPromise<CameraAvailability, nsresult, true>; + RefPtr<UpdateCameraAvailabilityPromise> UpdateCameraAvailability(); + + private: + ~VideoCaptureFactory() = default; + // aka OnCameraBackendInitialized + // this method override has to follow webrtc::VideoCaptureOptions::Callback + void OnInitialized(webrtc::VideoCaptureOptions::Status status) override; + + /** + * Resolves with true or false depending on whether there is a camera device + * advertised by the xdg-desktop-portal (Camera portal). Rejects with one + * of the following errors: + * 1) NS_ERROR_NOT_IMPLEMENTED - support for the Camera portal is not + * implemented or enabled + * 2) NS_ERROR_NO_INTERFACE - the camera portal is not available + * 3) NS_ERROR_UNEXPECTED - the camera portal returned wrong value + */ + using HasCameraDevicePromise = MozPromise<CameraAvailability, nsresult, true>; + RefPtr<HasCameraDevicePromise> HasCameraDevice(); + + std::atomic<bool> mCameraBackendInitialized = false; + CameraAvailability mCameraAvailability = Unknown; +#if (defined(WEBRTC_LINUX) || defined(WEBRTC_BSD)) && !defined(WEBRTC_ANDROID) + std::unique_ptr<webrtc::VideoCaptureOptions> mVideoCaptureOptions; +#endif + MozPromiseHolder<CameraBackendInitPromise> mPromiseHolder; + RefPtr<CameraBackendInitPromise> mPromise; +}; + +} // namespace mozilla + +#endif // MOZILLA_VIDEO_CAPTURE_FACTORY_H_ |