/* * 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 #include #include #include #include #include "common_video/libyuv/include/webrtc_libyuv.h" #include "modules/video_capture/device_info_impl.h" #include "rtc_base/logging.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_RGB: return VideoType::kRGB24; default: return VideoType::kUnknown; } } 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_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); } PipeWireNode::~PipeWireNode() { pw_proxy_destroy(proxy_); spa_hook_remove(&node_listener_); } // static void PipeWireNode::OnNodeInfo(void* data, const pw_node_info* info) { PipeWireNode* that = static_cast(data); if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { const char* vid_str; const char* pid_str; absl::optional vid; absl::optional 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(vid_str) : absl::nullopt; pid = pid_str ? rtc::StringToNumber(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; } } else 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 void PipeWireNode::OnNodeParam(void* data, int seq, uint32_t id, uint32_t index, uint32_t next, const spa_pod* param) { PipeWireNode* that = static_cast(data); auto* obj = reinterpret_cast(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_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_POD_BODY(val)); cap.width = rect[0].width; cap.height = rect[0].height; RTC_LOG(LS_VERBOSE) << "Found Format(" << that->display_name_ << "): " << static_cast(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(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(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(this); portal_ = std::make_unique(portal_notifier_.get()); portal_->Start(); } } void PipeWireSession::InitPipeWire(int fd) { if (!InitializePipeWire()) Finish(VideoCaptureOptions::Status::UNAVAILABLE); if (!StartPipeWire(fd)) Finish(VideoCaptureOptions::Status::ERROR); } 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; } } 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(data); if (id == PW_ID_CORE) { if (seq == that->sync_seq_) { RTC_LOG(LS_VERBOSE) << "Enumerating PipeWire camera devices complete."; that->Finish(VideoCaptureOptions::Status::SUCCESS); } } } // static 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(data); 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_.emplace_back(that, id, props); that->PipeWireSync(); } // static void PipeWireSession::OnRegistryGlobalRemove(void* data, uint32_t id) { PipeWireSession* that = static_cast(data); for (auto it = that->nodes_.begin(); it != that->nodes().end(); ++it) { if ((*it).id() == id) { that->nodes_.erase(it); break; } } } 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