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 |
Quick start
Prerequisites
- Rust 1.88 or newer (see
rust-versioninCargo.toml) - System dependencies:
- Linux:
pkg-config,libasound2-dev,libdbus-1-dev,libudev-dev,libxcb1-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. - macOS/Windows: none
- Linux:
Install
From crates.io, on any machine with a Rust toolchain:
That installs one binary, pcc. If you would rather not build it, the
release page carries prebuilt archives for macOS, Linux and Windows:
https://github.com/carterlasalle/PixelChangeCheck/releases
Either route still needs the system dependencies above on Linux, because the capture path links against them.
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 never inspects frame contents, so end-to-end encryption can be added without changing it.
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.