How the screen is streamed. Pick with --capture (or CAPTURE=). The choice
sets the wire codec, which the client renders as <video> (H.264 via jMuxer/MSE)
or <canvas> (PNG frames).
screenrecord (default) |
scrcpy |
grpc |
|
|---|---|---|---|
| Stream | H.264 → <video> |
H.264 → <video> |
PNG frames → <canvas> |
| Extra setup | none | none — v4.1 jar auto-downloads | none (emulator gRPC) |
| Works on | any device | any device | emulator only |
| FPS / latency | ok (~200–400 ms) | high FPS, lower latency | frame-driven (updates on change) |
| Notes | ~3 min clip, auto-respawned | continuous; Android 14/15/16 via DisplayManager | server-side PNG (CPU-heavy at high fps); also exposes sensors/GPS/battery |
Zero setup: adb exec-out screenrecord --output-format=h264 -, respawned every
~3 min (its clip limit). H.264 over the WebSocket, decoded by jMuxer into MSE.
bun run src/server.ts # this is the default
Higher FPS / lower latency, continuous. Uses the scrcpy-server v4.1 jar
(older versions crash on Android 14+). Video only, raw_stream=true (a clean
H.264 Annex-B stream); input is a separate control socket.
bun run src/server.ts --capture scrcpy # jar auto-downloads on first use
Getting the jar — you don't have to. On the first --capture scrcpy run,
stream-droid downloads the server component (~730 KB, no scrcpy desktop app
needed), verifies its SHA-256 against a pinned value, and caches it in
~/.cache/stream-droid/ (respects XDG_CACHE_HOME); later runs reuse the cache
silently. It logs ✓ downloaded scrcpy-server v4.1 (verified) → … when it does.
To supply your own instead (offline, air-gapped, or a custom build):
curl -L -o scrcpy-server-v4.1 \
https://github.com/Genymobile/scrcpy/releases/download/v4.1/scrcpy-server-v4.1
bun run src/server.ts --capture scrcpy --scrcpy-server ./scrcpy-server-v4.1
Version pinning matters. scrcpy's server verifies that the version string we
pass exactly matches the jar's build version, and its socket/stream protocol
changes between majors. stream-droid targets v4.1 and requests
raw_stream=true — a pure H.264 Annex-B stream with no framing, no device
header, and none of the 12-byte session-meta blocks scrcpy otherwise injects
mid-stream on rotation (which would corrupt the decoder feed). The version is
pinned in src/capture/scrcpy.ts and scrcpyServer.ts; moving to a new release
means updating both the version string and the pinned download + checksum, and
re-checking the protocol. (v1.24 and older will crash on Android 14+ — they
use SurfaceControl.createDisplay, removed in Android 14.)
Talks to the emulator's EmulatorController gRPC service (discovered from
pid_*.ini, authenticated with the grpc.token bearer token). It needs no jar
and no adb for capture — the cleanest programmatic hook — but is emulator-only.
Video is streamScreenshot (PNG); input is sendTouch/sendKey.
CAPTURE=grpc bun run src/server.ts
Frame-driven: an idle screen produces no new frames until it changes.
--max-size, --bit-rate)For screenrecord and scrcpy, the stream is captured at the device's native
resolution and a fixed bit-rate by default. On a high-res device most of the
streaming cost is the on-device H.264 encode — profiling a 1080×2424 emulator
under motion showed the encode adding ~1.3 cores to the emulator, versus the
transport (WebSocket relay) being negligible. Two knobs trade a little sharpness
for a lot less encode CPU and bandwidth:
--max-size <px> (STREAM_DROID_MAX_SIZE) — downscale so the longer edge is
≤ px, preserving aspect (screenrecord gets a computed --size WxH; scrcpy takes
max_size and scales itself). 0 = native.--bit-rate <n> (STREAM_DROID_BIT_RATE) — encoder bit-rate; accepts
4000000, 3M, or 800K. Unset uses each backend's default (screenrecord 4 Mbps,
scrcpy 8 Mbps).stream-droid --max-size 900 --bit-rate 2M # ~3× less encode CPU, ~2.4× smaller frames
Input is unaffected — control coordinates still map to real device pixels (the
meta size stays the device's resolution); only the video is downscaled. These
knobs don't apply to grpc, which sends native-resolution PNGs.