Bug 1844020 - Add a camera device placeholder in case PipeWire is not initialized yet r=pehrsons
authorJan Grulich <jgrulich@redhat.com>
Thu, 30 Nov 2023 11:49:36 +0000 (19 months ago)
changeset 687779 fef894fcc4294027fdfb62af3997b87411bef554
parent 687778 ed31b2acb5fbca3e2d0691a64bc52e65952070c0
child 687780 ae5d8a4c6f186bba953930b247d145d0a39ff343
push id41379
push usernbeleuzu@mozilla.com
push dateThu, 30 Nov 2023 17:23:44 +0000 (19 months ago)
treeherdermozilla-central@1d1fb9d5a497 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspehrsons
bugs1844020
milestone122.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1844020 - Add a camera device placeholder in case PipeWire is not initialized yet r=pehrsons Implement DeviceInfoPipeWire with option to add a device placeholder when there is a camera device, but we don't have access to PipeWire yet. We ask the Camera portal whether there is a camera device present. Differential Revision: https://phabricator.services.mozilla.com/D189089
dom/media/MediaManager.cpp
dom/media/MediaManager.h
dom/media/systemservices/CamerasChild.cpp
dom/media/systemservices/CamerasChild.h
dom/media/systemservices/CamerasParent.cpp
dom/media/systemservices/CamerasParent.h
dom/media/systemservices/CamerasTypes.h
dom/media/systemservices/PCameras.ipdl
dom/media/systemservices/VideoEngine.cpp
dom/media/systemservices/VideoEngine.h
dom/media/systemservices/moz.build
dom/media/systemservices/video_engine/placeholder_device_info.cc
dom/media/systemservices/video_engine/placeholder_device_info.h
dom/media/systemservices/video_engine/video_capture_factory.cc
dom/media/systemservices/video_engine/video_capture_factory.h
dom/media/webrtc/MediaEngineWebRTC.cpp
ipc/glue/BackgroundParentImpl.cpp
ipc/glue/BackgroundParentImpl.h
ipc/glue/PBackground.ipdl
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -37,16 +37,17 @@
 #include "mozilla/dom/MediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/UserActivation.h"
 #include "mozilla/dom/WindowContext.h"
 #include "mozilla/dom/WindowGlobalChild.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/media/CamerasTypes.h"
 #include "mozilla/media/MediaChild.h"
 #include "mozilla/media/MediaTaskUtils.h"
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsArray.h"
 #include "nsContentUtils.h"
 #include "nsGlobalWindowInner.h"
 #include "nsHashPropertyBag.h"
 #include "nsIEventTarget.h"
@@ -146,16 +147,17 @@ namespace mozilla {
 
 LazyLogModule gMediaManagerLog("MediaManager");
 #define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))
 
 class GetUserMediaStreamTask;
 class LocalTrackSource;
 class SelectAudioOutputTask;
 
+using camera::CamerasAccessStatus;
 using dom::BFCacheStatus;
 using dom::CallerType;
 using dom::ConstrainDOMStringParameters;
 using dom::ConstrainDoubleRange;
 using dom::ConstrainLongRange;
 using dom::DisplayMediaStreamConstraints;
 using dom::Document;
 using dom::Element;
@@ -853,26 +855,28 @@ class AudioCaptureTrackSource : public L
 /**
  * nsIMediaDevice implementation.
  */
 NS_IMPL_ISUPPORTS(LocalMediaDevice, nsIMediaDevice)
 
 MediaDevice::MediaDevice(MediaEngine* aEngine, MediaSourceEnum aMediaSource,
                          const nsString& aRawName, const nsString& aRawID,
                          const nsString& aRawGroupID, IsScary aIsScary,
-                         const OsPromptable canRequestOsLevelPrompt)
+                         const OsPromptable canRequestOsLevelPrompt,
+                         const IsPlaceholder aIsPlaceholder)
     : mEngine(aEngine),
       mAudioDeviceInfo(nullptr),
       mMediaSource(aMediaSource),
       mKind(MediaEngineSource::IsVideo(aMediaSource)
                 ? MediaDeviceKind::Videoinput
                 : MediaDeviceKind::Audioinput),
       mScary(aIsScary == IsScary::Yes),
       mCanRequestOsLevelPrompt(canRequestOsLevelPrompt == OsPromptable::Yes),
       mIsFake(mEngine->IsFake()),
+      mIsPlaceholder(aIsPlaceholder == IsPlaceholder::Yes),
       mType(
           NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
       mRawID(aRawID),
       mRawGroupID(aRawGroupID),
       mRawName(aRawName) {
   MOZ_ASSERT(mEngine);
 }
 
@@ -885,30 +889,32 @@ MediaDevice::MediaDevice(MediaEngine* aE
                        ? MediaSourceEnum::Microphone
                        : MediaSourceEnum::Other),
       mKind(mMediaSource == MediaSourceEnum::Microphone
                 ? MediaDeviceKind::Audioinput
                 : MediaDeviceKind::Audiooutput),
       mScary(false),
       mCanRequestOsLevelPrompt(false),
       mIsFake(false),
+      mIsPlaceholder(false),
       mType(
           NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
       mRawID(aRawID),
       mRawGroupID(mAudioDeviceInfo->GroupID()),
       mRawName(mAudioDeviceInfo->Name()) {}
 
 /* static */
 RefPtr<MediaDevice> MediaDevice::CopyWithNewRawGroupId(
     const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID) {
   MOZ_ASSERT(!aOther->mAudioDeviceInfo, "device not supported");
   return new MediaDevice(aOther->mEngine, aOther->mMediaSource,
                          aOther->mRawName, aOther->mRawID, aRawGroupID,
                          IsScary(aOther->mScary),
-                         OsPromptable(aOther->mCanRequestOsLevelPrompt));
+                         OsPromptable(aOther->mCanRequestOsLevelPrompt),
+                         IsPlaceholder(aOther->mIsPlaceholder));
 }
 
 MediaDevice::~MediaDevice() = default;
 
 LocalMediaDevice::LocalMediaDevice(RefPtr<const MediaDevice> aRawDevice,
                                    const nsString& aID,
                                    const nsString& aGroupID,
                                    const nsString& aName)
@@ -1814,25 +1820,25 @@ class DeviceSetPromiseHolderWithFallback
     }
   }
 };
 
 // Class to hold the promise used to request device access and to resolve
 // even if |task| does not run, which can happen in case there is no
 // observer for the ask-device-permission event.
 class DeviceAccessRequestPromiseHolderWithFallback
-    : public MozPromiseHolder<
-          MozPromise<nsresult, mozilla::ipc::ResponseRejectReason, true>> {
+    : public MozPromiseHolder<MozPromise<
+          CamerasAccessStatus, mozilla::ipc::ResponseRejectReason, true>> {
  public:
   DeviceAccessRequestPromiseHolderWithFallback() = default;
   DeviceAccessRequestPromiseHolderWithFallback(
       DeviceAccessRequestPromiseHolderWithFallback&&) = default;
   ~DeviceAccessRequestPromiseHolderWithFallback() {
     if (!IsEmpty()) {
-      Resolve(NS_OK, __func__);
+      Resolve(CamerasAccessStatus::Granted, __func__);
     }
   }
 };
 
 }  // anonymous namespace
 
 /**
  * EnumerateRawDevices - Enumerate a list of audio & video devices that
@@ -1887,65 +1893,77 @@ RefPtr<DeviceSetPromise> MediaManager::E
       hasFakeMics = fakeByPref && audioLoopDev.IsEmpty();
     }
   }
   // True if at least one of video input or audio input is a real device
   // or there is audio output.
   const bool realDeviceRequested = (!hasFakeCams && hasVideo) ||
                                    (!hasFakeMics && hasAudio) || hasAudioOutput;
 
-  using NativePromise = MozPromise<nsresult, mozilla::ipc::ResponseRejectReason,
-                                   /* IsExclusive = */ true>;
+  using NativePromise =
+      MozPromise<CamerasAccessStatus, mozilla::ipc::ResponseRejectReason,
+                 /* IsExclusive = */ true>;
   RefPtr<NativePromise> deviceAccessPromise;
   if (realDeviceRequested &&
-      aFlags.contains(EnumerationFlag::AllowPermissionRequest)) {
-    if (Preferences::GetBool("media.navigator.permission.device", false)) {
-      // Need to ask permission to retrieve list of all devices;
-      // notify frontend observer and wait for callback notification to post
-      // task.
-      const char16_t* const type =
-          (aVideoInputType != MediaSourceEnum::Camera)       ? u"audio"
-          : (aAudioInputType != MediaSourceEnum::Microphone) ? u"video"
-                                                             : u"all";
-      nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
-      DeviceAccessRequestPromiseHolderWithFallback deviceAccessPromiseHolder;
-      deviceAccessPromise = deviceAccessPromiseHolder.Ensure(__func__);
-      RefPtr task = NS_NewRunnableFunction(
-          __func__, [holder = std::move(deviceAccessPromiseHolder)]() mutable {
-            holder.Resolve(NS_OK, "getUserMedia:got-device-permission");
-          });
-      obs->NotifyObservers(static_cast<nsIRunnable*>(task),
-                           "getUserMedia:ask-device-permission", type);
-    } else if (hasVideo && aVideoInputType == MediaSourceEnum::Camera) {
-      ipc::PBackgroundChild* backgroundChild =
-          ipc::BackgroundChild::GetOrCreateForCurrentThread();
-      deviceAccessPromise = backgroundChild->SendRequestCameraAccess();
-    }
+      aFlags.contains(EnumerationFlag::AllowPermissionRequest) &&
+      Preferences::GetBool("media.navigator.permission.device", false)) {
+    // Need to ask permission to retrieve list of all devices;
+    // notify frontend observer and wait for callback notification to post
+    // task.
+    const char16_t* const type =
+        (aVideoInputType != MediaSourceEnum::Camera)       ? u"audio"
+        : (aAudioInputType != MediaSourceEnum::Microphone) ? u"video"
+                                                           : u"all";
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    DeviceAccessRequestPromiseHolderWithFallback deviceAccessPromiseHolder;
+    deviceAccessPromise = deviceAccessPromiseHolder.Ensure(__func__);
+    RefPtr task = NS_NewRunnableFunction(
+        __func__, [holder = std::move(deviceAccessPromiseHolder)]() mutable {
+          holder.Resolve(CamerasAccessStatus::Granted,
+                         "getUserMedia:got-device-permission");
+        });
+    obs->NotifyObservers(static_cast<nsIRunnable*>(task),
+                         "getUserMedia:ask-device-permission", type);
+  } else if (realDeviceRequested && hasVideo &&
+             aVideoInputType == MediaSourceEnum::Camera) {
+    ipc::PBackgroundChild* backgroundChild =
+        ipc::BackgroundChild::GetOrCreateForCurrentThread();
+    deviceAccessPromise = backgroundChild->SendRequestCameraAccess(
+        aFlags.contains(EnumerationFlag::AllowPermissionRequest));
   }
 
   if (!deviceAccessPromise) {
     // No device access request needed. Proceed directly.
-    deviceAccessPromise = NativePromise::CreateAndResolve(NS_OK, __func__);
+    deviceAccessPromise =
+        NativePromise::CreateAndResolve(CamerasAccessStatus::Granted, __func__);
   }
 
   deviceAccessPromise->Then(
       mMediaThread, __func__,
       [holder = std::move(holder), aVideoInputType, aAudioInputType,
        hasFakeCams, hasFakeMics, videoLoopDev, audioLoopDev, hasVideo, hasAudio,
        hasAudioOutput, realDeviceRequested](
           NativePromise::ResolveOrRejectValue&& aValue) mutable {
         if (aValue.IsReject()) {
           // IPC failure probably means we're in shutdown. Resolve with
           // an empty set, so that callers do not need to handle rejection.
           holder.Resolve(new MediaDeviceSetRefCnt(),
                          "EnumerateRawDevices: ipc failure");
           return;
         }
 
-        if (nsresult value = aValue.ResolveValue(); NS_FAILED(value)) {
+        const CamerasAccessStatus value = aValue.ResolveValue();
+        if (value == CamerasAccessStatus::Rejected ||
+            value == CamerasAccessStatus::Error) {
+          LOG("Request to camera access %s",
+              value == CamerasAccessStatus::Rejected ? "was rejected"
+                                                     : "failed");
+          if (value == CamerasAccessStatus::Error) {
+            NS_WARNING("Failed to request camera access");
+          }
           holder.Reject(
               MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
               "EnumerateRawDevices: camera access rejected");
           return;
         }
 
         // Only enumerate what's asked for, and only fake cams and mics.
         RefPtr<MediaEngine> fakeBackend, realBackend;
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -78,20 +78,26 @@ class MediaDevice final {
    */
   enum class IsScary { No, Yes };
 
   /**
    * Whether source device can use OS level selection prompt
    */
   enum class OsPromptable { No, Yes };
 
+  /**
+   * Whether source device is just a placeholder
+   */
+  enum class IsPlaceholder { No, Yes };
+
   MediaDevice(MediaEngine* aEngine, dom::MediaSourceEnum aMediaSource,
               const nsString& aRawName, const nsString& aRawID,
               const nsString& aRawGroupID, IsScary aIsScary,
-              const OsPromptable canRequestOsLevelPrompt);
+              const OsPromptable canRequestOsLevelPrompt,
+              const IsPlaceholder aIsPlaceholder = IsPlaceholder::No);
 
   MediaDevice(MediaEngine* aEngine,
               const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
               const nsString& aRawID);
 
   static RefPtr<MediaDevice> CopyWithNewRawGroupId(
       const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID);
 
@@ -103,16 +109,17 @@ class MediaDevice final {
  public:
   const RefPtr<MediaEngine> mEngine;
   const RefPtr<AudioDeviceInfo> mAudioDeviceInfo;
   const dom::MediaSourceEnum mMediaSource;
   const dom::MediaDeviceKind mKind;
   const bool mScary;
   const bool mCanRequestOsLevelPrompt;
   const bool mIsFake;
+  const bool mIsPlaceholder;
   const nsString mType;
   const nsString mRawID;
   const nsString mRawGroupID;
   const nsString mRawName;
 };
 
 /**
  * Device info that is specific to a particular Window.  If the device is a
--- a/dom/media/systemservices/CamerasChild.cpp
+++ b/dom/media/systemservices/CamerasChild.cpp
@@ -284,48 +284,55 @@ mozilla::ipc::IPCResult CamerasChild::Re
   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) {
+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& 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__));
--- a/dom/media/systemservices/CamerasChild.h
+++ b/dom/media/systemservices/CamerasChild.h
@@ -155,17 +155,17 @@ class CamerasChild final : public PCamer
   // 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) override;
+      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);
@@ -182,18 +182,18 @@ class CamerasChild final : public PCamer
                       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 = nullptr);
+                       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
@@ -248,15 +248,16 @@ class CamerasChild final : public PCamer
   // 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
--- a/dom/media/systemservices/CamerasParent.cpp
+++ b/dom/media/systemservices/CamerasParent.cpp
@@ -2,16 +2,17 @@
 /* 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 "mozilla/AppShutdown.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/ProfilerMarkers.h"
@@ -645,62 +646,64 @@ ipc::IPCResult CamerasParent::RecvGetCap
 
 ipc::IPCResult CamerasParent::RecvGetCaptureDevice(
     const CaptureEngine& aCapEngine, const int& aDeviceIndex) {
   MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
   MOZ_ASSERT(!mDestroyed);
 
   LOG_FUNCTION();
 
-  using Data = std::tuple<nsCString, nsCString, pid_t, int>;
+  using 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);
+                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,
-                            error),
+                            placeholder, error),
             "CamerasParent::RecvGetCaptureDevice");
       })
       ->Then(
           mPBackgroundEventTarget, __func__,
           [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
-            const auto& [name, uniqueId, devicePid, error] =
+            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);
+            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) {
@@ -1174,60 +1177,97 @@ CamerasParent::CamerasParent()
              "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.
 }
 
-auto CamerasParent::RequestCameraAccess()
+/* 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(
-                  aRv,
+                  CamerasAccessStatus::Granted,
                   "CamerasParent::RequestCameraAccess camera backend init "
                   "resolve");
             },
             [](nsresult aRv) mutable {
               MOZ_ASSERT(NS_FAILED(aRv));
-              return CameraAccessRequestPromise::CreateAndReject(
-                  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",
-      [](nsresult aRv) {
+      [](CamerasAccessStatus aStatus) {
         return CameraAccessRequestPromise::CreateAndResolve(
-            aRv, "CamerasParent::RequestCameraAccess resolve");
+            aStatus, "CamerasParent::RequestCameraAccess resolve");
       },
-      [promise = RefPtr(sCameraAccessRequestPromise.get())](nsresult aRv) {
+      [promise = RefPtr(sCameraAccessRequestPromise.get()),
+       aAllowPermissionRequest](void_t aRv) {
         if (promise == sCameraAccessRequestPromise) {
           sCameraAccessRequestPromise = nullptr;
-          return CameraAccessRequestPromise::CreateAndReject(
-              aRv, "CamerasParent::RequestCameraAccess reject");
+          return CameraAccessRequestPromise::CreateAndResolve(
+              CamerasAccessStatus::Error,
+              "CamerasParent::RequestCameraAccess reject");
         }
-        return CamerasParent::RequestCameraAccess();
+        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());
 
--- a/dom/media/systemservices/CamerasParent.h
+++ b/dom/media/systemservices/CamerasParent.h
@@ -51,39 +51,41 @@ class CallbackHelper : public rtc::Video
 };
 
 class DeliverFrameRunnable;
 
 class CamerasParent final : public PCamerasParent,
                             private webrtc::VideoInputFeedBack {
  public:
   using ShutdownMozPromise = media::ShutdownBlockingTicket::ShutdownMozPromise;
-  using CameraAccessRequestPromise =
-      MozPromise<nsresult, nsresult, /* IsExclusive = */ false>;
+
+  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 used only on desktop. This will make an xdg-desktop-portal
-   *   call to request access to camera when PipeWire is used, otherwise it
-   *   automatically grants access for other implementations.
-   *
-   *    1) NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR - access to camera has been
-   *       rejected by the user.
-   *    2) NS_ERROR_FAILURE - generic error, for instance with pipewire most
-   *       likely the xdg-desktop-portal request failed.
+   *   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();
+  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;
--- a/dom/media/systemservices/CamerasTypes.h
+++ b/dom/media/systemservices/CamerasTypes.h
@@ -16,23 +16,46 @@ 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 ContiguousEnumSerializer<
+          mozilla::camera::CamerasAccessStatus,
+          mozilla::camera::CamerasAccessStatus::Granted,
+          mozilla::camera::CamerasAccessStatus::Error> {};
 }  // namespace IPC
 
 #endif  // mozilla_CamerasTypes_h
--- a/dom/media/systemservices/PCameras.ipdl
+++ b/dom/media/systemservices/PCameras.ipdl
@@ -57,17 +57,17 @@ 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);
+  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);
 
--- a/dom/media/systemservices/VideoEngine.cpp
+++ b/dom/media/systemservices/VideoEngine.cpp
@@ -153,16 +153,21 @@ VideoEngine::GetOrCreateVideoCaptureDevi
 
   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)));
 }
 
--- a/dom/media/systemservices/VideoEngine.h
+++ b/dom/media/systemservices/VideoEngine.h
@@ -84,16 +84,22 @@ class VideoEngine {
    *   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:
--- a/dom/media/systemservices/moz.build
+++ b/dom/media/systemservices/moz.build
@@ -53,16 +53,19 @@ if CONFIG["MOZ_WEBRTC"]:
     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"]
new file mode 100644
--- /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
new file mode 100644
--- /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_
--- a/dom/media/systemservices/video_engine/video_capture_factory.cc
+++ b/dom/media/systemservices/video_engine/video_capture_factory.cc
@@ -5,25 +5,29 @@
  * 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(MOZ_ENABLE_DBUS)
 #  include "mozilla/widget/AsyncDBus.h"
 #endif
 
 #include <memory>
 
 namespace mozilla {
 
-VideoCaptureFactory::VideoCaptureFactory() : mCameraBackendInitialized(false) {
+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 =
@@ -39,16 +43,26 @@ VideoCaptureFactory::VideoCaptureFactory
 }
 
 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)
+    // 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) {
+      MOZ_ASSERT(mCameraAvailability != Unknown);
+      deviceInfo.reset(
+          new PlaceholderDeviceInfo(mCameraAvailability == Available));
+      return deviceInfo;
+    }
+
     deviceInfo.reset(webrtc::VideoCaptureFactory::CreateDeviceInfo(
         mVideoCaptureOptions.get()));
 #else
     deviceInfo.reset(webrtc::VideoCaptureFactory::CreateDeviceInfo());
 #endif
     return deviceInfo;
   }
 
@@ -95,18 +109,18 @@ auto VideoCaptureFactory::InitCameraBack
     mPromiseHolder.Resolve(NS_OK,
                            "VideoCaptureFactory::InitCameraBackend Resolve");
 #endif
   }
 
   return mPromise;
 }
 
-RefPtr<VideoCaptureFactory::HasCameraDevicePromise>
-VideoCaptureFactory::HasCameraDevice() {
+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(
@@ -124,29 +138,51 @@ VideoCaptureFactory::HasCameraDevice() {
                 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, "VideoCaptureFactory::HasCameraDevice Resolve");
+                  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 Resolve");
 }
 
+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");
+        }
+
+        mCameraAvailability = Unknown;
+        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:
--- a/dom/media/systemservices/video_engine/video_capture_factory.h
+++ b/dom/media/systemservices/video_engine/video_capture_factory.h
@@ -18,17 +18,19 @@ 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_REFCOUNTING(VideoCaptureFactory);
+  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,
@@ -43,35 +45,43 @@ class VideoCaptureFactory : webrtc::Vide
    * 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<bool, nsresult, true>;
+  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
--- a/dom/media/webrtc/MediaEngineWebRTC.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTC.cpp
@@ -97,25 +97,26 @@ void MediaEngineWebRTC::EnumerateVideoDe
   camera::CaptureEngine capEngine =
       MediaEngineRemoteVideoSource::CaptureEngine(aMediaSource);
   num = GetChildAndCall(&CamerasChild::NumberOfCaptureDevices, capEngine);
 
   for (int i = 0; i < num; i++) {
     char deviceName[MediaEngineSource::kMaxDeviceNameLength];
     char uniqueId[MediaEngineSource::kMaxUniqueIdLength];
     bool scarySource = false;
+    bool placeholder = false;
 
     // paranoia
     deviceName[0] = '\0';
     uniqueId[0] = '\0';
     int error;
 
     error = GetChildAndCall(&CamerasChild::GetCaptureDevice, capEngine, i,
                             deviceName, sizeof(deviceName), uniqueId,
-                            sizeof(uniqueId), &scarySource);
+                            sizeof(uniqueId), &scarySource, &placeholder);
     if (error) {
       LOG(("camera:GetCaptureDevice: Failed %d", error));
       continue;
     }
 #ifdef DEBUG
     LOG(("  Capture Device Index %d, Name %s", i, deviceName));
 
     webrtc::CaptureCapability cap;
@@ -132,21 +133,23 @@ void MediaEngineWebRTC::EnumerateVideoDe
     }
 #endif
 
     NS_ConvertUTF8toUTF16 name(deviceName);
     NS_ConvertUTF8toUTF16 uuid(uniqueId);
     // The remote video backend doesn't implement group id. We return the
     // device name and higher layers will correlate this with the name of
     // audio devices.
-    aDevices->EmplaceBack(new MediaDevice(
-        this, aMediaSource, name, uuid, uuid,
-        MediaDevice::IsScary(scaryKind || scarySource),
-        canRequestOsLevelPrompt ? MediaDevice::OsPromptable::Yes
-                                : MediaDevice::OsPromptable::No));
+
+    aDevices->EmplaceBack(
+        new MediaDevice(this, aMediaSource, name, uuid, uuid,
+                        MediaDevice::IsScary(scaryKind || scarySource),
+                        canRequestOsLevelPrompt ? MediaDevice::OsPromptable::Yes
+                                                : MediaDevice::OsPromptable::No,
+                        MediaDevice::IsPlaceholder(placeholder)));
   }
 }
 
 void MediaEngineWebRTC::EnumerateMicrophoneDevices(
     nsTArray<RefPtr<MediaDevice>>* aDevices) {
   AssertIsOnOwningThread();
 
   RefPtr<const AudioDeviceSet> devices =
--- a/ipc/glue/BackgroundParentImpl.cpp
+++ b/ipc/glue/BackgroundParentImpl.cpp
@@ -1354,29 +1354,30 @@ BackgroundParentImpl::RecvEnsureUtilityP
                        resolver(Type(NS_OK, std::move(aValue.ResolveValue())));
                      });
         }
       }));
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult BackgroundParentImpl::RecvRequestCameraAccess(
+    const bool& aAllowPermissionRequest,
     RequestCameraAccessResolver&& aResolver) {
 #ifdef MOZ_WEBRTC
-  mozilla::camera::CamerasParent::RequestCameraAccess()->Then(
-      GetCurrentSerialEventTarget(), __func__,
-      [resolver = std::move(aResolver)](
-          const mozilla::camera::CamerasParent::CameraAccessRequestPromise::
-              ResolveOrRejectValue& aValue) {
-        if (aValue.IsResolve()) {
-          resolver(aValue.ResolveValue());
-        } else {
-          resolver(aValue.RejectValue());
-        }
-      });
+  mozilla::camera::CamerasParent::RequestCameraAccess(aAllowPermissionRequest)
+      ->Then(GetCurrentSerialEventTarget(), __func__,
+             [resolver = std::move(aResolver)](
+                 const mozilla::camera::CamerasParent::
+                     CameraAccessRequestPromise::ResolveOrRejectValue& aValue) {
+               if (aValue.IsResolve()) {
+                 resolver(aValue.ResolveValue());
+               } else {
+                 resolver(CamerasAccessStatus::Error);
+               }
+             });
 #else
   aResolver(NS_ERROR_NOT_IMPLEMENTED);
 #endif
   return IPC_OK();
 }
 
 bool BackgroundParentImpl::DeallocPEndpointForReportParent(
     PEndpointForReportParent* aActor) {
--- a/ipc/glue/BackgroundParentImpl.h
+++ b/ipc/glue/BackgroundParentImpl.h
@@ -348,16 +348,17 @@ class BackgroundParentImpl : public PBac
   mozilla::ipc::IPCResult RecvEnsureRDDProcessAndCreateBridge(
       EnsureRDDProcessAndCreateBridgeResolver&& aResolver) override;
 
   mozilla::ipc::IPCResult RecvEnsureUtilityProcessAndCreateBridge(
       const RemoteDecodeIn& aLocation,
       EnsureUtilityProcessAndCreateBridgeResolver&& aResolver) override;
 
   mozilla::ipc::IPCResult RecvRequestCameraAccess(
+      const bool& aAllowPermissionRequest,
       RequestCameraAccessResolver&& aResolver) override;
 
   bool DeallocPEndpointForReportParent(
       PEndpointForReportParent* aActor) override;
 
   mozilla::ipc::IPCResult RecvRemoveEndpoint(
       const nsAString& aGroupName, const nsACString& aEndpointURL,
       const PrincipalInfo& aPrincipalInfo) override;
--- a/ipc/glue/PBackground.ipdl
+++ b/ipc/glue/PBackground.ipdl
@@ -70,16 +70,18 @@ include "mozilla/layers/LayersMessageUti
 
 using mozilla::dom::cache::Namespace
   from "mozilla/dom/cache/Types.h";
 
 using class mozilla::dom::SSCacheCopy from "mozilla/dom/PBackgroundSessionStorageCache.h";
 
 using mozilla::RemoteDecodeIn from "mozilla/RemoteDecoderManagerChild.h";
 
+using mozilla::camera::CamerasAccessStatus from "mozilla/media/CamerasTypes.h";
+
 namespace mozilla {
 namespace ipc {
 
 [NeedsOtherPid, ChildImpl=virtual, ParentImpl=virtual]
 sync protocol PBackground
 {
   manages PBackgroundIDBFactory;
   manages PBackgroundIndexedDBUtils;
@@ -276,17 +278,17 @@ parent:
 
   async EnsureUtilityProcessAndCreateBridge(RemoteDecodeIn aLocation)
       returns (nsresult rv, Endpoint<PRemoteDecoderManagerChild> aEndpoint);
 
   async PLockManager(nsIPrincipal aPrincipalInfo, nsID aClientId);
 
   async PFetch();
 
-  async RequestCameraAccess() returns (nsresult rv);
+  async RequestCameraAccess(bool aAllowPermissionRequest) returns (CamerasAccessStatus rv);
 
 child:
   async PCache();
   async PCacheStreamControl();
 
   async PRemoteWorker(RemoteWorkerData data);
 };