feat(windows): bundle PixelPass viewer
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
+15
-8
@@ -4,9 +4,11 @@ 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
|
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,
|
VM smoke test on 2026-08-22 for install/upgrade, launch, GUI/settings render,
|
||||||
device selection and persistence, microphone capture startup, WASAPI playback
|
device selection and persistence, microphone capture startup, WASAPI playback
|
||||||
pacing, room create/leave, and the missing-PixelPass guard. The build environment
|
pacing, room create/leave, and the missing-PixelPass guard. The v0.6.8 candidate
|
||||||
is **not** the Windows VM; current Windows binaries are built from Linux in a
|
adds the matching viewer-only PixelPass helper to the installer: Windows can
|
||||||
distrobox with the GNU target environment.
|
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.
|
||||||
@@ -25,6 +27,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`:
|
||||||
|
|
||||||
@@ -71,8 +74,9 @@ 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.
|
||||||
@@ -120,7 +124,7 @@ probe quality, and uninstall/firewall cleanup.
|
|||||||
| 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 current 0.6.7 build passed the bounded Windows 10 smoke above; the remaining two-party and feature-specific rows still need coverage. |
|
| Runtime coverage | The current 0.6.7 build passed the bounded Windows 10 smoke above; the remaining two-party and feature-specific rows still need coverage. |
|
||||||
@@ -128,8 +132,8 @@ probe quality, and uninstall/firewall cleanup.
|
|||||||
## Current smoke checklist
|
## Current smoke checklist
|
||||||
|
|
||||||
Before calling Windows feature parity complete, finish these checks on the
|
Before calling Windows feature parity complete, finish these checks on the
|
||||||
Windows VM or real Windows hardware. Items checked below passed for v0.6.7 at
|
Windows VM or real Windows hardware. The original checks below passed for
|
||||||
`7217015d`:
|
v0.6.7 at `7217015d`; screen-share rows track the v0.6.8 candidate separately:
|
||||||
|
|
||||||
- [x] 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`; automated `playout-health` passed, but listen for glitches on physical hardware.
|
- [ ] Run `audio_probe.exe 440 30`; automated `playout-health` passed, but listen for glitches on physical hardware.
|
||||||
@@ -141,4 +145,7 @@ Windows VM or real Windows hardware. Items checked below passed for v0.6.7 at
|
|||||||
- [ ] 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.
|
||||||
|
- [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.
|
||||||
|
- [ ] Confirm Windows clearly refuses screen-share hosting while Watch remains available.
|
||||||
- [ ] Install/upgrade/uninstall through the Inno installer, including firewall rule cleanup. Clean install and idempotent upgrade passed; uninstall remains.
|
- [ ] Install/upgrade/uninstall through the Inno installer, including firewall rule cleanup. Clean install and idempotent upgrade passed; uninstall remains.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -61,7 +62,10 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilen
|
|||||||
; its no-match exit code on a first install is harmless to Inno Setup.
|
; 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 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
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
};
|
};
|
||||||
|
|||||||
+84
-12
@@ -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::*;
|
||||||
@@ -1811,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
|
||||||
|
|||||||
Reference in New Issue
Block a user