Filter out devices that do not support any format supported by WebRTC. This will for example be IR cameras that show as duplicated in the list of cameras, but support only GRAY8 format and for that reason do not work at all. Bug: webrtc:42225999 Change-Id: Ic2905bc66b55c3f48b49ff4097167f10d17ad656 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/358864 Commit-Queue: Jan Grulich <grulja@gmail.com> Reviewed-by: Andreas Pehrson <apehrson@mozilla.com> Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org> Cr-Commit-Position: refs/heads/main@{#42785}
442 lines
13 KiB
C++
442 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2022 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 "modules/video_capture/linux/pipewire_session.h"
|
|
|
|
#include <spa/monitor/device.h>
|
|
#include <spa/param/format-utils.h>
|
|
#include <spa/param/format.h>
|
|
#include <spa/param/video/raw.h>
|
|
#include <spa/pod/parser.h>
|
|
|
|
#include "common_video/libyuv/include/webrtc_libyuv.h"
|
|
#include "modules/video_capture/device_info_impl.h"
|
|
#include "rtc_base/logging.h"
|
|
#include "rtc_base/sanitizer.h"
|
|
#include "rtc_base/string_encode.h"
|
|
#include "rtc_base/string_to_number.h"
|
|
|
|
namespace webrtc {
|
|
namespace videocapturemodule {
|
|
|
|
VideoType PipeWireRawFormatToVideoType(uint32_t id) {
|
|
switch (id) {
|
|
case SPA_VIDEO_FORMAT_I420:
|
|
return VideoType::kI420;
|
|
case SPA_VIDEO_FORMAT_NV12:
|
|
return VideoType::kNV12;
|
|
case SPA_VIDEO_FORMAT_YUY2:
|
|
return VideoType::kYUY2;
|
|
case SPA_VIDEO_FORMAT_UYVY:
|
|
return VideoType::kUYVY;
|
|
case SPA_VIDEO_FORMAT_RGB16:
|
|
return VideoType::kRGB565;
|
|
case SPA_VIDEO_FORMAT_RGB:
|
|
return VideoType::kBGR24;
|
|
case SPA_VIDEO_FORMAT_BGR:
|
|
return VideoType::kRGB24;
|
|
case SPA_VIDEO_FORMAT_BGRA:
|
|
return VideoType::kARGB;
|
|
case SPA_VIDEO_FORMAT_RGBA:
|
|
return VideoType::kABGR;
|
|
case SPA_VIDEO_FORMAT_ARGB:
|
|
return VideoType::kBGRA;
|
|
default:
|
|
return VideoType::kUnknown;
|
|
}
|
|
}
|
|
|
|
void PipeWireNode::PipeWireNodeDeleter::operator()(
|
|
PipeWireNode* node) const noexcept {
|
|
pw_proxy_destroy(node->proxy_);
|
|
spa_hook_remove(&node->node_listener_);
|
|
}
|
|
|
|
// static
|
|
PipeWireNode::PipeWireNodePtr PipeWireNode::Create(PipeWireSession* session,
|
|
uint32_t id,
|
|
const spa_dict* props) {
|
|
return PipeWireNodePtr(new PipeWireNode(session, id, props));
|
|
}
|
|
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
PipeWireNode::PipeWireNode(PipeWireSession* session,
|
|
uint32_t id,
|
|
const spa_dict* props)
|
|
: session_(session),
|
|
id_(id),
|
|
display_name_(spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)),
|
|
unique_id_(rtc::ToString(id)) {
|
|
RTC_LOG(LS_VERBOSE) << "Found Camera: " << display_name_;
|
|
|
|
proxy_ = static_cast<pw_proxy*>(pw_registry_bind(
|
|
session_->pw_registry_, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0));
|
|
|
|
static const pw_node_events node_events{
|
|
.version = PW_VERSION_NODE_EVENTS,
|
|
.info = OnNodeInfo,
|
|
.param = OnNodeParam,
|
|
};
|
|
|
|
pw_node_add_listener(proxy_, &node_listener_, &node_events, this);
|
|
}
|
|
|
|
// static
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
void PipeWireNode::OnNodeInfo(void* data, const pw_node_info* info) {
|
|
PipeWireNode* that = static_cast<PipeWireNode*>(data);
|
|
|
|
if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) {
|
|
const char* vid_str;
|
|
const char* pid_str;
|
|
absl::optional<int> vid;
|
|
absl::optional<int> pid;
|
|
|
|
vid_str = spa_dict_lookup(info->props, SPA_KEY_DEVICE_VENDOR_ID);
|
|
pid_str = spa_dict_lookup(info->props, SPA_KEY_DEVICE_PRODUCT_ID);
|
|
vid = vid_str ? rtc::StringToNumber<int>(vid_str) : absl::nullopt;
|
|
pid = pid_str ? rtc::StringToNumber<int>(pid_str) : absl::nullopt;
|
|
|
|
if (vid && pid) {
|
|
char model_str[10];
|
|
snprintf(model_str, sizeof(model_str), "%04x:%04x", vid.value(),
|
|
pid.value());
|
|
that->model_id_ = model_str;
|
|
}
|
|
}
|
|
|
|
if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) {
|
|
for (uint32_t i = 0; i < info->n_params; i++) {
|
|
uint32_t id = info->params[i].id;
|
|
if (id == SPA_PARAM_EnumFormat &&
|
|
info->params[i].flags & SPA_PARAM_INFO_READ) {
|
|
pw_node_enum_params(that->proxy_, 0, id, 0, UINT32_MAX, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
that->session_->PipeWireSync();
|
|
}
|
|
}
|
|
|
|
// static
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
void PipeWireNode::OnNodeParam(void* data,
|
|
int seq,
|
|
uint32_t id,
|
|
uint32_t index,
|
|
uint32_t next,
|
|
const spa_pod* param) {
|
|
PipeWireNode* that = static_cast<PipeWireNode*>(data);
|
|
auto* obj = reinterpret_cast<const spa_pod_object*>(param);
|
|
const spa_pod_prop* prop = nullptr;
|
|
VideoCaptureCapability cap;
|
|
spa_pod* val;
|
|
uint32_t n_items, choice;
|
|
|
|
cap.videoType = VideoType::kUnknown;
|
|
cap.maxFPS = 0;
|
|
|
|
prop = spa_pod_object_find_prop(obj, prop, SPA_FORMAT_VIDEO_framerate);
|
|
if (prop) {
|
|
val = spa_pod_get_values(&prop->value, &n_items, &choice);
|
|
if (val->type == SPA_TYPE_Fraction) {
|
|
spa_fraction* fract;
|
|
|
|
fract = static_cast<spa_fraction*>(SPA_POD_BODY(val));
|
|
|
|
if (choice == SPA_CHOICE_None)
|
|
cap.maxFPS = 1.0 * fract[0].num / fract[0].denom;
|
|
else if (choice == SPA_CHOICE_Range && fract[1].num > 0)
|
|
cap.maxFPS = 1.0 * fract[1].num / fract[1].denom;
|
|
}
|
|
}
|
|
|
|
prop = spa_pod_object_find_prop(obj, prop, SPA_FORMAT_VIDEO_size);
|
|
if (!prop)
|
|
return;
|
|
|
|
val = spa_pod_get_values(&prop->value, &n_items, &choice);
|
|
if (val->type != SPA_TYPE_Rectangle)
|
|
return;
|
|
|
|
if (choice != SPA_CHOICE_None)
|
|
return;
|
|
|
|
if (!ParseFormat(param, &cap))
|
|
return;
|
|
|
|
spa_rectangle* rect;
|
|
rect = static_cast<spa_rectangle*>(SPA_POD_BODY(val));
|
|
cap.width = rect[0].width;
|
|
cap.height = rect[0].height;
|
|
|
|
RTC_LOG(LS_VERBOSE) << "Found Format(" << that->display_name_
|
|
<< "): " << static_cast<int>(cap.videoType) << "("
|
|
<< cap.width << "x" << cap.height << "@" << cap.maxFPS
|
|
<< ")";
|
|
|
|
that->capabilities_.push_back(cap);
|
|
}
|
|
|
|
// static
|
|
bool PipeWireNode::ParseFormat(const spa_pod* param,
|
|
VideoCaptureCapability* cap) {
|
|
auto* obj = reinterpret_cast<const spa_pod_object*>(param);
|
|
uint32_t media_type, media_subtype;
|
|
|
|
if (spa_format_parse(param, &media_type, &media_subtype) < 0) {
|
|
RTC_LOG(LS_ERROR) << "Failed to parse video format.";
|
|
return false;
|
|
}
|
|
|
|
if (media_type != SPA_MEDIA_TYPE_video)
|
|
return false;
|
|
|
|
if (media_subtype == SPA_MEDIA_SUBTYPE_raw) {
|
|
const spa_pod_prop* prop = nullptr;
|
|
uint32_t n_items, choice;
|
|
spa_pod* val;
|
|
uint32_t* id;
|
|
|
|
prop = spa_pod_object_find_prop(obj, prop, SPA_FORMAT_VIDEO_format);
|
|
if (!prop)
|
|
return false;
|
|
|
|
val = spa_pod_get_values(&prop->value, &n_items, &choice);
|
|
if (val->type != SPA_TYPE_Id)
|
|
return false;
|
|
|
|
if (choice != SPA_CHOICE_None)
|
|
return false;
|
|
|
|
id = static_cast<uint32_t*>(SPA_POD_BODY(val));
|
|
|
|
cap->videoType = PipeWireRawFormatToVideoType(id[0]);
|
|
if (cap->videoType == VideoType::kUnknown) {
|
|
RTC_LOG(LS_INFO) << "Unsupported PipeWire pixel format " << id[0];
|
|
return false;
|
|
}
|
|
|
|
} else if (media_subtype == SPA_MEDIA_SUBTYPE_mjpg) {
|
|
cap->videoType = VideoType::kMJPEG;
|
|
} else {
|
|
RTC_LOG(LS_INFO) << "Unsupported PipeWire media subtype " << media_subtype;
|
|
}
|
|
|
|
return cap->videoType != VideoType::kUnknown;
|
|
}
|
|
|
|
CameraPortalNotifier::CameraPortalNotifier(PipeWireSession* session)
|
|
: session_(session) {}
|
|
|
|
void CameraPortalNotifier::OnCameraRequestResult(
|
|
xdg_portal::RequestResponse result,
|
|
int fd) {
|
|
if (result == xdg_portal::RequestResponse::kSuccess) {
|
|
session_->InitPipeWire(fd);
|
|
} else if (result == xdg_portal::RequestResponse::kUserCancelled) {
|
|
session_->Finish(VideoCaptureOptions::Status::DENIED);
|
|
} else {
|
|
session_->Finish(VideoCaptureOptions::Status::ERROR);
|
|
}
|
|
}
|
|
|
|
PipeWireSession::PipeWireSession()
|
|
: status_(VideoCaptureOptions::Status::UNINITIALIZED) {}
|
|
|
|
PipeWireSession::~PipeWireSession() {
|
|
Cleanup();
|
|
}
|
|
|
|
void PipeWireSession::Init(VideoCaptureOptions::Callback* callback, int fd) {
|
|
{
|
|
webrtc::MutexLock lock(&callback_lock_);
|
|
callback_ = callback;
|
|
}
|
|
|
|
if (fd != kInvalidPipeWireFd) {
|
|
InitPipeWire(fd);
|
|
} else {
|
|
portal_notifier_ = std::make_unique<CameraPortalNotifier>(this);
|
|
portal_ = std::make_unique<CameraPortal>(portal_notifier_.get());
|
|
portal_->Start();
|
|
}
|
|
}
|
|
|
|
void PipeWireSession::InitPipeWire(int fd) {
|
|
if (!InitializePipeWire())
|
|
Finish(VideoCaptureOptions::Status::UNAVAILABLE);
|
|
|
|
if (!StartPipeWire(fd))
|
|
Finish(VideoCaptureOptions::Status::ERROR);
|
|
}
|
|
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
bool PipeWireSession::StartPipeWire(int fd) {
|
|
pw_init(/*argc=*/nullptr, /*argv=*/nullptr);
|
|
|
|
pw_main_loop_ = pw_thread_loop_new("pipewire-main-loop", nullptr);
|
|
|
|
pw_context_ =
|
|
pw_context_new(pw_thread_loop_get_loop(pw_main_loop_), nullptr, 0);
|
|
if (!pw_context_) {
|
|
RTC_LOG(LS_ERROR) << "Failed to create PipeWire context";
|
|
return false;
|
|
}
|
|
|
|
pw_core_ = pw_context_connect_fd(pw_context_, fd, nullptr, 0);
|
|
if (!pw_core_) {
|
|
RTC_LOG(LS_ERROR) << "Failed to connect PipeWire context";
|
|
return false;
|
|
}
|
|
|
|
static const pw_core_events core_events{
|
|
.version = PW_VERSION_CORE_EVENTS,
|
|
.done = &OnCoreDone,
|
|
.error = &OnCoreError,
|
|
};
|
|
|
|
pw_core_add_listener(pw_core_, &core_listener_, &core_events, this);
|
|
|
|
static const pw_registry_events registry_events{
|
|
.version = PW_VERSION_REGISTRY_EVENTS,
|
|
.global = OnRegistryGlobal,
|
|
.global_remove = OnRegistryGlobalRemove,
|
|
};
|
|
|
|
pw_registry_ = pw_core_get_registry(pw_core_, PW_VERSION_REGISTRY, 0);
|
|
pw_registry_add_listener(pw_registry_, ®istry_listener_, ®istry_events,
|
|
this);
|
|
|
|
PipeWireSync();
|
|
|
|
if (pw_thread_loop_start(pw_main_loop_) < 0) {
|
|
RTC_LOG(LS_ERROR) << "Failed to start main PipeWire loop";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PipeWireSession::StopPipeWire() {
|
|
if (pw_main_loop_)
|
|
pw_thread_loop_stop(pw_main_loop_);
|
|
|
|
if (pw_core_) {
|
|
pw_core_disconnect(pw_core_);
|
|
pw_core_ = nullptr;
|
|
}
|
|
|
|
if (pw_context_) {
|
|
pw_context_destroy(pw_context_);
|
|
pw_context_ = nullptr;
|
|
}
|
|
|
|
if (pw_main_loop_) {
|
|
pw_thread_loop_destroy(pw_main_loop_);
|
|
pw_main_loop_ = nullptr;
|
|
}
|
|
}
|
|
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
void PipeWireSession::PipeWireSync() {
|
|
sync_seq_ = pw_core_sync(pw_core_, PW_ID_CORE, sync_seq_);
|
|
}
|
|
|
|
// static
|
|
void PipeWireSession::OnCoreError(void* data,
|
|
uint32_t id,
|
|
int seq,
|
|
int res,
|
|
const char* message) {
|
|
RTC_LOG(LS_ERROR) << "PipeWire remote error: " << message;
|
|
}
|
|
|
|
// static
|
|
void PipeWireSession::OnCoreDone(void* data, uint32_t id, int seq) {
|
|
PipeWireSession* that = static_cast<PipeWireSession*>(data);
|
|
|
|
if (id == PW_ID_CORE) {
|
|
if (seq == that->sync_seq_) {
|
|
RTC_LOG(LS_VERBOSE) << "Enumerating PipeWire camera devices complete.";
|
|
|
|
// Remove camera devices with no capabilities
|
|
auto it = std::remove_if(that->nodes_.begin(), that->nodes_.end(),
|
|
[](const PipeWireNode::PipeWireNodePtr& node) {
|
|
return node->capabilities().empty();
|
|
});
|
|
that->nodes_.erase(it, that->nodes_.end());
|
|
|
|
that->Finish(VideoCaptureOptions::Status::SUCCESS);
|
|
}
|
|
}
|
|
}
|
|
|
|
// static
|
|
RTC_NO_SANITIZE("cfi-icall")
|
|
void PipeWireSession::OnRegistryGlobal(void* data,
|
|
uint32_t id,
|
|
uint32_t permissions,
|
|
const char* type,
|
|
uint32_t version,
|
|
const spa_dict* props) {
|
|
PipeWireSession* that = static_cast<PipeWireSession*>(data);
|
|
|
|
// Skip already added nodes to avoid duplicate camera entries
|
|
if (std::find_if(that->nodes_.begin(), that->nodes_.end(),
|
|
[id](const PipeWireNode::PipeWireNodePtr& node) {
|
|
return node->id() == id;
|
|
}) != that->nodes_.end())
|
|
return;
|
|
|
|
if (type != absl::string_view(PW_TYPE_INTERFACE_Node))
|
|
return;
|
|
|
|
if (!spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION))
|
|
return;
|
|
|
|
auto node_role = spa_dict_lookup(props, PW_KEY_MEDIA_ROLE);
|
|
if (!node_role || strcmp(node_role, "Camera"))
|
|
return;
|
|
|
|
that->nodes_.push_back(PipeWireNode::Create(that, id, props));
|
|
that->PipeWireSync();
|
|
}
|
|
|
|
// static
|
|
void PipeWireSession::OnRegistryGlobalRemove(void* data, uint32_t id) {
|
|
PipeWireSession* that = static_cast<PipeWireSession*>(data);
|
|
|
|
auto it = std::remove_if(that->nodes_.begin(), that->nodes_.end(),
|
|
[id](const PipeWireNode::PipeWireNodePtr& node) {
|
|
return node->id() == id;
|
|
});
|
|
that->nodes_.erase(it, that->nodes_.end());
|
|
}
|
|
|
|
void PipeWireSession::Finish(VideoCaptureOptions::Status status) {
|
|
webrtc::MutexLock lock(&callback_lock_);
|
|
|
|
if (callback_) {
|
|
callback_->OnInitialized(status);
|
|
callback_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void PipeWireSession::Cleanup() {
|
|
webrtc::MutexLock lock(&callback_lock_);
|
|
callback_ = nullptr;
|
|
|
|
StopPipeWire();
|
|
}
|
|
|
|
} // namespace videocapturemodule
|
|
} // namespace webrtc
|