From 995e20be218e9c9adb496cef1e460b78c51ddd3d Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 28 Jan 2026 14:59:30 +0000 Subject: [PATCH] Replace PulseAudio with PipeWire/WirePlumber audio stack in WSLg --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CONTRIBUTING.md | 9 +- Dockerfile | 115 +++++- README.md | 38 +- WSLGd/main.cpp | 57 ++- azure-pipelines.yml | 27 +- cgmanifest.json | 14 +- config/BUILD.md | 8 +- config/pipewire-pulse.conf | 58 +++ config/pipewire-rdp-module.c | 575 ++++++++++++++++++++++++++ config/pipewire-wslg-rdp.patch | 36 ++ config/pipewire.conf | 98 +++++ samples/container/Containers.md | 8 +- 13 files changed, 973 insertions(+), 72 deletions(-) create mode 100644 config/pipewire-pulse.conf create mode 100644 config/pipewire-rdp-module.c create mode 100644 config/pipewire-wslg-rdp.patch create mode 100644 config/pipewire.conf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4fbdc2ac..c7c496ad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -51,7 +51,7 @@ body: You can access the wslg logs using explorer at: `\\wsl$\\mnt\wslg` (e.g.: `\\wsl$\Ubuntu-20.04\mnt\wslg`) - * `pulseaudio.log` + * `pipewire-pulse.log` * `weston.log` * `stderr.log` label: "WSL logs:" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1a3a2c7..71c734ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,13 +15,13 @@ or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any addi # Building the WSLg System Distro -The heart of WSLg is what we call the WSL system distro. This is where the Weston compositor, XWayland and the PulseAudio server are running. The system distro runs these components and projects their communication sockets into the user distro. Every user distro is paired with a unique instance of the system distro. There is a single version of the system distro on disk which is instantiated in memory when a user distro is launched. +The heart of WSLg is what we call the WSL system distro. This is where the Weston compositor, XWayland and the PipeWire audio stack (with pipewire-pulse) are running. The system distro runs these components and projects their communication sockets into the user distro. Every user distro is paired with a unique instance of the system distro. There is a single version of the system distro on disk which is instantiated in memory when a user distro is launched. The system distro is essentially a Linux container packaged and distributed as a vhd. The system distro is accessible to the user, but is mounted read-only. Any changes made by the user to the system distro while it is running are discarded when WSL is restarted. Although a user can log into the system distro, it is not meant to be used as a general purpose user distro. The reason behind this choice is due to the way we service WSLg. When updating WSLg we simply replace the existing system distro with a new one. If the user had data embedded into the system distro vhd, this data would be lost. For folks who want to tinker with or customize their system distro, we give the ability to run a private version of the system distro. When running a private version of WSLg, Windows will load and run your private and ignore the Microsoft published one. If you update your WSL setup (`wsl --update`), the Microsoft published WSLg vhd will be updated, but you will continue to be running your private. You can switch between the Microsoft pulished WSLg system distro and a private one at any time although it does require restarting WSL (`wsl --shutdown`). -The WSLg system distro is built using docker build. We essentially start from a [Azure Linux 3.0](https://github.com/microsoft/azurelinux) base image, install various packages, then build and install version of Weston, FreeRDP and PulseAudio from our mirror repo. This repository contains a Dockerfile and supporting tools to build the WSLg container and convert the container into an ext4 vhd that Windows will load as the system distro. +The WSLg system distro is built using docker build. We essentially start from a [Azure Linux 3.0](https://github.com/microsoft/azurelinux) base image, install various packages, then build and install version of Weston, FreeRDP, PipeWire, and WirePlumber. This repository contains a Dockerfile and supporting tools to build the WSLg container and convert the container into an ext4 vhd that Windows will load as the system distro. ## Build instructions @@ -39,12 +39,13 @@ The WSLg system distro is built using docker build. We essentially start from a git clone https://github.com/microsoft/wslg wslg ``` -2. Clone the FreeRDP, Weston and PulseAudio mirror. These need to be located in a **vendor** sub-directory where you clone the wslg project (e.g. wslg/vendor), this is where our docker build script expects to find the source code. Make sure to checkout the **working** branch from each of these projects, the **main** branch references the upstream code. +2. Clone the FreeRDP, Weston, PipeWire, and WirePlumber repositories. These need to be located in a **vendor** sub-directory where you clone the wslg project (e.g. wslg/vendor), this is where our docker build script expects to find the source code. ```bash git clone https://github.com/microsoft/FreeRDP-mirror wslg/vendor/FreeRDP -b working git clone https://github.com/microsoft/weston-mirror wslg/vendor/weston -b working - git clone https://github.com/microsoft/PulseAudio-mirror wslg/vendor/pulseaudio -b working + git clone https://github.com/PipeWire/pipewire wslg/vendor/pipewire -b 1.4.10 + git clone https://github.com/PipeWire/wireplumber wslg/vendor/wireplumber -b 0.5.13 git clone https://github.com/microsoft/DirectX-Headers.git wslg/vendor/DirectX-Headers-1.0 -b v1.608.0 git clone https://gitlab.freedesktop.org/mesa/mesa.git wslg/vendor/mesa -b mesa-23.1.0 ``` diff --git a/Dockerfile b/Dockerfile index cdf060c6..0fe7436e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,8 @@ RUN echo "== Install UI dependencies ==" && \ libSM-devel \ libsndfile \ libsndfile-devel \ + lua \ + lua-devel \ libXcursor \ libXcursor-devel \ libXdamage-devel \ @@ -188,17 +190,88 @@ RUN /usr/bin/meson --prefix=${PREFIX} build \ ninja -C build -j8 install && \ echo 'mesa:' `git --git-dir=/work/vendor/mesa/.git rev-parse --verify HEAD` >> /work/versions.txt -# Build PulseAudio -COPY vendor/pulseaudio /work/vendor/pulseaudio -WORKDIR /work/vendor/pulseaudio -RUN /usr/bin/meson --prefix=${PREFIX} build \ +# Build PipeWire +COPY vendor/pipewire /work/vendor/pipewire +RUN /usr/bin/meson setup /work/vendor/pipewire/build /work/vendor/pipewire \ + --prefix=${PREFIX} \ + --buildtype=${BUILDTYPE_NODEBUGSTRIP} \ + -Ddocs=disabled \ + -Dman=disabled \ + -Dexamples=disabled \ + -Dtests=disabled \ + -Dsystemd-system-service=disabled \ + -Dsystemd-user-service=disabled \ + -Dx11=disabled \ + -Dx11-xfixes=disabled \ + -Dsession-managers=[] \ + -Dpipewire-jack=enabled \ + -Dpipewire-v4l2=enabled \ + -Dlibpulse=disabled \ + -Dbluez5=disabled \ + -Dffmpeg=disabled \ + -Dgsettings=disabled \ + -Davahi=disabled \ + -Dsnap=disabled \ + -Drlimits-install=false \ + -Dgstreamer=disabled \ + -Dgstreamer-device-provider=disabled \ + -Dlv2=disabled \ + -Droc=disabled \ + -Dspa-plugins=enabled && \ + ninja -C /work/vendor/pipewire/build -j8 install && \ + echo 'pipewire:' `git --git-dir=/work/vendor/pipewire/.git rev-parse --verify HEAD` >> /work/versions.txt + +# Build WSLg PipeWire RDP modules +COPY config/pipewire-rdp-module.c /work/vendor/pipewire/src/modules/module-wslg-rdp.c +COPY config/pipewire-wslg-rdp.patch /work/vendor/pipewire/pipewire-wslg-rdp.patch +RUN cd /work/vendor/pipewire && \ + git apply pipewire-wslg-rdp.patch && \ + /usr/bin/meson setup /work/vendor/pipewire/build-wslg /work/vendor/pipewire \ + --prefix=${PREFIX} \ --buildtype=${BUILDTYPE_NODEBUGSTRIP} \ - -Ddatabase=simple \ - -Ddoxygen=false \ + -Ddocs=disabled \ + -Dman=disabled \ + -Dexamples=disabled \ + -Dtests=disabled \ + -Dsystemd-system-service=disabled \ + -Dsystemd-user-service=disabled \ + -Dsession-managers=[] \ + -Dpipewire-jack=enabled \ + -Dpipewire-v4l2=enabled \ + -Dpipewire-alsa=enabled \ + -Dlibpulse=disabled \ + -Dbluez5=disabled \ + -Dffmpeg=disabled \ -Dgsettings=disabled \ - -Dtests=false && \ + -Davahi=disabled \ + -Dsnap=disabled \ + -Drlimits-install=false \ + -Dgstreamer=disabled \ + -Dgstreamer-device-provider=disabled \ + -Dlv2=disabled \ + -Droc=disabled \ + -Dspa-plugins=enabled && \ + ninja -C /work/vendor/pipewire/build-wslg -j8 pipewire-module-wslg-rdp-sink pipewire-module-wslg-rdp-source && \ + install -m 0755 /work/vendor/pipewire/build-wslg/src/modules/pipewire-module-wslg-rdp-sink.so ${DESTDIR}${PREFIX}/lib/pipewire-0.3/pipewire-module-wslg-rdp-sink.so && \ + install -m 0755 /work/vendor/pipewire/build-wslg/src/modules/pipewire-module-wslg-rdp-source.so ${DESTDIR}${PREFIX}/lib/pipewire-0.3/pipewire-module-wslg-rdp-source.so + + +# Build WirePlumber +COPY vendor/wireplumber /work/vendor/wireplumber +WORKDIR /work/vendor/wireplumber +RUN /usr/bin/meson --prefix=${PREFIX} build \ + --buildtype=${BUILDTYPE_NODEBUGSTRIP} \ + -Ddoc=disabled \ + -Dtests=false \ + -Dsystemd=disabled \ + -Dsystemd-user-service=false \ + -Dsystemd-system-service=false \ + -Dintrospection=disabled \ + -Ddaemon=true \ + -Dtools=false \ + -Dmodules=true && \ ninja -C build -j8 install && \ - echo 'pulseaudio:' `git --git-dir=/work/vendor/pulseaudio/.git rev-parse --verify HEAD` >> /work/versions.txt + echo 'wireplumber:' `git --git-dir=/work/vendor/wireplumber/.git rev-parse --verify HEAD` >> /work/versions.txt # Build FreeRDP COPY vendor/FreeRDP /work/vendor/FreeRDP @@ -337,6 +410,7 @@ RUN echo "== Install Core/UI Runtime Dependencies ==" && \ libpng \ librsvg2 \ libsndfile \ + lua \ libwayland-client \ libwayland-server \ libwayland-cursor \ @@ -428,17 +502,20 @@ COPY resources/linux.png /usr/share/icons/wsl/linux.png COPY --from=dev /work/build/usr/ /usr/ COPY --from=dev /work/build/etc/ /etc/ -# Append WSLg setttings to pulseaudio. -COPY config/default_wslg.pa /etc/pulse/default_wslg.pa -RUN cat /etc/pulse/default_wslg.pa >> /etc/pulse/default.pa -RUN rm /etc/pulse/default_wslg.pa - -# Copy the licensing information for PulseAudio -COPY --from=dev /work/vendor/pulseaudio/GPL \ - /work/vendor/pulseaudio/LGPL \ - /work/vendor/pulseaudio/LICENSE \ - /work/vendor/pulseaudio/NEWS \ - /work/vendor/pulseaudio/README /usr/share/doc/pulseaudio/ +# Append WSLg settings to PipeWire. +COPY config/pipewire.conf /etc/pipewire/pipewire.conf +COPY config/pipewire-pulse.conf /etc/pipewire/pipewire-pulse.conf + +# Copy the licensing information for PipeWire +COPY --from=dev /work/vendor/pipewire/COPYING \ + /work/vendor/pipewire/LICENSE \ + /work/vendor/pipewire/NEWS \ + /work/vendor/pipewire/README.md /usr/share/doc/pipewire/ + +# Copy the licensing information for WirePlumber +COPY --from=dev /work/vendor/wireplumber/LICENSE \ + /work/vendor/wireplumber/NEWS.rst \ + /work/vendor/wireplumber/README.rst /usr/share/doc/wireplumber/ # Copy the licensing information for Weston COPY --from=dev /work/vendor/weston/COPYING /usr/share/doc/weston/COPYING diff --git a/README.md b/README.md index 4839cab3..f64172d3 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,8 @@ The user distro is essentially the WSL distribution you are using for your Linux All user and system distros for a particular Windows user run within the same WSL virtual machine against a single instance of the Linux kernel. Different Windows users on a PC have their own VM and instance of WSL. Your Linux environment is guaranteed to always be your own and not shared with other Windows users on the same PC. -## WSLg System Distro -The system distro is where all of the magic happens. The system distro is a containerized Linux environment where the WSLg XServer, Wayland server and Pulse Audio server are running. Communication socket for each of these servers are projected into the user distro so client applications can connect to them. We preconfigure the user distro environment variables DISPLAY, WAYLAND_DISPLAY and PULSE_SERVER to refer these servers by default so WSLg lights up out of the box. +## WSLg System Distro +The system distro is where all of the magic happens. The system distro is a containerized Linux environment where the WSLg XServer, Wayland server and PipeWire (with a PulseAudio-compatible server) are running. Communication socket for each of these servers are projected into the user distro so client applications can connect to them. We preconfigure the user distro environment variables DISPLAY, WAYLAND_DISPLAY and PULSE_SERVER to refer these servers by default so WSLg lights up out of the box. Users wanting to use different servers than the one provided by WSLg can change these environment variables. User can also choose to turn off the system distro entirely by adding the following entry in their `.wslconfig` file (located at `c:\users\MyUser\.wslconfig`). This will turn off support for GUI applications in WSL. @@ -142,8 +142,8 @@ While a user can get a terminal into the system distro, the system distro is not Although the Microsoft published WSLg system distro as read-only, we do want to encourage folks to tinker with it and experiment. Although we expect very few folks to actually need or want to do that, we've shared detailed instruction on our [contributing](CONTRIBUTING.md) page on how to both build and deploy a private version of the system distro. Most users who just want to use GUI applications in WSL don't need to worry about those details. -## WSLGd -**WSLGd** is the first process to launch after **init**. **WSLGd** launches **Weston** (with XWayland), **PulseAudio** and establishes the RDP connection by launching **mstsc.exe** on the host in silent mode. The RDP connection will remain active and ready to show a new GUI applications being launch on a moment's notice, without any connection establishment delays. **WSLGd** then monitors these processes and if they exit by error (say as a result of a crash), it automatically restarts them. +## WSLGd +**WSLGd** is the first process to launch after **init**. **WSLGd** launches **Weston** (with XWayland), **PipeWire** (with **wireplumber** and **pipewire-pulse**) and establishes the RDP connection by launching **mstsc.exe** on the host in silent mode. The RDP connection will remain active and ready to show a new GUI applications being launch on a moment's notice, without any connection establishment delays. **WSLGd** then monitors these processes and if they exit by error (say as a result of a crash), it automatically restarts them. ## Weston Weston is the Wayland project reference compositor and the heart of WSLg. For WSLg, we've extended the existing RDP backend of libweston to teach it how to remote applications rather than monitor/desktop. We've also added various functionality to it, such as support for multi-monitor, cut/paste, audio in/out, etc... @@ -156,8 +156,8 @@ Weston is modular and has various shells today, such as the desktop shell, fulls ## FreeRDP Weston leverages FreeRDP to implement its backend RDP Server. FreeRDP is used to encode all communications going from the RDP Server (in Weston) to the RDP Client (mstsc on Windows) according to the RDP protocol specifications. It is also used to decode all traffic coming from the RDP Client into the RDP server. -## Pulse Audio Plugin -For audio in (microphone) and out (speakers/headphone) WSLg runs a PulseAudio server. WSLg uses a [sink plugin](https://github.com/microsoft/pulseaudio-mirror/blob/working/src/modules/rdp/module-rdp-sink.c) for audio out, and a [source plugin](https://github.com/microsoft/pulseaudio-mirror/blob/working/src/modules/rdp/module-rdp-source.c) for audio in. These plugins effectively transfer audio samples between the PulseServer and the Weston RDP Server. The audio streams are merged by the Weston RDP Server onto the RDP transport, effectively enabling audio in/out in the Weston RDP backend across all scenarios (Desktop/RAIL/VAIL style remoting), including WSLg. +## PipeWire Audio +For audio in (microphone) and out (speakers/headphone) WSLg runs PipeWire with a PulseAudio-compatible server. The PipeWire RDP modules bridge audio to the Weston RDP backend using the same RDP sink/source sockets as before, so PulseAudio clients in the user distro keep working through the `PULSE_SERVER` socket. The audio streams are merged by the Weston RDP Server onto the RDP transport, effectively enabling audio in/out in the Weston RDP backend across all scenarios (Desktop/RAIL/VAIL style remoting), including WSLg. ## WSL Dynamic Virtual Channel Plugin (WSLDVCPlugin) WSLg makes use of a custom RDP virtual channel between the Weston RDP Server and the mstsc RDP Client running on the Windows host. This channel is used by Weston to enumerate all Linux GUI applications (i.e. applications which have a desktop file entry of type gui) along with their launch command line and icon. The open source [WSLDVCPlugin](https://github.com/microsoft/wslg/tree/main/WSLDVCPlugin) processes the list of Linux GUI applications sent over this channel and creates links for them in the Windows start menu. @@ -173,7 +173,7 @@ Support for Linux, including support for WSLg, has been upstream and part of the Please note that for the first release of WSLg, vGPU interops with the Weston compositor through system memory. If running on a discrete GPU, this effectively means that the rendered data is copied from VRAM to system memory before being presented to the compositor within WSLg, and uploaded onto the GPU again on the Windows side. As a result, there is a performance penalty proportionate to the presentation rate. At very high frame rates such as 600fps on a discrete GPU, that overhead can be as high as 50%. At lower frame rate or on integrated GPU, performance much closer to native can be achieved depending on the workload. Using a vGPU still provides a very significant performance and experience improvement over using a software renderer despite this v1 limitation. # WSLg Code Flow -WSLg builds on the great work of the Linux community and makes use of a large number of open source projects. Most components are used as-is from their upstream version and didn't require any changes to light up in WSLg. Some components at the heart of WSLg, in particular Weston, FreeRDP and PulseAudio, required changes to enable the rich WSLg integration. These changes aren't yet upstream. Microsoft is working with the community to share these contributions back with each project such that, over time, WSLg can be built from upstream component directly, without the need for any WSLg specific modifications. +WSLg builds on the great work of the Linux community and makes use of a large number of open source projects. Most components are used as-is from their upstream version and didn't require any changes to light up in WSLg. Some components at the heart of WSLg, in particular Weston, FreeRDP and PipeWire, required changes to enable the rich WSLg integration. These changes aren't yet upstream. Microsoft is working with the community to share these contributions back with each project such that, over time, WSLg can be built from upstream component directly, without the need for any WSLg specific modifications. All of these in-flight contributions are kept in Microsoft mirror repos. We keep these mirrors up to date with upstream releases and stage our WSLg changes in those repos. WSLg pulls and builds code from these mirror repos as part of our Insider WSLg Preview releases. These mirrors are public and accessible to everyone. Curious developers can take a peek at early stages of our contribution by looking at code in those mirrors, keeping in mind that the final version of the code will likely look different once the contribution reaches the upstream project and is adapted based on the feedback receives by the various project owners. All of our mirrors follow the same model. There is a **main** branch which correspond to the upstream branch at our last synchronization point. We update the **main** branch from time to time to pick update from the upstream project. There is also a **working** branch that contains all of our in-flight changes. WSLg is built using the **working** branch from each of the mirror projects. @@ -185,7 +185,8 @@ At this point in time, we have the following project mirrors for currently in-fl |---|---|---| | Weston | https://github.com/wayland-project/weston | https://github.com/microsoft/Weston-mirror| | FreeRDP | https://github.com/FreeRDP/FreeRDP | https://github.com/microsoft/FreeRDP-mirror | -| PulseAudio | https://github.com/pulseaudio/pulseaudio | https://github.com/microsoft/PulseAudio-mirror | +| PipeWire | https://github.com/PipeWire/pipewire | https://github.com/PipeWire/pipewire | +| WirePlumber | https://github.com/PipeWire/wireplumber | https://github.com/PipeWire/wireplumber | The following is a high level overview of the currently in-flight contributions to each project contained within these mirrors. @@ -200,8 +201,25 @@ We've also fixed several bugs impacting various applications. Generally, these w ## FreeRDP Weston currently uses FreeRDP for its RDP Backend. WSLg continues to leverage FreeRDP and we have added support for a new RDP Protocol/Channel to enable VAIL optimized scenario as well as support for the WSLg plugin. We've also fixed various bugs that were impacting interops with mstsc or causing instability. -## PulseAudio -For PulseAudio, our contributions focused on a sink and a source plugin that shuffle audio data between PulseAudio and the Weston RDP backend such that the audio data can be integrated over the RDP connection back to the host. There are no changes to the core of PulseAudio outside of adding these new plugins. +## PipeWire +For PipeWire, our contributions focus on RDP sink/source modules that shuffle audio data between PipeWire and the Weston RDP backend such that the audio data can be integrated over the RDP connection back to the host. + +## WirePlumber +WSLg relies on WirePlumber as the PipeWire session manager to handle policy and device routing in the system distro. + +### Verifying PipeWire availability +After installing a system distro built with these changes, run `wsl --shutdown`, then launch a WSL distro to start WSLg. From a system distro shell (`wsl --system `), confirm the audio stack and sockets: + +``` +ps -ax | grep -E "pipewire|wireplumber|pipewire-pulse" +ls -la /mnt/wslg/PulseServer +``` + +From the user distro, verify that PulseAudio clients connect to PipeWire: + +``` +pactl info | grep "Server Name" +``` # Contributing diff --git a/WSLGd/main.cpp b/WSLGd/main.cpp index accd8313..8e4ca92e 100644 --- a/WSLGd/main.cpp +++ b/WSLGd/main.cpp @@ -447,7 +447,7 @@ try { } ); - // Wait weston to be ready before starting RDP client, pulseaudio server. + // Wait weston to be ready before starting RDP client, PipeWire stack. WaitForReadyNotify(notifyFd.get()); unlink(WESTON_NOTIFY_SOCKET); @@ -519,32 +519,49 @@ try { std::vector{CAP_SETGID, CAP_SETUID} ); - // Construct pulseaudio launch command line. - std::string pulseaudioLaunchArgs = + // Construct pipewire launch command line. + std::string pipewireLaunchArgs = "/usr/bin/dbus-launch " - "/usr/bin/pulseaudio " - "--log-time=true " - "--disallow-exit=true " - "--exit-idle-time=-1 " - "--load=\"module-rdp-sink sink_name=RDPSink\" " - "--load=\"module-rdp-source source_name=RDPSource\" " - "--load=\"module-native-protocol-unix socket=" SHARE_PATH "/PulseServer auth-anonymous=true\" "; + "/usr/bin/pipewire"; - // Construct log file option string. - std::string pulseaudioLogFileOption("--log-target="); - auto pulseAudioLogFilePathEnv = getenv("WSLG_PULSEAUDIO_LOG_PATH"); - if (pulseAudioLogFilePathEnv) { - pulseaudioLogFileOption += pulseAudioLogFilePathEnv; - } else { - pulseaudioLogFileOption += "newfile:" SHARE_PATH "/pulseaudio.log"; + // Construct pipewire log file option string. + auto pipewireLogFilePathEnv = getenv("WSLG_PIPEWIRE_LOG_PATH"); + if (pipewireLogFilePathEnv && pipewireLogFilePathEnv[0] != '\0') { + pipewireLaunchArgs += " --log="; + pipewireLaunchArgs += pipewireLogFilePathEnv; + } + + // Launch pipewire and the associated dbus daemon. + monitor.LaunchProcess(std::vector{ + "/usr/bin/sh", + "-c", + std::move(pipewireLaunchArgs) + }); + + // Launch wireplumber session manager. + monitor.LaunchProcess(std::vector{ + "/usr/bin/sh", + "-c", + "/usr/bin/wireplumber" + }); + + // Construct pipewire-pulse launch command line. + std::string pipewirePulseLaunchArgs = + "/usr/bin/dbus-launch " + "/usr/bin/pipewire-pulse"; + + // Construct pipewire-pulse log file option string. + auto pipewirePulseLogFilePathEnv = getenv("WSLG_PIPEWIRE_PULSE_LOG_PATH"); + if (pipewirePulseLogFilePathEnv && pipewirePulseLogFilePathEnv[0] != '\0') { + pipewirePulseLaunchArgs += " --log="; + pipewirePulseLaunchArgs += pipewirePulseLogFilePathEnv; } - pulseaudioLaunchArgs += pulseaudioLogFileOption; - // Launch pulseaudio and the associated dbus daemon. + // Launch pipewire-pulse and the associated dbus daemon. monitor.LaunchProcess(std::vector{ "/usr/bin/sh", "-c", - std::move(pulseaudioLaunchArgs) + std::move(pipewirePulseLaunchArgs) }); return monitor.Run(); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 620f5560..072be754 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,11 +10,16 @@ resources: endpoint: GitHub connection 1 name: microsoft/weston-mirror ref: working - - repository: pulseaudio + - repository: pipewire type: github endpoint: GitHub connection 1 - name: microsoft/pulseaudio-mirror - ref: working + name: PipeWire/pipewire + ref: 1.4.10 + - repository: wireplumber + type: github + endpoint: GitHub connection 1 + name: PipeWire/wireplumber + ref: 0.5.13 trigger: - main @@ -33,7 +38,8 @@ stages: steps: - checkout: FreeRDP - checkout: weston - - checkout: pulseaudio + - checkout: pipewire + - checkout: wireplumber - checkout: self - template: devops/common-linux.yml @@ -48,10 +54,11 @@ stages: - script: mv FreeRDP-mirror/ wslg/vendor/FreeRDP && mv weston-mirror/ wslg/vendor/weston && - mv pulseaudio-mirror/ wslg/vendor/pulseaudio && + mv pipewire/ wslg/vendor/pipewire && + mv wireplumber/ wslg/vendor/wireplumber && mv mesa-23.1.0/ wslg/vendor/mesa && mv DirectX-Headers-1.608.0/ wslg/vendor/DirectX-Headers-1.0 - displayName: 'Move sub projects (FreeRDP, Weston, PulseAudio, Mesa, DirectX-Headers)' + displayName: 'Move sub projects (FreeRDP, Weston, PipeWire, WirePlumber, Mesa, DirectX-Headers)' - script: docker build -f ./wslg/Dockerfile -t system-distro-x64 ./wslg @@ -165,7 +172,8 @@ stages: steps: - checkout: FreeRDP - checkout: weston - - checkout: pulseaudio + - checkout: pipewire + - checkout: wireplumber - checkout: self - template: devops/common-linux.yml @@ -196,10 +204,11 @@ stages: - script: mv FreeRDP-mirror/ wslg/vendor/FreeRDP && mv weston-mirror/ wslg/vendor/weston && - mv pulseaudio-mirror/ wslg/vendor/pulseaudio && + mv pipewire/ wslg/vendor/pipewire && + mv wireplumber/ wslg/vendor/wireplumber && mv mesa-23.1.0/ wslg/vendor/mesa && mv DirectX-Headers-1.608.0/ wslg/vendor/DirectX-Headers-1.0 - displayName: 'Move sub projects (FreeRDP, Weston, PulseAudio, Mesa, DirectX-Headers)' + displayName: 'Move sub projects (FreeRDP, Weston, PipeWire, WirePlumber, Mesa, DirectX-Headers)' - script: ~/.docker/cli-plugins/docker-buildx build -f ./wslg/Dockerfile --output type=tar,dest=$(Agent.BuildDirectory)/system_arm64.tar diff --git a/cgmanifest.json b/cgmanifest.json index 04d5b651..03aba188 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -4,8 +4,18 @@ "Component": { "Type": "git", "Git": { - "RepositoryUrl": "https://github.com/pulseaudio/pulseaudio", - "CommitHash": "0e691b96640919b1c7ed91ae9240761c5775deeb" + "RepositoryUrl": "https://github.com/PipeWire/pipewire", + "CommitHash": "ced36a5315135044a22c4b373be44368d312f9c8" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "git", + "Git": { + "RepositoryUrl": "https://github.com/PipeWire/wireplumber", + "CommitHash": "84429b47943d789389fbde17c06b82efb197d04e" } }, "DevelopmentDependency": false diff --git a/config/BUILD.md b/config/BUILD.md index f5c1552e..f10c3098 100644 --- a/config/BUILD.md +++ b/config/BUILD.md @@ -10,14 +10,16 @@ For self-hosting WSLG check use this instructions https://github.com/microsoft/w 0. Install and start Docker in a Linux or WSL2 environment. -1. Clone the FreeRDP ,Weston and PulseAudio side by side this repo repositories and checkout the "working" branch from each: +1. Clone the FreeRDP, Weston, PipeWire and WirePlumber repositories side by side and checkout the versions we build against: ```bash git clone https://github.com/microsoft/FreeRDP-mirror vendor/FreeRDP -b working git clone https://github.com/microsoft/weston-mirror.git vendor/weston -b working - git clone https://github.com/microsoft/pulseaudio-mirror.git vendor/pulseaudio -b working + git clone https://github.com/PipeWire/pipewire.git vendor/pipewire --branch 1.4.10 + + git clone https://github.com/PipeWire/wireplumber.git vendor/wireplumber --branch 0.5.13 ``` 2. Download the mesa and directx headers code. @@ -84,7 +86,7 @@ For self-hosting WSLG check use this instructions https://github.com/microsoft/w wsl --system [DistroName] ``` - For instance you should check if weston and pulse audio are running inside the system distro using `ps -ax | grep weston` or `ps -ax | grep pulse` + For instance you should check if weston and PipeWire are running inside the system distro using `ps -ax | grep weston` or `ps -ax | grep pipewire` and verify the PulseAudio socket with `ls -la /mnt/wslg/PulseServer`. You should see something like this: ```bash diff --git a/config/pipewire-pulse.conf b/config/pipewire-pulse.conf new file mode 100644 index 00000000..ff7ff84d --- /dev/null +++ b/config/pipewire-pulse.conf @@ -0,0 +1,58 @@ +# PulseAudio config file for PipeWire version 1.4.10 +# +# Based on PipeWire upstream pipewire-pulse.conf and tailored for WSLg. +# + +context.properties = { + log.level = 2 +} + +context.spa-libs = { + audio.convert.* = audioconvert/libspa-audioconvert + support.* = support/libspa-support +} + +context.modules = [ + { name = libpipewire-module-rt + args = { + nice.level = -11 + } + flags = [ ifexists nofail ] + } + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-client-node } + { name = libpipewire-module-adapter } + { name = libpipewire-module-metadata } + { name = libpipewire-module-protocol-pulse + args = { + } + } +] + +pulse.cmd = [ + { cmd = "load-module" args = "module-wslg-rdp-sink sink_name=RDPSink sink_properties=node.description=WSLg RDP Sink media.class=Audio/Sink channels=2" } + { cmd = "load-module" args = "module-wslg-rdp-source source_name=RDPSource source_properties=node.description=WSLg RDP Source media.class=Audio/Source channels=1" } +] + +stream.properties = { +} + +pulse.properties = { + server.address = [ + "unix:/mnt/wslg/PulseServer" + ] + pulse.idle.timeout = 0 +} + +pulse.properties.rules = [ + { matches = [ { cpu.vm.name = !null } ] + actions = { + update-props = { + pulse.min.quantum = 1024/48000 + } + } + } +] + +pulse.rules = [ +] diff --git a/config/pipewire-rdp-module.c b/config/pipewire-rdp-module.c new file mode 100644 index 00000000..987a8e1e --- /dev/null +++ b/config/pipewire-rdp-module.c @@ -0,0 +1,575 @@ +/* SPDX-FileCopyrightText: Copyright (c) Microsoft Corporation. + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +static const struct pw_proxy_events core_proxy_events = { + PW_VERSION_PROXY_EVENTS, +}; + +#define NAME "wslg-rdp" + +PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME); +#define PW_LOG_TOPIC_DEFAULT mod_topic + +static const char *const pulse_module_options = + "socket= " + "sink_name= " + "sink_properties= " + "source_name= " + "source_properties= " + "format= " + "rate= " + "channels= " + "channel_map= "; + +static const struct spa_dict_item sink_module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Microsoft" }, + { PW_KEY_MODULE_DESCRIPTION, "WSLg RDP Sink" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, "1.0" }, +}; + +static const struct spa_dict_item source_module_props[] = { + { PW_KEY_MODULE_AUTHOR, "Microsoft" }, + { PW_KEY_MODULE_DESCRIPTION, "WSLg RDP Source" }, + { PW_KEY_MODULE_USAGE, pulse_module_options }, + { PW_KEY_MODULE_VERSION, "1.0" }, +}; + +#define DEFAULT_RATE 44100 +#define DEFAULT_CHANNELS 2 +#define DEFAULT_LATENCY_USEC 10000 + +typedef struct _rdp_audio_cmd_header +{ + uint32_t cmd; + union { + uint32_t version; + struct { + uint32_t bytes; + uint64_t timestamp; + } transfer; + uint64_t reserved[8]; + }; +} rdp_audio_cmd_header; + +#define RDP_AUDIO_CMD_VERSION 0 +#define RDP_AUDIO_CMD_TRANSFER 1 +#define RDP_AUDIO_CMD_GET_LATENCY 2 +#define RDP_AUDIO_CMD_RESET_LATENCY 3 + +#define RDP_SINK_INTERFACE_VERSION 1 + +static uint64_t rdp_audio_timestamp(void) +{ + struct timeval tv; + gettimeofday(&tv, NULL); + return (uint64_t)tv.tv_sec * 1000000ull + (uint64_t)tv.tv_usec; +} + +static int send_all(int fd, const void *data, size_t bytes) +{ + const uint8_t *ptr = data; + size_t sent = 0; + while (sent < bytes) { + ssize_t res = send(fd, ptr + sent, bytes - sent, 0); + if (res <= 0) + return -1; + sent += (size_t)res; + } + return 0; +} + +static int connect_unix_socket(const char *path) +{ + struct sockaddr_un addr; + int fd = socket(PF_LOCAL, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + return fd; +} + +static size_t calc_frame_size(const struct spa_audio_info_raw *info) +{ + size_t bytes_per_sample = 0; + switch (info->format) { + case SPA_AUDIO_FORMAT_S16: + case SPA_AUDIO_FORMAT_S16_OE: + case SPA_AUDIO_FORMAT_U16: + bytes_per_sample = 2; + break; + case SPA_AUDIO_FORMAT_S24: + case SPA_AUDIO_FORMAT_S24_OE: + case SPA_AUDIO_FORMAT_U24: + bytes_per_sample = 3; + break; + case SPA_AUDIO_FORMAT_S24_32: + case SPA_AUDIO_FORMAT_S24_32_OE: + case SPA_AUDIO_FORMAT_S32: + case SPA_AUDIO_FORMAT_S32_OE: + case SPA_AUDIO_FORMAT_U32: + case SPA_AUDIO_FORMAT_U32_OE: + case SPA_AUDIO_FORMAT_F32: + case SPA_AUDIO_FORMAT_F32_OE: + bytes_per_sample = 4; + break; + case SPA_AUDIO_FORMAT_F64: + case SPA_AUDIO_FORMAT_F64_OE: + bytes_per_sample = 8; + break; + default: + bytes_per_sample = 2; + break; + } + return bytes_per_sample * info->channels; +} + +struct rdp_sink { + struct pw_context *context; + struct pw_impl_module *module; + struct pw_core *core; + struct pw_stream *stream; + struct spa_hook module_listener; + struct spa_hook stream_listener; + struct spa_hook core_listener; + struct spa_hook core_proxy_listener; + struct spa_audio_info_raw info; + size_t frame_size; + int fd; + int remote_version; + char *socket_path; +}; + +struct rdp_source { + struct pw_context *context; + struct pw_impl_module *module; + struct pw_core *core; + struct pw_stream *stream; + struct spa_hook module_listener; + struct spa_hook stream_listener; + struct spa_hook core_listener; + struct spa_hook core_proxy_listener; + struct spa_audio_info_raw info; + size_t frame_size; + int fd; + char *socket_path; +}; + +static void sink_module_destroy(void *data) +{ + struct rdp_sink *sink = data; + if (sink->stream) + pw_stream_destroy(sink->stream); + if (sink->core) + pw_core_disconnect(sink->core); + if (sink->fd != -1) + close(sink->fd); + free(sink->socket_path); + free(sink); +} + +static void source_module_destroy(void *data) +{ + struct rdp_source *source = data; + if (source->stream) + pw_stream_destroy(source->stream); + if (source->core) + pw_core_disconnect(source->core); + if (source->fd != -1) + close(source->fd); + free(source->socket_path); + free(source); +} + +static const struct pw_impl_module_events sink_module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = sink_module_destroy, +}; + +static const struct pw_impl_module_events source_module_events = { + PW_VERSION_IMPL_MODULE_EVENTS, + .destroy = source_module_destroy, +}; + +static void stream_sink_destroy(void *data) +{ + struct rdp_sink *sink = data; + spa_hook_remove(&sink->stream_listener); + sink->stream = NULL; +} + +static void stream_source_destroy(void *data) +{ + struct rdp_source *source = data; + spa_hook_remove(&source->stream_listener); + source->stream = NULL; +} + +static void sink_core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct rdp_sink *sink = data; + pw_log_error("rdp sink core error: %s", message); + if (id == PW_ID_CORE && res == -EPIPE) + pw_impl_module_schedule_destroy(sink->module); +} + +static void source_core_error(void *data, uint32_t id, int seq, int res, const char *message) +{ + struct rdp_source *source = data; + pw_log_error("rdp source core error: %s", message); + if (id == PW_ID_CORE && res == -EPIPE) + pw_impl_module_schedule_destroy(source->module); +} + +static const struct pw_core_events sink_core_events = { + PW_VERSION_CORE_EVENTS, + .error = sink_core_error, +}; + +static const struct pw_core_events source_core_events = { + PW_VERSION_CORE_EVENTS, + .error = source_core_error, +}; + +static int rdp_sink_connect(struct rdp_sink *sink) +{ + if (sink->fd != -1) + return 0; + + sink->fd = connect_unix_socket(sink->socket_path); + if (sink->fd < 0) + return -1; + + rdp_audio_cmd_header header = {0}; + header.cmd = RDP_AUDIO_CMD_VERSION; + header.version = RDP_SINK_INTERFACE_VERSION; + if (send_all(sink->fd, &header, sizeof(header)) < 0) + return -1; + + if (read(sink->fd, &sink->remote_version, sizeof(sink->remote_version)) != sizeof(sink->remote_version)) { + close(sink->fd); + sink->fd = -1; + return -1; + } + return 0; +} + +static void sink_stream_process(void *data) +{ + struct rdp_sink *sink = data; + struct pw_buffer *buf; + struct spa_data *bd; + uint32_t size; + + if (rdp_sink_connect(sink) < 0) + return; + + if ((buf = pw_stream_dequeue_buffer(sink->stream)) == NULL) + return; + + bd = &buf->buffer->datas[0]; + size = SPA_MIN(bd->chunk->size, bd->maxsize); + if (size > 0 && bd->data) { + rdp_audio_cmd_header header = {0}; + header.cmd = RDP_AUDIO_CMD_TRANSFER; + header.transfer.bytes = size; + header.transfer.timestamp = rdp_audio_timestamp(); + if (send_all(sink->fd, &header, sizeof(header)) < 0 || + send_all(sink->fd, SPA_PTROFF(bd->data, bd->chunk->offset, void), size) < 0) { + close(sink->fd); + sink->fd = -1; + } + } + + pw_stream_queue_buffer(sink->stream, buf); +} + +static const struct pw_stream_events sink_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_sink_destroy, + .process = sink_stream_process, +}; + +static int create_sink_stream(struct rdp_sink *sink) +{ + uint32_t n_params = 0; + const struct spa_pod *params[1]; + uint8_t buffer[256]; + struct spa_pod_builder b; + + sink->stream = pw_stream_new(sink->core, "wslg-rdp-sink", NULL); + if (sink->stream == NULL) + return -errno; + + pw_stream_add_listener(sink->stream, &sink->stream_listener, &sink_stream_events, sink); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &sink->info); + + return pw_stream_connect(sink->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params); +} + +static int rdp_source_connect(struct rdp_source *source) +{ + if (source->fd != -1) + return 0; + + source->fd = connect_unix_socket(source->socket_path); + if (source->fd < 0) + return -1; + + return 0; +} + +static void source_stream_process(void *data) +{ + struct rdp_source *source = data; + struct pw_buffer *buf; + struct spa_data *bd; + uint32_t size; + ssize_t bytes_read; + + if (rdp_source_connect(source) < 0) + return; + + if ((buf = pw_stream_dequeue_buffer(source->stream)) == NULL) + return; + + bd = &buf->buffer->datas[0]; + size = SPA_MIN(bd->maxsize, bd->chunk->size); + if (size == 0) + size = bd->maxsize; + + bytes_read = read(source->fd, bd->data, size); + if (bytes_read <= 0) { + close(source->fd); + source->fd = -1; + pw_stream_queue_buffer(source->stream, buf); + return; + } + + bd->chunk->size = (uint32_t)bytes_read; + bd->chunk->stride = source->frame_size; + bd->chunk->offset = 0; + + pw_stream_queue_buffer(source->stream, buf); +} + +static const struct pw_stream_events source_stream_events = { + PW_VERSION_STREAM_EVENTS, + .destroy = stream_source_destroy, + .process = source_stream_process, +}; + +static int create_source_stream(struct rdp_source *source) +{ + uint32_t n_params = 0; + const struct spa_pod *params[1]; + uint8_t buffer[256]; + struct spa_pod_builder b; + + source->stream = pw_stream_new(source->core, "wslg-rdp-source", NULL); + if (source->stream == NULL) + return -errno; + + pw_stream_add_listener(source->stream, &source->stream_listener, &source_stream_events, source); + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + params[n_params++] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &source->info); + + return pw_stream_connect(source->stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + params, n_params); +} + +static int parse_audio_info(struct spa_audio_info_raw *info, struct pw_properties *props) +{ + const char *str; + info->format = SPA_AUDIO_FORMAT_S16; + info->rate = DEFAULT_RATE; + info->channels = DEFAULT_CHANNELS; + info->position[0] = SPA_AUDIO_CHANNEL_FL; + info->position[1] = SPA_AUDIO_CHANNEL_FR; + + str = pw_properties_get(props, "rate"); + if (str) + info->rate = (uint32_t)atoi(str); + str = pw_properties_get(props, "channels"); + if (str) + info->channels = (uint32_t)atoi(str); + if (info->channels == 1) { + info->position[0] = SPA_AUDIO_CHANNEL_MONO; + info->position[1] = SPA_AUDIO_CHANNEL_UNKNOWN; + } + return 0; +} + +static int init_sink(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = pw_properties_new_string(args ? args : ""); + const char *sink_props = pw_properties_get(props, "sink_properties"); + const char *socket_path = pw_properties_get(props, "socket"); + struct rdp_sink *sink; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + sink = calloc(1, sizeof(*sink)); + if (sink == NULL) { + return -errno; + } + + sink->context = context; + sink->module = module; + sink->fd = -1; + sink->socket_path = strdup(socket_path ? socket_path : "/mnt/wslg/PulseAudioRDPSink"); + + res = parse_audio_info(&sink->info, props); + if (res < 0) + goto error; + + sink->frame_size = calc_frame_size(&sink->info); + + sink->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core); + if (sink->core == NULL) { + sink->core = pw_context_connect(context, NULL, 0); + } + if (sink->core == NULL) { + res = -errno; + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)sink->core, &sink->core_proxy_listener, &core_proxy_events, sink); + pw_core_add_listener(sink->core, &sink->core_listener, &sink_core_events, sink); + + res = create_sink_stream(sink); + if (res < 0) + goto error; + + if (sink_props) { + struct pw_properties *update_props = pw_properties_new_string(sink_props); + if (update_props) + pw_stream_update_properties(sink->stream, update_props); + } + + pw_impl_module_add_listener(module, &sink->module_listener, &sink_module_events, sink); + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(sink_module_props)); + pw_impl_module_update_properties(module, &props->dict); + pw_properties_free(props); + return 0; + +error: + pw_properties_free(props); + sink_module_destroy(sink); + return res; +} + +static int init_source(struct pw_impl_module *module, const char *args) +{ + struct pw_context *context = pw_impl_module_get_context(module); + struct pw_properties *props = pw_properties_new_string(args ? args : ""); + const char *source_props = pw_properties_get(props, "source_properties"); + const char *socket_path = pw_properties_get(props, "socket"); + struct rdp_source *source; + int res; + + PW_LOG_TOPIC_INIT(mod_topic); + + source = calloc(1, sizeof(*source)); + if (source == NULL) { + return -errno; + } + + source->context = context; + source->module = module; + source->fd = -1; + source->socket_path = strdup(socket_path ? socket_path : "/mnt/wslg/PulseAudioRDPSource"); + + res = parse_audio_info(&source->info, props); + if (res < 0) + goto error; + + source->frame_size = calc_frame_size(&source->info); + + source->core = pw_context_get_object(context, PW_TYPE_INTERFACE_Core); + if (source->core == NULL) { + source->core = pw_context_connect(context, NULL, 0); + } + if (source->core == NULL) { + res = -errno; + goto error; + } + + pw_proxy_add_listener((struct pw_proxy*)source->core, &source->core_proxy_listener, &core_proxy_events, source); + pw_core_add_listener(source->core, &source->core_listener, &source_core_events, source); + + res = create_source_stream(source); + if (res < 0) + goto error; + + if (source_props) { + struct pw_properties *update_props = pw_properties_new_string(source_props); + if (update_props) + pw_stream_update_properties(source->stream, update_props); + } + + pw_impl_module_add_listener(module, &source->module_listener, &source_module_events, source); + pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(source_module_props)); + pw_impl_module_update_properties(module, &props->dict); + pw_properties_free(props); + return 0; + +error: + pw_properties_free(props); + source_module_destroy(source); + return res; +} + +SPA_EXPORT +int pipewire__module_init(struct pw_impl_module *module, const char *args) +{ +#ifdef WSLG_RDP_MODE_SOURCE + return init_source(module, args); +#else + return init_sink(module, args); +#endif +} diff --git a/config/pipewire-wslg-rdp.patch b/config/pipewire-wslg-rdp.patch new file mode 100644 index 00000000..b2c850c2 --- /dev/null +++ b/config/pipewire-wslg-rdp.patch @@ -0,0 +1,36 @@ +diff --git a/src/modules/meson.build b/src/modules/meson.build +index 3d97bb5..e6f7b15 100644 +--- a/src/modules/meson.build ++++ b/src/modules/meson.build +@@ -11,6 +11,7 @@ module_sources = [ + 'module-combine-stream.c', + 'module-echo-cancel.c', + 'module-example-filter.c', ++ 'module-wslg-rdp.c', + 'module-example-sink.c', + 'module-example-source.c', + 'module-fallback-sink.c', +@@ -462,6 +463,22 @@ pipewire_module_example_sink = shared_library('pipewire-module-example-source', + install_rpath: modules_install_dir, + dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], + ) ++ ++pipewire_module_wslg_rdp_sink = shared_library('pipewire-module-wslg-rdp-sink', ++ [ 'module-wslg-rdp.c' ], ++ c_args : ['-DWSLG_RDP_MODE_SINK'], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], ++) ++ ++pipewire_module_wslg_rdp_source = shared_library('pipewire-module-wslg-rdp-source', ++ [ 'module-wslg-rdp.c' ], ++ c_args : ['-DWSLG_RDP_MODE_SOURCE'], ++ include_directories : [configinc], ++ install : true, ++ install_dir : modules_install_dir, ++ install_rpath: modules_install_dir, ++ dependencies : [spa_dep, mathlib, dl_lib, pipewire_dep], ++) diff --git a/config/pipewire.conf b/config/pipewire.conf new file mode 100644 index 00000000..2fbff7f0 --- /dev/null +++ b/config/pipewire.conf @@ -0,0 +1,98 @@ +# Daemon config file for PipeWire version 1.4.10 +# +# Based on PipeWire upstream pipewire.conf and tailored for WSLg. +# + +context.properties = { + log.level = 2 + core.daemon = true + core.name = pipewire-0 + module.session-manager = false +} + +context.spa-libs = { + audio.convert.* = audioconvert/libspa-audioconvert + support.* = support/libspa-support +} + +context.modules = [ + { name = libpipewire-module-rt + args = { + nice.level = -11 + rt.prio = 88 + } + flags = [ ifexists nofail ] + condition = [ { module.rt = !false } ] + } + { name = libpipewire-module-protocol-native } + { name = libpipewire-module-profiler + condition = [ { module.profiler = !false } ] + } + { name = libpipewire-module-metadata + condition = [ { module.metadata = !false } ] + } + { name = libpipewire-module-spa-device-factory + condition = [ { module.spa-device-factory = !false } ] + } + { name = libpipewire-module-spa-node-factory + condition = [ { module.spa-node-factory = !false } ] + } + { name = libpipewire-module-client-node + condition = [ { module.client-node = !false } ] + } + { name = libpipewire-module-client-device + condition = [ { module.client-device = !false } ] + } + { name = libpipewire-module-portal + flags = [ ifexists nofail ] + condition = [ { module.portal = !false } ] + } + { name = libpipewire-module-access + condition = [ { module.access = !false } ] + } + { name = libpipewire-module-adapter + condition = [ { module.adapter = !false } ] + } + { name = libpipewire-module-link-factory + condition = [ { module.link-factory = !false } ] + } + { name = libpipewire-module-session-manager + condition = [ { module.session-manager = !false } ] + } + { name = libpipewire-module-x11-bell + flags = [ ifexists nofail ] + condition = [ { module.x11.bell = !false } ] + } +] + +context.objects = [ + { factory = metadata + args = { + metadata.name = default + } + } + { factory = spa-node-factory + args = { + factory.name = support.node.driver + node.name = Dummy-Driver + node.group = pipewire.dummy + node.sync-group = sync.dummy + priority.driver = 200000 + } + condition = [ { factory.dummy-driver = !false } ] + } + { factory = spa-node-factory + args = { + factory.name = support.node.driver + node.name = Freewheel-Driver + priority.driver = 190000 + node.group = pipewire.freewheel + node.sync-group = sync.dummy + node.freewheel = true + } + condition = [ { factory.freewheel-driver = !false } ] + } +] + +context.exec = [ +] diff --git a/samples/container/Containers.md b/samples/container/Containers.md index 89acaeb2..e784e42b 100644 --- a/samples/container/Containers.md +++ b/samples/container/Containers.md @@ -1,6 +1,6 @@ # Containerizing GUI applications with WSLg -For containerized applications to work properly under WSLg developers need to be aware of a few peculiarity of our environment in order to allow applications to properly connect to our X11, Wayland or PulseAudio server or to use the vGPU. +For containerized applications to work properly under WSLg developers need to be aware of a few peculiarity of our environment in order to allow applications to properly connect to our X11, Wayland or PipeWire (PulseAudio-compatible) server or to use the vGPU. ## Containerized GUI applications connecting to X11, Wayland or Pulse server @@ -10,7 +10,7 @@ In order for a containerized application to access the servers provided by WSLg, |---|---| | X11 | ```/tmp/.X11-unix``` | | Wayland | ```/mnt/wslg``` | -| PulseAudio | ```/mnt/wslg``` | +| PipeWire (PulseAudio-compatible) | ```/mnt/wslg``` | And the following environment variable must be share with the container. @@ -18,7 +18,7 @@ And the following environment variable must be share with the container. |---|---| | X11 | ```DISPLAY``` | | Wayland | ```WAYLAND_DISPLAY``` && ```XDG_RUNTIME_DIR``` | -| PulseAudio | ```PULSE_SERVER``` | +| PipeWire (PulseAudio-compatible) | ```PULSE_SERVER``` | For example, to run ```xclock``` as a containerized application, the following docker file can be use. @@ -42,7 +42,7 @@ sudo docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -v /mnt/wslg:/mnt/wslg \ -e XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR -e PULSE_SERVER=$PULSE_SERVER xclock ``` -Please note that in this example we make all servers visible to ```xclock``` even though it only uses the X11 server and will not make use of the Wayland or PulseAudio servers. This is for illustrative purposes only. There is no real harm in exposing a server that is unused by an application. However it is good practice to only exposed containerized application to the resource they need. In this case we could have launch the containerized version of ```xclock``` with the following minimal command line. +Please note that in this example we make all servers visible to ```xclock``` even though it only uses the X11 server and will not make use of the Wayland or PipeWire (PulseAudio-compatible) servers. This is for illustrative purposes only. There is no real harm in exposing a server that is unused by an application. However it is good practice to only exposed containerized application to the resource they need. In this case we could have launch the containerized version of ```xclock``` with the following minimal command line. ``` sudo docker run -it -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY xclock