Compare commits

...
5 Commits
Author SHA1 Message Date
mollusk 51a480a311 docs(windows): record 0.6.8 viewer smoke
CI / check (push) Failing after 1m22s
2026-08-22 21:25:54 -04:00
mollusk 437c95380e feat(windows): bundle PixelPass viewer 2026-08-22 21:02:32 -04:00
mollusk f2220ed84e docs(windows): record 0.6.7 VM smoke 2026-08-22 19:36:46 -04:00
mollusk 7217015d1f fix(windows): keep firewall install idempotent 2026-08-22 19:27:10 -04:00
mollusk 6b9a72aee5 fix(windows): restore screen-share cross-build 2026-08-22 18:57:29 -04:00
13 changed files with 366 additions and 101 deletions
+1
View File
@@ -10,6 +10,7 @@
# Windows installer build artifacts (the staged exe + compiled setup.exe); # Windows installer build artifacts (the staged exe + compiled setup.exe);
# the .iss script and .ico are the tracked sources. # the .iss script and .ico are the tracked sources.
/packaging/windows/peerspeak.exe /packaging/windows/peerspeak.exe
/packaging/windows/pixelpass.exe
/packaging/windows/output/ /packaging/windows/output/
# Nix: the symlink `nix build` drops, and direnv's local cache. flake.nix and # Nix: the symlink `nix build` drops, and direnv's local cache. flake.nix and
+16
View File
@@ -4,6 +4,22 @@ All notable changes to PeerSpeak are documented here.
## [Unreleased] ## [Unreleased]
### Added
- **The Windows installer now includes the matching PixelPass viewer.** Windows
participants can watch a screen share hosted by Linux through the existing
PeerSpeak Watch flow without separately installing PixelPass.
### Changed
- **Windows presents screen sharing as viewer-only.** Watch remains available,
while the local share control explains that Windows capture/hosting is not
implemented yet instead of starting an unsupported helper mode.
- **The installer owns both networking rules.** Idempotent PeerSpeak and
PixelPass firewall rules are installed together and removed on uninstall.
### Compatibility
- **Wire-compatible with 0.6.7.** The new Windows helper bundle and UI boundary
do not change PeerSpeak or PixelPass wire formats.
## [0.6.7] — 2026-08-22 ## [0.6.7] — 2026-08-22
### Changed ### Changed
Generated
+1 -1
View File
@@ -4871,7 +4871,7 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]] [[package]]
name = "peerspeak" name = "peerspeak"
version = "0.6.7" version = "0.6.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "peerspeak" name = "peerspeak"
version = "0.6.7" version = "0.6.8"
edition = "2024" edition = "2024"
description = "Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)" description = "Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
license = "MIT" license = "MIT"
+107 -24
View File
@@ -1,10 +1,15 @@
# PeerSpeak on Windows # PeerSpeak on Windows
Current status: PeerSpeak cross-compiles to `x86_64-pc-windows-gnu` from Linux and Current status: PeerSpeak cross-compiles to `x86_64-pc-windows-gnu` from Linux.
has passed an older native Windows 11 VM smoke test for launch, GUI render, call PeerSpeak v0.6.7 at commit `7217015d` passed a fresh Windows 10 22H2/build 19045
join, and audio flow. The build environment is **not** the Windows VM; current VM smoke test on 2026-08-22 for install/upgrade, launch, GUI/settings render,
Windows binaries are built from Linux, normally inside the `peerspeak-win` device selection and persistence, microphone capture startup, WASAPI playback
distrobox or with the same GNU target environment. pacing, room create/leave, and the missing-PixelPass guard. The v0.6.8 candidate
at commit `437c9538` passed its installer/helper/UI smoke on the same VM and adds
the matching viewer-only PixelPass helper: Windows can watch Linux-hosted shares,
while Windows capture/hosting remains intentionally unavailable. The build
environment is **not** the Windows VM; current Windows binaries are built from
Linux with the GNU target environment.
The Windows runtime still trails Linux in a few important areas. See the Claude The Windows runtime still trails Linux in a few important areas. See the Claude
handoff file `windows-parity-audit.md` for the full audit and task breakdown. handoff file `windows-parity-audit.md` for the full audit and task breakdown.
@@ -13,8 +18,8 @@ handoff file `windows-parity-audit.md` for the full audit and task breakdown.
| Area | Status | | Area | Status |
|---|---| |---|---|
| GUI | Iced/wgpu builds for Windows and rendered in the Windows 11 VM. | | GUI | Iced/wgpu renders on Windows 10; the current build's landing and settings screens were smoke-tested. |
| Networking | Iroh QUIC transport and gossip compile on Windows; VM call reached two peers. | | Networking | Iroh QUIC transport and gossip compile on Windows; the current build creates/leaves a room, and an older VM build completed a two-peer call. |
| Audio backend | `cpal` drives WASAPI capture/playback behind `AudioBackend`. | | Audio backend | `cpal` drives WASAPI capture/playback behind `AudioBackend`. |
| Device selection | cpal enumerates input/output devices; see caveat below about stable IDs. | | Device selection | cpal enumerates input/output devices; see caveat below about stable IDs. |
| Resampling/remap | WASAPI devices can run non-48 kHz formats; PeerSpeak converts at the backend boundary. | | Resampling/remap | WASAPI devices can run non-48 kHz formats; PeerSpeak converts at the backend boundary. |
@@ -23,6 +28,7 @@ handoff file `windows-parity-audit.md` for the full audit and task breakdown.
| Chimes | Windows uses PowerShell `System.Media.SoundPlayer` for WAV playback. | | Chimes | Windows uses PowerShell `System.Media.SoundPlayer` for WAV playback. |
| Game detection | Steam registry `RunningAppID` plus Toolhelp process-scan fallback compile on Windows. | | Game detection | Steam registry `RunningAppID` plus Toolhelp process-scan fallback compile on Windows. |
| File dialogs | `rfd` uses the native Win32 dialog backend. | | File dialogs | `rfd` uses the native Win32 dialog backend. |
| Screen-share viewing | The installer bundles the matching PixelPass viewer; VLC or mpv renders Linux-hosted shares. |
Windows paths are resolved through `dirs`: Windows paths are resolved through `dirs`:
@@ -69,35 +75,112 @@ cargo build --release
## First run and networking ## First run and networking
Expect a Windows Firewall prompt the first time the app opens network sockets, or Expect a Windows Firewall prompt the first time the app opens network sockets, or
use the Inno installer option that pre-adds a firewall allow rule. PeerSpeak uses use the Inno installer option that pre-adds idempotent PeerSpeak and PixelPass
UDP for QUIC plus relay traffic when direct NAT traversal is unavailable. firewall allow rules. Both use UDP/QUIC plus relay traffic when direct NAT
traversal is unavailable.
The default network mode keeps the n0 relay available for NAT traversal without The default network mode keeps the n0 relay available for NAT traversal without
publishing presence to n0 DNS. Relayed connections are expected and valid. publishing presence to n0 DNS. Relayed connections are expected and valid.
## Fresh Windows 10 smoke — 2026-08-22
Validated on Windows 10 Enterprise Evaluation 22H2/build 19045 in the
`peerspeak-windows10` libvirt VM. The installed title screen reported
`PeerSpeak v0.6.7 (7217015d)`.
Definitive artifacts built from committed `main`:
- `peerspeak.exe`: 80,601,595 bytes, SHA-256
`a9225ca8878acdbc54a424427a591b3f55774a9fc4d20decd40993274b9336ff`
- `peerspeak-0.6.7-setup.exe`: 18,578,658 bytes, SHA-256
`5ac551709814918f9c75a49aabed929d3f6618cdbeaf3cb4b22add8dfcbb68d9`
Proven in this run:
- The final installer transferred with a matching Windows-side hash, installed
under `C:\Program Files\PeerSpeak`, and upgraded in place without losing config.
- Upgrade testing exposed duplicate firewall rules. Commit `7217015d` made the
installer replace its owned rule; a final upgrade left exactly one enabled
inbound `PeerSpeak` rule and logged exit code 0 for delete/add.
- The landing screen and all Settings categories rendered. Input `Line In (High
Definition Audio Device)` and output `Speakers (High Definition Audio Device)`
were selectable and restored after a clean app restart.
- Test Mic opened the real cpal/WASAPI capture path. The VM endpoint negotiated
44.1 kHz stereo and PeerSpeak resampled it to 48 kHz as designed.
- `audio_probe.exe 440 30` exited 0. After one startup-fill event, every
steady-state health line reported `underrun +0` and `dropped +0`; the expected
drain after tone generation stopped registered at shutdown. No human audible
tone assessment was available during this automated run.
- A one-participant room connected and left cleanly. With no `pixelpass.exe`,
Share Screen opened the dependency instructions rather than crashing.
- The app log ended with the correct version/hash and Windows Event Viewer had
no PeerSpeak application errors.
Still manual or two-party: current-build Linux↔Windows audio in both directions,
chimes, chat/files/music/recording, friends/presence, game detection, audible
probe quality, and uninstall/firewall cleanup.
## v0.6.8 viewer/installer smoke — 2026-08-22
Built from committed PeerSpeak source `437c9538` and committed PixelPass source
`ca3122b`:
- `peerspeak.exe`: 80,997,180 bytes, SHA-256
`952e77a0693d0e12390c8f39c5fbc3233b19eb069a31f49a922954692b2705e8`
- `pixelpass.exe`: 13,165,056 bytes, SHA-256
`3eabd9f0dcd8565136a5c87a79470eceedd426cea5708904d7492a6fa37b3c49`
- `peerspeak-0.6.8-setup.exe`: 22,273,495 bytes, SHA-256
`8e23f26d758bd803748dee6d8316f617e0a7b8e31fa1b21ca754eeae846d819f`
Proven in this run:
- The installer transferred to Windows with an exact matching hash, upgraded
the existing install in place, and installed both payload binaries with hashes
matching the staged build artifacts.
- The title screen reported `PeerSpeak v0.6.8 (437c9538)`. A solo room created
and left cleanly, and the log recorded the matching version/hash.
- Windows displayed **Sharing unavailable** instead of attempting capture. Its
explainer accurately said the bundled helper can watch Linux-hosted shares and
that Windows hosting is not available yet.
- The installer left exactly one enabled inbound `PeerSpeak` rule and one
enabled inbound `PeerSpeak PixelPass` rule, each pointing at its installed
binary. The rules cover Domain, Private, and Public profiles, matching the
existing installer policy.
- The installed helper reported the viewer-only capability envelope, completed
`--doctor` with exit 0, and refused `--host` with exit 1.
- The exact PixelPass payload hash above had already consumed a fresh ticket
from a live Linux PixelPass host and rendered the Linux desktop in VLC. Closing
VLC reaped the viewer and notified the host. This proves the helper/wire/player
path; the final in-room PeerSpeak **Watch** click and a human audio check remain
separate manual gates.
## Known gaps ## Known gaps
| Item | Status | | Item | Status |
|---|---| |---|---|
| Echo cancellation | Linux-only today. The Windows UI shows it disabled as unavailable. | | Echo cancellation | Linux-only today. The Windows UI shows it disabled as unavailable. |
| Screen share | Blocked by PixelPass, which is currently Linux-only in practice. PeerSpeak can spawn `pixelpass.exe`, but there is no Windows PixelPass host/viewer parity yet. | | Screen share | Viewing Linux-hosted shares is implemented with the bundled PixelPass helper. Windows capture/hosting remains pending. |
| Device persistence | Uses cpal friendly names as keys. These can duplicate or change across Windows driver/profile changes; stable WASAPI endpoint IDs are still needed. | | Device persistence | Uses cpal friendly names as keys. These can duplicate or change across Windows driver/profile changes; stable WASAPI endpoint IDs are still needed. |
| Release hygiene | Keep `.iss` and installer output in sync with `Cargo.toml`; rebuild Windows artifacts during each release. | | Release hygiene | Keep `.iss` and installer output in sync with `Cargo.toml`; rebuild Windows artifacts during each release. |
| Runtime coverage | The Windows VM smoke test proved an older tester build. Current `main` needs a fresh VM smoke matrix before calling parity current. | | Runtime coverage | The 0.6.8 candidate passed the bounded installer/helper/UI smoke above; the remaining two-party and feature-specific rows still need coverage. |
## Current smoke checklist ## Current smoke checklist
Before calling a Windows build current, verify on the Windows VM or real Windows Before calling Windows feature parity complete, finish these checks on the
hardware: Windows VM or real Windows hardware. The original checks below passed for
v0.6.7 at `7217015d`; screen-share rows track the v0.6.8 candidate separately:
- Launch current `peerspeak.exe`; GUI renders and settings open. - [x] Launch current `peerspeak.exe`; GUI renders and settings open.
- Run `audio_probe.exe 440 30`; listen for glitches and inspect `playout-health`. - [ ] Run `audio_probe.exe 440 30`; automated `playout-health` passed, but listen for glitches on physical hardware.
- Create/join a Linux <-> Windows room; confirm mic and playback both directions. - [ ] Create/join a Linux <-> Windows room; confirm mic and playback both directions.
- Select input/output devices, restart, and confirm selections persist or fall back clearly. - [x] Select input/output devices, restart, and confirm selections persist or fall back clearly.
- Play chimes and custom chime paths. - [ ] Play chimes and custom chime paths.
- Send chat, image/file attachments, and save an attachment through the native dialog. - [ ] Send chat, image/file attachments, and save an attachment through the native dialog.
- Import/play/share/listen to music from Windows file paths. - [ ] Import/play/share/listen to music from Windows file paths.
- Record mixed/stems/both and inspect the WAV output path. - [ ] Record mixed/stems/both and inspect the WAV output path.
- Exercise friends/presence/recents and the clock-skew banner. - [ ] Exercise friends/presence/recents and the clock-skew banner.
- Test Steam and non-Steam game detection on a real Windows Steam install. - [ ] Test Steam and non-Steam game detection on a real Windows Steam install.
- Install/upgrade/uninstall through the Inno installer, including firewall rule cleanup. - [x] Run the Windows PixelPass viewer against a live Linux PixelPass host; video rendered and the viewer exited when VLC closed.
- [ ] Watch a Linux-hosted share through the installed PeerSpeak UI; confirm video and audio on real Windows hardware.
- [x] Confirm Windows clearly refuses screen-share hosting without treating the bundled helper as missing.
- [ ] Install/upgrade/uninstall through the Inno installer, including firewall rule cleanup. Clean install and idempotent 0.6.8 upgrade passed; uninstall remains.
+18 -3
View File
@@ -21,8 +21,9 @@ it once, then you and I connect directly to each other.
4. The setup window opens. Just keep clicking **Next**. Two checkboxes you'll 4. The setup window opens. Just keep clicking **Next**. Two checkboxes you'll
see along the way: see along the way:
- **"Allow PeerSpeak through Windows Firewall"** — leave this **checked** - **"Allow PeerSpeak and PixelPass through Windows Firewall"** — leave this
(it lets the call connect without interruptions). **checked** (it lets calls and incoming screen shares connect without
interruptions).
- **"Create a desktop shortcut"** — check it if you'd like an icon on your - **"Create a desktop shortcut"** — check it if you'd like an icon on your
desktop. desktop.
@@ -69,7 +70,19 @@ Either way works the same; it just depends on who makes the room.
--- ---
## 4. Chatting and sharing photos/files ## 4. Watching a shared screen
This Windows build can watch a screen shared by someone using PeerSpeak on
Linux. Install [VLC](https://www.videolan.org/vlc/) (or mpv) first. When someone
starts sharing, click **Watch** next to their name; the shared desktop opens in
the player.
Starting your own screen share from Windows is not available yet. The Windows
build labels that control **Sharing unavailable** while keeping Watch enabled.
---
## 5. Chatting and sharing photos/files
There's a **text chat** box at the bottom of the call window — type a message There's a **text chat** box at the bottom of the call window — type a message
and press **Enter** to send it to everyone in the room. and press **Enter** to send it to everyone in the room.
@@ -102,6 +115,8 @@ A couple of notes:
ticket — they're meant to be used right away. Also make sure we're both on the ticket — they're meant to be used right away. Also make sure we're both on the
**same version** — if I've sent you an updated installer, install it (an old **same version** — if I've sent you an updated installer, install it (an old
version and a new one can't connect to each other). version and a new one can't connect to each other).
- **"Watch doesn't open anything."** Install VLC, restart PeerSpeak, and try
Watch again. The PixelPass viewer is already included with PeerSpeak.
- **The blue warning again.** Same as install: **More info → Run anyway**. It's - **The blue warning again.** Same as install: **More info → Run anyway**. It's
the unsigned-app warning, not malware. the unsigned-app warning, not malware.
+19 -10
View File
@@ -3,11 +3,12 @@
This directory builds a Windows setup installer for PeerSpeak using This directory builds a Windows setup installer for PeerSpeak using
[Inno Setup](https://jrsoftware.org/isinfo.php). [Inno Setup](https://jrsoftware.org/isinfo.php).
PeerSpeak ships as a **single self-contained `peerspeak.exe`** — the GUI icon, PeerSpeak itself is a **self-contained `peerspeak.exe`** — the GUI icon,
notification chimes, and avatar presets are all embedded in the binary notification chimes, and avatar presets are embedded in the binary
(`include_bytes!`), and the executable is statically linked against the GNU (`include_bytes!`), and the executable is statically linked against the GNU
runtime, so there are no extra DLLs to bundle. The installer payload is just the runtime. The installer also bundles the matching viewer-only `pixelpass.exe`,
`.exe` plus an `.ico` for the Start-menu / desktop shortcuts. which lets Windows participants watch shares hosted by Linux. VLC or mpv is
still required to render the received stream.
## Version compatibility ## Version compatibility
@@ -30,6 +31,7 @@ per-peer noise-gate release; it cannot connect to a 0.2.x peer.)
| `peerspeak.ico` | yes | multi-resolution app icon (from `assets/icons/*.png`) | | `peerspeak.ico` | yes | multi-resolution app icon (from `assets/icons/*.png`) |
| `README.md` | yes | this file | | `README.md` | yes | this file |
| `peerspeak.exe` | no (gitignored) | staged build artifact, copied from `target/x86_64-pc-windows-gnu/release/` | | `peerspeak.exe` | no (gitignored) | staged build artifact, copied from `target/x86_64-pc-windows-gnu/release/` |
| `pixelpass.exe` | no (gitignored) | matching staged Windows viewer from the PixelPass repository |
| `output/peerspeak-<ver>-setup.exe` | no (gitignored) | the compiled installer | | `output/peerspeak-<ver>-setup.exe` | no (gitignored) | the compiled installer |
## Build steps ## Build steps
@@ -45,12 +47,19 @@ per-peer noise-gate release; it cannot connect to a 0.2.x peer.)
installed in that toolchain. The result is a statically-linked, installed in that toolchain. The result is a statically-linked,
GUI-subsystem `.exe` (no stray console window). GUI-subsystem `.exe` (no stray console window).
2. **Stage the binary** next to the script: 2. **Cross-compile PixelPass's Windows viewer** from the matching PixelPass
source, then stage both binaries next to the script:
```sh ```sh
cp target/x86_64-pc-windows-gnu/release/peerspeak.exe packaging/windows/ cp target/x86_64-pc-windows-gnu/release/peerspeak.exe packaging/windows/
cp ../pixelpass/target/x86_64-pc-windows-gnu/release/pixelpass.exe \
packaging/windows/
``` ```
PixelPass may use a separate target directory on low-space builders; copy
the final `pixelpass.exe` from that directory instead. Do not substitute an
older helper without re-running the ticket/viewer compatibility test.
3. **Regenerate the icon** if the source PNGs changed: 3. **Regenerate the icon** if the source PNGs changed:
```sh ```sh
@@ -71,12 +80,12 @@ per-peer noise-gate release; it cannot connect to a 0.2.x peer.)
## What the installer does ## What the installer does
- Installs `peerspeak.exe` to `Program Files\PeerSpeak` (requires admin / one - Installs `peerspeak.exe` and the matching `pixelpass.exe` to
UAC prompt). `Program Files\PeerSpeak` (requires admin / one UAC prompt).
- Creates a Start-menu shortcut, with an optional desktop shortcut. - Creates a Start-menu shortcut, with an optional desktop shortcut.
- Optionally adds a Windows Firewall allow-rule for PeerSpeak (recommended — - Optionally adds idempotent Windows Firewall allow-rules for PeerSpeak and
iroh uses UDP hole-punching, so this avoids a mid-call firewall prompt). The PixelPass (recommended — both use iroh/QUIC). The rules are replaced on
rule is removed on uninstall. upgrade and removed on uninstall.
- Provides a standard uninstaller. - Provides a standard uninstaller.
> **Note:** the installer and the binary are **not code-signed**, so Windows > **Note:** the installer and the binary are **not code-signed**, so Windows
+13 -5
View File
@@ -1,8 +1,7 @@
; Inno Setup script for PeerSpeak (Windows installer). ; Inno Setup script for PeerSpeak (Windows installer).
; ;
; PeerSpeak is a single self-contained binary: the GUI icon, notification ; PeerSpeak itself is a self-contained binary. The installer also carries the
; chimes, and avatar presets are all embedded in the .exe (include_bytes!), ; matching viewer-only PixelPass helper so Windows can watch Linux-hosted shares.
; so the only payload here is peerspeak.exe plus an .ico for the shortcuts.
; ;
; Build (under Wine on Linux, or native Windows): ; Build (under Wine on Linux, or native Windows):
; wine "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" peerspeak.iss ; wine "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" peerspeak.iss
@@ -12,9 +11,10 @@
; (x86_64-pc-windows-gnu, statically linked -- no extra DLLs needed). ; (x86_64-pc-windows-gnu, statically linked -- no extra DLLs needed).
#define MyAppName "PeerSpeak" #define MyAppName "PeerSpeak"
#define MyAppVersion "0.6.7" #define MyAppVersion "0.6.8"
#define MyAppPublisher "mollusk" #define MyAppPublisher "mollusk"
#define MyAppExeName "peerspeak.exe" #define MyAppExeName "peerspeak.exe"
#define PixelPassExeName "pixelpass.exe"
[Setup] [Setup]
; A stable AppId keeps upgrades/uninstall tracking consistent across versions. ; A stable AppId keeps upgrades/uninstall tracking consistent across versions.
@@ -43,10 +43,11 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "firewall"; Description: "Allow PeerSpeak through Windows Firewall (recommended for voice calls)"; GroupDescription: "Network:" Name: "firewall"; Description: "Allow PeerSpeak and PixelPass through Windows Firewall (recommended)"; GroupDescription: "Network:"
[Files] [Files]
Source: "peerspeak.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "peerspeak.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "pixelpass.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "peerspeak.ico"; DestDir: "{app}"; Flags: ignoreversion Source: "peerspeak.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons] [Icons]
@@ -56,8 +57,15 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilen
[Run] [Run]
; iroh uses UDP hole-punching; pre-authorizing avoids a mid-call firewall prompt. ; iroh uses UDP hole-punching; pre-authorizing avoids a mid-call firewall prompt.
; Delete first so an upgrade replaces our owned rule instead of accumulating an
; identical rule on every install. `netsh delete` removes every matching rule;
; its no-match exit code on a first install is harmless to Inno Setup.
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak"""; Flags: runhidden; Tasks: firewall
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""PeerSpeak"" dir=in action=allow program=""{app}\{#MyAppExeName}"" enable=yes profile=any"; Flags: runhidden; Tasks: firewall Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""PeerSpeak"" dir=in action=allow program=""{app}\{#MyAppExeName}"" enable=yes profile=any"; Flags: runhidden; Tasks: firewall
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak PixelPass"""; Flags: runhidden; Tasks: firewall
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall add rule name=""PeerSpeak PixelPass"" dir=in action=allow program=""{app}\{#PixelPassExeName}"" enable=yes profile=any"; Flags: runhidden; Tasks: firewall
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: nowait postinstall skipifsilent
[UninstallRun] [UninstallRun]
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak"""; Flags: runhidden; RunOnceId: "DelPeerSpeakFirewall" Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak"""; Flags: runhidden; RunOnceId: "DelPeerSpeakFirewall"
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""PeerSpeak PixelPass"""; Flags: runhidden; RunOnceId: "DelPeerSpeakPixelPassFirewall"
+93 -43
View File
@@ -16,7 +16,7 @@ use crate::hotkeys::{HotkeyAction, HotkeyContext, KeyBinding, format_binding};
use crate::network::PeerState; use crate::network::PeerState;
use crate::notify::{self, Sound}; use crate::notify::{self, Sound};
use crate::presence::PresenceMode; use crate::presence::PresenceMode;
use crate::screenshare::{AudioExclusionStatus, ShareAudioSelection}; use crate::screenshare::{self, AudioExclusionStatus, ShareAudioSelection};
use crate::theme::{AppTheme, Palette}; use crate::theme::{AppTheme, Palette};
use crate::widget::context_input::{context_input, locked_value}; use crate::widget::context_input::{context_input, locked_value};
use crate::widget::selectable_text::selectable_rich_text; use crate::widget::selectable_text::selectable_rich_text;
@@ -1208,7 +1208,8 @@ pub struct AppState {
settings_category: SettingsCategory, settings_category: SettingsCategory,
/// Whether we're currently sharing our own screen (confirmed by the core). /// Whether we're currently sharing our own screen (confirmed by the core).
self_sharing: bool, self_sharing: bool,
/// Whether the `pixelpass` binary is available, gating the Share controls. /// Whether the `pixelpass` binary is available, gating share hosting on
/// Linux and watching on every supported viewer platform.
pixelpass_available: bool, pixelpass_available: bool,
/// Our persistent node id (W7), known from startup regardless of room state /// Our persistent node id (W7), known from startup regardless of room state
/// (distinct from `self_id`, which is room-scoped). `None` until the core /// (distinct from `self_id`, which is room-scoped). `None` until the core
@@ -7642,10 +7643,9 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
}, },
vertical_space(20.0), vertical_space(20.0),
{ {
// Screen share. Always enabled and labelled "Share Screen": if the // Screen share hosting is currently Linux-only. Windows ships the
// optional pixelpass companion isn't installed, clicking opens a // viewer helper, so keep Watch enabled there while making the local
// short how-to-install explainer instead of being a dead disabled // hosting boundary explicit instead of presenting a broken action.
// button (A11) — so it never reads as a broken in-app feature.
let (share_kind, share_label, share_bg, share_hover, share_fg) = let (share_kind, share_label, share_bg, share_hover, share_fg) =
if state.self_sharing { if state.self_sharing {
( (
@@ -7655,7 +7655,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
color_maroon, color_maroon,
color_crust, color_crust,
) )
} else { } else if screenshare::hosting_supported() {
( (
IconKind::Monitor, IconKind::Monitor,
"Share Screen", "Share Screen",
@@ -7663,8 +7663,18 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
color_blue, color_blue,
color_text, color_text,
) )
} else {
(
IconKind::Monitor,
"Sharing unavailable",
color_surface,
color_blue,
color_subtext,
)
}; };
let share_press = if state.pixelpass_available { let share_press = if state.self_sharing
|| (state.pixelpass_available && screenshare::hosting_supported())
{
AppMessage::ToggleScreenShare AppMessage::ToggleScreenShare
} else { } else {
AppMessage::OpenPixelpassHelp AppMessage::OpenPixelpassHelp
@@ -7674,23 +7684,24 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.style(b_style(share_bg, share_hover, share_fg, 8.0)) .style(b_style(share_bg, share_hover, share_fg, 8.0))
.padding(14) .padding(14)
.width(iced::Length::Fill); .width(iced::Length::Fill);
let share_control: Element<'_, AppMessage> = if state.self_sharing { let share_control: Element<'_, AppMessage> =
share_button.into() if state.self_sharing || !screenshare::hosting_supported() {
} else { share_button.into()
row![ } else {
share_button, row![
pick_list( share_button,
&ShareQuality::ALL[..], pick_list(
Some(state.share_quality_selection), &ShareQuality::ALL[..],
AppMessage::SelectShareQualityOverride, Some(state.share_quality_selection),
) AppMessage::SelectShareQualityOverride,
.width(iced::Length::Fixed(112.0)), )
] .width(iced::Length::Fixed(112.0)),
.spacing(8) ]
.align_y(iced::alignment::Vertical::Center) .spacing(8)
.width(iced::Length::Fill) .align_y(iced::alignment::Vertical::Center)
.into() .width(iced::Length::Fill)
}; .into()
};
share_control share_control
}, },
]; ];
@@ -8757,11 +8768,8 @@ fn with_hotkey_info<'a>(
.into() .into()
} }
/// Overlays the "screen sharing needs pixelpass" explainer popup over `base` /// Overlays the screen-sharing explainer popup over `base` when open (A11).
/// when open (A11). Triggered by the Share Screen / Watch controls when the /// It distinguishes a missing companion from a bundled viewer-only platform.
/// optional `pixelpass` companion isn't installed, so those controls open a
/// short how-to instead of being dead/disabled. Returns the base unchanged when
/// the popup is closed.
fn with_pixelpass_help<'a>( fn with_pixelpass_help<'a>(
base: Element<'a, AppMessage>, base: Element<'a, AppMessage>,
state: &'a AppState, state: &'a AppState,
@@ -8788,6 +8796,50 @@ fn with_pixelpass_help<'a>(
) )
.on_press(AppMessage::ClosePixelpassHelp); .on_press(AppMessage::ClosePixelpassHelp);
let explanation: Element<'_, AppMessage> =
if !screenshare::hosting_supported() && state.pixelpass_available {
column![
text(
"This Windows build can watch screen shares hosted by Linux. \
Starting a share from Windows is not available yet."
)
.size(13)
.color(text_c),
text("To watch a share, install VLC or mpv and click Watch on the sharing participant.")
.size(12)
.color(subtext),
text("PeerSpeak already includes the matching PixelPass viewer helper.")
.size(12)
.color(subtext),
]
.spacing(8)
.into()
} else {
column![
text(
"Screen sharing uses pixelpass, a small companion tool that \
streams video peer-to-peer alongside your call. It's optional \
and ships separately, so peerspeak works fully without it."
)
.size(13)
.color(text_c),
text("To enable sharing and watching:")
.size(13)
.color(text_c),
text("• Install pixelpass and make sure the `pixelpass` command is on your PATH.")
.size(12)
.color(subtext),
text("• Install `mpv` or VLC to open a peer's shared screen.")
.size(12)
.color(subtext),
text("Once both are present, restart PeerSpeak.")
.size(12)
.color(subtext),
]
.spacing(8)
.into()
};
let dialog = container( let dialog = container(
column![ column![
row![ row![
@@ -8802,17 +8854,7 @@ fn with_pixelpass_help<'a>(
.padding(2), .padding(2),
] ]
.align_y(iced::alignment::Vertical::Center), .align_y(iced::alignment::Vertical::Center),
text( explanation,
"Screen sharing uses pixelpass, a small companion tool that \
streams video peer-to-peer alongside your call. It's optional \
and ships separately, so peerspeak works fully without it."
)
.size(13)
.color(text_c),
text("To enable sharing and watching:").size(13).color(text_c),
text("• Install pixelpass and make sure the `pixelpass` command is on your PATH.").size(12).color(subtext),
text("• Install `mpv` too — it's what opens a peer's shared screen.").size(12).color(subtext),
text("Once both are present, restart peerspeak and the Share Screen button will start a share.").size(12).color(subtext),
row![ row![
horizontal_space(), horizontal_space(),
button(text("Got it").size(13)) button(text("Got it").size(13))
@@ -8823,7 +8865,11 @@ fn with_pixelpass_help<'a>(
_ => surface, _ => surface,
})), })),
text_color: text_c, text_color: text_c,
border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 6.0.into() }, border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 6.0.into()
},
..Default::default() ..Default::default()
}) })
.padding(8), .padding(8),
@@ -8834,7 +8880,11 @@ fn with_pixelpass_help<'a>(
.style(move |_t: &Theme| container::Style { .style(move |_t: &Theme| container::Style {
text_color: Some(text_c), text_color: Some(text_c),
background: Some(Background::Color(mantle)), background: Some(Background::Color(mantle)),
border: Border { color: surface, width: 1.0, radius: 12.0.into() }, border: Border {
color: surface,
width: 1.0,
radius: 12.0.into(),
},
..Default::default() ..Default::default()
}) })
.padding(20) .padding(20)
+1
View File
@@ -348,6 +348,7 @@ mod tests {
/// `cargo test --lib -- --ignored --test-threads=1 clip_player_node` /// `cargo test --lib -- --ignored --test-threads=1 clip_player_node`
#[test] #[test]
#[ignore = "live: requires a running PipeWire daemon and pw-dump; run with --test-threads=1"] #[ignore = "live: requires a running PipeWire daemon and pw-dump; run with --test-threads=1"]
#[cfg(not(windows))]
fn clip_player_node_carries_both_ownership_carriers() { fn clip_player_node_carries_both_ownership_carriers() {
use crate::audio::ownership::{self, live_test}; use crate::audio::ownership::{self, live_test};
+1 -2
View File
@@ -605,8 +605,7 @@ fn choose_config(device: &Device, output: bool) -> Result<cpal::SupportedStreamC
.iter() .iter()
.filter(|r| usable_range(r) && channels.is_none_or(|c| r.channels() == c)) .filter(|r| usable_range(r) && channels.is_none_or(|c| r.channels() == c))
.filter_map(|r| { .filter_map(|r| {
bounded_rate(r.min_sample_rate().0, r.max_sample_rate().0) bounded_rate(r.min_sample_rate().0, r.max_sample_rate().0).map(|rate| (*r, rate))
.map(|rate| (r.clone(), rate))
}) })
.min_by_key(|(_, rate)| rate.abs_diff(SAMPLE_RATE)) .min_by_key(|(_, rate)| rate.abs_diff(SAMPLE_RATE))
}; };
+10
View File
@@ -3576,6 +3576,7 @@ async fn run_core_loop(
let session = active_session let session = active_session
.as_mut() .as_mut()
.expect("session presence checked before PixelPass probe"); .expect("session presence checked before PixelPass probe");
#[cfg(target_os = "linux")]
let aec_module_index = matches!( let aec_module_index = matches!(
audio, audio,
crate::screenshare::ShareAudioSelection::DesktopExcluding crate::screenshare::ShareAudioSelection::DesktopExcluding
@@ -3587,6 +3588,15 @@ async fn run_core_loop(
.map(crate::audio::echo_cancel::EchoCancelGuard::module_index) .map(crate::audio::echo_cancel::EchoCancelGuard::module_index)
}) })
.flatten(); .flatten();
// Windows has no PipeWire echo-cancel module, so there is no
// module identity to hand to PixelPass. Keep the shared host
// spawn path platform-neutral without importing the Linux-only
// `audio::echo_cancel` module into a Windows build.
#[cfg(not(target_os = "linux"))]
let aec_module_index = {
debug_assert!(session.teardown.echo_cancel().is_none());
None
};
// Every share gets a notice forwarder — not just app-audio ones. // Every share gets a notice forwarder — not just app-audio ones.
// App-audio and desktop-exclusion events become UI state, and // App-audio and desktop-exclusion events become UI state, and
// the drain's terminal `Eof` becomes a generation-scoped fault. // the drain's terminal `Eof` becomes a generation-scoped fault.
+85 -12
View File
@@ -27,8 +27,8 @@ pub mod livesync;
use crate::config::{ScreenShareSettings, ShareBuffering, SharePlayer, ShareQuality}; use crate::config::{ScreenShareSettings, ShareBuffering, SharePlayer, ShareQuality};
/// The binary we shell out to. Looked up on `$PATH` unless a config override /// The binary we shell out to. A config override wins, followed by a helper
/// points elsewhere. /// bundled beside PeerSpeak, then `$PATH`.
const PIXELPASS_BIN: &str = "pixelpass"; const PIXELPASS_BIN: &str = "pixelpass";
#[cfg(windows)] #[cfg(windows)]
@@ -41,6 +41,13 @@ fn pixelpass_path_candidates(dir: &Path) -> [PathBuf; 1] {
[dir.join(PIXELPASS_BIN)] [dir.join(PIXELPASS_BIN)]
} }
/// Whether this platform's PixelPass build can host a share. Windows currently
/// bundles the viewer milestone, so it may watch a Linux-hosted share without
/// offering a host flow that the helper will refuse.
pub const fn hosting_supported() -> bool {
cfg!(target_os = "linux")
}
/// Pixelpass endpoint tickets are normally ~140 chars. Leave headroom for format /// Pixelpass endpoint tickets are normally ~140 chars. Leave headroom for format
/// growth, but reject unbounded gossip payloads before the UI offers "Watch". /// growth, but reject unbounded gossip payloads before the UI offers "Watch".
const MAX_TICKET_LEN: usize = 512; const MAX_TICKET_LEN: usize = 512;
@@ -544,11 +551,18 @@ pub fn sanitize_ticket(ticket: String) -> Option<String> {
(valid_len && valid_shape).then(|| ticket.to_string()) (valid_len && valid_shape).then(|| ticket.to_string())
} }
fn bundled_pixelpass_for(current_exe: &Path) -> Option<PathBuf> {
let dir = current_exe.parent()?;
pixelpass_path_candidates(dir)
.into_iter()
.find(|candidate| candidate.is_file())
}
/// Resolve the pixelpass binary: an explicit config override (used only if it /// Resolve the pixelpass binary: an explicit config override (used only if it
/// points at an existing file), otherwise the first `pixelpass` found on /// points at an existing file), then a helper installed beside this executable,
/// `$PATH`. `None` means it isn't installed — a normal, handled state. An /// otherwise the first `pixelpass` found on `$PATH`. `None` means it isn't
/// override that doesn't resolve falls through to the `$PATH` search rather than /// installed — a normal, handled state. An override that doesn't resolve falls
/// failing outright. /// through rather than failing outright.
pub fn pixelpass_path(config_override: Option<&str>) -> Option<PathBuf> { pub fn pixelpass_path(config_override: Option<&str>) -> Option<PathBuf> {
if let Some(p) = config_override { if let Some(p) = config_override {
let p = p.trim(); let p = p.trim();
@@ -557,13 +571,21 @@ pub fn pixelpass_path(config_override: Option<&str>) -> Option<PathBuf> {
if pb.is_file() { if pb.is_file() {
return Some(pb); return Some(pb);
} }
// Override set but missing — fall through to the $PATH search. // Override set but missing — fall through to the bundled/PATH search.
} }
} }
let path_var = std::env::var_os("PATH")?; if let Some(path) = std::env::current_exe()
std::env::split_paths(&path_var) .ok()
.flat_map(|dir| pixelpass_path_candidates(&dir)) .as_deref()
.find(|c| c.is_file()) .and_then(bundled_pixelpass_for)
{
return Some(path);
}
std::env::var_os("PATH").and_then(|path_var| {
std::env::split_paths(&path_var)
.flat_map(|dir| pixelpass_path_candidates(&dir))
.find(|candidate| candidate.is_file())
})
} }
/// Whether pixelpass is available to shell out to. /// Whether pixelpass is available to shell out to.
@@ -590,6 +612,12 @@ pub async fn spawn_host(
quality: ShareQuality, quality: ShareQuality,
notices: tokio::sync::mpsc::UnboundedSender<HostNotice>, notices: tokio::sync::mpsc::UnboundedSender<HostNotice>,
) -> std::io::Result<(Child, String)> { ) -> std::io::Result<(Child, String)> {
if !hosting_supported() {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"this PixelPass integration can view shares on Windows, but hosting remains Linux-only",
));
}
let args = host_args_for_selection(audio, aec_module_index, settings, quality); let args = host_args_for_selection(audio, aec_module_index, settings, quality);
// Log the exact argv we hand pixelpass so a field log can confirm which // Log the exact argv we hand pixelpass so a field log can confirm which
// encode/quality flags (e.g. --bitrate) actually reached the host — these // encode/quality flags (e.g. --bitrate) actually reached the host — these
@@ -984,7 +1012,7 @@ fn spawn_player(bin: &str, args: &[String], url: &str) -> std::io::Result<Child>
// and is not needed to verify the flags. Logged on each attempt, so a // and is not needed to verify the flags. Logged on each attempt, so a
// fallback from the preferred player to the other one is visible too. // fallback from the preferred player to the other one is visible too.
crate::log_msg(&format!("player spawn: {bin} {}", args.join(" "))); crate::log_msg(&format!("player spawn: {bin} {}", args.join(" ")));
let mut command = Command::new(bin); let mut command = Command::new(player_program(bin));
command command
.args(args) .args(args)
.arg(url) .arg(url)
@@ -1000,6 +1028,24 @@ fn spawn_player(bin: &str, args: &[String], url: &str) -> std::io::Result<Child>
command.spawn() command.spawn()
} }
/// Resolve VLC from its conventional Windows install locations before falling
/// back to PATH. mpv and custom player names remain PATH-resolved.
fn player_program(bin: &str) -> PathBuf {
#[cfg(windows)]
if bin.eq_ignore_ascii_case("vlc") {
for variable in ["ProgramFiles", "ProgramFiles(x86)"] {
if let Some(root) = std::env::var_os(variable) {
let candidate = Path::new(&root).join("VideoLAN/VLC/vlc.exe");
if candidate.is_file() {
return candidate;
}
}
}
}
PathBuf::from(bin)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -1016,6 +1062,7 @@ mod tests {
/// `cargo test --lib -- --ignored spawned_player` /// `cargo test --lib -- --ignored spawned_player`
#[tokio::test] #[tokio::test]
#[ignore = "live: requires a running PipeWire daemon, mpv and pw-dump"] #[ignore = "live: requires a running PipeWire daemon, mpv and pw-dump"]
#[cfg(not(windows))]
async fn spawned_player_node_carries_both_ownership_carriers() { async fn spawned_player_node_carries_both_ownership_carriers() {
use crate::audio::ownership::live_test; use crate::audio::ownership::live_test;
@@ -1810,6 +1857,32 @@ Install hint: sudo apt install gstreamer1.0-plugins-bad
assert_eq!(candidates, vec![dir.join("pixelpass")]); assert_eq!(candidates, vec![dir.join("pixelpass")]);
} }
#[test]
fn bundled_pixelpass_is_found_beside_the_main_executable() {
let dir = std::env::temp_dir().join(format!(
"peerspeak-bundled-pixelpass-test-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let main = dir.join(if cfg!(windows) {
"peerspeak.exe"
} else {
"peerspeak"
});
let helper = pixelpass_path_candidates(&dir).into_iter().last().unwrap();
std::fs::write(&main, b"test").unwrap();
std::fs::write(&helper, b"test").unwrap();
assert_eq!(bundled_pixelpass_for(&main), Some(helper));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn hosting_support_is_linux_only() {
assert_eq!(hosting_supported(), cfg!(target_os = "linux"));
}
/// The host-fault contract, clean-exit half: events are forwarded in order /// The host-fault contract, clean-exit half: events are forwarded in order
/// and the stream ending yields exactly one terminal [`HostNotice::Eof`], /// and the stream ending yields exactly one terminal [`HostNotice::Eof`],
/// after which the drain task drops its sender (the closed channel is what /// after which the drain task drops its sender (the closed channel is what