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;
+};