| /* | |
| * 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 <pipewire/pipewire.h> | |
| #include <spa/monitor/device.h> | |
| #include <spa/param/format-utils.h> | |
| #include <spa/param/format.h> | |
| #include <spa/param/param.h> | |
| #include <spa/param/video/raw.h> | |
| #include <spa/pod/iter.h> | |
| #include <spa/pod/pod.h> | |
| #include <spa/utils/defs.h> | |
| #include <spa/utils/dict.h> | |
| #include <spa/utils/hook.h> | |
| #include <spa/utils/type.h> | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <cstdio> | |
| #include <cstring> | |
| #include <memory> | |
| #include <optional> | |
| #include "absl/strings/string_view.h" | |
| #include "common_video/libyuv/include/webrtc_libyuv.h" | |
| #include "modules/portal/pipewire_utils.h" | |
| #include "modules/portal/portal_request_response.h" | |
| #include "modules/video_capture/linux/camera_portal.h" | |
| #include "modules/video_capture/video_capture_defines.h" | |
| #include "modules/video_capture/video_capture_options.h" | |
| #include "rtc_base/logging.h" | |
| #include "rtc_base/sanitizer.h" | |
| #include "rtc_base/string_to_number.h" | |
| #include "rtc_base/synchronization/mutex.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 { | |
| spa_hook_remove(&node->node_listener_); | |
| pw_proxy_destroy(node->proxy_); | |
| } | |
| // 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(reinterpret_cast<pw_node*>(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 ? webrtc::StringToNumber<int>(vid_str) : std::nullopt; | |
| pid = pid_str ? webrtc::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) { | |
| RTC_LOG(LS_INFO) | |
| << "Clearing existing capabilities before re-enumeration for " | |
| << that->display_name_; | |
| that->capabilities_.clear(); | |
| pw_node_enum_params(reinterpret_cast<pw_node*>(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_initializer_ = std::make_unique<PipeWireInitializer>(); | |
| 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 | |
| std::erase_if(that->nodes_, | |
| [](const PipeWireNode::PipeWireNodePtr& node) { | |
| return node->capabilities().empty(); | |
| }); | |
| 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); | |
| std::erase_if(that->nodes_, [id](const PipeWireNode::PipeWireNodePtr& node) { | |
| return node->id() == id; | |
| }); | |
| } | |
| 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 |