Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
PixelChangeCheck
Lossless desktop replication: send only the pixels that changed.
Install · How it works · Architecture · Relay · Limitations · Design records
A conventional screen stream re-encodes the whole picture every frame. PixelChangeCheck keeps a lossless authoritative surface: the sharer diffs each captured frame against the framebuffer viewers actually hold, and sends only what changed. For a terminal, an IDE, or a document, that is usually a few kilobytes per frame. When nothing changes, it sends nothing but a keep-alive.
It shares to a native viewer over QUIC, to any web browser including a phone over a WebSocket that speaks the same change protocol, or to a relay when both ends are behind NAT.
How it works
flowchart LR
A[Captured frame] --> B[Detect changed blocks]
B --> C{Diff against the<br/>authoritative surface}
C --> D[Plan: fill / copy / rect]
D --> E{Is the patch set<br/>cheaper than a snapshot?}
E -->|no| F[Lossless snapshot]
E -->|yes| G[Partial update]
C -->|nothing changed| H[Keep-alive]
F --> I[Sequence: revision + epoch]
G --> I
I --> J[Encode once]
J --> K[QUIC viewer]
J --> L[Relay, TCP + TLS]
J --> M[Browser over WebSocket]
K --> N[Compositor]
L --> N
M --> O[Browser compositor]
N --> P[Bytes identical to<br/>the sharer's surface]
O --> P
The diff is against the reference — the framebuffer an up-to-date viewer holds — not against the previous capture. That is what makes sub-threshold changes accumulate instead of being discarded forever. The reference advances only over rectangles that actually shipped, so anything too small to send this frame is still there to send later.
Capabilities
| Area | What PixelChangeCheck provides |
|---|---|
| Detection | Allocation-free block comparison, tile-hash region merging, and a bounded verified-displacement search for scroll reuse |
| Representation | A three-way choice per region: solid fill, verified copy, or an exact LZ4 patch — whichever is genuinely cheaper |
| Cost control | When the patches for a frame would cost more than a fresh lossless snapshot, the sharer sends the snapshot instead |
| Snapshots | Chunked and atomic (begin/chunk/commit), so an interrupted snapshot can never half-replace a working surface |
| Sequencing | One authority for revisions and epochs, so a late joiner, a recovered viewer, and a viewer that fell behind all converge to the same pixels |
| Authentication | A viewer token authorizes access; a certificate pin proves who you are talking to |
| Encryption | Every frame sealed end to end, per viewer, on the native path and in the browser |
| Relay | Per-viewer byte budgets, generational ownership, a defined slow-viewer policy, and no inspection of frame contents |
| Reachability | An explicit ladder — global IPv6, STUN, NAT-PMP, an ICE-lite peer check — with the relay as the deliberate last rung |
| Observability | A stats table, Prometheus metrics on loopback, and JSON logs for bug reports |
| Viewers | A native window, and a WebSocket compositor for any browser, with an explicitly lossy MJPEG fallback |
| Transports | Direct QUIC, multi-relay fan with redirect-based broadcast mode, iroh tickets (ADR 0007), and manual-signalling WebRTC data channels (ADR 0008) |
Quick start
Prerequisites
- Rust 1.88 or newer (see
rust-versioninCargo.toml) - System dependencies:
- Linux:
pkg-config,libasound2-dev,libdbus-1-dev,libegl-dev,libgbm-dev,libpipewire-0.3-dev,libudev-dev,libwayland-dev,libxcb1-dev,libxkbcommon-dev,libxrandr-dev. The ALSA headers are not optional:alsa-sys, which the audio capture path links against, runspkg-configat build time and fails the build without them; the native window (minifb) needs the Wayland pair the same way, cpal's PipeWire backend needslibpipewire-0.3-dev, and the capture path's GBM/EGL stack (libwayshot-xcapvia xcap) needslibegl-devandlibgbm-dev. - macOS/Windows: none
- Linux:
Install
The fastest route is a prebuilt binary — no compiler, no system headers, no twenty-minute build:
That downloads the release archive for your platform from the
releases page
and installs one binary, pcc. The archives are named
pcc-<version>-<target> (.tar.gz, .zip on Windows), covering
(x86_64 Linux and Windows, Apple Silicon Macs). Intel Macs are not
covered: GitHub retired the last Intel macOS runner, so there is no
x86_64-apple-darwin archive — install from source there, or run the
Apple Silicon build under Rosetta 2.
From source, on any machine with a Rust toolchain:
--locked matters: the committed Cargo.lock pins every transitive
dependency, so the build you get is the build CI tested rather than
whatever is newest today. The default build includes audio capture and
the native window. A headless machine that needs neither — a relay
host, a CI runner — can skip the system libraries entirely:
That drops cpal/Opus (no ALSA headers, no vendored libopus cmake build) and minifb (no X11/Wayland dev headers); the web viewer and every transport keep working. With default features on Linux you still need the system dependencies above, because the capture path links against them.
A note on Opus specifically: with libopus-dev (Debian/Ubuntu) or
opus (Homebrew) plus pkg-config installed, the build links the
system library instead of compiling a vendored copy with cmake. That
one package is the difference between a pure-Rust build and a C
toolchain step. There is deliberately no .cargo/config.toml forcing
a linker here, so nothing about that choice is hidden: the default
build works with stock rustup, and faster linkers stay opt-in.
Share your screen
That prints the certificate fingerprint and a complete, copy-pasteable viewer command:
Certificate fingerprint (sha256): 9f2c...
Browser viewer: http://192.168.1.20:8080/#token=ABC...
Direct viewers on 0.0.0.0:5800
pcc view --connect 192.168.1.20:5800 --token ABC... --pin 9f2c...
pcc pair prints a single line a viewer can open or paste, carrying both
the token and the pin, so nobody retypes a 64-character hex string.
View it
Native window, on another machine:
Any browser, including a phone: open
http://<sharer-ip>:8080/#token=<token>
That page runs the same compositor as the native client. If your browser
cannot, /fallback serves a lossy MJPEG preview, labelled as such.
Behind NAT on both ends? Run a relay on any reachable host:
It prints its own token and fingerprint. On the sharer:
and on each viewer:
Do I need a relay?
Most of the time, no. Ask the tool:
It reports your local addresses, whether you have a global IPv6 address (a link-local one is not routable and saying otherwise sends you down a dead end), whether there is a default route, and a STUN reflexive address. Then:
The relay is the last rung on purpose. It is TCP+TLS, so it already traverses the corporate proxies that block UDP -- which is exactly where a direct-only path fails. Symmetric NAT and UDP-blocking proxies are the two cases nothing free fixes.
Running one is cheap here specifically: at roughly 570 bytes/frame, a
session is about 160 MB per viewer per hour. See deploy/relay/ for a
free-tier setup.
Browser over TLS
The web port is plaintext unless you give it a real certificate, because a self-signed one would only produce a browser warning:
The surface content is encrypted either way. A certificate stops the key exchange itself from being readable on the wire.
Commands
Watching it work
--stats-interval prints a table; --metrics-listen serves Prometheus
text on loopback. RUST_LOG=pcc=debug narrows the log, and
--log-format json is what you want in a bug report.
How PCC works
- Capture a frame.
- Diff it against the reference -- the framebuffer an up-to-date viewer holds. The reference advances only over rectangles that actually shipped, so a change too small to send this frame is still there to be sent later.
- If much of the screen moved as one displacement, verify it byte for
byte and emit a
Copy; hash agreement alone is never trusted. - Represent each remaining rectangle as a solid
Fillor an exact, LZ4-compressed patch, whichever is smaller. - If the patches would cost more than a fresh lossless snapshot, send the snapshot instead.
- Send nothing but a keep-alive if nothing changed.
The viewer applies each transaction atomically against a single
sequencing authority, and a Copy reads the surface as it was before
the transaction -- which is what lets two regions swap in one update.
Architecture
capture
|
v
reference <- detect <- current frame
(what | ^
viewers v |
hold) planner (fill / copy / rect, cost-based)
|
v
snapshot | partial update <- one sequencer: revision + epoch
|
v
encoded ONCE, shared with every viewer
|
+-------------+--------------+-------------------+
v v v
QUIC viewer relay (TLS) web (WS / MJPEG)
| | |
v v v
Compositor <--------------- forwards ------------> browser compositor
(a JS port of the same
state machine)
- Sharer (
pcc share): captures, diffs against the reference, plans, encodes once, and fans the same bytes out to every viewer. - Viewer (
pcc view): authenticates, reconstructs through theCompositor, and presents. - Relay (
pcc relay): pairs a host with viewers by session code and forwards framed bytes. It authenticates a session with a value derived from the session token, so it never holds the secret that authenticates the end-to-end encryption handshake. A relay that learns that derived value still cannot forge a viewer's proof, and so cannot read a stream it claims not to be able to read.
Safety model
The properties below are asserted by the test suite, not promised in prose:
- Exactness is measured. After a snapshot plus its updates, a viewer's pixels are byte-identical to the sender's reference. The suite asserts exactly that, including over a real socket and through the browser's JavaScript compositor.
- Malformed input is refused atomically. A hostile frame length, an impossible geometry, or a truncated payload leaves the framebuffer unchanged and the revision unadvanced.
- Revisions and epochs make out-of-order delivery harmless, and a geometry change starts a new epoch instead of ending the share.
- A wrong token is refused before any key agreement, and a pinned certificate is what the client trusts -- not the CA chain.
- A viewer that falls behind gets a fresh snapshot, never silently dropped patches that later ones depended on.
- Each viewer has its own keys. The sharer's plaintext broadcast exists nowhere; it is sealed per viewer, which is what makes a per-viewer revocation meaningful on a broadcast path.
- Budgets are tripwires, not silent truncation. Every limit failure names the budget, the configured limit, and the observed value.
The reasoning behind these choices is in docs/adr/. The normative
roadmap is in docs/spec/roadmap.md.
Known limitations
- A browser gets a sealed stream, but over a different primitive than
the native client.
crypto.subtlehas no X25519 in the versions most people run, so the browser does ECDH on P-256 with HKDF and AES-GCM while the native client does X25519 with ChaCha20-Poly1305. The protocol shape is identical; the primitives differ. A certificate is still worth supplying, because it stops the key exchange from being readable on the wire, but the surface content is encrypted either way. - Screen capture is full-frame; platforms that expose dirty rectangles (DXGI, ScreenCaptureKit, PipeWire) are not yet used to skip work.
- Motion video is not implemented. The exact-replication path is the
product; see
docs/adr/0001-lossless-authoritative-surface.mdfor why a lossy base is not an option, anddocs/adr/for the rest. - There is no input injection or clipboard sync.
Documentation
| Document | Purpose |
|---|---|
| Architecture decisions | Why the surface is lossless, how tokens and pins work, and how sequencing is enforced |
| Roadmap | The specified workstreams and their definition of done |
| Design | Product and design guidance, including what was deliberately not built |
| Contributing | Development workflow, the release process, and the trusted-publisher setup |
| Agent guidance | Repository-specific instructions for coding agents |
| Changelog | What changed in each release |
Contributing
Read CONTRIBUTING.md before making changes. The short version:
scripts/smoke.sh is the only check that exercises the CLI end to end.
cargo test passes while the printed pin does not work.
License
AGPL-3.0-only. See LICENSE.