diff --git a/CMakeLists.txt b/CMakeLists.txt index b93daeda114bb70517a8eb432bf7d93b461d372f..e802357f16a755bf88948df4440935213a550b99 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 54f149fc1858cac7f560b9a6140e5412e7f68acb..c5c3afae9a333c3aab1161f9ffe4ce9ef3dd24bf 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 44a86858bce6483101480bf402e5267f2dbacb3b..b9dc066e2a21ef98640cf09306ce1b11dcbac064 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 b91bc11839925cb58debb29dcc6b884cd2552616..a42dcba38f29ca3d9449f7b7175aa58a42b1b9bb 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 324ca3055992e359210e3bb284104b2588585710..574a4f440e22f880df7a1ff5eb24ba36d83afa8b 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 eab4b1545c7d2417e2bd9a2c761e756fe8681177..790222e5e4759a83b13dcfc27df9c8f96cd16077 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 0623bee82d5262aeb30d22a76222d04c19ba4dbb..33684f753bb0bdd198e43ff566b3959385850d80 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 eb688c5e79a8b3167a4a19216307948976e078cb..fde510dd7ee3dad7bac0bd77af6f1b469f088dbd 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 086848b0dbe52d0a6436bfa00573cea585f1075f..69d6ba401de89637395ae7a2e198b749ab640aed 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 5da01417ae21392ff53600cb14b887305e553626..9b46f00e29e68ddd9d88d67490caa035cc5e0698 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 12219e0c92fc3b689ebbd6d1fe409505936b654c..ed6b76d051e4bb96d0ad23acfb03bc7fb21976b5 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 239bac963917f4031287ff78e9eaa495306d328a..2bd8e9e5ca5d72fa2b65dc7bc0213aaa4c928575 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 08ae5d909d7bb1e1ee7df8da0ed59809da8d53df..70e9c8d4d15b9033593b1a2f5c801ab536e9ecd8 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 44524de227aa4e552b0ef7e8453a5c660507c083..24e6f6bb68797e1f6687ff6d655a025d3ab170a2 100644 --- a/resources/misc/projectcredits.html +++ b/resources/misc/projectcredits.html @@ -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> diff --git a/src/app/avadapter.cpp b/src/app/avadapter.cpp index 4c3ead4b25dfe32f3e355eab52214276c5d4496c..613c088c17fadae0c3da7b51d9319cb8781e59f0 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 <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; diff --git a/src/app/avadapter.h b/src/app/avadapter.h index ce5427fb9f7cc5524b5b4be578df58e9f21e9947..475f95d0d6fbdbf67563ec1143b6b944498729e3 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 9604e49e7f7c0dd0c71c61dde45fcdf1c59be2e8..bc69295960433131c2da1dabb7fe825883198de9 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 26e47c1027041deb86b250f54216d002d84dd678..d3c98761e3746b9845b510aa2f62a02e634f322c 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 0000000000000000000000000000000000000000..07490395d463aa138c38a249d084af8a492a433b --- /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 <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 +} \ No newline at end of file diff --git a/src/app/screencastportal.h b/src/app/screencastportal.h new file mode 100644 index 0000000000000000000000000000000000000000..b3ade79391bbce935a18f99f2921f23eff5bfe2c --- /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 <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; +};