Skip to content

About

Bridge scrcpy H.264 video and control to one RFB/VNC port: Open H.264 passthrough for TigerVNC/noVNC, Tight/JPEG fallback for ordinary clients

Resources

Stars

28 stars

Watchers

0 watching

Forks

Latest commit

 

History

15 Commits

Folders and files

Repository files navigation

scrcpy-rfb

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.

Building

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 tests

Every pull request builds both architectures and runs the self tests. Pushes to master publish, via GitHub Actions:

  • scrcpy-rfb-linux-amd64 / -arm64 binaries, su1000, the patched scrcpy-server-secure.jar and the unpatched scrcpy-server.jar, plus SHA256SUMS and build-info.json, to the moving master prerelease;
  • 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 5900

Running

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

Client notes

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

Ordinary 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

Behavior details

  • 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-port session 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.

About

Bridge scrcpy H.264 video and control to one RFB/VNC port: Open H.264 passthrough for TigerVNC/noVNC, Tight/JPEG fallback for ordinary clients

Resources

Stars

28 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages