blob: b51a19efc8247ca6eed18638231120c0c290864e [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "remoting/host/linux/pipewire_capture_stream_manager.h"
#include <algorithm>
#include <memory>
#include <vector>
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "base/sequence_checker.h"
#include "base/types/expected.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/dbus_interfaces/org_gnome_Mutter_ScreenCast.h"
#include "remoting/host/linux/gnome_display_config.h"
#include "remoting/host/linux/gnome_display_config_dbus_client.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
namespace remoting {
namespace {
using gvariant::Boxed;
constexpr char kScreenCastBusName[] = "org.gnome.Mutter.ScreenCast";
} // namespace
PipewireCaptureStreamManager::AddStreamRequest::AddStreamRequest() = default;
PipewireCaptureStreamManager::AddStreamRequest::AddStreamRequest(
AddStreamRequest&&) = default;
PipewireCaptureStreamManager::AddStreamRequest::AddStreamRequest(
VirtualStreamInfo virtual_stream_info,
AddStreamCallback callback)
: virtual_stream_info(std::move(virtual_stream_info)),
callback(std::move(callback)) {}
PipewireCaptureStreamManager::AddStreamRequest::AddStreamRequest(
MonitorStreamInfo monitor_stream_info,
AddStreamCallback callback)
: monitor_stream_info(std::move(monitor_stream_info)),
callback(std::move(callback)) {}
PipewireCaptureStreamManager::AddStreamRequest::~AddStreamRequest() = default;
PipewireCaptureStreamManager::StreamInfo::StreamInfo() = default;
PipewireCaptureStreamManager::StreamInfo::StreamInfo(StreamInfo&&) = default;
PipewireCaptureStreamManager::StreamInfo&
PipewireCaptureStreamManager::StreamInfo::operator=(StreamInfo&&) = default;
PipewireCaptureStreamManager::StreamInfo::~StreamInfo() = default;
PipewireCaptureStreamManager::PipewireCaptureStreamManager() = default;
PipewireCaptureStreamManager::~PipewireCaptureStreamManager() = default;
PipewireCaptureStreamManager::Observer::Subscription
PipewireCaptureStreamManager::AddObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.AddObserver(observer);
return Observer::Subscription(base::BindOnce(
&PipewireCaptureStreamManager::RemoveObserver, GetWeakPtr(), observer));
}
void PipewireCaptureStreamManager::Init(
GDBusConnectionRef* connection,
base::WeakPtr<GnomeDisplayConfigMonitor> display_config_monitor,
gvariant::ObjectPath screencast_session_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(connection);
DCHECK(display_config_monitor);
connection_ = connection;
screencast_session_path_ = std::move(screencast_session_path);
if (display_config_monitor) {
monitors_changed_subscription_ = display_config_monitor->AddCallback(
base::BindRepeating(
&PipewireCaptureStreamManager::OnGnomeDisplayConfigChanged,
GetWeakPtr()),
/*call_with_current_config=*/true);
}
}
base::WeakPtr<CaptureStream> PipewireCaptureStreamManager::GetStream(
webrtc::ScreenId screen_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const auto& it = streams_.find(screen_id);
if (it == streams_.end()) {
return nullptr;
}
return it->second.stream->GetWeakPtr();
}
void PipewireCaptureStreamManager::AddVirtualStream(
const ScreenResolution& initial_resolution,
AddStreamCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
pending_add_stream_requests_.emplace_back(
AddStreamRequest::VirtualStreamInfo{initial_resolution},
std::move(callback));
if (!last_seen_display_config_.has_value()) {
// We can't safely start adding the stream if we haven't received the
// initial display config, since otherwise we wouldn't tell which monitor is
// newly added.
HOST_LOG << "Stream will be added after initial display config is loaded.";
} else if (pending_add_stream_requests_.size() > 1) {
HOST_LOG << "Stream will be added after pending stream is added.";
} else {
MaybeAddStreamForCurrentRequest();
}
}
void PipewireCaptureStreamManager::RemoveVirtualStream(
webrtc::ScreenId screen_id) {
RemoveStream(screen_id, /*can_remove_monitor_stream=*/false);
}
void PipewireCaptureStreamManager::RemoveStream(
webrtc::ScreenId screen_id,
bool can_remove_monitor_stream) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = streams_.find(screen_id);
if (it == streams_.end()) {
LOG(ERROR) << "Cannot find stream for screen ID: " << screen_id;
return;
}
StreamInfo& stream_info = it->second;
if (!can_remove_monitor_stream && !stream_info.is_virtual_stream) {
LOG(ERROR) << "Cannot remove monitor stream: " << screen_id;
return;
}
if (stream_info.is_deleting) {
VLOG(1) << "Stream " << screen_id << " is already being deleted.";
return;
}
stream_info.is_deleting = true;
// The virtual monitor will not be removed until the screencast Stop() method
// is called.
connection_->Call<org_gnome_Mutter_ScreenCast_Stream::Stop>(
kScreenCastBusName, stream_info.stream_path, std::tuple(),
base::BindOnce(&PipewireCaptureStreamManager::OnStreamStopped,
GetWeakPtr(), screen_id));
}
base::flat_map<webrtc::ScreenId, base::WeakPtr<CaptureStream>>
PipewireCaptureStreamManager::GetActiveStreams() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::flat_map<webrtc::ScreenId, base::WeakPtr<CaptureStream>> output;
for (auto& [screen_id, stream_info] : streams_) {
output[screen_id] = stream_info.stream->GetWeakPtr();
}
return output;
}
base::WeakPtr<PipewireCaptureStreamManager>
PipewireCaptureStreamManager::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void PipewireCaptureStreamManager::RemoveObserver(Observer* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.RemoveObserver(observer);
}
template <typename SuccessType, typename String>
GDBusConnectionRef::CallCallback<SuccessType>
PipewireCaptureStreamManager::CheckAddStreamResultAndContinue(
void (PipewireCaptureStreamManager::*success_method)(SuccessType),
String&& error_context) {
return base::BindOnce(
[](base::WeakPtr<PipewireCaptureStreamManager> that,
decltype(success_method) success_method,
std::string_view error_context,
base::expected<SuccessType, Loggable> result) {
if (!that) {
return;
}
if (result.has_value()) {
(that.get()->*success_method)(std::move(result).value());
} else {
that->OnAddStreamError(error_context, std::move(result).error());
}
},
GetWeakPtr(), success_method, std::forward<String>(error_context));
}
void PipewireCaptureStreamManager::MaybeAddStreamForCurrentRequest() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(last_seen_display_config_.has_value());
DCHECK(!pending_stream_);
if (pending_add_stream_requests_.empty()) {
return;
}
// Include the cursor in the Pipewire stream metadata.
constexpr std::uint32_t kCursorModeMetadata = 2;
pending_stream_ = std::make_unique<PipewireCaptureStream>();
auto& current_request = pending_add_stream_requests_.front();
if (current_request.virtual_stream_info.has_value()) {
// Add virtual stream.
connection_->Call<org_gnome_Mutter_ScreenCast_Session::RecordVirtual>(
kScreenCastBusName, screencast_session_path_,
std::tuple{std::array{
std::pair{"cursor-mode", GVariantFrom(Boxed{kCursorModeMetadata})},
std::pair{"is-platform", GVariantFrom(Boxed{true})}}},
CheckAddStreamResultAndContinue(
&PipewireCaptureStreamManager::OnStreamCreated,
"Failed to create and record virtual monitor"));
} else if (current_request.monitor_stream_info.has_value()) {
// Add monitor stream.
connection_->Call<org_gnome_Mutter_ScreenCast_Session::RecordMonitor>(
kScreenCastBusName, screencast_session_path_,
std::tuple{
current_request.monitor_stream_info->connector,
std::array{std::pair{"cursor-mode",
GVariantFrom(Boxed{kCursorModeMetadata})}}},
CheckAddStreamResultAndContinue(
&PipewireCaptureStreamManager::OnStreamCreated,
"Failed to record monitor"));
} else {
NOTREACHED();
}
}
void PipewireCaptureStreamManager::MaybeAddMonitorStreams() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(last_seen_display_config_.has_value());
for (const auto& [connector, monitor] : last_seen_display_config_->monitors) {
auto pending_add_stream_request_it = std::find_if(
pending_add_stream_requests_.begin(),
pending_add_stream_requests_.end(),
[connector](const AddStreamRequest& request) {
return request.monitor_stream_info.has_value() &&
request.monitor_stream_info->connector == connector;
});
// Only add monitor stream if it is not in `stream_` and not already in the
// add stream request queue.
if (pending_add_stream_request_it == pending_add_stream_requests_.end() &&
!streams_.contains(GnomeDisplayConfig::GetScreenId(connector))) {
HOST_LOG << "Adding monitor stream for " << connector;
pending_add_stream_requests_.emplace_back(
AddStreamRequest::MonitorStreamInfo{connector},
base::BindOnce(
[](const std::string& conn, AddStreamResult result) {
if (!result.has_value()) {
LOG(ERROR) << "Failed to add monitor stream for monitor "
<< conn << ": " << result.error();
}
},
connector));
}
}
if (!pending_add_stream_requests_.empty() && !pending_stream_) {
MaybeAddStreamForCurrentRequest();
}
}
void PipewireCaptureStreamManager::RemoveInvalidStreams() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::flat_set<webrtc::ScreenId> unseen_active_screen_ids;
for (auto& [screen_id, stream_info] : streams_) {
if (!stream_info.is_deleting) {
unseen_active_screen_ids.insert(screen_id);
}
}
for (const auto& [connector, _] : last_seen_display_config_->monitors) {
unseen_active_screen_ids.erase(GnomeDisplayConfig::GetScreenId(connector));
}
if (!unseen_active_screen_ids.empty()) {
for (auto screen_id : unseen_active_screen_ids) {
HOST_LOG << "Removing stream for screen ID " << screen_id;
RemoveStream(screen_id, /*can_remove_monitor_stream=*/true);
}
}
}
void PipewireCaptureStreamManager::RunCurrentAddStreamCallback(
AddStreamResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!pending_add_stream_requests_.empty());
pending_stream_.reset();
auto callback = std::move(pending_add_stream_requests_.front().callback);
pending_add_stream_requests_.pop_front();
auto stream = result.value_or(nullptr);
std::move(callback).Run(std::move(result));
if (stream) {
observers_.Notify(&Observer::OnPipewireCaptureStreamAdded, stream);
}
MaybeAddStreamForCurrentRequest();
}
void PipewireCaptureStreamManager::OnAddStreamError(
std::string_view error_message,
Loggable error_context) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
RunCurrentAddStreamCallback(base::unexpected(
base::StrCat({error_message, ": ", error_context.ToString()})));
}
void PipewireCaptureStreamManager::OnStreamCreated(
std::tuple<gvariant::ObjectPath> args) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(pending_stream_);
HOST_LOG << "PipeWire stream created";
std::tie(pending_stream_path_) = args;
connection_->GetProperty<org_gnome_Mutter_ScreenCast_Stream::Parameters>(
kScreenCastBusName, pending_stream_path_,
CheckAddStreamResultAndContinue(
&PipewireCaptureStreamManager::OnStreamParameters,
"Failed to retrieve stream parameters"));
}
void PipewireCaptureStreamManager::OnStreamParameters(
GVariantRef<"a{sv}"> parameters) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(pending_stream_);
gchar* param_str = g_variant_print(parameters.raw(), true);
HOST_LOG << "Stream parameters: " << param_str;
g_free(param_str);
auto maybe_boxed_mapping_id = parameters.LookUp("mapping-id");
if (!maybe_boxed_mapping_id.has_value()) {
RunCurrentAddStreamCallback(
base::unexpected("mapping-id stream parameter not present"));
return;
}
std::string mapping_id;
auto destructure_result = maybe_boxed_mapping_id->TryDestructure(mapping_id);
if (!destructure_result.has_value()) {
RunCurrentAddStreamCallback(base::unexpected(
base::StrCat({" Failed to retrieve mapping-id stream parameter: ",
destructure_result.error().ToString()})));
return;
}
// Note that both OnStreamStarted and OnPipeWireStreamAdded may invoke
// the AddStreamCallback, but the former only does so on error and the latter
// unsubscribes from the signal, meaning that it is guaranteed only to
// be called once per stream.
pending_stream_added_signal_ = connection_->SignalSubscribe<
org_gnome_Mutter_ScreenCast_Stream::PipeWireStreamAdded>(
kScreenCastBusName, pending_stream_path_,
base::BindRepeating(&PipewireCaptureStreamManager::OnPipeWireStreamAdded,
GetWeakPtr(), std::move(mapping_id)));
connection_->Call<org_gnome_Mutter_ScreenCast_Stream::Start>(
kScreenCastBusName, pending_stream_path_, std::tuple(),
CheckAddStreamResultAndContinue(
&PipewireCaptureStreamManager::OnStreamStarted,
"Failed to start monitor stream"));
}
void PipewireCaptureStreamManager::OnStreamStarted(std::tuple<> args) {
// Do nothing. Still need to wait for PipeWire-stream-added signal.
}
void PipewireCaptureStreamManager::OnStreamStopped(
webrtc::ScreenId screen_id,
base::expected<std::tuple<>, Loggable> result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!result.has_value()) {
LOG(ERROR) << "Failed to stop stream: " << result.error();
streams_[screen_id].is_deleting = false;
return;
}
if (streams_.erase(screen_id) != 0) {
observers_.Notify(&Observer::OnPipewireCaptureStreamRemoved, screen_id);
}
}
void PipewireCaptureStreamManager::OnPipeWireStreamAdded(
std::string mapping_id,
std::tuple<std::uint32_t> args) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(pending_stream_);
DCHECK(!pending_add_stream_requests_.empty());
// Ensure method is only run this once per stream.
pending_stream_added_signal_.reset();
auto& current_request = pending_add_stream_requests_.front();
if (current_request.virtual_stream_info.has_value()) {
pending_stream_->SetPipeWireStream(
get<0>(args),
current_request.virtual_stream_info->initial_resolution.dimensions(),
mapping_id, webrtc::kInvalidPipeWireFd);
} else if (current_request.monitor_stream_info.has_value()) {
pending_stream_->SetPipeWireStream(get<0>(args),
/*initial_resolution=*/{}, mapping_id,
webrtc::kInvalidPipeWireFd);
} else {
NOTREACHED();
}
// Start capturing now, which creates the virtual monitor (if applicable) and
// allows the video capturer to be created.
pending_stream_->StartVideoCapture();
if (current_request.monitor_stream_info.has_value()) {
AssociatePendingStream(GnomeDisplayConfig::GetScreenId(
current_request.monitor_stream_info->connector));
}
}
void PipewireCaptureStreamManager::OnGnomeDisplayConfigChanged(
const GnomeDisplayConfig& config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// See comment in AddVirtualStream().
if (!last_seen_display_config_.has_value()) {
DCHECK(!pending_stream_);
DCHECK(streams_.empty());
last_seen_display_config_ = config;
MaybeAddMonitorStreams();
if (!pending_add_stream_requests_.empty() && !pending_stream_) {
HOST_LOG
<< "Adding virtual stream after initial display config is loaded.";
MaybeAddStreamForCurrentRequest();
}
SetUseDamageRegion();
return;
}
// Early-return if we are not in the process of adding a virtual stream.
if (!pending_stream_ ||
(!pending_add_stream_requests_.empty() &&
pending_add_stream_requests_.front().monitor_stream_info.has_value())) {
last_seen_display_config_ = config;
MaybeAddMonitorStreams();
RemoveInvalidStreams();
SetUseDamageRegion();
return;
}
// Find the new virtual monitor and associate it with the pending virtual
// stream. We can't call MaybeAddMonitorStreams() here since it would create
// a monitor stream for the newly created virtual monitor and cause problems.
// This, however, means if a new physical or virtual monitor is created
// externally, it will cause a race condition and the virtual stream could be
// associated with the wrong monitor.
GnomeDisplayConfig previous_config = std::move(*last_seen_display_config_);
last_seen_display_config_ = config;
RemoveInvalidStreams();
std::vector<webrtc::ScreenId> new_screen_ids;
for (const auto& [name, monitor] : last_seen_display_config_->monitors) {
if (!previous_config.monitors.contains(name)) {
new_screen_ids.push_back(GnomeDisplayConfig::GetScreenId(name));
}
}
if (new_screen_ids.empty()) {
LOG(WARNING)
<< "No new screen ID to be associated with the pending stream.";
return;
}
if (new_screen_ids.size() > 1) {
LOG(WARNING) << "Multiple new screen IDs are found. The lowest screen "
<< "ID will be associated with the pending stream.";
// Ensure that screen IDs less than `screen_id_adjustment` will be chosen
// over the other IDs.
std::sort(new_screen_ids.begin(), new_screen_ids.end());
}
AssociatePendingStream(new_screen_ids.front());
}
void PipewireCaptureStreamManager::AssociatePendingStream(
webrtc::ScreenId screen_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!pending_add_stream_requests_.empty());
DCHECK(!streams_.contains(screen_id));
if (!pending_stream_) {
LOG(WARNING) << "There is no pending stream to associate with.";
return;
}
HOST_LOG << "Associating pending stream with screen ID " << screen_id;
pending_stream_->set_screen_id(screen_id);
auto weak_ptr = pending_stream_->GetWeakPtr();
StreamInfo info;
info.is_virtual_stream =
pending_add_stream_requests_.front().virtual_stream_info.has_value();
info.stream = std::move(pending_stream_);
info.stream_path = std::move(pending_stream_path_);
streams_[screen_id] = std::move(info);
SetUseDamageRegion();
RunCurrentAddStreamCallback(base::ok(weak_ptr));
}
void PipewireCaptureStreamManager::SetUseDamageRegion() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (auto& [screen_id, stream_info] : streams_) {
const auto monitor_it = last_seen_display_config_->FindMonitor(screen_id);
if (monitor_it == last_seen_display_config_->monitors.end()) {
LOG(ERROR) << "Cannot find monitor for screen ID " << screen_id;
continue;
}
// Given mutter's bug with the reported damage region, it is only safe to
// enable damage region if the monitor is at the top-left corner with
// 100% scaling.
// See: https://gitlab.gnome.org/GNOME/mutter/-/issues/4269
// Note: This bug only seems to happen in virtual streams.
bool use_damage_region =
!stream_info.is_virtual_stream ||
(monitor_it->second.x == 0 && monitor_it->second.y == 0 &&
monitor_it->second.scale == 1.0);
stream_info.stream->SetUseDamageRegion(use_damage_region);
}
}
} // namespace remoting