| /* |
| * 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_(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; |
| 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_, ®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) { |
| 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 |