feat(windows): bundle PixelPass viewer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -4871,7 +4871,7 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "peerspeak"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+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
|
||||
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 build environment
|
||||
is **not** the Windows VM; current Windows binaries are built from Linux in a
|
||||
distrobox with the GNU target environment.
|
||||
pacing, room create/leave, and the missing-PixelPass guard. The v0.6.8 candidate
|
||||
adds the matching viewer-only PixelPass helper to the installer: 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.
|
||||
@@ -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. |
|
||||
| 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`:
|
||||
|
||||
@@ -71,8 +74,9 @@ 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.
|
||||
@@ -120,7 +124,7 @@ probe quality, and uninstall/firewall cleanup.
|
||||
| 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 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
|
||||
|
||||
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
|
||||
`7217015d`:
|
||||
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:
|
||||
|
||||
- [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.
|
||||
@@ -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.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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.
|
||||
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
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
|
||||
+84
-12
@@ -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::*;
|
||||
@@ -1811,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
|
||||
|
||||
Reference in New Issue
Block a user