feat(windows): bundle PixelPass viewer

This commit is contained in:
2026-08-22 21:02:32 -04:00
parent f2220ed84e
commit 437c95380e
11 changed files with 258 additions and 85 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"
+15 -8
View File
@@ -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.
+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
+9 -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]
@@ -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
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 -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))
};
+84 -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::*;
@@ -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