blob: 9feec1d841cfd76ca18f75dbe2a2288a4192439b [file] [log] [blame]
/*
* Copyright 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/desktop_capture/linux/wayland/screencast_portal.h"
#include <gio/gunixfdlist.h>
#include <glib-object.h>
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
namespace webrtc {
const char kDesktopBusName[] = "org.freedesktop.portal.Desktop";
const char kDesktopObjectPath[] = "/org/freedesktop/portal/desktop";
const char kDesktopRequestObjectPath[] =
"/org/freedesktop/portal/desktop/request";
const char kSessionInterfaceName[] = "org.freedesktop.portal.Session";
const char kRequestInterfaceName[] = "org.freedesktop.portal.Request";
const char kScreenCastInterfaceName[] = "org.freedesktop.portal.ScreenCast";
template <class T>
class Scoped {
public:
Scoped() {}
explicit Scoped(T* val) { ptr_ = val; }
~Scoped() { RTC_DCHECK_NOTREACHED(); }
T* operator->() { return ptr_; }
bool operator!() { return ptr_ == nullptr; }
T* get() { return ptr_; }
T** receive() {
RTC_CHECK(!ptr_);
return &ptr_;
}
Scoped& operator=(T* val) {
ptr_ = val;
return *this;
}
protected:
T* ptr_ = nullptr;
};
template <>
Scoped<GError>::~Scoped() {
if (ptr_) {
g_error_free(ptr_);
}
}
template <>
Scoped<char>::~Scoped() {
if (ptr_) {
g_free(ptr_);
}
}
template <>
Scoped<GVariant>::~Scoped() {
if (ptr_) {
g_variant_unref(ptr_);
}
}
template <>
Scoped<GVariantIter>::~Scoped() {
if (ptr_) {
g_variant_iter_free(ptr_);
}
}
template <>
Scoped<GDBusMessage>::~Scoped() {
if (ptr_) {
g_object_unref(ptr_);
}
}
template <>
Scoped<GUnixFDList>::~Scoped() {
if (ptr_) {
g_object_unref(ptr_);
}
}
ScreenCastPortal::ScreenCastPortal(CaptureSourceType source_type,
PortalNotifier* notifier)
: notifier_(notifier), capture_source_type_(source_type) {}
ScreenCastPortal::~ScreenCastPortal() {
if (start_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(connection_, start_request_signal_id_);
}
if (sources_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(connection_,
sources_request_signal_id_);
}
if (session_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(connection_,
session_request_signal_id_);
}
if (!session_handle_.empty()) {
Scoped<GDBusMessage> message(
g_dbus_message_new_method_call(kDesktopBusName, session_handle_.c_str(),
kSessionInterfaceName, "Close"));
if (message.get()) {
Scoped<GError> error;
g_dbus_connection_send_message(connection_, message.get(),
G_DBUS_SEND_MESSAGE_FLAGS_NONE,
/*out_serial=*/nullptr, error.receive());
if (error.get()) {
RTC_LOG(LS_ERROR) << "Failed to close the session: " << error->message;
}
}
}
if (cancellable_) {
g_cancellable_cancel(cancellable_);
g_object_unref(cancellable_);
cancellable_ = nullptr;
}
if (proxy_) {
g_object_unref(proxy_);
proxy_ = nullptr;
}
if (pw_fd_ != -1) {
close(pw_fd_);
}
}
void ScreenCastPortal::Start() {
cancellable_ = g_cancellable_new();
g_dbus_proxy_new_for_bus(
G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, /*info=*/nullptr,
kDesktopBusName, kDesktopObjectPath, kScreenCastInterfaceName,
cancellable_, reinterpret_cast<GAsyncReadyCallback>(OnProxyRequested),
this);
}
void ScreenCastPortal::PortalFailed(RequestResponse result) {
notifier_->OnScreenCastRequestResult(result, pw_stream_node_id_, pw_fd_);
}
uint32_t ScreenCastPortal::SetupRequestResponseSignal(
const char* object_path,
GDBusSignalCallback callback) {
return g_dbus_connection_signal_subscribe(
connection_, kDesktopBusName, kRequestInterfaceName, "Response",
object_path, /*arg0=*/nullptr, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
callback, this, /*user_data_free_func=*/nullptr);
}
// static
void ScreenCastPortal::OnProxyRequested(GObject* /*object*/,
GAsyncResult* result,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
Scoped<GError> error;
GDBusProxy* proxy = g_dbus_proxy_new_finish(result, error.receive());
if (!proxy) {
if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
RTC_LOG(LS_ERROR) << "Failed to create a proxy for the screen cast portal: "
<< error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
that->proxy_ = proxy;
that->connection_ = g_dbus_proxy_get_connection(that->proxy_);
RTC_LOG(LS_INFO) << "Created proxy for the screen cast portal.";
that->SessionRequest();
}
// static
std::string ScreenCastPortal::PrepareSignalHandle(GDBusConnection* connection,
const char* token) {
Scoped<char> sender(
g_strdup(g_dbus_connection_get_unique_name(connection) + 1));
for (int i = 0; sender.get()[i]; ++i) {
if (sender.get()[i] == '.') {
sender.get()[i] = '_';
}
}
const char* handle = g_strconcat(kDesktopRequestObjectPath, "/", sender.get(),
"/", token, /*end of varargs*/ nullptr);
return handle;
}
void ScreenCastPortal::SessionRequest() {
GVariantBuilder builder;
Scoped<char> variant_string;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
variant_string =
g_strdup_printf("webrtc_session%d", g_random_int_range(0, G_MAXINT));
g_variant_builder_add(&builder, "{sv}", "session_handle_token",
g_variant_new_string(variant_string.get()));
variant_string = g_strdup_printf("webrtc%d", g_random_int_range(0, G_MAXINT));
g_variant_builder_add(&builder, "{sv}", "handle_token",
g_variant_new_string(variant_string.get()));
portal_handle_ = PrepareSignalHandle(connection_, variant_string.get());
session_request_signal_id_ = SetupRequestResponseSignal(
portal_handle_.c_str(), OnSessionRequestResponseSignal);
RTC_LOG(LS_INFO) << "Screen cast session requested.";
g_dbus_proxy_call(proxy_, "CreateSession", g_variant_new("(a{sv})", &builder),
G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable_,
reinterpret_cast<GAsyncReadyCallback>(OnSessionRequested),
this);
}
// static
void ScreenCastPortal::OnSessionRequested(GDBusProxy* proxy,
GAsyncResult* result,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
Scoped<GError> error;
Scoped<GVariant> variant(
g_dbus_proxy_call_finish(proxy, result, error.receive()));
if (!variant) {
if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
RTC_LOG(LS_ERROR) << "Failed to create a screen cast session: "
<< error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Initializing the screen cast session.";
Scoped<char> handle;
g_variant_get_child(variant.get(), 0, "o", &handle);
if (!handle) {
RTC_LOG(LS_ERROR) << "Failed to initialize the screen cast session.";
if (that->session_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(that->connection_,
that->session_request_signal_id_);
that->session_request_signal_id_ = 0;
}
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Subscribing to the screen cast session.";
}
// static
void ScreenCastPortal::OnSessionRequestResponseSignal(
GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
RTC_LOG(LS_INFO)
<< "Received response for the screen cast session subscription.";
uint32_t portal_response;
Scoped<GVariant> response_data;
g_variant_get(parameters, "(u@a{sv})", &portal_response,
response_data.receive());
Scoped<GVariant> session_handle(
g_variant_lookup_value(response_data.get(), "session_handle", nullptr));
that->session_handle_ = g_variant_dup_string(session_handle.get(), nullptr);
if (that->session_handle_.empty() || portal_response) {
RTC_LOG(LS_ERROR)
<< "Failed to request the screen cast session subscription.";
that->PortalFailed(RequestResponse::kError);
return;
}
that->session_closed_signal_id_ = g_dbus_connection_signal_subscribe(
that->connection_, kDesktopBusName, kSessionInterfaceName, "Closed",
that->session_handle_.c_str(), /*arg0=*/nullptr, G_DBUS_SIGNAL_FLAGS_NONE,
OnSessionClosedSignal, that, /*user_data_free_func=*/nullptr);
that->SourcesRequest();
}
// static
void ScreenCastPortal::OnSessionClosedSignal(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
RTC_LOG(LS_INFO) << "Received closed signal from session.";
that->notifier_->OnScreenCastSessionClosed();
// Unsubscribe from the signal and free the session handle to avoid calling
// Session::Close from the destructor since it's already closed
g_dbus_connection_signal_unsubscribe(that->connection_,
that->session_closed_signal_id_);
}
void ScreenCastPortal::SourcesRequest() {
GVariantBuilder builder;
Scoped<char> variant_string;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
// We want to record monitor content.
g_variant_builder_add(
&builder, "{sv}", "types",
g_variant_new_uint32(static_cast<uint32_t>(capture_source_type_)));
// We don't want to allow selection of multiple sources.
g_variant_builder_add(&builder, "{sv}", "multiple",
g_variant_new_boolean(false));
Scoped<GVariant> variant(
g_dbus_proxy_get_cached_property(proxy_, "AvailableCursorModes"));
if (variant.get()) {
uint32_t modes = 0;
g_variant_get(variant.get(), "u", &modes);
// Make request only if this mode is advertised by the portal
// implementation.
if (modes & static_cast<uint32_t>(cursor_mode_)) {
g_variant_builder_add(
&builder, "{sv}", "cursor_mode",
g_variant_new_uint32(static_cast<uint32_t>(cursor_mode_)));
}
}
variant_string = g_strdup_printf("webrtc%d", g_random_int_range(0, G_MAXINT));
g_variant_builder_add(&builder, "{sv}", "handle_token",
g_variant_new_string(variant_string.get()));
sources_handle_ = PrepareSignalHandle(connection_, variant_string.get());
sources_request_signal_id_ = SetupRequestResponseSignal(
sources_handle_.c_str(), OnSourcesRequestResponseSignal);
RTC_LOG(LS_INFO) << "Requesting sources from the screen cast session.";
g_dbus_proxy_call(
proxy_, "SelectSources",
g_variant_new("(oa{sv})", session_handle_.c_str(), &builder),
G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable_,
reinterpret_cast<GAsyncReadyCallback>(OnSourcesRequested), this);
}
// static
void ScreenCastPortal::OnSourcesRequested(GDBusProxy* proxy,
GAsyncResult* result,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
Scoped<GError> error;
Scoped<GVariant> variant(
g_dbus_proxy_call_finish(proxy, result, error.receive()));
if (!variant) {
if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
RTC_LOG(LS_ERROR) << "Failed to request the sources: " << error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Sources requested from the screen cast session.";
Scoped<char> handle;
g_variant_get_child(variant.get(), 0, "o", handle.receive());
if (!handle) {
RTC_LOG(LS_ERROR) << "Failed to initialize the screen cast session.";
if (that->sources_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(that->connection_,
that->sources_request_signal_id_);
that->sources_request_signal_id_ = 0;
}
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Subscribed to sources signal.";
}
// static
void ScreenCastPortal::OnSourcesRequestResponseSignal(
GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
RTC_LOG(LS_INFO) << "Received sources signal from session.";
uint32_t portal_response;
g_variant_get(parameters, "(u@a{sv})", &portal_response, nullptr);
if (portal_response) {
RTC_LOG(LS_ERROR)
<< "Failed to select sources for the screen cast session.";
that->PortalFailed(RequestResponse::kError);
return;
}
that->StartRequest();
}
void ScreenCastPortal::StartRequest() {
GVariantBuilder builder;
Scoped<char> variant_string;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
variant_string = g_strdup_printf("webrtc%d", g_random_int_range(0, G_MAXINT));
g_variant_builder_add(&builder, "{sv}", "handle_token",
g_variant_new_string(variant_string.get()));
start_handle_ = PrepareSignalHandle(connection_, variant_string.get());
start_request_signal_id_ = SetupRequestResponseSignal(
start_handle_.c_str(), OnStartRequestResponseSignal);
// "Identifier for the application window", this is Wayland, so not "x11:...".
const char parent_window[] = "";
RTC_LOG(LS_INFO) << "Starting the screen cast session.";
g_dbus_proxy_call(proxy_, "Start",
g_variant_new("(osa{sv})", session_handle_.c_str(),
parent_window, &builder),
G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, cancellable_,
reinterpret_cast<GAsyncReadyCallback>(OnStartRequested),
this);
}
// static
void ScreenCastPortal::OnStartRequested(GDBusProxy* proxy,
GAsyncResult* result,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
Scoped<GError> error;
Scoped<GVariant> variant(
g_dbus_proxy_call_finish(proxy, result, error.receive()));
if (!variant) {
if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
RTC_LOG(LS_ERROR) << "Failed to start the screen cast session: "
<< error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Initializing the start of the screen cast session.";
Scoped<char> handle;
g_variant_get_child(variant.get(), 0, "o", handle.receive());
if (!handle) {
RTC_LOG(LS_ERROR)
<< "Failed to initialize the start of the screen cast session.";
if (that->start_request_signal_id_) {
g_dbus_connection_signal_unsubscribe(that->connection_,
that->start_request_signal_id_);
that->start_request_signal_id_ = 0;
}
that->PortalFailed(RequestResponse::kError);
return;
}
RTC_LOG(LS_INFO) << "Subscribed to the start signal.";
}
// static
void ScreenCastPortal::OnStartRequestResponseSignal(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
RTC_LOG(LS_INFO) << "Start signal received.";
uint32_t portal_response;
Scoped<GVariant> response_data;
Scoped<GVariantIter> iter;
g_variant_get(parameters, "(u@a{sv})", &portal_response,
response_data.receive());
if (portal_response || !response_data) {
RTC_LOG(LS_ERROR) << "Failed to start the screen cast session.";
that->PortalFailed(static_cast<RequestResponse>(portal_response));
return;
}
// Array of PipeWire streams. See
// https://github.com/flatpak/xdg-desktop-portal/blob/master/data/org.freedesktop.portal.ScreenCast.xml
// documentation for <method name="Start">.
if (g_variant_lookup(response_data.get(), "streams", "a(ua{sv})",
iter.receive())) {
Scoped<GVariant> variant;
while (g_variant_iter_next(iter.get(), "@(ua{sv})", variant.receive())) {
uint32_t stream_id;
uint32_t type;
Scoped<GVariant> options;
g_variant_get(variant.get(), "(u@a{sv})", &stream_id, options.receive());
RTC_DCHECK(options.get());
if (g_variant_lookup(options.get(), "source_type", "u", &type)) {
that->capture_source_type_ =
static_cast<ScreenCastPortal::CaptureSourceType>(type);
}
that->pw_stream_node_id_ = stream_id;
break;
}
}
that->OpenPipeWireRemote();
}
void ScreenCastPortal::OpenPipeWireRemote() {
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
RTC_LOG(LS_INFO) << "Opening the PipeWire remote.";
g_dbus_proxy_call_with_unix_fd_list(
proxy_, "OpenPipeWireRemote",
g_variant_new("(oa{sv})", session_handle_.c_str(), &builder),
G_DBUS_CALL_FLAGS_NONE, /*timeout=*/-1, /*fd_list=*/nullptr, cancellable_,
reinterpret_cast<GAsyncReadyCallback>(OnOpenPipeWireRemoteRequested),
this);
}
// static
void ScreenCastPortal::OnOpenPipeWireRemoteRequested(GDBusProxy* proxy,
GAsyncResult* result,
gpointer user_data) {
ScreenCastPortal* that = static_cast<ScreenCastPortal*>(user_data);
RTC_DCHECK(that);
Scoped<GError> error;
Scoped<GUnixFDList> outlist;
Scoped<GVariant> variant(g_dbus_proxy_call_with_unix_fd_list_finish(
proxy, outlist.receive(), result, error.receive()));
if (!variant) {
if (g_error_matches(error.get(), G_IO_ERROR, G_IO_ERROR_CANCELLED))
return;
RTC_LOG(LS_ERROR) << "Failed to open the PipeWire remote: "
<< error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
int32_t index;
g_variant_get(variant.get(), "(h)", &index);
that->pw_fd_ = g_unix_fd_list_get(outlist.get(), index, error.receive());
if (that->pw_fd_ == -1) {
RTC_LOG(LS_ERROR) << "Failed to get file descriptor from the list: "
<< error->message;
that->PortalFailed(RequestResponse::kError);
return;
}
that->notifier_->OnScreenCastRequestResult(
ScreenCastPortal::RequestResponse::kSuccess, that->pw_stream_node_id_,
that->pw_fd_);
}
} // namespace webrtc