1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2024-10-28 08:49:35 +01:00

screensharing: add Wayland support

Change-Id: Ida5516630c6f95b16aa45f31ee8111a924273b3f
This commit is contained in:
François-Simon Fauteux-Chapleau 2024-02-16 09:55:34 -05:00 committed by Sébastien Blin
parent a673ff9890
commit 200978a044
20 changed files with 840 additions and 20 deletions

View file

@ -455,10 +455,12 @@ elseif (NOT APPLE)
${APP_SRC_DIR}/xrectsel.c
${APP_SRC_DIR}/connectivitymonitor.cpp
${APP_SRC_DIR}/dbuserrorhandler.cpp
${APP_SRC_DIR}/appversionmanager.cpp)
${APP_SRC_DIR}/appversionmanager.cpp
${APP_SRC_DIR}/screencastportal.cpp)
list(APPEND COMMON_HEADERS
${APP_SRC_DIR}/xrectsel.h
${APP_SRC_DIR}/dbuserrorhandler.h)
${APP_SRC_DIR}/dbuserrorhandler.h
${APP_SRC_DIR}/screencastportal.h)
list(APPEND QT_MODULES DBus)
find_package(PkgConfig REQUIRED)
@ -473,6 +475,11 @@ elseif (NOT APPLE)
add_definitions(${GIO_CFLAGS})
endif()
pkg_check_modules(GIOUNIX REQUIRED gio-unix-2.0)
if(GIOUNIX_FOUND)
add_definitions(${GIOUNIX_CFLAGS})
endif()
pkg_check_modules(LIBNM libnm)
if(LIBNM_FOUND)
add_definitions(-DUSE_LIBNM)
@ -584,6 +591,7 @@ include_directories(
if(ENABLE_LIBWRAP)
list(APPEND COMMON_HEADERS
${LIBCLIENT_SRC_DIR}/qtwrapper/instancemanager_wrap.h)
add_definitions(-DENABLE_LIBWRAP=true)
endif()
# SFPM

2
daemon

@ -1 +1 @@
Subproject commit 54f149fc1858cac7f560b9a6140e5412e7f68acb
Subproject commit c5c3afae9a333c3aab1161f9ffe4ce9ef3dd24bf

View file

@ -1,4 +1,4 @@
FROM ubuntu:20.04
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND noninteractive
ENV QT_QUICK_BACKEND software
@ -10,7 +10,7 @@ RUN apt-get update && \
RUN apt install gnupg dirmngr ca-certificates curl --no-install-recommends
RUN curl -s https://dl.jami.net/public-key.gpg | tee /usr/share/keyrings/jami-archive-keyring.gpg > /dev/null
RUN sh -c "echo 'deb [signed-by=/usr/share/keyrings/jami-archive-keyring.gpg] https://dl.jami.net/internal/ubuntu_20.04/ jami main' > /etc/apt/sources.list.d/jami.list"
RUN sh -c "echo 'deb [signed-by=/usr/share/keyrings/jami-archive-keyring.gpg] https://dl.jami.net/internal/ubuntu_22.04/ jami main' > /etc/apt/sources.list.d/jami.list"
RUN apt-get update && apt-get install libqt-jami -y
RUN apt-get install -y -o Acquire::Retries=10 \
@ -51,6 +51,7 @@ RUN apt-get install -y -o Acquire::Retries=10 \
libswscale-dev \
libavdevice-dev \
libopus-dev \
libpipewire-0.3-dev \
libudev-dev \
libgsm1-dev \
libjsoncpp-dev \

View file

@ -100,6 +100,7 @@ RUN dnf install -y \
cmake \
fmt-devel \
python3-html5lib \
cups-devel
cups-devel \
pipewire-devel
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
CMD ["/opt/build-package-rpm.sh"]

View file

@ -28,4 +28,10 @@ ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
RUN /opt/install-cmake.sh
ADD extras/packaging/gnu-linux/scripts/build-package-debian.sh /opt/build-package-debian.sh
# Setting this variable so that FFmpeg gets built without pipewiregrab
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
# We rely on PipeWire for screen sharing on Wayland, but the version available on Debian 11 is too old.
ENV DISABLE_PIPEWIRE=true
CMD ["/opt/build-package-debian.sh"]

View file

@ -98,6 +98,7 @@ RUN dnf install -y \
clang \
cmake \
fmt-devel \
pipewire-devel \
cups-devel #Chromium for Qt
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh

View file

@ -98,7 +98,8 @@ RUN dnf install -y \
cmake \
fmt-devel \
python3-html5lib \
cups-devel
cups-devel \
pipewire-devel
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh

View file

@ -97,7 +97,8 @@ RUN dnf install -y \
cmake \
fmt-devel \
python3.10 \
cups-devel
cups-devel \
pipewire-devel
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh

View file

@ -99,7 +99,8 @@ RUN zypper --non-interactive install -y \
gstreamer-plugins-bad-devel \
gstreamer-plugins-base-devel \
cmake \
wget
wget \
pipewire-devel
# openSUSE Leap 15.4 comes with Python 3.6 by default,
# but we need at least 3.7 to compile Qt 6.6.1
@ -112,4 +113,10 @@ ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-r
ENV CC=gcc
ENV CXX=g++
# Setting this variable so that FFmpeg gets built without pipewiregrab
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
# We rely on PipeWire for screen sharing on Wayland, but the version available on openSUSE Leap 15.4 is too old.
ENV DISABLE_PIPEWIRE=true
CMD ["/opt/build-package-rpm.sh"]

View file

@ -100,7 +100,8 @@ RUN zypper --non-interactive install -y \
gstreamer-plugins-bad-devel \
gstreamer-plugins-base-devel \
cmake \
wget
wget \
pipewire-devel
# openSUSE Leap 15.5 comes with Python 3.6 by default,
# but we need at least 3.7 to compile Qt 6.6.1

View file

@ -33,4 +33,10 @@ ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
RUN /opt/install-cmake.sh
ADD extras/packaging/gnu-linux/scripts/build-package-debian.sh /opt/build-package-debian.sh
# Setting this variable so that FFmpeg gets built without pipewiregrab
# (see daemon/contrib/bootstrap and daemon/contrib/src/ffmpeg/rules.mak)
# We rely on PipeWire for screen sharing on Wayland, but the version available on Ubuntu 20.04 is too old.
ENV DISABLE_PIPEWIRE=true
CMD ["/opt/build-package-debian.sh"]

View file

@ -45,6 +45,8 @@ Build-Depends: debhelper (>= 9),
libvdpau-dev,
libssl-dev,
libargon2-dev | libargon2-0-dev,
# TODO: remove libpipewire-0.2-dev once we stop supporting Ubuntu 20.04
libpipewire-0.3-dev | libpipewire-0.2-dev,
# other
nasm,
yasm,

View file

@ -50,6 +50,7 @@ BuildRequires: libuuid-devel
BuildRequires: libva-devel
BuildRequires: libvdpau-devel
BuildRequires: pcre-devel
BuildRequires: pipewire-devel
BuildRequires: uuid-devel
BuildRequires: yaml-cpp-devel

View file

@ -1,5 +1,6 @@
<h4 align="left"><span style="font-weight:600"> Created by</span></h4>
<p>Adrien Béraud<br>
<p>Abhishek Ojha<br>
Adrien Béraud<br>
Albert Babí<br>
Alexandre Lision<br>
Alexandr Sergheev<br>
@ -25,6 +26,7 @@ Emma Falkiewitz<br>
Emmanuel Lepage-Vallée<br>
Fadi Shehadeh<br>
Franck Laurent<br>
François-Simon Fauteux-Chapleau<br>
Frédéric Guimont<br>
Guillaume Heller<br>
Guillaume Roguez<br>

View file

@ -25,7 +25,11 @@
#include "api/devicemodel.h"
#ifdef Q_OS_LINUX
#include "screencastportal.h"
#include "xrectsel.h"
#ifndef ENABLE_LIBWRAP
#include <sys/prctl.h>
#endif
#endif
#include <QtConcurrent/QtConcurrent>
@ -58,6 +62,12 @@ AvAdapter::AvAdapter(LRCInstance* instance, QObject* parent)
&lrc::api::AVModel::onRendererFpsChange,
this,
&AvAdapter::updateRenderersFPSInfo);
#ifdef Q_OS_LINUX
connect(&lrcInstance_->behaviorController(),
&BehaviorController::callStatusChanged,
this,
&AvAdapter::onCallStatusChanged);
#endif
}
// The top left corner of primary screen is (0, 0).
@ -119,6 +129,93 @@ AvAdapter::shareEntireScreen(int screenNumber)
->addMedia(callId, resource, lrc::api::CallModel::MediaRequestType::SCREENSHARING);
}
#ifdef Q_OS_LINUX
static std::map<QString, std::unique_ptr<ScreenCastPortal>> callPortal;
void
AvAdapter::onCallStatusChanged(const QString& accountId, const QString& callId)
{
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
auto& callModel = accInfo.callModel;
const auto call = callModel->getCall(callId);
if (call.status == lrc::api::call::Status::ENDED) {
closePortal(callId);
}
}
void
AvAdapter::closePortal(const QString& callId)
{
if (callPortal.count(callId)) {
lrcInstance_->avModel().stopPreview(callPortal[callId]->videoInputId);
callPortal.erase(callId);
}
}
void
AvAdapter::shareWayland(bool entireScreen)
{
QString callId = lrcInstance_->getCurrentCallId();
closePortal(callId);
PortalCaptureType captureType = entireScreen ? PortalCaptureType::SCREEN
: PortalCaptureType::WINDOW;
auto portal = std::make_unique<ScreenCastPortal>(captureType);
int err = portal->getPipewireFd();
if (err == EACCES) {
qInfo() << "Can't share screen: permission denied";
return;
} else if (err != 0) {
qWarning() << "Failed to get PipeWire fd. Error code:" << err;
return;
}
QString resource = QString("%1%2pipewire pid:%3 fd:%4 node:%5")
.arg(libjami::Media::VideoProtocolPrefix::DISPLAY)
.arg(libjami::Media::VideoProtocolPrefix::SEPARATOR)
.arg(getpid())
.arg(portal->pipewireFd)
.arg(portal->pipewireNode);
#ifndef ENABLE_LIBWRAP
// If the daemon is running as a separate process, then it can't directly use the
// PipeWire file descriptor opened by the client, so it will attempt to duplicate
// it using the pidfd_getfd system call. This requires the daemon process to have
// ptrace permission on the client process. On some systems, this will be true by
// default (as long as the client and daemon processes have the same uid), but it
// may not be if the Yama Linux Security Module is used. The call to prctl below
// will grant permission if the Yama LSM is enabled and set to mode 1.
//
// References:
// https://man7.org/linux/man-pages/man2/pidfd_getfd.2.html
// https://man7.org/linux/man-pages/man2/prctl.2.html
// https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/LSM/Yama.rst
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
#endif
// We open the video input here (instead of letting the daemon do it) to ensure
// that the daemon doesn't try to restart it while we still need it, since this
// would require getting a new file descriptor for PipeWire.
portal->videoInputId = lrcInstance_->avModel().startPreview(resource);
callPortal[callId] = std::move(portal);
muteCamera_ = !isCapturing();
lrcInstance_->getCurrentCallModel()
->addMedia(callId, resource, lrc::api::CallModel::MediaRequestType::SCREENSHARING);
}
void
AvAdapter::shareEntireScreenWayland()
{
shareWayland(true);
}
void
AvAdapter::shareWindowWayland()
{
shareWayland(false);
}
#endif // Q_OS_LINUX
void
AvAdapter::shareAllScreens()
{
@ -204,10 +301,14 @@ AvAdapter::shareFile(const QString& filePath)
&lrc::api::AVModel::fileOpened,
this,
[this, callId, filePath, resource](bool hasAudio, bool hasVideo) {
lrcInstance_->avModel().setAutoRestart(resource, true);
lrcInstance_->getCurrentCallModel()
->addMedia(callId, filePath, lrc::api::CallModel::MediaRequestType::FILESHARING, false, hasAudio);
lrcInstance_->avModel().pausePlayer(resource, false);
lrcInstance_->avModel().setAutoRestart(resource, true);
lrcInstance_->getCurrentCallModel()
->addMedia(callId,
filePath,
lrc::api::CallModel::MediaRequestType::FILESHARING,
false,
hasAudio);
lrcInstance_->avModel().pausePlayer(resource, false);
});
lrcInstance_->avModel().createMediaPlayer(resource);
@ -307,6 +408,9 @@ void
AvAdapter::stopSharing(const QString& source)
{
auto callId = lrcInstance_->getCurrentCallId();
#ifdef Q_OS_LINUX
closePortal(callId);
#endif
if (!source.isEmpty() && !callId.isEmpty()) {
if (source.startsWith(libjami::Media::VideoProtocolPrefix::DISPLAY)) {
qDebug() << "Stopping display: " << source;

View file

@ -69,9 +69,18 @@ protected:
*/
Q_INVOKABLE bool hasCamera() const;
// Share the screen specificed by screen number.
// Share the screen specificed by screen number (all platforms except Wayland).
Q_INVOKABLE void shareEntireScreen(int screenNumber);
#ifdef Q_OS_LINUX
// Share a screen on Wayland.
// Sharing a screen on Wayland requires getting permission from the user. The logic for
// this is handled by the ScreenCastPortal class using xdg-desktop-portal.
// The choice of screen is also handled by xdg-desktop-portal, which is why we don't need
// an argument for it (whereas we do on other platforms, cf. shareEntireScreen above).
Q_INVOKABLE void shareEntireScreenWayland();
#endif
// Share the all screens connected.
Q_INVOKABLE void shareAllScreens();
@ -87,9 +96,18 @@ protected:
// Select screen area to display (from all screens).
Q_INVOKABLE void shareScreenArea(unsigned x, unsigned y, unsigned width, unsigned height);
// Select window to display.
// Select window to display (all platforms except Wayland).
Q_INVOKABLE void shareWindow(const QString& windowProcessId, const QString& windowId);
#ifdef Q_OS_LINUX
// Share a window on Wayland.
// Sharing a window on Wayland requires getting permission from the user. The logic for
// this is handled by the ScreenCastPortal class using xdg-desktop-portal.
// The choice of window is also handled by xdg-desktop-portal, which is why we don't need
// arguments for it (whereas we do on other platforms, cf. shareWindow above).
Q_INVOKABLE void shareWindowWayland();
#endif
// Returns the screensharing resource
Q_INVOKABLE QString getSharingResource(int screenId = -2,
const QString& windowProcessId = "",
@ -121,11 +139,25 @@ private Q_SLOTS:
void onAudioDeviceEvent();
void onRendererStarted(const QString& id, const QSize& size);
void onRendererStopped(const QString& id);
#ifdef Q_OS_LINUX
// This function needs to be called whenever a screen/window share stops on Wayland.
// Failure to do so can cause subsequent sharing attempts to fail.
void closePortal(const QString& callId);
// On Wayland, we need to be informed of call status changes so that we can call
// closePortal if a call ends while a screen/window share was in progress.
void onCallStatusChanged(const QString& accountId, const QString& callId);
#endif
private:
// Get screens arrangement rect relative to primary screen.
const QRect getAllScreensBoundingRect();
#ifdef Q_OS_LINUX
// Used internally by shareEntireScreenWayland and shareWindowWayland
void shareWayland(bool entireScreen);
#endif
// Get the screen number
int getScreenNumber(int screenId = 0) const;

View file

@ -112,6 +112,7 @@ Control {
},
Action {
id: shareMenuAction
enabled: !CurrentCall.isSharing
text: JamiStrings.selectShareMethod
property int popupMode: CallActionBar.ActionPopupMode.ListElement
property var listModel: ListModel {
@ -123,7 +124,7 @@ Control {
"Name": JamiStrings.shareScreen,
"IconSource": JamiResources.laptop_black_24dp_svg
});
if (Qt.platform.os.toString() !== "osx" && !UtilsAdapter.isWayland()) {
if (Qt.platform.os.toString() !== "osx") {
shareModel.append({
"Name": JamiStrings.shareWindow,
"IconSource": JamiResources.window_black_24dp_svg
@ -293,7 +294,24 @@ Control {
},
Action {
id: muteVideoAction
onTriggered: CallAdapter.muteCameraToggle()
onTriggered: {
if (CurrentCall.isSharing && UtilsAdapter.isWayland()) {
// Unmuting the camera while a screen share is ongoing causes the daemon
// to stop sharing. However, on Wayland, every share has an associated
// ScreenCastPortal object which is managed by the client and needs to
// be destroyed when the share ends. This is why we explicitly call the
// stopSharing function below.
//
// The muteCamera variable is set whenever a share starts and is normally used
// by the stopSharing function to restore the camera to its previous state
// when a share ends. Here we know that the user wants to unmute the camera,
// so we have to explicitly set muteCamera to false.
AvAdapter.muteCamera = false;
AvAdapter.stopSharing(CurrentCall.sharingSource);
} else {
CallAdapter.muteCameraToggle();
}
}
checkable: true
icon.source: checked ? JamiResources.videocam_off_24dp_svg : JamiResources.videocam_24dp_svg
icon.color: checked ? "red" : "white"

View file

@ -114,7 +114,9 @@ Item {
}
function openShareScreen() {
if (Qt.application.screens.length === 1) {
if (UtilsAdapter.isWayland()) {
AvAdapter.shareEntireScreenWayland();
} else if (Qt.application.screens.length === 1) {
AvAdapter.shareEntireScreen(0);
} else {
SelectScreenWindowCreation.presentSelectScreenWindow(appWindow, false);
@ -122,6 +124,10 @@ Item {
}
function openShareWindow() {
if (UtilsAdapter.isWayland()) {
AvAdapter.shareWindowWayland();
return;
}
AvAdapter.getListWindows();
if (AvAdapter.windowsNames.length >= 1) {
SelectScreenWindowCreation.presentSelectScreenWindow(appWindow, true);

View file

@ -0,0 +1,520 @@
/*!
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "screencastportal.h"
#include <QDebug>
#include <unistd.h>
#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/%s"
/*
* PipeWire supported cursor modes
*/
enum PortalCursorMode {
PORTAL_CURSOR_MODE_HIDDEN = 1 << 0,
PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1,
PORTAL_CURSOR_MODE_METADATA = 1 << 2,
};
/*
* Helper function to allow getPipewireFd to stop and return an error
* code if a DBus operation/callback fails.
*/
void
ScreenCastPortal::abort(int error, const char* message)
{
portal_error = error;
qWarning() << "Aborting:" << message;
if (glib_main_loop && g_main_loop_is_running(glib_main_loop)) {
g_main_loop_quit(glib_main_loop);
}
}
/*
* Callback to free a DbusCallData object's memory and unsubscribe from the
* associated dbus signal.
*/
void
ScreenCastPortal::dbusCallDataFree(DbusCallData* ptr_dbus_call_data)
{
if (!ptr_dbus_call_data)
return;
if (ptr_dbus_call_data->signal_id)
g_dbus_connection_signal_unsubscribe(ptr_dbus_call_data->portal->connection,
ptr_dbus_call_data->signal_id);
g_clear_pointer(&ptr_dbus_call_data->request_path, g_free);
}
DbusCallData*
ScreenCastPortal::subscribeToSignal(const char* path, GDBusSignalCallback callback)
{
DbusCallData* ptr_dbus_call_data = new DbusCallData;
ptr_dbus_call_data->portal = this;
ptr_dbus_call_data->request_path = g_strdup(path);
ptr_dbus_call_data->signal_id
= g_dbus_connection_signal_subscribe(connection,
"org.freedesktop.portal.Desktop" /*sender*/,
"org.freedesktop.portal.Request" /*interface_name*/,
"Response" /*member: dbus signal name*/,
ptr_dbus_call_data->request_path /*object_path*/,
NULL,
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
callback,
ptr_dbus_call_data,
NULL);
return ptr_dbus_call_data;
}
void
ScreenCastPortal::openPipewireRemote()
{
GUnixFDList* fd_list = NULL;
GVariant* result = NULL;
GError* error = NULL;
int fd_index;
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
result = g_dbus_proxy_call_with_unix_fd_list_sync(proxy,
"OpenPipeWireRemote",
g_variant_new("(oa{sv})",
session_handle,
&builder),
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&fd_list,
NULL,
&error);
if (error)
goto fail;
g_variant_get(result, "(h)", &fd_index);
g_variant_unref(result);
pipewireFd = g_unix_fd_list_get(fd_list, fd_index, &error);
g_object_unref(fd_list);
if (error)
goto fail;
g_main_loop_quit(glib_main_loop);
return;
fail:
qWarning() << "Error retrieving PipeWire fd:" << error->message;
g_error_free(error);
abort(EIO, "Failed to open PipeWire remote");
}
void
ScreenCastPortal::onStartResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
GVariant* stream_properties = NULL;
GVariant* streams = NULL;
GVariant* result = NULL;
GVariantIter iter;
uint32_t response;
DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &result);
if (response) {
g_variant_unref(result);
portal->abort(EACCES, "Failed to start screencast, denied or cancelled by user");
return;
}
streams = g_variant_lookup_value(result, "streams", G_VARIANT_TYPE_ARRAY);
g_variant_iter_init(&iter, streams);
g_variant_iter_loop(&iter, "(u@a{sv})", &portal->pipewireNode, &stream_properties);
qInfo() << "Monitor selected, setting up screencast\n";
g_variant_unref(result);
g_variant_unref(streams);
g_variant_unref(stream_properties);
portal->openPipewireRemote();
}
int
ScreenCastPortal::callDBusMethod(const gchar* method_name, GVariant* parameters)
{
GVariant* result;
GError* error = NULL;
result = g_dbus_proxy_call_sync(proxy,
method_name,
parameters,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
&error);
if (error) {
qWarning() << "Call to DBus method" << method_name << "failed:" << error->message;
g_error_free(error);
return EIO;
}
g_variant_unref(result);
return 0;
}
void
ScreenCastPortal::start()
{
int ret;
const char* request_token;
g_autofree char* request_path;
GVariantBuilder builder;
GVariant* parameters;
struct DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabStart";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
qInfo() << "Asking for monitor...";
ptr_dbus_call_data = subscribeToSignal(request_path, onStartResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
parameters = g_variant_new("(osa{sv})", session_handle, "", &builder);
ret = callDBusMethod("Start", parameters);
if (ret != 0)
abort(ret, "Failed to start screen cast session");
}
void
ScreenCastPortal::onSelectSourcesResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
GVariant* ret = NULL;
uint32_t response;
struct DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &ret);
g_variant_unref(ret);
if (response) {
portal->abort(EACCES, "Failed to select screencast sources, denied or cancelled by user");
return;
}
portal->start();
}
void
ScreenCastPortal::selectSources()
{
int ret;
const char* request_token;
g_autofree char* request_path;
GVariantBuilder builder;
GVariant* parameters;
struct DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabSelectSources";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
ptr_dbus_call_data = subscribeToSignal(request_path, onSelectSourcesResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(capture_type));
g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(FALSE));
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
if ((available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) && draw_mouse)
g_variant_builder_add(&builder,
"{sv}",
"cursor_mode",
g_variant_new_uint32(PORTAL_CURSOR_MODE_EMBEDDED));
else
g_variant_builder_add(&builder,
"{sv}",
"cursor_mode",
g_variant_new_uint32(PORTAL_CURSOR_MODE_HIDDEN));
parameters = g_variant_new("(oa{sv})", session_handle, &builder);
ret = callDBusMethod("SelectSources", parameters);
if (ret != 0)
abort(ret, "Failed to select sources for screen cast session");
}
void
ScreenCastPortal::onCreateSessionResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data)
{
uint32_t response;
GVariant* result = NULL;
DbusCallData* ptr_dbus_call_data = (DbusCallData*) user_data;
ScreenCastPortal* portal = ptr_dbus_call_data->portal;
g_clear_pointer(&ptr_dbus_call_data, dbusCallDataFree);
g_variant_get(parameters, "(u@a{sv})", &response, &result);
if (response != 0) {
g_variant_unref(result);
portal->abort(EACCES, "Failed to create screencast session, denied or cancelled by user");
return;
}
qDebug() << "Screencast session created";
g_variant_lookup(result, "session_handle", "s", &portal->session_handle);
g_variant_unref(result);
portal->selectSources();
}
void
ScreenCastPortal::createSession()
{
int ret;
GVariantBuilder builder;
GVariant* parameters;
const char* request_token;
g_autofree char* request_path;
DbusCallData* ptr_dbus_call_data;
request_token = "pipewiregrabCreateSession";
request_path = g_strdup_printf(REQUEST_PATH, sender_name, request_token);
ptr_dbus_call_data = subscribeToSignal(request_path, onCreateSessionResponseReceivedCallback);
if (!ptr_dbus_call_data) {
abort(ENOMEM, "Failed to allocate DBus call data");
return;
}
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(request_token));
g_variant_builder_add(&builder,
"{sv}",
"session_handle_token",
g_variant_new_string("pipewiregrab"));
parameters = g_variant_new("(a{sv})", &builder);
ret = callDBusMethod("CreateSession", parameters);
if (ret != 0)
abort(ret, "Failed to create screen cast session");
}
/*
* Helper function: get available cursor modes and update the
* PipewireGrabContext accordingly
*/
void
ScreenCastPortal::updateAvailableCursorModes()
{
GVariant* cached_cursor_modes = NULL;
cached_cursor_modes = g_dbus_proxy_get_cached_property(proxy, "AvailableCursorModes");
available_cursor_modes = cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) : 0;
// Only use embedded or hidden mode for now
available_cursor_modes &= PORTAL_CURSOR_MODE_EMBEDDED | PORTAL_CURSOR_MODE_HIDDEN;
g_variant_unref(cached_cursor_modes);
}
int
ScreenCastPortal::createDBusProxy()
{
GError* error = NULL;
proxy = g_dbus_proxy_new_sync(connection,
G_DBUS_PROXY_FLAGS_NONE,
NULL,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.ScreenCast",
NULL,
&error);
if (error) {
qWarning() << "Error creating proxy:" << error->message;
g_error_free(error);
return EPERM;
}
return 0;
}
/*
* Create DBus connection and related objects
*/
int
ScreenCastPortal::createDBusConnection()
{
char* aux;
GError* error = NULL;
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (error) {
qWarning() << "Error getting session bus:" << error->message;
g_error_free(error);
return EPERM;
}
sender_name = g_strdup(g_dbus_connection_get_unique_name(connection) + 1);
while ((aux = g_strstr_len(sender_name, -1, ".")) != NULL)
*aux = '_';
return 0;
}
/*
* Use XDG Desktop Portal's ScreenCast interface to open a file descriptor that
* can be used by PipeWire to access the screen cast streams.
* (https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html)
*/
int
ScreenCastPortal::getPipewireFd()
{
int ret = 0;
GMainContext* glib_main_context;
// Create a new GLib context and set it as the default for the current thread.
// This ensures that the callbacks from DBus operations started in this thread are
// handled by the GLib main loop defined below, even if pipewiregrab_init was
// called by a program which also uses GLib and already had its own main loop running.
glib_main_context = g_main_context_new();
g_main_context_push_thread_default(glib_main_context);
glib_main_loop = g_main_loop_new(glib_main_context, FALSE);
if (!glib_main_loop) {
qWarning() << "g_main_loop_new failed!";
ret = ENOMEM;
}
ret = createDBusConnection();
if (ret != 0)
goto exit_glib_loop;
ret = createDBusProxy();
if (ret != 0)
goto exit_glib_loop;
updateAvailableCursorModes();
createSession();
if (portal_error) {
ret = portal_error;
goto exit_glib_loop;
}
g_main_loop_run(glib_main_loop);
// The main loop will run until it's stopped by openPipewireRemote (if
// all DBus method calls were successfully), abort (in case of error) or
// on_cancelled_callback (if a DBus request is cancelled).
// In the latter two cases, pw_ctx->portal_error gets set to a nonzero value.
if (portal_error)
ret = portal_error;
exit_glib_loop:
g_main_loop_unref(glib_main_loop);
glib_main_loop = NULL;
g_main_context_pop_thread_default(glib_main_context);
g_main_context_unref(glib_main_context);
return ret;
}
ScreenCastPortal::ScreenCastPortal(PortalCaptureType captureType)
: draw_mouse(true)
, pipewireFd(0)
{
switch (captureType) {
case PortalCaptureType::SCREEN:
capture_type = 1;
break;
case PortalCaptureType::WINDOW:
capture_type = 2;
break;
}
}
ScreenCastPortal::~ScreenCastPortal()
{
if (session_handle) {
g_dbus_connection_call(connection,
"org.freedesktop.portal.Desktop",
session_handle,
"org.freedesktop.portal.Session",
"Close",
NULL,
NULL,
G_DBUS_CALL_FLAGS_NONE,
-1,
NULL,
NULL,
NULL);
g_clear_pointer(&session_handle, g_free);
}
g_clear_object(&connection);
g_clear_object(&proxy);
g_clear_pointer(&sender_name, g_free);
#ifndef ENABLE_LIBWRAP
// If the daemon is running as a separate process, then it can't directly use the
// PipeWire file descriptor opened by the client, so it will have to duplicate it.
// The duplicated file descriptor will be closed by the daemon, but the original
// file descriptor needs to be closed by the client.
if (close(pipewireFd) != 0) {
int err = errno;
qWarning() << "Error while attempting to close PipeWire file descriptor: errno =" << err;
} else {
qInfo() << "Successfully closed PipeWire file descriptor";
}
#endif
}

102
src/app/screencastportal.h Normal file
View file

@ -0,0 +1,102 @@
/*!
* Copyright (C) 2024 Savoir-faire Linux Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include <cstdint>
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
enum class PortalCaptureType {
SCREEN = 1,
WINDOW = 2,
};
struct DbusCallData;
class ScreenCastPortal
{
public:
ScreenCastPortal(PortalCaptureType captureType);
~ScreenCastPortal();
int getPipewireFd();
int pipewireFd;
uint32_t pipewireNode = 0;
QString videoInputId;
private:
void createSession();
void selectSources();
void start();
void openPipewireRemote();
void abort(int error, const char* message);
static void onCreateSessionResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data);
static void onSelectSourcesResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data);
static void onStartResponseReceivedCallback(GDBusConnection* connection,
const char* sender_name,
const char* object_path,
const char* interface_name,
const char* signal_name,
GVariant* parameters,
gpointer user_data);
int callDBusMethod(const gchar* method_name, GVariant* parameters);
int createDBusProxy();
int createDBusConnection();
void updateAvailableCursorModes();
DbusCallData* subscribeToSignal(const char* path, GDBusSignalCallback callback);
static void dbusCallDataFree(DbusCallData* ptr_dbus_call_data);
GDBusConnection* connection = nullptr;
GDBusProxy* proxy = nullptr;
char* sender_name = nullptr;
char* session_handle = nullptr;
uint32_t available_cursor_modes = 0;
GMainLoop* glib_main_loop = nullptr;
struct pw_thread_loop* thread_loop = nullptr;
struct pw_context* context = nullptr;
guint32 capture_type;
bool draw_mouse;
int portal_error = 0;
};
struct DbusCallData
{
ScreenCastPortal* portal;
char* request_path;
guint signal_id;
};