scrcpy-rfb bridges the packetized H.264 video and control sockets of a
scrcpy 4.x server to one RFB/VNC port.
- Clients that negotiate the Open H.264 encoding (50) receive the original Android H.264 access units without server-side transcoding. TigerVNC and noVNC decode this encoding natively.
- Other VNC clients automatically receive Tight/JPEG, ZRLE, Hextile or Raw from a lazily decoded framebuffer on the same port.
The project depends on upstream LibVNC/libvncserver
at a pinned commit, fetched at configure time, plus the two patches in
patches/ (the Open H.264 server-side encoding and a deferUpdateTime=0
busy-loop fix). The patches have been submitted upstream; once merged, the
directory empties and the pin moves to a release tag.
Release binaries (Ubuntu 22.04 ABI, static FFmpeg H.264 decoder):
docker build -t scrcpy-rfb-builder -f docker/Dockerfile.builder .
docker run --rm -v "$PWD:/src" scrcpy-rfb-builder
# -> dist/scrcpy-rfb-linux-<arch>Local build (needs cmake, git, libjpeg-turbo, zlib and FFmpeg dev packages):
make # cmake configure + build into build/
make test # build + run the self testsPushes to master publish, via GitHub Actions:
scrcpy-rfb-linux-amd64/-arm64binaries, checksums andbuild-info.jsonto the movingmasterprerelease;- a multi-arch container image at
ghcr.io/cocoonstack/scrcpy-rfb:master(also tagged with the commit SHA):
docker run --rm --network host ghcr.io/cocoonstack/scrcpy-rfb:master \
--scrcpy-host 127.0.0.1 -rfbport 5900Start a scrcpy 4.x server with H.264 video and a TCP listener, then:
scrcpy-rfb [--scrcpy-host 127.0.0.1] [--scrcpy-port 27183] \
[--turn-screen-off] \
[libvncserver options]--turn-screen-off asks the scrcpy server to power off the physical display
after the video, control, and RFB paths are ready. Mirroring continues through
the virtual display. On every normal exit and handled SIGINT/SIGTERM, the
bridge sends a best-effort display-on message before closing the control
socket. A forced kill or a broken control connection cannot guarantee restore.
All standard libvncserver options are accepted, notably:
-rfbport port RFB listen port (default 5900)
-listen ipaddr bind to one interface, e.g. -listen 127.0.0.1
-passwd password require a VNC password (plain, use at your own risk)
-rfbauth file require a VNC password from a vncpasswd file
The bridge runs unauthenticated on all IPv4 interfaces by default; anyone who
can reach the port controls the Android device. In production either bind to
localhost and tunnel, or set -passwd/-rfbauth.
For an H.264-enabled TigerVNC viewer, disable automatic encoding selection so it does not override the explicit H.264 preference, and disable remote resizing since the bridge exposes the fixed scrcpy session size:
vncviewer -AutoSelect=0 -PreferredEncoding=H.264 -RemoteResize=0 -Shared=1 \
host::5900Ordinary VNC clients work with their defaults. Apache Guacamole does not include Tight in its default VNC encoding list; for the optimized ordinary path configure the connection with:
encodings: tight copyrect zrle hextile raw
compress-level: 1
quality-level: 8
disable-display-resize: true
- When the first ordinary VNC client connects, the bridge asks scrcpy to reset the video encoder so a static Android screen still produces an immediate config packet and keyframe. An H.264 client that has no recent keyframe to join from triggers the same rate-limited reset; a recent queued keyframe remains usable when the screen is static.
- H.264 passthrough is push-based: a new access unit wakes only the H.264 clients (no per-frame RFB request/response round trip), and each client keeps its own frame cursor. A client with no decoder state (one that just connected, or one whose cursor fell out of the queue) joins from a keyframe within six access units of the live edge, or else waits at the live edge for a fresh one instead of replaying an old GOP; if that reset is rate-limited or cannot be sent it replays the cached GOP, so a static screen cannot leave it waiting indefinitely. A client that is already mid-stream only ever skips forward to a keyframe it has not reached yet, so no cursor moves backwards and none is left waiting for a keyframe its decoder does not need.
- The fallback decode (FFmpeg + swscale) runs on its own thread with a bounded queue; if decoding cannot keep up, the backlog is dropped and the decoder waits at the live edge for the next encoder keyframe instead of stalling the passthrough socket reads. It requests a keyframe only if an active stream does not provide one within 1.5 seconds; queue pressure alone does not repeatedly tear down and recreate the scrcpy capture pipeline.
- Ordinary clients get change-detection at 32-pixel tile granularity, RFB
CopyRect for recognized vertical scrolling, adaptive Tight/JPEG quality
(Q92/Q86/Q80, never above the client's request) and an adaptive 60/30/20
fps publish rate driven by the slowest client's measured update time. Each
client socket is capped to a 256 KiB send buffer and, on Linux, a 64 KiB
TCP_NOTSENT_LOWAT, so slow connections apply backpressure instead of queueing stale updates. - Input: left button drags map to touch events with single-owner drag arbitration across clients, wheel buttons map to Android scroll injection, common keys map to Android keycodes and printable keysyms are injected as UTF-8 text.
- The scrcpy session size is fixed; device rotation or resolution change requires a restart.