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:
parent
a673ff9890
commit
200978a044
20 changed files with 840 additions and 20 deletions
|
@ -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
2
daemon
|
@ -1 +1 @@
|
|||
Subproject commit 54f149fc1858cac7f560b9a6140e5412e7f68acb
|
||||
Subproject commit c5c3afae9a333c3aab1161f9ffe4ce9ef3dd24bf
|
|
@ -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 \
|
||||
|
|
|
@ -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"]
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
520
src/app/screencastportal.cpp
Normal file
520
src/app/screencastportal.cpp
Normal 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
102
src/app/screencastportal.h
Normal 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;
|
||||
};
|
Loading…
Reference in a new issue