#include "remoting/host/linux/gnome_desktop_resizer.h"
#include <functional>
#include <optional>
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/host/base/screen_resolution.h"
#include "remoting/host/linux/gnome_capture_stream_manager.h"
#include "remoting/host/linux/gnome_display_config.h"
#include "remoting/host/linux/gnome_interaction_strategy.h"
#include "remoting/host/linux/pipewire_capture_stream.h"
#include "remoting/proto/control.pb.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capture_types.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h"
#include "ui/base/glib/gsettings.h"
namespace remoting {
namespace {
constexpr GnomeDisplayConfig::LayoutMode kPreferredLayoutMode =
GnomeDisplayConfig::LayoutMode::kLogical;
constexpr base::TimeDelta kClearPreferredConfigDelay = base::Seconds(5);
constexpr double kTextScaleThreshold = 1.25;
inline double InverseIfLessThanOne(double v) {
return v < 1.0 ? 1.0 / v : v;
}
inline double FindBestScale(double preferred_scale,
const std::vector<double>& supported_scales,
bool ignore_fractional_scales) {
DCHECK_GT(preferred_scale, 0.0);
auto it = std::ranges::min_element(
supported_scales,
[preferred_scale, ignore_fractional_scales](double s1, double s2) {
DCHECK_GT(s1, 0.0);
DCHECK_GT(s2, 0.0);
if (ignore_fractional_scales && trunc(s1) == s1 && trunc(s2) != s2) {
return true;
}
return InverseIfLessThanOne(preferred_scale / s1) <
InverseIfLessThanOne(preferred_scale / s2);
});
if (it == supported_scales.end()) {
LOG(ERROR) << "Cannot find best scale for " << preferred_scale;
return 1.0;
}
if (ignore_fractional_scales && trunc(*it) != *it) {
LOG(ERROR) << "Cannot find non-fractional scales";
return 1.0;
}
return *it;
}
inline bool IsSameScale(double s1, double s2) {
return std::abs(s1 - s2) < 0.01;
}
void AddMonitorForLayoutCalculation(GnomeDisplayConfig& config,
const webrtc::DesktopVector& position,
const ScreenResolution& resolution) {
GnomeDisplayConfig::MonitorInfo& info =
config.monitors[base::NumberToString(config.monitors.size())];
info.x = position.x();
info.y = position.y();
info.scale = resolution.dpi().x() == 0.0
? 1.0
: static_cast<double>(resolution.dpi().x()) / kDefaultDpi;
GnomeDisplayConfig::MonitorMode mode;
mode.width = resolution.dimensions().width();
mode.height = resolution.dimensions().height();
mode.is_current = true;
info.modes.push_back(mode);
}
inline ScopedGObject<GSettings> CreateGsettingsRegistry() {
auto registry = ui::GSettingsNew("org.gnome.desktop.interface");
CHECK(registry)
<< "ui::GSettingsNew(\"org.gnome.desktop.interface\") failed.";
return registry;
}
}
GnomeDesktopResizer::GnomeDesktopResizer(
base::WeakPtr<CaptureStreamManager> stream_manager,
base::WeakPtr<GnomeDisplayConfigDBusClient> display_config_client,
base::WeakPtr<GnomeDisplayConfigMonitor> display_config_monitor)
: GnomeDesktopResizer(
stream_manager,
display_config_monitor,
CreateGsettingsRegistry(),
base::BindRepeating(
&GnomeDisplayConfigDBusClient::ApplyMonitorsConfig,
display_config_client)) {}
GnomeDesktopResizer::GnomeDesktopResizer(
base::WeakPtr<CaptureStreamManager> stream_manager,
base::WeakPtr<GnomeDisplayConfigMonitor> display_config_monitor,
ScopedGObject<GSettings> registry,
base::RepeatingCallback<void(const GnomeDisplayConfig&)>
apply_monitors_config)
: stream_manager_(stream_manager),
apply_monitors_config_(apply_monitors_config),
registry_(std::move(registry)) {
if (display_config_monitor) {
monitors_changed_subscription_ = display_config_monitor->AddCallback(
base::BindRepeating(&GnomeDesktopResizer::OnGnomeDisplayConfigReceived,
weak_ptr_factory_.GetWeakPtr()),
true);
}
}
GnomeDesktopResizer::~GnomeDesktopResizer() = default;
ScreenResolution GnomeDesktopResizer::GetCurrentResolution(
webrtc::ScreenId screen_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!stream_manager_) {
return {};
}
base::WeakPtr<CaptureStream> stream = stream_manager_->GetStream(screen_id);
if (!stream) {
LOG(ERROR) << "Cannot find pipewire stream for screen ID: " << screen_id;
return {};
}
double text_scaling_factor = GetTextScalingFactor();
double dpi = kDefaultDpi * text_scaling_factor;
auto monitor_it = current_display_config_.FindMonitor(screen_id);
if (monitor_it == current_display_config_.monitors.end()) {
LOG(ERROR) << "Cannot find monitor with screen ID: " << screen_id;
} else {
dpi *= monitor_it->second.scale;
}
return {stream->resolution(), {static_cast<int>(dpi), static_cast<int>(dpi)}};
}
std::list<ScreenResolution> GnomeDesktopResizer::GetSupportedResolutions(
const ScreenResolution& preferred,
webrtc::ScreenId screen_id) {
return {preferred};
}
void GnomeDesktopResizer::SetResolution(const ScreenResolution& resolution,
webrtc::ScreenId screen_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!preferred_layout_) {
preferred_layout_ = current_display_config_.GetLayoutInfo();
MaybeDelayClearPreferredConfig();
}
SetResolutionAndPosition(resolution, std::nullopt, screen_id);
}
void GnomeDesktopResizer::RestoreResolution(const ScreenResolution& original,
webrtc::ScreenId screen_id) {
SetResolution(original, screen_id);
}
void GnomeDesktopResizer::SetVideoLayout(const protocol::VideoLayout& layout) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!stream_manager_) {
return;
}
auto active_screen_ids = base::MakeFlatSet<webrtc::ScreenId>(
stream_manager_->GetActiveStreams(), std::less<>(),
[](const auto& kv) { return kv.first; });
base::flat_set<webrtc::ScreenId> screen_ids_in_video_track;
for (const auto& track : layout.video_track()) {
if (track.has_screen_id()) {
screen_ids_in_video_track.emplace(track.screen_id());
}
}
auto streams_to_be_removed =
base::STLSetDifference<base::flat_set<webrtc::ScreenId>>(
active_screen_ids, screen_ids_in_video_track);
if (!streams_to_be_removed.empty()) {
if (!streams_being_removed_.empty()) {
LOG(WARNING) << "Streams will not be removed since there are already "
<< "streams being removed.";
} else {
streams_being_removed_ = streams_to_be_removed;
for (webrtc::ScreenId stream_id : streams_being_removed_) {
stream_manager_->RemoveVirtualStream(stream_id);
preferred_monitors_config_.erase(stream_id);
}
}
}
GnomeDisplayConfig display_config_for_layout_calculation;
switch (layout.pixel_type()) {
case protocol::VideoLayout::PixelType::VideoLayout_PixelType_LOGICAL:
display_config_for_layout_calculation.layout_mode =
GnomeDisplayConfig::LayoutMode::kLogical;
break;
case protocol::VideoLayout::PixelType::VideoLayout_PixelType_PHYSICAL:
case protocol::VideoLayout::PixelType::
VideoLayout_PixelType_UNSPECIFIED_PIXEL_TYPE:
display_config_for_layout_calculation.layout_mode =
GnomeDisplayConfig::LayoutMode::kPhysical;
break;
}
for (const auto& track : layout.video_track()) {
double scale = static_cast<double>(track.x_dpi()) / kDefaultDpi;
if (scale == 0.0) {
scale = 1.0;
}
double physical_resolution_multiplier =
layout.pixel_type() ==
protocol::VideoLayout::PixelType::VideoLayout_PixelType_LOGICAL
? scale
: 1.0;
webrtc::DesktopSize physical_resolution{
static_cast<int>(track.width() * physical_resolution_multiplier),
static_cast<int>(track.height() * physical_resolution_multiplier)};
ScreenResolution screen_resolution{physical_resolution,
{track.x_dpi(), track.y_dpi()}};
webrtc::DesktopVector position{track.position_x(), track.position_y()};
if (!track.has_screen_id()) {
stream_manager_->AddVirtualStream(
screen_resolution,
base::BindOnce(&GnomeDesktopResizer::OnAddStreamResult,
weak_ptr_factory_.GetWeakPtr(),
PreferredMonitorConfig{
.expected_resolution = physical_resolution,
.position = position,
.scale = scale,
}));
} else {
SetResolutionAndPosition(screen_resolution, position, track.screen_id());
}
AddMonitorForLayoutCalculation(display_config_for_layout_calculation,
position, screen_resolution);
}
preferred_layout_ = display_config_for_layout_calculation.GetLayoutInfo();
MaybeDelayClearPreferredConfig();
}
base::WeakPtr<GnomeDesktopResizer> GnomeDesktopResizer::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void GnomeDesktopResizer::SetResolutionAndPosition(
const ScreenResolution& resolution,
std::optional<webrtc::DesktopVector> position,
webrtc::ScreenId screen_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!streams_being_removed_.empty()) {
do_after_stream_removal_.AddUnsafe(base::BindOnce(
&GnomeDesktopResizer::SetResolutionAndPosition,
weak_ptr_factory_.GetWeakPtr(), resolution, position, screen_id));
return;
}
if (!stream_manager_) {
return;
}
base::WeakPtr<CaptureStream> stream = stream_manager_->GetStream(screen_id);
if (!stream) {
LOG(ERROR) << "Cannot find pipewire stream for screen ID: " << screen_id;
return;
}
bool resolution_changed =
!resolution.dimensions().equals(stream->resolution());
if (resolution_changed) {
stream->SetResolution(resolution.dimensions());
}
DCHECK_EQ(resolution.dpi().x(), resolution.dpi().y());
double preferred_scale =
static_cast<double>(resolution.dpi().x()) / kDefaultDpi;
bool has_preferred_config = preferred_monitors_config_.find(screen_id) !=
preferred_monitors_config_.end();
PreferredMonitorConfig& preferred_config =
preferred_monitors_config_[screen_id];
preferred_config.expected_resolution = resolution.dimensions(),
preferred_config.scale = preferred_scale;
if (position.has_value()) {
preferred_config.position = *position;
} else if (!has_preferred_config) {
auto monitor_it = current_display_config_.FindMonitor(screen_id);
if (monitor_it == current_display_config_.monitors.end()) {
LOG(ERROR) << "Cannot find monitor with screen ID: " << screen_id;
} else {
preferred_config.position = {monitor_it->second.x, monitor_it->second.y};
}
}
MaybeDelayClearPreferredConfig();
if (!resolution_changed) {
ScheduleApplyPreferredMonitorsConfig();
}
}
void GnomeDesktopResizer::OnAddStreamResult(
const PreferredMonitorConfig& monitor_config,
CaptureStreamManager::AddStreamResult result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!result.has_value()) {
LOG(ERROR) << "Failed to add stream: " << result.error();
return;
}
preferred_monitors_config_[result.value()->screen_id()] = monitor_config;
MaybeDelayClearPreferredConfig();
ScheduleApplyPreferredMonitorsConfig();
}
void GnomeDesktopResizer::OnGnomeDisplayConfigReceived(
const GnomeDisplayConfig& config) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
current_display_config_ = config;
bool found_stream_pending_removal = false;
for (const auto& screen_id : streams_being_removed_) {
if (current_display_config_.FindMonitor(screen_id) !=
current_display_config_.monitors.end()) {
found_stream_pending_removal = true;
break;
}
}
current_display_config_.RemoveInvalidMonitors();
current_display_config_.SwitchLayoutMode(kPreferredLayoutMode);
if (!streams_being_removed_.empty() && !found_stream_pending_removal) {
streams_being_removed_.clear();
do_after_stream_removal_.Notify();
}
ScheduleApplyPreferredMonitorsConfig();
}
void GnomeDesktopResizer::ScheduleApplyPreferredMonitorsConfig() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (apply_monitors_config_scheduled_) {
return;
}
apply_monitors_config_scheduled_ = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&GnomeDesktopResizer::DoApplyPreferredMonitorsConfig,
weak_ptr_factory_.GetWeakPtr()));
}
void GnomeDesktopResizer::DoApplyPreferredMonitorsConfig() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
apply_monitors_config_scheduled_ = false;
if (on_trying_to_apply_preferred_monitors_config_for_testing_) {
std::move(on_trying_to_apply_preferred_monitors_config_for_testing_).Run();
}
if (preferred_monitors_config_.empty()) {
return;
}
GnomeDisplayConfig new_config = current_display_config_;
bool ignore_fractional_scales =
ignore_fractional_scales_in_multimon_ && new_config.monitors.size() > 1;
bool config_changed = false;
for (auto [screen_id, preferred_config] : preferred_monitors_config_) {
auto monitor_it = new_config.FindMonitor(screen_id);
if (monitor_it == new_config.monitors.end()) {
return;
}
GnomeDisplayConfig::MonitorInfo& monitor = monitor_it->second;
const GnomeDisplayConfig::MonitorMode* mode = monitor.GetCurrentMode();
if (!mode) {
LOG(ERROR) << "Cannot find current mode for monitor with screen ID: "
<< screen_id;
return;
} else if (!preferred_config.expected_resolution.equals(
webrtc::DesktopSize{mode->width, mode->height})) {
return;
}
if (monitor.x != preferred_config.position.x() ||
monitor.y != preferred_config.position.y()) {
monitor.x = preferred_config.position.x();
monitor.y = preferred_config.position.y();
config_changed = true;
}
double best_monitor_scale = FindBestScale(
preferred_config.scale, monitor.GetCurrentMode()->supported_scales,
ignore_fractional_scales);
if (monitor.scale != best_monitor_scale) {
monitor.scale = best_monitor_scale;
config_changed = true;
}
if (monitor.is_primary) {
SetTextScalingFactor(preferred_config.scale / best_monitor_scale);
}
}
if (preferred_layout_.has_value()) {
preferred_layout_->layout_mode = kPreferredLayoutMode;
new_config.Relayout(*preferred_layout_);
for (const auto& [monitor_name, monitor] : new_config.monitors) {
if (!config_changed) {
auto current_monitor_it =
current_display_config_.monitors.find(monitor_name);
DCHECK(current_monitor_it != current_display_config_.monitors.end());
if (current_monitor_it->second.x != monitor.x ||
current_monitor_it->second.y != monitor.y) {
config_changed = true;
}
}
auto it = preferred_monitors_config_.find(
GnomeDisplayConfig::GetScreenId(monitor_name));
if (it != preferred_monitors_config_.end()) {
it->second.position.set(monitor.x, monitor.y);
}
}
}
if (config_changed) {
new_config.method = GnomeDisplayConfig::Method::kTemporary;
apply_monitors_config_.Run(new_config);
if (!clear_preferred_config_timer_.IsRunning()) {
clear_preferred_config_timer_.Start(
FROM_HERE, kClearPreferredConfigDelay, this,
&GnomeDesktopResizer::ClearPreferredConfig);
}
}
}
void GnomeDesktopResizer::ClearPreferredConfig() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
preferred_monitors_config_.clear();
preferred_layout_.reset();
streams_being_removed_.clear();
do_after_stream_removal_.Clear();
}
void GnomeDesktopResizer::MaybeDelayClearPreferredConfig() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (clear_preferred_config_timer_.IsRunning()) {
clear_preferred_config_timer_.Reset();
}
}
double GnomeDesktopResizer::GetTextScalingFactor() const {
if (!registry_) {
return 1.0;
}
return g_settings_get_double(registry_.get(), "text-scaling-factor");
}
void GnomeDesktopResizer::SetTextScalingFactor(double text_scaling_factor) {
if (!registry_) {
return;
}
if (InverseIfLessThanOne(text_scaling_factor) < kTextScaleThreshold) {
text_scaling_factor = 1.0;
}
if (!IsSameScale(GetTextScalingFactor(), text_scaling_factor) &&
!g_settings_set_double(registry_.get(), "text-scaling-factor",
text_scaling_factor)) {
LOG(ERROR) << "Failed to set text-scaling-factor";
}
}
}