Peer-to-peer screen sharing for a local network, written in Rust. Anyone on the LAN can broadcast a monitor or a single window, several people can broadcast at the same time, and each viewer watches one stream at a time. There is no server: peers find each other with mDNS and stream directly over encrypted QUIC.
Status: MVP. Video, plus optional audio. Sharing audio needs a Windows broadcaster (Windows 10 2004+ or 11); playback is built for every platform. Verified on Windows 11. macOS and Linux code paths exist but are untested; since 0.6.1 a Linux x64 package is published, and it has only been started on Arch (Hyprland, Wayland) so far.
Target platforms: Windows 10 and 11, macOS, and Linux on both X11 and Wayland. Windows comes first; macOS and Linux follow in 0.8 (see the roadmap).
0.6 changed the protocol for H.265 (version 3), so it can't talk to 0.5 or 0.4: both sides show "incompatible version". Since 0.5 the app updates itself when it starts, so 0.5 installs become 0.6 on their own and a group stays on the same version. 0.5 and 0.4 worked together. Earlier releases couldn't talk to the one before either: 0.4 added audio and can't talk to 0.3 (both sides show "incompatible version"). 0.3 can't talk to 0.2 or earlier (released as "P2P Screen Share"), because the protocol and discovery names changed with the rename.
Design documents: functional requirements Β· non-functional requirements Β· use cases Β· abuse cases (STRIDE) Β· roadmap Β· releasing
- Broadcast: pick a source (a monitor, a window, or the built-in test pattern) and a quality preset, then press Start broadcasting. Capture and encoding only run while at least one person is watching.
- Presets: 720p and 1080p, each at 30 or 60 fps (at most 4, 6, 8 and 12 Mbps), and Internet / VPN Β· 720p Β· 24 fps (at most 2 Mbps) for Radmin VPN, Hamachi and other links with limited upload. 24 fps is the frame rate of films, series and anime.
- Video codec: H.265, encoded by the graphics card (NVIDIA, AMD or Intel, on Windows). It takes little CPU, makes 60 fps easy, looks sharper than H.264 at the same bitrate, and sends less when the picture is simple. Without such an encoder (older graphics cards, virtual machines, and macOS/Linux for now), the broadcast uses H.264 on the CPU by itself; 1080p at 60 fps may then not keep up. The stats under the broadcast show which one is in use. Viewers decode both, on every platform.
- Each broadcast reuses the same UDP port, so a connect string you shared keeps working.
- Share audio (off by default, remembered) adds sound to the broadcast. It says exactly what it captures: for a window, only that app's sound; for a monitor, all sound on the computer except Peeroxide itself (so broadcasting while watching someone never feeds their stream back) and the apps you mute. The microphone is never captured. Turning audio on or off is fixed for the broadcast; stop and start again to change it.
- Mute apps (monitor with audio, Windows): a checklist of the apps playing sound. Viewers don't hear the ones you tick, but you still do, so friends in a Discord or TeamSpeak call with you don't hear themselves through your stream. Voice chat apps (Discord, TeamSpeak, Mumble, Skype, Teams, Zoom, WhatsApp, Telegram, Slack, Signal and a few more) start ticked. The checklist stays usable while live, changes apply within a second, and choices are remembered per app. While any app is muted, Windows' own notification sounds aren't shared either.
- Watch: broadcasters on your network appear under Broadcasting on this network. Click one to watch it; click another to switch. You only ever watch one stream at a time.
- If the broadcaster shares audio, a π mute button and a volume slider appear under the stream's name. They only affect what you hear, and are remembered. Audio plays in sync with the video.
- Fullscreen: press F11, double-click the video, or click βΆ Fullscreen. Only the stream is shown. Move the mouse for a bar with the name, mute, volume and an exit button; it hides again, with the cursor, after 2 seconds. Esc, F11 or a double-click go back, and fullscreen ends by itself when the stream ends.
- Mini player: minimize Peeroxide while watching (or click π Mini player, next to Fullscreen and in the fullscreen bar), and the stream keeps playing in a small window that stays on top of the others, like Discord's picture-in-picture. It opens in the bottom-right corner; drag it anywhere and resize it from its top-left grip, and it reopens where you left it. Moving the mouse over it shows the broadcaster's name, mute, π to go back to Peeroxide (or double-click the video) and π to stop watching. It closes by itself when the stream ends, and Peeroxide flashes in the taskbar.
- Saved: everyone you have watched is remembered (β ). When they aren't showing up in the list, e.g. discovery doesn't reach them, they appear under Saved. Click to connect at their last address, or π to forget them.
- Check who you are watching: every peer has an ID such as
7268-E22A, shown next to its name. It is derived from that peer's certificate, and the connection is refused if the broadcaster can't prove it owns that ID.- If two broadcasters share a name, a β appears; ask the person you expect for their ID (shown in the top bar of their app).
- A red β means someone is using the name of a saved contact with a different ID: a reinstall, or an impersonation attempt.
Your display name can be changed with the β button next to it (not while broadcasting). Name, quality preset, the audio choice, and volume/mute are remembered.
Every time it opens, Peeroxide checks for a newer version, like Discord or Steam do. If there is one, it downloads it (with a progress bar and a Skip button to open right away instead), checks it, replaces itself in the same folder, and restarts. "Updated to X" then shows in the top bar.
- Only genuine updates are installed. Each release is signed with a key that never leaves the maintainer's computer. The app checks that signature, and that the package is exactly the version it claims to be, before touching anything (AC-12). A package that fails the check is thrown away, and the app says so.
- It never gets in the way. Offline, or GitHub unreachable: the app opens within about 5 seconds with a quiet note. Any error leaves the installed version as it was.
- Same folder, same permissions. The firewall permission you gave keeps working after an update.
- A folder it can't write to (such as Program Files): a note offers the download page instead.
- Windows and Linux (x64) packages are published. On macOS the app just opens.
- To turn it off: start with
--no-update, or set the environment variablePEEROXIDE_NO_UPDATE=1(0,false,offornoleave the check on). Development builds (cargo run) never update themselves. - Privacy: the check tells GitHub your IP address and the app version, nothing else (AC-13).
- The first version with the updater has to be installed by hand once; later ones arrive by themselves.
How releases are signed and published: docs/releasing.md.
Requirements:
- Rust stable (edition 2024; tested with 1.97).
- A C/C++ compiler, because OpenH264 and libde265 (the H.265 decoder) are built from source:
- Windows: Visual Studio 2022 Build Tools with the "Desktop development with C++" workload.
- macOS: Xcode Command Line Tools.
- Linux:
build-essential, plus PipeWire and D-Bus development packages for screen capture (libpipewire-0.3-dev,libdbus-1-dev,libclang-dev), ALSA for audio playback (libasound2-dev), and the usual eframe dependencies (libxkbcommon-dev,libwayland-dev,libgl1-mesa-dev).
- Optional: NASM on
PATH. OpenH264 then builds its SIMD assembly and encodes noticeably faster. Without it, it silently falls back to plain C, which is what the numbers below were measured with.
cargo build --release
./target/release/peeroxide # peeroxide.exe on WindowsThe result is a single self-contained executable.
The Justfile wraps the usual commands; install just with cargo install just (or winget install Casey.Just) and run just to list them. The main ones:
| Command | Does |
|---|---|
just run <options> |
Run the app, e.g. just run --profile a --broadcast test --share-audio. |
just demo |
Two instances on this machine: Alice broadcasts the test pattern with sound, Bob watches. |
just check |
What CI checks: formatting, clippy (warnings as errors) and the tests. |
just package [label] |
Windows: release build plus quickstart, zipped into dist/peeroxide-<version>[-<label>]-windows-x64.zip and signed for the updater (.minisig). |
just release-keygen |
One time: create the release signing key (see releasing). |
just publish <notes.md> |
Publish the packaged release, zip and signature, as a GitHub pre-release. |
just serve-release <zip> |
Pretend to be GitHub on this PC, to test the self-update without publishing anything. |
just probe-audio, just probe-capture, just bench |
The developer tools listed under Testing. |
| Option | Purpose |
|---|---|
--name <NAME> |
Name shown to others (defaults to the saved name, then the computer name). |
--no-update |
Don't check for updates at start (see Updates). |
--profile <NAME> |
Separate identity, settings and logs. Lets you run several instances on one machine. |
--broadcast <SOURCE> |
Start broadcasting immediately: test, monitor, or part of a window title. |
--share-audio |
With --broadcast: turn on Share audio (remembered, like the checkbox). With test, the audio is a beep in step with a flashing square. |
--watch <NAME> |
Watch the first discovered broadcaster whose name contains NAME. |
--connect <IP:PORT#FINGERPRINT> |
Watch a broadcaster directly, bypassing discovery. The broadcaster's Copy connect string button produces this. |
Try it on one machine:
peeroxide --profile a --name Alice --broadcast test --share-audio
peeroxide --profile b --name Bob --watch aliceBob sees the test pattern and hears a beep every second while its top-right square flashes.
- Peers must be on the same subnet (mDNS does not cross routers). Discovery uses UDP port 5353 (multicast); video uses one random UDP port per broadcaster.
- Virtual LANs such as Hamachi or Radmin VPN work like a LAN: the broadcaster is reachable on the adapter's address (25.x / 26.x). Whether discovery works depends on the VPN forwarding multicast. When it doesn't, use the connect string once; the broadcaster is saved from then on. Over the internet, use the Internet / VPN preset.
- Windows firewall: the first run triggers a Windows Defender Firewall prompt. Allow the app on every network type you will use. Virtual LAN adapters (and many home networks) are classified Public, so tick Public too; otherwise discovery and incoming connections are blocked.
- If discovery is blocked (e.g. guest Wi-Fi with client isolation, some VPNs), the broadcaster uses Copy connect string, picks the network adapter the viewer shares with them, and the viewer pastes the string into Connect manually.
.cargo/config.toml links the C runtime statically, and release builds use the Windows GUI subsystem (no console window), so target/release/peeroxide.exe runs on any Windows 10 (2004+) or 11 PC with no extra installs. The exe carries the Peeroxide icon (assets/) and version details (Properties β Details, Task Manager), embedded by crates/app/build.rs.
"Windows protected your PC". The binary isn't code-signed, so Microsoft Defender SmartScreen warns the first time someone runs a downloaded copy. It only warns about files carrying Windows' "downloaded from the internet" mark. Testers can either click More info β Run anyway, or clear the mark first: right-click the zip (or the exe) β Properties β tick Unblock β OK. Updates installed by the app itself carry no mark, so the warning only ever appears on the first manual install. Removing it for good needs code signing (see the roadmap, 0.9).
| Crate | Responsibility |
|---|---|
crates/capture |
Enumerate and capture monitors/windows as BGRA frames. Windows: Windows Graphics Capture via windows-capture. macOS/Linux: scap (ScreenCaptureKit / PipeWire portal). Includes a synthetic test pattern. |
crates/codec |
Fixed-size canvas (scale + letterbox). Video behind VideoEncoder/VideoDecoder traits: H.265 encoded by the graphics card through Media Foundation (Windows) and decoded with libde265, H.264 encoded and decoded with OpenH264, and new_encoder, which falls back to H.264 when there is no hardware H.265 encoder. Opus audio (pure Rust, opus-rs) behind AudioEncoder/AudioDecoder. |
crates/de265-sys |
libde265 1.1.3 (LGPL-3.0), vendored and compiled with the cc crate (no CMake, no bindgen), and bindings to the part of its C API the codec uses. |
crates/audio |
Audio capture: Windows process loopback via wasapi (one app's process tree, or everything except Peeroxide), plus a test tone. For a monitor with muted apps: the list of apps playing sound, and one capture per other app, mixed on a timeline by capture time. Playback via cpal with a lock-free ring buffer and volume/mute. The playout scheduler that keeps audio in sync with video. |
crates/net |
Peer identity, fingerprint-pinned TLS 1.3 over QUIC (quinn), wire protocol, BroadcastServer, ViewerClient. |
crates/discovery |
mDNS announce/browse (mdns-sd) with validation of untrusted announcements. |
crates/update |
Self-update: GitHub release lookup (ureq, rustls + the OS certificate store), download with size caps, minisign signature check, and replacing the running exe (self-replace). Also the release-sign and serve-release tools. |
crates/app |
peeroxide binary: egui UI, controller, captureβencode and decodeβdisplay pipelines (video and audio), viewer state machine, settings, logging. |
Broadcaster: capture ββΆ latest-frame slot ββΆ encoder thread (canvas β NV12 β H.265 on the GPU, or I420 β H.264)
ββΆ broadcast channel ββΆ one task per viewer ββΆ QUIC unidirectional stream
audio capture ββΆ audio encoder thread (20 ms frames β Opus)
ββΆ broadcast channel ββΆ one task per viewer ββΆ second stream, sent ahead of video
Viewer: QUIC stream ββΆ bounded queue ββΆ decoder thread (H.265 or H.264 β RGBA) ββΆ latest-frame slot ββΆ GPU texture
audio stream ββΆ bounded queue ββΆ audio thread (Opus β PCM β playout) ββΆ ring buffer ββΆ device
- The canvas size is fixed when capture starts, so resizing a shared window letterboxes instead of changing the stream resolution.
- A viewer that falls behind skips ahead to the next keyframe instead of accumulating delay. Keyframes are produced on demand: when a viewer joins, lags, or reports a decode error.
- Protocol (version 3): viewers open a control stream (
HelloβWelcome, which says whether audio is shared, thenRequestKeyframe). The broadcaster opens a video stream carrying[seq, capture time, keyframe flag, length, codec] + Annex-B(H.265 or H.264) and, with audio, an audio stream carrying[seq, capture time, length] + Opus; each stream starts with a kind byte. Why a session ended (stopped, source closed, busy, version mismatch) travels as a QUIC application close code. In the code, the close codes, stream kinds and codecs are enums decoded with strum'sFromRepr; their numbers never change, because older peers read them. - Codec: the broadcaster asks for H.265 and gets H.264 when the graphics card has no working H.265 encoder. The codec travels in every frame, not once per session, so if the GPU encoder fails mid-broadcast, the broadcaster switches to H.264 for the rest of it and viewers switch decoders at the next keyframe. The H.265 encoder runs with low-delay VBR capped at the preset's bitrate (CBR padded every frame to the full bitrate for no gain in quality), no B-frames, and parameter sets on every keyframe, so each frame comes out right away and a viewer can join at any keyframe.
- Audio/video sync: both streams carry the broadcaster's capture time. The viewer compares
local time β capture timefor the video being shown and for arriving audio; the unknown clock difference cancels out. It then delays audio (never video) to match, on top of a jitter margin of at least 40 ms. Drift and gaps are absorbed with short skips or silences. - Muting apps: Windows process loopback can include or exclude only one process tree per capture, and "everything except Peeroxide" already uses that. So while nothing that plays is muted, a monitor's sound is that single capture, as before. Once an app is muted, every other app is captured on its own (with its child processes) and the captures are mixed: chunks are placed by capture time (small jitter snapped, so each stream continues seamlessly), summed, and released in 10 ms blocks after 40 ms. The list of apps is refreshed every second and right after a choice changes. A process that started a muted app (e.g. a launcher that started Discord) isn't captured, because its capture would include the muted app. Other Peeroxide instances are never captured in this mode.
- Audio never takes video down: if audio can't be captured, the broadcast goes out video-only with a note; a broken audio stream or missing output device only silences audio.
- Mini player: eframe repaints a minimized window at most every 100 ms and, while it is minimized, runs only
App::logicfor it: no ui pass, and no new windows. So the mini player is a deferred viewport (its own window and repaint loop, at the stream's frame rate) that exists, hidden, for as long as a stream is showing;App::logicreveals it the moment the main window is minimized, and from then on the main window's passes run again (every 100 ms) and keep it registered. It shares the main window's video texture, so switching between the two never shows a black frame, even on a still screen. - Threads: capture, video encode, audio encode, video decode and audio decode each run on their own thread; the network runs on Tokio. Per-frame pixel work that splits well (converting macOS/Linux captures to BGRA, and
windows-capture's copy of padded frames) uses rayon, with a pool sized at start: one thread on Windows, two elsewhere. rayon's default of one thread per core spun more CPU than it saved on jobs of a few milliseconds, so scaling the canvas stays on one thread. A paused encoder is woken through a crossbeam channel, and the viewer builds each image on the decoder thread so the UI thread only uploads it. - Errors: code that can fail returns
anyhow::Result, with context saying what it was doing (which file, which socket), and errors are logged and shown with{e:#}so their causes aren't lost. Typed errors (thiserror) remain only where the caller acts on the kind of error:UpdateError(which note the updater shows),CaptureError(a source that's gone ends the broadcast as "closed", not as a failure) and the wire protocol'sProtocolError. The developer tools (release-sign,serve-release,probe,bench,audio-probe) print the whole cause chain and exit with code 1 when they fail.
Mapping to abuse-cases.md:
| Abuse case | MVP mitigation |
|---|---|
| AC-01 Rogue broadcaster impersonation | Each peer's ID is the SHA-256 of its long-lived certificate. The viewer pins the fingerprint from the announcement and aborts the TLS handshake on mismatch ("Identity check failed"). Duplicate names are flagged in the UI, and a saved contact's name showing up with a different ID is flagged as a possible impersonation (trust on first use). |
| AC-03 Stream tampering Β· AC-05 Eavesdropping | All traffic is QUIC with TLS 1.3 (AEAD); tampered packets are dropped and nothing is sent in the clear. |
| AC-04 Repudiation | Daily-rotated session logs record broadcasts, viewers (name and address), watch sessions and end reasons. |
| AC-06 Enumeration | Peers are only announced while broadcasting. |
| AC-07 UDP flood / oversized messages | Hard size limits on control messages (64 KiB), frames (8 MiB) and audio packets (4 KiB) checked before allocation, 8-viewer cap, handshake timeouts; a broadcaster may open at most two streams to a viewer and viewers none; QUIC discards unauthenticated packets cheaply. |
| AC-08 Fake announcement flood | Announcements are validated (version, 64-hex fingerprint, port, IPv4 addresses, sanitized name) and capped at 64 peers. |
| AC-10 Capture beyond the selection | Windows are captured through the OS window-capture API, never by cropping a desktop capture. A window's audio is captured through the OS per-process loopback API (that app's process tree only), never by filtering the whole system's sound. The microphone is never captured. |
| AC-11 Unintended audio disclosure | Audio is off by default and chosen per broadcast; the UI states exactly what is captured; a window shares only its app's sound; Peeroxide's own playback is excluded; a monitor share can leave out any app playing sound, and voice chat apps are left out unless the broadcaster says otherwise; nothing is captured while no one watches. |
| AC-12 Malicious update | Updates must carry a minisign signature from the release key, which is kept offline and never on GitHub. The signed comment must name the exact package, and only strictly newer versions are accepted, so neither an old package nor a downgrade can be slipped in. HTTPS only, with size caps; nothing is extracted before verification, and the package's own paths are never used. |
| AC-13 Update check exposure | One request per start, carrying only the app version; --no-update turns it off. |
Not yet addressed: AC-02 (viewers are not authenticated; any peer on the LAN can watch, and hear, a broadcast) and AC-09 (the video decoders, OpenH264 and libde265, are C and C++ code running in-process; sandboxing and fuzzing are future work). The Opus decoder is pure Rust on its own thread with panics caught, and survives a 5,000-packet garbage test, but it isn't fuzzed or sandboxed either. Treat broadcasts as visible and audible to everyone on the network.
Measured on the development machine (Windows 11, 12-thread desktop CPU, Radeon RX 6600 for H.265, OpenH264 built without NASM), both ends on the same machine:
| Scenario | Result |
|---|---|
| 1080p30 monitor broadcast | Broadcaster 3.9 % total CPU (47 % of one core); viewer 2.3 % |
| Encode time, 1080p | ~9β10 ms typical desktop; 22 ms worst case (full-screen scrolling text) |
| Encode time, 720p | ~5 ms typical; 11 ms worst case |
| 1080p60, full-screen scrolling text (worst case) | H.265 on the GPU: 5.8 ms per frame including the conversion to NV12, 5.3 Mbps. H.264 on the CPU manages only 39 fps (21 ms per frame) |
| Internet / VPN preset (720p, 24 fps), scrolling text | H.265: 1.2 Mbps at 55 dB PSNR, under the 2 Mbps budget. H.264: 2.2 Mbps |
| Decode time (one thread) | H.265 with libde265: 8β9 ms at 1080p, 3β4 ms at 720p. H.264: 4.6 ms and 1.9 ms |
| 720p60 test pattern, H.265, viewer on the same PC | 61 fps shown, capture β decoded 13 ms |
| Scaling a 1080p screen to the 720p canvas | 3.5 ms per frame (one thread; rayon was faster but cost 2β5Γ the CPU) |
| Sharing a window, viewer on the same PC (Windows 11 23H2) | Broadcaster 29β36 % of one core; 68β100 % before rayon's pool was sized |
| Capture β decoded frame | 5 ms (test pattern), 15β18 ms (monitor), ~40 ms (window) |
| Discovery β watching | ~1.3 s after launch |
| Broadcaster stops β viewer notified | ~2 ms (graceful) Β· ~6 s (crash, QUIC idle timeout) |
| Opus, 48 kHz stereo at 128 kbps | 10 s of audio: encode 46 ms, decode 28 ms (under 1 % of one core) |
| Audio vs. video, test pattern on one machine | Audio plays ~64 ms after the video (20 ms Opus frames + 40 ms jitter margin; the test pattern's video path is unusually fast, 6 ms) |
Glass-to-glass latency also includes display refresh (one or two frames). To measure it, show a millisecond stopwatch on the broadcaster and put the viewer next to it on the same screen.
cargo test --workspace
cargo clippy --workspace --all-targets.github/workflows/ci.yml runs formatting, clippy and the tests on Windows, macOS and Linux on every push. It is also the only compile check of the macOS/Linux code so far.
Automated tests (166) cover:
- the wire protocol, including malformed and oversized input and unknown codecs, for video and audio, and the fixed values of close codes, stream kinds and codecs;
- identity persistence and fingerprint rejection;
- real QUIC sessions on localhost: ordering, keyframe-first, stop reasons, viewer cap, version mismatch (including 0.3 peers), lagging viewers, switching, unreachable peers, which address answered;
- audio over QUIC: in order next to video, absent when not shared, a lagging viewer skipping ahead, and broken or unexpected streams leaving the video running;
- Opus round trips (tone levels and stereo separation at both bitrates, silence, garbage packets, loss concealment);
- muting apps: grouping audio sessions by app (leaving out Peeroxide, other instances and Windows' own sounds), which processes to capture (none with a muted descendant, no tree twice, one capture while nothing is muted), the voice chat defaults and remembered choices, the mixer (sums, jitter, gaps, late chunks, clamping, release timing), and the checklist's rows and the live audio line;
- the playout scheduler (jitter margin, sync with slower and faster video, the 200 ms cap, drift corrections, gaps), 20 ms framing of captured audio, the resampler, the volume control (lock-free) and the test tone;
- the encoder's wake-up (one sent just before it sleeps isn't lost);
- announcement validation and the peer-table cap, plus a real mDNS round trip;
- the self-update, against a local server standing in for GitHub:
- choosing the release: newest newer signed one, pre-releases included; drafts, older or equal versions, unsigned packages and other platforms ignored;
- signatures: valid, tampered, wrong key, replayed under another name, edited signed comment;
- the package: extracting the exe, rejecting archives with none or several, and paths that try to escape;
- behaviour: size caps, skip, rate limits, a silent server (5 s), a second instance, insecure URLs;
- restart options, and the notes shown after a failure;
- saved contacts (merge, cap, corrupt files, ID-change detection), the sticky broadcast port, and audio settings defaults for older settings files;
- the viewer state machine against the use-case diagram, the fullscreen rules (when to enter and leave, when the controls show), and the mini player's (when it shows, and where it opens: the corner, or where it was left if that's still on screen);
- the command-line options: clap's own consistency check, contradictory options refused, and the update settings' defaults;
- H.264 round trips, canvas letterboxing, and the Internet preset holding its budget on scrolling text without dropping frames;
- H.265 decoding of a 24 KB fixture made by the AMD encoder: each access unit gives its picture at once, with good quality; joining at a later keyframe; garbage input survived; the viewer switching decoders when the codec changes;
- the presets: every one saved by id, and names saved by older versions still loading.
just test-hw runs what CI can't, in a release build: the GPU's H.265 encoder (keyframes on request, round trips through libde265, joining at a keyframe, the Internet budget and sharpness on scrolling text, 1080p encode time) and the timing measurements.
Developer tools: cargo run --release -p peeroxide-capture --example probe (list sources, measure capture rate), cargo run --release -p peeroxide-codec --example bench [source|test|scroll] [seconds] [720|720-60|1080|1080-60|internet] [--codec h264|h265] [--dump FILE] and cargo run --release -p peeroxide-audio --example audio-probe [system|tone|PID] [seconds] [--play] [--list-apps] [--mute EXE]... [--unmute-after SECONDS] (record an audio source to audio-probe.wav and check the output device; list the apps playing sound, or record a monitor's sound with apps muted). Each tool, like release-sign and serve-release, explains its options with --help (after -- with cargo run).
- A broadcasts a monitor; B discovers it via mDNS and watches at ~30 fps.
- A shares a window; resizing letterboxes it, minimizing holds the last frame, closing it shows "The shared window was closed" on B.
- B leaves β A pauses capture.
- A is killed β B returns to the list (~6 s); A closed normally β B is told immediately and A disappears from the list.
- Two different machines over Radmin VPN, with discovery and pasted connect strings.
- Connecting once by connect string saves the broadcaster; they keep the same port after a restart; a different identity using their name is flagged.
- Reconnecting from Saved by clicking, and switching broadcasters by clicking (both covered by automated tests; click once by hand).
- Renaming yourself with β persists across restarts.
- A session over Radmin VPN with the Internet / VPN preset while scrolling or playing video.
- Test pattern with Share audio on one machine, viewer muted: the viewer receives, decodes and plays the stream (A/V offset ~+64 ms, no underruns, no bad packets); without it, the viewer shows "The broadcaster isn't sharing audio"; audio capture pauses when the viewer leaves.
- System loopback capture (everything except Peeroxide) starts on Windows 11 and delivers 10 ms chunks (audio probe; nothing was playing, so only silence was recorded).
- Hearing it: the beep matches the flashing square; the volume slider and mute work and are remembered.
- Sharing a browser window: only the browser's sound is heard, not other apps'.
- Sharing a monitor while also watching someone: no echo or feedback.
- Two machines on the LAN with audio.
- Muting apps, recorded with the audio probe while two processes play different tones (PowerShell 440 Hz, Python 880 Hz): muting either one removes exactly its tone, muting both leaves silence, the mix has no clicks or gaps, and unmuting live brings the tone back within the same second.
- Muting apps in the app: a monitor broadcast with audio lists Discord (ticked), LibreWolf and Steam, the live audio line says "except Peeroxide and Discord", and a viewer on the same PC receives the audio (128 kbps, no bad packets).
- Muting Discord or TeamSpeak in a real call, with the friends in the call watching from other PCs.
- Mini player, two instances on one PC driven with Win32 input: minimizing shows it in the corner (repainting about 60 times a second while the main window is minimized); dragging moves it; a double-click brings Peeroxide back and hides it; minimizing again reopens it where it was left (also after a restart, from
settings.toml); π stops watching and leaves Peeroxide minimized; the broadcaster stopping closes it. The icons render, and the main window shows the stream again at once. - Mini player by hand: the resize grip, the mute button, and a second monitor or a display scale other than 100%.
- Audio over Radmin VPN with the Internet / VPN preset.
- H.265 on the RX 6600 (AMD): test pattern at 60 fps with the viewer on the same PC shows "H.265 61.0 fps", nothing dropped.
- H.265 between two PCs on the LAN, at 1080p60 and with the Internet / VPN preset over Radmin VPN.
- H.265 on NVIDIA and Intel graphics.
- A PC without a hardware H.265 encoder falls back to H.264 (so far only forced with
--codec h264). - Self-update on one PC with a throwaway key and a local server: 0.4.99 updated itself to 0.5.0 in about 2 s and restarted showing "Updated to 0.5.0"; started again, it found nothing newer; a tampered package was rejected and the app opened on its old version; with no server it opened in about 2 s.
- A release build with the real key checks GitHub itself over HTTPS (Windows certificate store) and reports "up to date" in about 0.4 s.
- Self-update by hand: Skip, a read-only folder, two profiles starting at once, and a real update through GitHub (the release after the first one with the updater).
- macOS and Linux (see below).
- macOS and Linux are untested. On macOS, grant Screen Recording permission (System Settings β Privacy & Security) and restart the app. On Linux/Wayland the source is chosen in the system's screen-share dialog.
- IPv4 only.
- H.265 is only encoded on Windows, by the graphics card (tested on AMD; NVIDIA and Intel not yet). Elsewhere, and without such an encoder, broadcasts use H.264 on the CPU, where 1080p at 60 fps may not keep up. VideoToolbox (macOS) and VAAPI (Linux) are planned for 0.8.
- H.265 is decoded on the CPU, on one thread (libde265): 8β9 ms per 1080p frame on the development PC, fine for 60 fps there, but a slower PC may not keep up with 1080p60.
- Audio can only be shared from Windows (10 2004 or later, or 11) for now; macOS and Linux capture is planned for 0.8. Playback is built for all three but has only been tested on Windows. Audio on Windows 10 has not been tested yet either.
- Windows Store (UWP) apps: their windows belong to
ApplicationFrameHost.exe, so sharing such a window shares none of its sound. Share the monitor instead. - The mini player opens on the main monitor's area (it remembers a position only if it's still on that monitor). It has mute but no volume slider. On Linux with Wayland, the system doesn't report minimized windows, so it doesn't appear there (like the rest of Linux, untested).
- Audio is turned on or off before a broadcast starts; there is no mute-everything while live. Muting single apps works live.
- Muting apps is Windows-only, like audio capture. While any app is muted, Windows' own notification sounds aren't shared, and an app that starts playing sound is picked up within about a second. An app that started a muted app (e.g. a launcher that started Discord) isn't shared either, since its sound can't be captured without the muted app's. Apps that play sound through another process (e.g. some Windows Store apps) are listed under that process's name.
- Self-update: Windows only, and the first version with the updater has to be installed by hand. Some antivirus programs distrust apps that replace their own executable; if yours blocks it, download the new version by hand.
- The window list may include a few invisible system windows.
- OpenH264 built from source is not covered by Cisco's patent license, which only applies to Cisco's prebuilt binary. That's fine for personal LAN use; distribution would need the prebuilt library, which the
openh264crate can load. - H.265 is covered by several patent pools, with no equivalent of Cisco's free license. The GPU encoders are licensed through their makers, but the libde265 decoder in the app isn't. As with OpenH264, that's fine for personal LAN use; wider distribution needs a look first (roadmap 0.8).
In the platform's application-data directory; on Windows, %APPDATA%\Peeroxide\data. --profile x uses profiles\x inside it. The path is printed on startup (starting β¦ dir=β¦). Data from versions up to 0.2, when the app was called "P2P Screen Share", is moved there on first run.
identity.cert.der,identity.key.der: this peer's identity. Deleting them creates a new ID.settings.toml: display name, quality preset, broadcast port, whether to share audio, which apps are muted in a shared monitor's sound, volume/mute, and where the mini player was left.contacts.toml: saved broadcasters (ID, name, last working addresses).logs/session.log.YYYY-MM-DD: session log, including update checks and installs.
While updating, peeroxide.update.* files briefly appear next to peeroxide.exe; they are removed when it finishes.
Licensed under the Apache License 2.0. The software is provided "as is", without warranty of any kind.
Peeroxide includes libde265, the H.265 decoder, which is licensed under the LGPL-3.0 (see crates/de265-sys/vendor/COPYING). Release zips carry its notice in THIRD-PARTY-NOTICES.txt.