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);
# the .iss script and .ico are the tracked sources.
/packaging/windows/peerspeak.exe
/packaging/windows/pixelpass.exe
/packaging/windows/output/
# 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]
### 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
### Changed
Generated
+1 -1
View File
@@ -4871,7 +4871,7 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "peerspeak"
version = "0.6.7"
version = "0.6.8"
dependencies = [
"anyhow",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "peerspeak"
version = "0.6.7"
version = "0.6.8"
edition = "2024"
description = "Decentralized peer-to-peer voice chat (Rust/iroh/PipeWire/Opus/iced)"
license = "MIT"
+107 -24
View File
@@ -1,10 +1,15 @@
# PeerSpeak on Windows
Current status: PeerSpeak cross-compiles to `x86_64-pc-windows-gnu` from Linux and
has passed an older native Windows 11 VM smoke test for launch, GUI render, call
join, and audio flow. The build environment is **not** the Windows VM; current
Windows binaries are built from Linux, normally inside the `peerspeak-win`
distrobox or with the same GNU target environment.
Current status: PeerSpeak cross-compiles to `x86_64-pc-windows-gnu` from Linux.
PeerSpeak v0.6.7 at commit `7217015d` passed a fresh Windows 10 22H2/build 19045
VM smoke test on 2026-08-22 for install/upgrade, launch, GUI/settings render,
device selection and persistence, microphone capture startup, WASAPI playback
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
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 |
|---|---|
| GUI | Iced/wgpu builds for Windows and rendered in the Windows 11 VM. |
| Networking | Iroh QUIC transport and gossip compile on Windows; VM call reached two peers. |
| 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; 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`. |
| 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. |
@@ -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. |
| Game detection | Steam registry `RunningAppID` plus Toolhelp process-scan fallback compile on Windows. |
| 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`:
@@ -69,35 +75,112 @@ cargo build --release
## First run and networking
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
UDP for QUIC plus relay traffic when direct NAT traversal is unavailable.
use the Inno installer option that pre-adds idempotent PeerSpeak and PixelPass
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
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
| Item | Status |
|---|---|
| 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. |
| 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
Before calling a Windows build current, verify on the Windows VM or real Windows
hardware:
Before calling Windows feature parity complete, finish these checks on the
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.
- Run `audio_probe.exe 440 30`; listen for glitches and inspect `playout-health`.
- 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.
- Play chimes and custom chime paths.
- Send chat, image/file attachments, and save an attachment through the native dialog.
- Import/play/share/listen to music from Windows file paths.
- Record mixed/stems/both and inspect the WAV output path.
- Exercise friends/presence/recents and the clock-skew banner.
- 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] Launch current `peerspeak.exe`; GUI renders and settings open.
- [ ] 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.
- [x] Select input/output devices, restart, and confirm selections persist or fall back clearly.
- [ ] Play chimes and custom chime paths.
- [ ] Send chat, image/file attachments, and save an attachment through the native dialog.
- [ ] Import/play/share/listen to music from Windows file paths.
- [ ] Record mixed/stems/both and inspect the WAV output path.
- [ ] Exercise friends/presence/recents and the clock-skew banner.
- [ ] Test Steam and non-Steam game detection on a real Windows Steam install.
- [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
see along the way:
- **"Allow PeerSpeak through Windows Firewall"** — leave this **checked**
(it lets the call connect without interruptions).
- **"Allow PeerSpeak and PixelPass through Windows Firewall"** — leave this
**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
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
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
**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).
- **"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 unsigned-app warning, not malware.
+19 -10
View File
@@ -3,11 +3,12 @@
This directory builds a Windows setup installer for PeerSpeak using
[Inno Setup](https://jrsoftware.org/isinfo.php).
PeerSpeak ships as a **single self-contained `peerspeak.exe`** — the GUI icon,
notification chimes, and avatar presets are all embedded in the binary
PeerSpeak itself is a **self-contained `peerspeak.exe`** — the GUI icon,
notification chimes, and avatar presets are embedded in the binary
(`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
`.exe` plus an `.ico` for the Start-menu / desktop shortcuts.
runtime. The installer also bundles the matching viewer-only `pixelpass.exe`,
which lets Windows participants watch shares hosted by Linux. VLC or mpv is
still required to render the received stream.
## 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`) |
| `README.md` | yes | this file |
| `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 |
## 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,
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
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:
```sh
@@ -71,12 +80,12 @@ per-peer noise-gate release; it cannot connect to a 0.2.x peer.)
## What the installer does
- Installs `peerspeak.exe` to `Program Files\PeerSpeak` (requires admin / one
UAC prompt).
- Installs `peerspeak.exe` and the matching `pixelpass.exe` to
`Program Files\PeerSpeak` (requires admin / one UAC prompt).
- Creates a Start-menu shortcut, with an optional desktop shortcut.
- Optionally adds a Windows Firewall allow-rule for PeerSpeak (recommended —
iroh uses UDP hole-punching, so this avoids a mid-call firewall prompt). The
rule is removed on uninstall.
- Optionally adds idempotent Windows Firewall allow-rules for PeerSpeak and
PixelPass (recommended — both use iroh/QUIC). The rules are replaced on
upgrade and removed on uninstall.
- Provides a standard uninstaller.
> **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).
;
; PeerSpeak is a single self-contained binary: the GUI icon, notification
; chimes, and avatar presets are all embedded in the .exe (include_bytes!),
; so the only payload here is peerspeak.exe plus an .ico for the shortcuts.
; PeerSpeak itself is a self-contained binary. The installer also carries the
; matching viewer-only PixelPass helper so Windows can watch Linux-hosted shares.
;
; Build (under Wine on Linux, or native Windows):
; 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).
#define MyAppName "PeerSpeak"
#define MyAppVersion "0.6.7"
#define MyAppVersion "0.6.8"
#define MyAppPublisher "mollusk"
#define MyAppExeName "peerspeak.exe"
#define PixelPassExeName "pixelpass.exe"
[Setup]
; A stable AppId keeps upgrades/uninstall tracking consistent across versions.
@@ -43,10 +43,11 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
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]
Source: "peerspeak.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "pixelpass.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "peerspeak.ico"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
@@ -56,8 +57,15 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilen
[Run]
; 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 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
[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 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::notify::{self, Sound};
use crate::presence::PresenceMode;
use crate::screenshare::{AudioExclusionStatus, ShareAudioSelection};
use crate::screenshare::{self, AudioExclusionStatus, ShareAudioSelection};
use crate::theme::{AppTheme, Palette};
use crate::widget::context_input::{context_input, locked_value};
use crate::widget::selectable_text::selectable_rich_text;
@@ -1208,7 +1208,8 @@ pub struct AppState {
settings_category: SettingsCategory,
/// Whether we're currently sharing our own screen (confirmed by the core).
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,
/// Our persistent node id (W7), known from startup regardless of room state
/// (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),
{
// Screen share. Always enabled and labelled "Share Screen": if the
// optional pixelpass companion isn't installed, clicking opens a
// short how-to-install explainer instead of being a dead disabled
// button (A11) — so it never reads as a broken in-app feature.
// Screen share hosting is currently Linux-only. Windows ships the
// viewer helper, so keep Watch enabled there while making the local
// hosting boundary explicit instead of presenting a broken action.
let (share_kind, share_label, share_bg, share_hover, share_fg) =
if state.self_sharing {
(
@@ -7655,7 +7655,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
color_maroon,
color_crust,
)
} else {
} else if screenshare::hosting_supported() {
(
IconKind::Monitor,
"Share Screen",
@@ -7663,8 +7663,18 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
color_blue,
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
} else {
AppMessage::OpenPixelpassHelp
@@ -7674,23 +7684,24 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.style(b_style(share_bg, share_hover, share_fg, 8.0))
.padding(14)
.width(iced::Length::Fill);
let share_control: Element<'_, AppMessage> = if state.self_sharing {
share_button.into()
} else {
row![
share_button,
pick_list(
&ShareQuality::ALL[..],
Some(state.share_quality_selection),
AppMessage::SelectShareQualityOverride,
)
.width(iced::Length::Fixed(112.0)),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center)
.width(iced::Length::Fill)
.into()
};
let share_control: Element<'_, AppMessage> =
if state.self_sharing || !screenshare::hosting_supported() {
share_button.into()
} else {
row![
share_button,
pick_list(
&ShareQuality::ALL[..],
Some(state.share_quality_selection),
AppMessage::SelectShareQualityOverride,
)
.width(iced::Length::Fixed(112.0)),
]
.spacing(8)
.align_y(iced::alignment::Vertical::Center)
.width(iced::Length::Fill)
.into()
};
share_control
},
];
@@ -8757,11 +8768,8 @@ fn with_hotkey_info<'a>(
.into()
}
/// Overlays the "screen sharing needs pixelpass" explainer popup over `base`
/// when open (A11). Triggered by the Share Screen / Watch controls when the
/// 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.
/// Overlays the screen-sharing explainer popup over `base` when open (A11).
/// It distinguishes a missing companion from a bundled viewer-only platform.
fn with_pixelpass_help<'a>(
base: Element<'a, AppMessage>,
state: &'a AppState,
@@ -8788,6 +8796,50 @@ fn with_pixelpass_help<'a>(
)
.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(
column![
row![
@@ -8802,17 +8854,7 @@ fn with_pixelpass_help<'a>(
.padding(2),
]
.align_y(iced::alignment::Vertical::Center),
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` 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),
explanation,
row![
horizontal_space(),
button(text("Got it").size(13))
@@ -8823,7 +8865,11 @@ fn with_pixelpass_help<'a>(
_ => surface,
})),
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()
})
.padding(8),
@@ -8834,7 +8880,11 @@ fn with_pixelpass_help<'a>(
.style(move |_t: &Theme| container::Style {
text_color: Some(text_c),
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()
})
.padding(20)
+1
View File
@@ -348,6 +348,7 @@ mod tests {
/// `cargo test --lib -- --ignored --test-threads=1 clip_player_node`
#[test]
#[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() {
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()
.filter(|r| usable_range(r) && channels.is_none_or(|c| r.channels() == c))
.filter_map(|r| {
bounded_rate(r.min_sample_rate().0, r.max_sample_rate().0)
.map(|rate| (r.clone(), rate))
bounded_rate(r.min_sample_rate().0, r.max_sample_rate().0).map(|rate| (*r, 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
.as_mut()
.expect("session presence checked before PixelPass probe");
#[cfg(target_os = "linux")]
let aec_module_index = matches!(
audio,
crate::screenshare::ShareAudioSelection::DesktopExcluding
@@ -3587,6 +3588,15 @@ async fn run_core_loop(
.map(crate::audio::echo_cancel::EchoCancelGuard::module_index)
})
.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.
// App-audio and desktop-exclusion events become UI state, and
// 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};
/// The binary we shell out to. Looked up on `$PATH` unless a config override
/// points elsewhere.
/// The binary we shell out to. A config override wins, followed by a helper
/// bundled beside PeerSpeak, then `$PATH`.
const PIXELPASS_BIN: &str = "pixelpass";
#[cfg(windows)]
@@ -41,6 +41,13 @@ fn pixelpass_path_candidates(dir: &Path) -> [PathBuf; 1] {
[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
/// growth, but reject unbounded gossip payloads before the UI offers "Watch".
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())
}
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
/// points at an existing file), otherwise the first `pixelpass` found on
/// `$PATH`. `None` means it isn't installed — a normal, handled state. An
/// override that doesn't resolve falls through to the `$PATH` search rather than
/// failing outright.
/// points at an existing file), then a helper installed beside this executable,
/// otherwise the first `pixelpass` found on `$PATH`. `None` means it isn't
/// installed — a normal, handled state. An override that doesn't resolve falls
/// through rather than failing outright.
pub fn pixelpass_path(config_override: Option<&str>) -> Option<PathBuf> {
if let Some(p) = config_override {
let p = p.trim();
@@ -557,13 +571,21 @@ pub fn pixelpass_path(config_override: Option<&str>) -> Option<PathBuf> {
if pb.is_file() {
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")?;
std::env::split_paths(&path_var)
.flat_map(|dir| pixelpass_path_candidates(&dir))
.find(|c| c.is_file())
if let Some(path) = std::env::current_exe()
.ok()
.as_deref()
.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.
@@ -590,6 +612,12 @@ pub async fn spawn_host(
quality: ShareQuality,
notices: tokio::sync::mpsc::UnboundedSender<HostNotice>,
) -> 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);
// 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
@@ -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
// fallback from the preferred player to the other one is visible too.
crate::log_msg(&format!("player spawn: {bin} {}", args.join(" ")));
let mut command = Command::new(bin);
let mut command = Command::new(player_program(bin));
command
.args(args)
.arg(url)
@@ -1000,6 +1028,24 @@ fn spawn_player(bin: &str, args: &[String], url: &str) -> std::io::Result<Child>
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)]
mod tests {
use super::*;
@@ -1016,6 +1062,7 @@ mod tests {
/// `cargo test --lib -- --ignored spawned_player`
#[tokio::test]
#[ignore = "live: requires a running PipeWire daemon, mpv and pw-dump"]
#[cfg(not(windows))]
async fn spawned_player_node_carries_both_ownership_carriers() {
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")]);
}
#[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
/// and the stream ending yields exactly one terminal [`HostNotice::Eof`],
/// after which the drain task drops its sender (the closed channel is what