diff --git a/CMakeLists.txt b/CMakeLists.txt
index b93daeda..e802357f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/daemon b/daemon
index 54f149fc..c5c3afae 160000
--- a/daemon
+++ b/daemon
@@ -1 +1 @@
-Subproject commit 54f149fc1858cac7f560b9a6140e5412e7f68acb
+Subproject commit c5c3afae9a333c3aab1161f9ffe4ce9ef3dd24bf
diff --git a/extras/build/docker/Dockerfile.client-qt-gnulinux b/extras/build/docker/Dockerfile.client-qt-gnulinux
index 44a86858..b9dc066e 100644
--- a/extras/build/docker/Dockerfile.client-qt-gnulinux
+++ b/extras/build/docker/Dockerfile.client-qt-gnulinux
@@ -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 \
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_alma_9 b/extras/packaging/gnu-linux/docker/Dockerfile_alma_9
index b91bc118..a42dcba3 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_alma_9
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_alma_9
@@ -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"]
\ No newline at end of file
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_debian_11 b/extras/packaging/gnu-linux/docker/Dockerfile_debian_11
index 324ca305..574a4f44 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_debian_11
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_debian_11
@@ -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"]
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_37 b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_37
index eab4b154..790222e5 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_37
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_37
@@ -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
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_38 b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_38
index 0623bee8..33684f75 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_38
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_38
@@ -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
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_39 b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_39
index eb688c5e..fde510dd 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_fedora_39
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_fedora_39
@@ -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
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.4 b/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.4
index 086848b0..69d6ba40 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.4
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.4
@@ -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"]
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.5 b/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.5
index 5da01417..9b46f00e 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.5
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_opensuse-leap_15.5
@@ -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
diff --git a/extras/packaging/gnu-linux/docker/Dockerfile_ubuntu_20.04 b/extras/packaging/gnu-linux/docker/Dockerfile_ubuntu_20.04
index 12219e0c..ed6b76d0 100644
--- a/extras/packaging/gnu-linux/docker/Dockerfile_ubuntu_20.04
+++ b/extras/packaging/gnu-linux/docker/Dockerfile_ubuntu_20.04
@@ -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"]
diff --git a/extras/packaging/gnu-linux/rules/debian/control b/extras/packaging/gnu-linux/rules/debian/control
index 239bac96..2bd8e9e5 100644
--- a/extras/packaging/gnu-linux/rules/debian/control
+++ b/extras/packaging/gnu-linux/rules/debian/control
@@ -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,
diff --git a/extras/packaging/gnu-linux/rules/rpm/jami-daemon.spec b/extras/packaging/gnu-linux/rules/rpm/jami-daemon.spec
index 08ae5d90..70e9c8d4 100644
--- a/extras/packaging/gnu-linux/rules/rpm/jami-daemon.spec
+++ b/extras/packaging/gnu-linux/rules/rpm/jami-daemon.spec
@@ -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
diff --git a/resources/misc/projectcredits.html b/resources/misc/projectcredits.html
index 44524de2..24e6f6bb 100644
--- a/resources/misc/projectcredits.html
+++ b/resources/misc/projectcredits.html
@@ -1,5 +1,6 @@
Created by
-Adrien Béraud
+
Abhishek Ojha
+Adrien Béraud
Albert Babí
Alexandre Lision
Alexandr Sergheev
@@ -25,6 +26,7 @@ Emma Falkiewitz
Emmanuel Lepage-Vallée
Fadi Shehadeh
Franck Laurent
+François-Simon Fauteux-Chapleau
Frédéric Guimont
Guillaume Heller
Guillaume Roguez
diff --git a/src/app/avadapter.cpp b/src/app/avadapter.cpp
index 4c3ead4b..613c088c 100644
--- a/src/app/avadapter.cpp
+++ b/src/app/avadapter.cpp
@@ -25,7 +25,11 @@
#include "api/devicemodel.h"
#ifdef Q_OS_LINUX
+#include "screencastportal.h"
#include "xrectsel.h"
+#ifndef ENABLE_LIBWRAP
+#include
+#endif
#endif
#include
@@ -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> 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(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;
diff --git a/src/app/avadapter.h b/src/app/avadapter.h
index ce5427fb..475f95d0 100644
--- a/src/app/avadapter.h
+++ b/src/app/avadapter.h
@@ -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;
diff --git a/src/app/mainview/components/CallActionBar.qml b/src/app/mainview/components/CallActionBar.qml
index 9604e49e..bc692959 100644
--- a/src/app/mainview/components/CallActionBar.qml
+++ b/src/app/mainview/components/CallActionBar.qml
@@ -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"
diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml
index 26e47c10..d3c98761 100644
--- a/src/app/mainview/components/CallOverlay.qml
+++ b/src/app/mainview/components/CallOverlay.qml
@@ -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);
diff --git a/src/app/screencastportal.cpp b/src/app/screencastportal.cpp
new file mode 100644
index 00000000..07490395
--- /dev/null
+++ b/src/app/screencastportal.cpp
@@ -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 .
+ */
+
+#include "screencastportal.h"
+
+#include
+#include
+
+#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
+}
\ No newline at end of file
diff --git a/src/app/screencastportal.h b/src/app/screencastportal.h
new file mode 100644
index 00000000..b3ade793
--- /dev/null
+++ b/src/app/screencastportal.h
@@ -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 .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+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;
+};