Skip to content

Desktop app for Linux (PipeWire) - #8

Merged
Lyten02 merged 6 commits into
mainfrom
feat/linux-pipewire
Oct 31, 2026
Merged

Lyten02 merged 6 commits into
mainfrom
feat/linux-pipewire

Conversation

@Lyten02

@Lyten02 Lyten02 commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

The desktop app now runs on Linux with PipeWire (tested target: Arch / Omarchy, Hyprland), in both directions, with the same protocol v3: phones on 0.4.0 work with it unchanged. Windows code is unchanged apart from moving shared bits (DeviceSummary, tray refresh, current_devices) behind a common API.

Linux (crates/desktop/src/linux/)

  • PC audio → phone: a PipeWire capture stream of the default sink's monitor (stream.capture.sink, no target, so WirePlumber links it and moves it with the default). Runs only while a phone is connected and "Звук ПК" is on.
  • Phone mic → PC: AudioBridge's own source node audiobridge_mic («AudioBridge: микрофон телефона», Audio/Source + node.virtual). No driver; it exists while the service runs and disappears with the process (also on a crash). Mic demand = the node is actually being recorded from (stream state Streaming).
  • "Микрофон по умолчанию" / one-switch "Микрофон" / service off: default.configured.audio.source in the default metadata, with the same remember/restore/resume logic as Windows.
  • PC volume/mute: read from the default sink's Props, set with wpctl; external changes are reported to the phone.
  • Headphone buttons: MPRIS (zbus), exactly one target player; the phone sees app/title/artist.
  • Tray (StatusNotifierItem via ksni, optional), XDG autostart, single instance over a Unix socket (--quit to stop the running one), XDG data/log dirs, PRETTY_HOSTNAME as the PC name, SIGTERM/SIGINT/SIGHUP clean shutdown.
  • UI: Linux wording, system fonts (Noto/DejaVu/Liberation + a fallback for "→"), app id audiobridge.
  • One PipeWire main loop thread, event-driven, reconnects if PipeWire restarts. RT callbacks don't allocate or block.

Packaging and CI

  • scripts/package-linux.sh: AudioBridge-<ver>-linux-x86_64.tar.gz (binary, launcher entry, icon, install.sh for a per-user install in ~/.local) and audiobridge-<ver>-1-x86_64.pkg.tar.zst (pacman; depends on pipewire, wireplumber, libglvnd, libxkbcommon, wayland).
  • CI: new linux job on the self-hosted runner (clippy, tests, release build, package, artifact AudioBridge-linux-x86_64). The runner gets the PipeWire SDK without root from ~/opt/pipewire-sdk/env.sh.
  • Docs: README (EN/RU), AGENTS.md, CONTRIBUTING.md.

How it was checked (all on the server)

  • cargo clippy -p audiobridge-desktop --all-targets -D warnings on Linux and with cargo xwin for Windows; cargo xwin build --release; cargo test -p audiobridge-desktop (Linux: 15 tests incl. new graph/volume/MPRIS/paths/autostart ones); core lib + rt_alloc; android-native host tests; :app:assembleDebug :app:testDebugUnitTest.
  • Headless end-to-end against a private PipeWire 1.4 + WirePlumber 0.5 (null sink as speakers), with examples/probe as the phone:
    • 1 kHz tone played on the PC arrives at the phone at −9.1 dBFS (the tone's level), 203 kbit/s, 0 underruns;
    • the phone's 440 Hz mic recorded from audiobridge_mic with pw-record: 440 Hz at −9.0 dBFS; mic demand: true while recording, false after;
    • remote control from an emulated phone: MicDefault on/off switches the configured default source, Volume 30 → wpctl shows 0.30, mute/unmute, Audio off/on stops/starts the loopback; an outside wpctl set-volume 0.8 is reported as 80;
    • service stop with the virtual mic as default restores the user's mic and resume_mic_default, the next start makes it the default again;
    • --quit and kill -9 both leave no audiobridge_* nodes behind.
  • On a private session bus with a fake panel and a fake MPRIS player: the tray item registers with title, tooltip, icons and the full menu; headphone commands PlayPause/Next/Play/Previous reach the player (Play while playing is skipped as redundant), the phone gets app/title/artist.
  • The egui window under Xvfb (software GL): pairing screen, connected screen with live stream tiles.
  • Not checked here (no hardware on the server): real sound card and Bluetooth, a real phone; that's the next step on the PC/laptop.

Found while testing: WirePlumber 0.5 configures Audio/Source/Virtual nodes with input ports (null-sink style); for a stream-backed source that never completes, and a mono one stalls all of WirePlumber's linking. Hence Audio/Source + node.virtual (documented in AGENTS.md).

Fixes after the first install on real Omarchy/Hyprland machines

  • install.sh falsely reported missing PipeWire (d9dddb8): ldconfig -p | grep -q under pipefail — grep -q closes the pipe, ldconfig dies of SIGPIPE, the pipeline fails. grep now reads the whole output. The per-user install then succeeded on both machines.
  • The window froze on a hidden workspace (74d1faa): with EGL vsync, a repaint while the window is on another workspace (device change, streaming, "open" from the tray or a second launch) blocked the UI thread in the EGL swap, waiting for a frame callback the compositor never sends to hidden windows. The app stopped answering compositor pings — Hyprland then offers to kill the "not responding" app with SIGKILL, so nothing is cleaned up and the phone mic stays the configured default — and SIGTERM/"Выход" could not finish until the window was shown again. Linux now renders without vsync (the UI repaints only on changes).
    • Checked with the release binary under headless sway (Wayland, pixman) + private PipeWire: window on a hidden workspace, then a device added / a phone streaming / a show request. Before: the main thread sat in poll inside libEGL's wl_display_dispatch_queue until the window was shown, SIGTERM did not exit within 10 s. After: the main thread stays in the event loop (epoll_pwait), SIGTERM exits in ~0.1 s with the user's mic restored. Compositor close → reopen → --quit still work; the X11 window still renders; CPU while streaming stays low (no busy loop).
    • Note for diagnosing: after a kill -9 with the phone mic as default, wpctl status still shows audiobridge_mic under "Default Configured Devices". That is the remembered default in the metadata, not a leftover node (pw-cli ls Node has none).

Release notes draft

Что нового: AudioBridge для Linux (PipeWire). Звук компьютера идёт на телефон, микрофон телефона появляется в Linux как «AudioBridge: микрофон телефона» — без драйверов. Пульт с телефона (громкость, mute, микрофон по умолчанию), кнопки наушников управляют плеерами через MPRIS, значок в трее, автозапуск. Пакет для Arch/Omarchy (.pkg.tar.zst) и архив с install.sh для любого дистрибутива. Телефон обновлять не нужно: протокол прежний (v3).

AudioBridge for Linux (PipeWire): PC audio to the phone, the phone mic as a PipeWire source, remote control, MPRIS media keys, tray, autostart. Pacman package and a tarball with a per-user installer. Same protocol v3: no phone update needed.

… a PipeWire source

- crates/desktop/src/linux: PipeWire engine (default-sink monitor capture, virtual mic
  audiobridge_mic, mic demand, default source, volume), MPRIS media keys, SNI tray, XDG
  autostart/paths, single instance (--quit); Windows modules behind cfg(windows)
- packaging: scripts/package-linux.sh (tar.gz + install.sh, pacman package), launcher entry, icon
- CI: linux job on the self-hosted runner; docs: README, AGENTS.md, CONTRIBUTING.md
Copilot AI balanced review requested due to automatic review settings October 30, 2026 18:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

With EGL vsync the swap waits for a frame callback that compositors do not
send to a window on a hidden workspace. Any repaint there (device change,
streaming, show request) blocked the UI thread in wl_display_dispatch_queue:
compositor pings went unanswered, Hyprland offered to kill the app with
SIGKILL (no cleanup, phone mic left as the configured default), and SIGTERM
or Выход could not finish until the window was shown again.

Linux now renders without vsync; the UI repaints only on changes.
@Lyten02
Lyten02 merged commit 5c4ab34 into main Oct 31, 2026
5 checks passed
@Lyten02
Lyten02 deleted the feat/linux-pipewire branch October 31, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants