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 four patches in
patches/ (the Open H.264 server-side encoding, a deferUpdateTime=0
busy-loop fix, ExtendedClipboard notify from the client, and a short provide
refused with the clipboard read under the send mutex; the last two are the
cocoonstack/libvncserver branch ext-clipboard-notify). The first two
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 (Linux only; needs cmake, git, libjpeg-turbo, zlib and FFmpeg dev packages):
make # cmake configure + build into build/
make test # build + run the self testsEvery pull request builds both architectures and runs the self tests. Pushes
to master publish, via GitHub Actions:
scrcpy-rfb-linux-amd64/-arm64binaries,su1000, the patchedscrcpy-server-secure.jarand the unpatchedscrcpy-server.jar, plusSHA256SUMSandbuild-info.json, to the movingmasterprerelease;- a multi-arch container image at
ghcr.io/cocoonstack/scrcpy-rfb:master(also tagged with the commit SHA).
Pushing a tag master-<9-char sha> publishes the same assets once to a release
of that name and never replaces them; pin that tag, never the moving master
release, and verify the files against its SHA256SUMS. The container image is
tagged with the release tag.
The moving release's tag and the default branch are both named master; scripts
that push or reset the branch must spell it refs/heads/master.
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. The bridge
expects video_codec=h264 audio=false send_device_meta=false send_dummy_byte=false: any extra preamble bytes on the video socket break
its handshake. Then:
scrcpy-rfb [--scrcpy-host 127.0.0.1] [--scrcpy-port 27183] \
[--clipboard-port PORT] [--turn-screen-off] \
[libvncserver options]--clipboard-port connects a second, control-only scrcpy session
(video=false audio=false control=true send_device_meta=false send_dummy_byte=false; the preamble would otherwise land on the control
socket, where the bridge reads device messages) and reads the Android
clipboard from it. Android lets only a focused app, the IME or a holder of
READ_CLIPBOARD_IN_BACKGROUND read the clipboard, so a server that runs as the
system uid (the patched FLAG_SECURE server) cannot; the unpatched
scrcpy-server.jar run as shell can. Without the option the main session reads
it. When the session ends the bridge reconnects with backoff, 250 ms to 8 s; the
box restarts the shell server.
--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, the right button is Back, wheel buttons map to Android scroll injection, common keys (End, Insert and F1-F12 included) map to Android keycodes with the held Shift, Control, Alt and Super as meta state, and printable keysyms are injected as UTF-8 text. A letter or digit with Control or Super becomes its keycode, and the X11 clipboard chords Shift+Insert, Control+Insert and Shift+Delete become Control+V, C and X.
- Clipboard: a client's cut text (Latin-1, or UTF-8 over the ExtendedClipboard
extension) becomes the Android clipboard without a paste of its own; the
client's paste key pastes it. Pastes pressed while the announced text is
still being requested run, one per press, when it arrives within 2 s. A new
Android clipboard goes to every client as ServerCutText, UTF-8 where the
client has the extension and Latin-1 otherwise. When the
--clipboard-portsession ends the bridge reconnects with backoff, 250 ms to 8 s; the box restarts the shell server. - The scrcpy session size is fixed; device rotation or resolution change requires a restart.