webrtc_m130/modules/video_capture/linux/pipewire_session.cc
Jan Grulich a5d71009ac PipeWire camera: use better unique device name for camera devices
Originally we used node id from PipeWire as an unique device name and
while this works, it will change everytime PipeWire is restarted. This
has an impact on default camera selection, where for example Firefox can
automatically request a camera device that was used before, but this can
break with the next PipeWire restart.

Bug: webrtc:42225999
Change-Id: I9440ee065ffeaa1ffb911a4dc7c405d57c9416dc
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/367880
Commit-Queue: Jan Grulich <grulja@gmail.com>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#43387}
2024-11-11 19:01:34 +00:00

452 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 <algorithm>
#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_(spa_dict_lookup(props, PW_KEY_NODE_NAME)) {
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;
std::optional<int> vid;
std::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) : std::nullopt;
pid = pid_str ? rtc::StringToNumber<int>(pid_str) : std::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_Enum) {
for (uint32_t i = 1; i < n_items; i++) {
cap.maxFPS = std::max(
static_cast<int32_t>(1.0 * fract[i].num / fract[i].denom),
cap.maxFPS);
}
} 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_, &registry_listener_, &registry_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) {
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