From 437c95380e7591e9d8a57bf267cd470477bfd709 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sat, 22 Aug 2026 21:02:32 -0400 Subject: [PATCH] feat(windows): bundle PixelPass viewer --- .gitignore | 1 + CHANGELOG.md | 16 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- docs/WINDOWS.md | 23 ++++-- packaging/windows/INSTALL.md | 21 ++++- packaging/windows/README.md | 29 ++++--- packaging/windows/peerspeak.iss | 14 ++-- src/app/mod.rs | 136 ++++++++++++++++++++++---------- src/audio/cpal_impl.rs | 3 +- src/screenshare/mod.rs | 96 +++++++++++++++++++--- 11 files changed, 258 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index e85e355..d3c02e5 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d82ba1c..815b2b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 10ac300..25a6941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4871,7 +4871,7 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "peerspeak" -version = "0.6.7" +version = "0.6.8" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 11368c7..9f9495b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 18b2d8a..8745cd9 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -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. diff --git a/packaging/windows/INSTALL.md b/packaging/windows/INSTALL.md index f24b1d1..9f53cce 100644 --- a/packaging/windows/INSTALL.md +++ b/packaging/windows/INSTALL.md @@ -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. diff --git a/packaging/windows/README.md b/packaging/windows/README.md index eb3cab5..f83871d 100644 --- a/packaging/windows/README.md +++ b/packaging/windows/README.md @@ -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--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 diff --git a/packaging/windows/peerspeak.iss b/packaging/windows/peerspeak.iss index 357d8cf..50ae9be 100644 --- a/packaging/windows/peerspeak.iss +++ b/packaging/windows/peerspeak.iss @@ -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" diff --git a/src/app/mod.rs b/src/app/mod.rs index f292d55..7c942c9 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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) diff --git a/src/audio/cpal_impl.rs b/src/audio/cpal_impl.rs index fb5648f..c4fa635 100644 --- a/src/audio/cpal_impl.rs +++ b/src/audio/cpal_impl.rs @@ -605,8 +605,7 @@ fn choose_config(device: &Device, output: bool) -> Result [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 { (valid_len && valid_shape).then(|| ticket.to_string()) } +fn bundled_pixelpass_for(current_exe: &Path) -> Option { + 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 { if let Some(p) = config_override { let p = p.trim(); @@ -557,13 +571,21 @@ pub fn pixelpass_path(config_override: Option<&str>) -> Option { 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, ) -> 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 // 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 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