Skip to content

About

Lossless desktop replication: sends only the pixels that changed. Native and browser viewers over QUIC, with a relay for NAT.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

132 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PixelChangeCheck

Lossless desktop replication: send only the pixels that changed.

CI Release Rust License crates.io

Install · Everyday tasks · Usage · 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
Loading

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

The easiest path: answer questions

pcc

That is the whole thing. Run with no arguments, pcc walks you through what to share, how viewers connect, audio, and whether to ask before admitting each viewer — then prints the exact command and what the other person runs, and offers to start it. Nothing is hidden behind flags.

pcc menu does the same when you want it explicitly (or off a terminal).

If a viewer sends you a pcc://view?... link, choose View → A link and paste it whole: both long values in it — the token, and a pin that means the relay in one form and the sharer in the other — are easy to mistype and impossible to tell apart afterwards.

The 30-second version

On the machine you want to show:

cargo install cargo-binstall          # once, if you do not have it
cargo binstall pixel-change-check-client
pcc share

pcc share prints a fingerprint and ready-made viewer lines. How the other person watches:

The two of you are… The viewer does
on the same network Runs the pcc view --connect … --token … --pin … line pcc share printed (they need the binary installed). The printed browser URL is loopback, so it only opens on the sharing machine.
anywhere else, or they want no install at all You both go through a relay: the viewer opens one link in any browser and installs nothing. See the one-link setup.

The one-link setup

Run a relay on any host both sides can reach — a cheap VPS, a home server, even your own machine for a quick test:

pcc relay --listen 0.0.0.0:5900 --web

--web makes the relay serve the browser viewer too, on the same port and behind the same certificate. It prints its token, its certificate fingerprint, and then a complete share line you can copy verbatim — its own token and pin already filled in, and an address a remote side can actually dial:

On the machine being shared, run:
  pcc share --relay 203.0.113.5:5900 --relay-pin e495ea… --token LBYYJV…

Run that on the machine being shared. It prints a Viewer link: pcc://… and a Browser link: https://…; send either one. The browser link needs nothing installed on the other end — just open it.

Do that once. Add --remember <name> to the relay and none of it is copied again:

# on the relay host, once
pcc relay --listen 0.0.0.0:5900 --web --remember hetzner

# on the machine being shared, from then on
pcc share --use hetzner

The address, the pin and the token are remembered ($XDG_CONFIG_HOME/pcc/relays.json, 0600 because it holds a token; PCC_RELAYS overrides the path). It is the ~/.ssh/config + known_hosts pattern rather than a certificate authority: the pin is recorded when you save and is still checked against the live certificate on every connection, so a relay that changes identity is refused. pcc with no arguments lists saved relays as a numbered choice, so you pick one instead of pasting.

Two things that will otherwise cost you a round trip:

  • The share's --token must be the relay's token, exactly as printed. One value across all three sides; a mismatch closes with bad credential. This is the thing --use exists to remove.
  • Without --cert/--cert-key the relay generates a certificate per process, so browsers warn and the fingerprint changes on restart. Supply a real certificate for anything beyond a test.

pcc with no arguments walks through all of this interactively and prints the same lines for you.

Prerequisites

  • Rust 1.91 or newer (see rust-version in Cargo.toml)

  • System dependencies, Linux only (macOS/Windows: none). Each entry is here because a release run failed without it or the lockfile proves the probe — never vibes. Install them all at once:

    You install Because Chain
    pkg-config every -sys probe needs the tool itself —
    libasound2-dev audio capture links ALSA alsa-sys ← alsa ← cpal ← audio feature
    libdbus-1-dev display enumeration over D-Bus libdbus-sys ← dbus ← screenshots
    libegl-dev, libgbm-dev GBM/EGL screen capture — a release run failed in khronos-egl's probe without them khronos-egl + gbm-sys ← gl/gbm ← libwayshot-xcap ← xcap
    libpipewire-0.3-dev PipeWire screen capture — a release run failed in libspa-sys's probe without it libspa-sys ← pipewire ← xcap (not cpal — cpal's Linux audio is ALSA-only)
    libwayland-dev Wayland capture + native window — a release run failed in wayland-sys's probe without it wayland-sys ← wayland-backend ← gbm/libwayshot-xcap ← xcap, and wayland-sys ← minifb (native-viewer feature)
    libxcb1-dev X11 display info xcb ← display-info/screenshots/xcap (all three list it directly)
    libx11-dev and friends the cursor pointer queries X11 x11 ← device_query ← capture (cursor sampler; xlib feature only, so the probe that fires today is x11 itself — the rest of x11's build-script list fires only on cargo features nothing enables)
    libudev-dev, libxkbcommon-dev, libxrandr-dev kept, unverified — no libudev-sys, xkbcommon-sys, or enabled xrandr probe anywhere in Cargo.lock. They ride with the stacks above on every real system, and dropping a working dep blind to prove a point risks the next release run. Prove-or-drop on the next'Dependencies bump: remove one, watch the Ubuntu release leg, keep or restore on evidence.

    Two things this table deliberately does not list:

    • libopus-dev: worth installing (apt install libopus-dev, or brew install opus on macOS) so the build links the system Opus instead of compiling a vendored copy with cmake — but optional, because the vendored build works without it. Set OPUS_LIB_DIR / OPUS_LIB_STATIC only if you need to point at a non-standard install; the default finds pkg-config's answer.
    • Anything for --no-default-features: that build drops cpal/Opus (no ALSA, no libopus cmake) and minifb (no X11/Wayland headers), so a headless relay needs only pkg-config plus the capture rows. The capture stack itself (screenshots, display-info, xcap, device_query) is not feature-gated — that is the honest gap in the table above, and gating it is future work, not a flag that exists today.

Install

The fastest route is a prebuilt binary — no compiler, no system headers, no twenty-minute build. binstall is a separate tool, so install it once first:

cargo install cargo-binstall
cargo binstall pixel-change-check-client

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 and ARM Linux, Apple Silicon Macs, and x86_64 Windows. 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.

If pcc runs a C compiler: the name collides with the Portable C Compiler, which some systems put earlier on PATH. pcc share answering clang: error: unknown argument means you are running clang, not this tool. Check which -a pcc, then use the cargo path directly (~/.cargo/bin/pcc …) or fix PATH order. Confirm with pcc --version — it must print pcc 0.1.x, and anything else is the wrong binary.

From source, on any machine with a Rust toolchain:

cargo install pixel-change-check-client --locked

--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:

cargo install pixel-change-check-client --locked --no-default-features

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.

Headless/CI rule of thumb: if a source build fails on a system library (libpipewire, libasound, wayland-client, …) and this machine only runs a relay — or never opens a window — stop installing -dev packages: you want --no-default-features, not more headers. The build error cannot say this (cargo owns that output), so the docs say it here instead.

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 (see the prerequisites table above for why it stays optional). 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

pcc share

That prints the certificate fingerprint and a complete, copy-pasteable viewer command:

Certificate fingerprint (sha256): 9f2c...
Browser viewer: http://127.0.0.1:8080/#token=ABC...
Direct viewers on 0.0.0.0:5800
  pcc view --connect 192.168.1.20:5800 --token ABC... --pin 9f2c...

The browser line is loopback because --web defaults to loopback: a non-loopback browser address without --web-cert/--web-key is refused rather than served over plaintext. The direct line prints the reachable address on your network (see below); the relay line, when --relay is set, prints the exact viewer command with the relay pin substituted.

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:

pcc view --connect <sharer-ip>:5800 --token <token> --pin <fingerprint>

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? Use the relay — see the one-link setup for running one and the links it produces. If you would rather pass the pieces explicitly:

pcc view --relay <relay-ip>:5900 --pin <relay-pin> --session <session> --token <token>
pcc view --connect <sharer-ip>:5800 --relay <relay-ip>:5900 --pin <relay-pin> --session <session> --token <token>

The second form races the direct path and the relay and takes whichever answers first, so you do not have to know which one will work.

No relay at all, if the two ends can reach each other directly:

pcc share --transport iroh                 # prints a ticket
pcc view --transport iroh --ticket <ticket> --token <token>

iroh handles hole-punching and falls back to its own relay; the ticket is the whole address, so there are no ports to open. (There is also a manual-signalling WebRTC transport — --transport webrtc — for the case where both ends can only exchange a blob by hand; see the usage guide.)

Through a disconnect: add --reconnect to the viewer. It keeps the last frame on screen, then resumes from where it stopped — the sharer replays the recent updates it still holds, or sends a fresh snapshot if the viewer fell too far behind.

Do I need a relay?

Most of the time, no. Ask the tool:

pcc diagnose

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:

pcc share --reach auto     # try IPv6, then STUN, then the relay
pcc share --reach direct   # refuse the relay rather than use it
pcc share --reach relay    # skip discovery

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:

pcc share --web-cert cert.pem --web-key key.pem

The surface content is encrypted either way. A certificate stops the key exchange itself from being readable on the wire.

Everyday tasks

I want to… Command
share my whole screen pcc share
share one display pcc share --display 1 — list them with pcc diagnose --displays
share part of the screen pcc share --region 100,200,1280,720 (x,y,w,h in display pixels)
share one window pcc share --window "Firefox" — title substring, fails fast on no match
share one app pcc share --application "Code"
include audio pcc share --audio-source system — or mic, both, none; list devices with pcc diagnose --audio
approve each viewer by hand pcc share --approve — prompts Admit? [y/N] per viewer
add a browser viewer on another machine without a certificate use the relay's --web (above) — the relay brings its own certificate
share with no open port and no relay to run pcc share --transport iroh — prints a ticket; the viewer runs pcc view --transport iroh --ticket <ticket> --token <token>
keep watching through a disconnect pcc view --reconnect — the last frame stays on screen and it catches up
see what this machine can do pcc diagnose (add --displays or --audio)

The exhaustive reference for every flag is docs/usage.md.

Commands

pcc share --help
pcc view --help
pcc relay --help
pcc diagnose

Watching it work

pcc share --synthetic --stats-interval 5 --metrics-listen 127.0.0.1:9100
curl -s 127.0.0.1:9100/metrics

--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

  1. Capture a frame.
  2. 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.
  3. If much of the screen moved as one displacement, verify it byte for byte and emit a Copy; hash agreement alone is never trusted.
  4. Represent each remaining rectangle as a solid Fill or an exact, LZ4-compressed patch, whichever is smaller.
  5. If the patches would cost more than a fresh lossless snapshot, send the snapshot instead.
  6. 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 the Compositor, 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.subtle has 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.md for why a lossy base is not an option, and docs/adr/ for the rest.
  • There is no input injection or clipboard sync.

Documentation

Document Purpose
Usage Every entry point — all five commands, every flag, the browser routes, the three transports — with how to invoke each
Agent skill Every flow, the three secrets, troubleshooting, and the observability recipes, written to be read by a coding agent (also installed at .omp/skills/pixelchangecheck/SKILL.md)
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:

cargo test                 # unit, replication, and real-socket end-to-end
cargo clippy --all-targets # lint, must be warning-free
cargo fmt --all -- --check
bash scripts/smoke.sh      # end-to-end against the real binaries

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.

About

Lossless desktop replication: sends only the pixels that changed. Native and browser viewers over QUIC, with a relay for NAT.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages