# ScrcpyWebRTC **Repository Path**: chenshaogui/scrcpy-web-rtc ## Basic Information - **Project Name**: ScrcpyWebRTC - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-21 - **Last Updated**: 2026-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # scrcpy-webrtc [![npm](https://img.shields.io/npm/v/scrcpy-webrtc)](https://www.npmjs.com/package/scrcpy-webrtc) [![license](https://img.shields.io/npm/l/scrcpy-webrtc)](./LICENSE) See and control your Android devices from any browser — including over the internet, without port forwarding. ```bash npx scrcpy-webrtc ``` That prints a link (and a QR code) you can open anywhere. No tunnel, no account, nothing hosted: the page finds your machine through encrypted messages on public brokers, and video and touch travel peer-to-peer. ## 🚀 Docker 部署(Windows → ARM 服务器 / 任意 Linux) 容器化的部署方式——把整个运行环境(Node + scrcpy-server + WebRTC)打包成镜像。 **支持 amd64 和 arm64**,适合在树莓派、Radxa、OrangePi 等 ARM 服务器上跑 7×24。 ### 前置条件 | 角色 | 环境 | 软件 | |---|---|---| | **构建机**(开发/打包) | Windows / macOS / Linux | Docker Desktop 23+ 或 docker-ce 24+,自带 buildx | | **运行机**(部署) | Linux(x86_64 或 arm64) | docker-ce 23+(要支持加载多架构 tar) | | **Android 设备** | Android 5+ | 开启 USB 调试或无线 adb | ### 1. 直接跑(最快路径) Linux 服务器上: ```bash docker run -d --name scrcpy-webrtc \ --network host \ --restart unless-stopped \ scrcpy-webrtc:latest ``` 或用 docker-compose(推荐,已包含 `host` 网络 + 健康检查): ```bash docker compose up -d docker compose logs -f ``` 打开浏览器访问 `http://:8010/` 即可。 ### 2. 从源码构建 #### A. Windows 上用脚本(推荐国内网络) 仓库自带 `build.ps1`,一键完成多架构构建/推送/打包: ```powershell cd D:\myProject\scrcpy-web-rtc # 本机单架构(开发调试) .\build.ps1 # 推送到 Docker Hub(多架构,ARM/x86 服务器都能拉) .\build.ps1 -MultiArch -Push -Registry "your-dockerhub-username" # 打包成 tar(离线传输到没公网的 ARM 服务器) .\build.ps1 -MultiArch -Save ``` 参数说明: | 参数 | 作用 | |---|---| | `-MultiArch` | 同时构建 `linux/amd64` + `linux/arm64`(必须搭配 `-Push` 或 `-Save`) | | `-Push` | 推送到镜像仓库 | | `-Save` | 导出为 tar 文件,可离线拷贝 | | `-Registry ` | 镜像命名空间,如 `qychan`、`ccr.ccs.tencentyun.com/your-ns` | | `-Tag ` | 镜像 tag,默认 `latest` | | `-BuildkitImage ` | 自定义 buildkit 镜像源,默认 `docker.io/moby/buildkit:buildx-stable-1` | 脚本会自动: 1. 检测 `docker` / `buildx` 版本 2. 创建 `docker-container` driver 的 `scrcpy-builder`(首次 ~5s) 3. 选定目标平台(Windows → amd64,Apple Silicon → arm64) 5. 用 `swr.cn-north-4.myhuaweicloud.com/.../node:22.23.2-slim` 加速 npm/esbuild #### B. 手动命令(Linux/macOS 或了解底层用) ```bash # 创建 buildx builder(一次性) docker buildx create --name scrcpy-builder --driver docker-container docker buildx use scrcpy-builder # 多架构构建并推送 Docker Hub docker buildx build \ --platform linux/amd64,linux/arm64 \ --build-arg NODE_IMAGE=docker.io/node:22.23.2-slim \ -t /scrcpy-webrtc:latest \ --push . # 多架构打包成 tar(离线传输) docker buildx build \ --platform linux/amd64,linux/arm64 \ --build-arg NODE_IMAGE=docker.io/node:22.23.2-slim \ -t scrcpy-webrtc:latest \ --output type=oci,dest=scrcpy-webrtc-multi.tar . ``` > 国内网络加速:在 `Dockerfile` 里默认用了 > `swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/node:22.23.2-slim` > (华为云 SWR 镜像,Docker Hub 的 CDN 副本)。如镜像没拉下来,手动指定为 `docker.io/node:22.23.2-slim`。 ### 3. 部署到 ARM 服务器 #### 方式 A:从 registry 拉取 ```bash # ARM 服务器(arm64)上: docker pull /scrcpy-webrtc:latest # 自动选 arm64 变体 docker compose up -d ``` #### 方式 B:离线 tar 传输 Windows 上: ```powershell .\build.ps1 -Save # 生成 scrcpy-webrtc-latest-linux-amd64.tar(仅 Windows 架构) # 或 .\build.ps1 -MultiArch -Save 生成多架构 tar ``` 传输(任选): ```bash scp scrcpy-webrtc-multi.tar root@rock5t:~ # 或用 U 盘、网盘、局域网 HTTP server ``` ARM 服务器上加载: ```bash docker load -i ~/scrcpy-webrtc-multi.tar docker compose up -d ``` ### 4. 网络配置:host vs bridge `docker-compose.yml` 默认是 **`network_mode: host`**——容器和宿主机共享网络栈。 ```yaml services: scrcpy-webrtc: network_mode: host # ← 关键 environment: ADB_HOST: 127.0.0.1 # 宿主机 adb-server ADB_PORT: 5037 # ADB_DEVICE_HOST 不用配 ``` **优势**:容器 `127.0.0.1:5037` 就是宿主机的 adb-server,免去 `host.docker.internal` 的跨网络解析。 如果需要隔离网络(不建议),可改成 bridge: ```yaml services: scrcpy-webrtc: # 删掉 network_mode: host ports: - "8010:8010" extra_hosts: - "host.docker.internal:host-gateway" environment: ADB_HOST: host.docker.internal # 容器内访问宿主机 adb-server ADB_PORT: 5037 # 若 adb 跑在远程/手机上: # ADB_DEVICE_HOST: 192.168.1.20 # ADB_DEVICE_PORT: 5555 ``` ### 5. ADB 配置:`ADB_HOST` vs `ADB_DEVICE_HOST` 两个变量指代**完全不同的对象**: ``` ┌──────────────────┐ server.mjs (Node) ┌──────────────┐ ┌──────────┐ │ 宿主机/容器 │ ──── ADB_HOST ────────► │ adb-server │ ◄── adbd │ Android │ │ 上跑的 scrcpy- │ (默认 127.0.0.1) │ (端口 5037) │ (5555) │ 设备 │ │ webrtc 进程 │ └──────────────┘ └──────────┘ │ │ ── ADB_DEVICE_HOST ──► 直连设备(无线 adb IP) └──────────────────┘ (可选, 默认无) ``` | 变量 | 指向 | 默认 | 设置场景 | |---|---|---|---| | `ADB_HOST` | adb-server(daemon) | `127.0.0.1` | `network_mode: host` 时无需改;bridge 模式用 `host.docker.internal` | | `ADB_PORT` | adb-server 端口 | `5037` | 一般不动 | | `ADB_DEVICE_HOST` | 无线 adb 设备 IP | 无 | 想让容器内自动 `adb connect ` 时设置 | | `ADB_DEVICE_PORT` | 无线 adb 端口 | `5555` | 一般不动 | `host` 模式下:`ADB_HOST: 127.0.0.1` 直接复用宿主机 adb-server,**删掉 `ADB_DEVICE_HOST`**。 ### 6. 故障排查 | 症状 | 原因 | 解决 | |---|---|---| | `exec format error` 启动失败 | 宿主机是 arm64,但镜像拉到了 amd64 | `docker compose pull` 重新拉;`platform: linux/arm64` 显式指定 | | 构建报 `OCI exporter is not supported for the docker driver` | 默认 `docker` driver 不支持 OCI | 脚本已自动建 `docker-container` driver;手动:`docker buildx create --driver docker-container` | | 构建报 `personal access token is expired` | Docker Hub PAT 过期 | 脚本默认用 CN 镜像不依赖 Docker Hub;或 Docker Desktop 重新登录 | | 容器起来但看不到设备 | adb-server 没起 / 端口不对 | 宿主机执行 `adb devices`;`host` 模式确认 `network_mode: host` | | `npx @yume-chan/fetch-scrcpy-server` 卡住 | npm 拉 GitHub release 网络不稳 | Dockerfile 已用 `curl --retry 30` 直拉,无需处理 | ### 7. 镜像验证 ```bash # 架构正确性 docker inspect scrcpy-webrtc:latest --format '{{.Architecture}}' # arm64 服务器上预期:arm64 # 多架构 manifest docker buildx imagetools inspect /scrcpy-webrtc:latest # 预期两个 Platform:linux/amd64 + linux/arm64 # 容器内确认 docker compose exec scrcpy-webrtc uname -m # arm64 服务器上预期:aarch64 ``` --- ## What you get - **Any browser, any device** — no app to install; open the page on a phone, laptop, or tablet - **Real remote access** — a stable link gets you in from outside your network, and WebRTC connects browser⇆host directly for low latency - **Screen + audio + touch** — hardware H.264 and Opus straight from the device, no transcoding - **Clipboard sync** in both directions - **Device shell** — a real terminal on the device (`--shell`) - **Adjustable quality** with automatic downshift on weak networks - Works with several devices at once; pick from a list ## Screenshots

Main screen Control panel Custom scrcpy options

## Requirements - Node.js 20+ - `adb` on the host, with your devices already visible to it: `adb devices` should list them (`adb connect :5555` for WiFi, or plug in USB) - Android 5+ for screen; audio needs Android 11+ - A recent browser (uses WebCodecs / WebRTC). The page must be served over `https://` or `localhost` — the printed links already are ## Getting started **From anywhere (phone, another network)** ```bash npx scrcpy-webrtc ``` Scan the QR code or open the printed link — it stays the same across restarts, and no router or firewall changes are needed. For viewers on hostile networks (symmetric NAT, CGNAT), add a TURN relay: `--turn cloudflare`. See [Serverless signaling](#serverless-signaling) for how the link works. **Just on this machine** Open the printed `http://localhost:8010/` link — or pass `--no-signal` to stay strictly local, with nothing leaving the machine. **Through a tunnel instead** ```bash npx scrcpy-webrtc --tunnel ``` A tunnel gives you a public HTTPS address and, unlike signaling, a WebSocket fallback that shows video even where WebRTC is blocked outright. **Between two computers, no relay at all** Install [wush](https://github.com/coder/wush/releases) on both machines, then: ```bash npx scrcpy-webrtc --tunnel wush ``` Run the printed `wush port-forward …` command on the viewing machine and open the printed localhost link there. Traffic is WireGuard end to end — direct P2P when hole-punching works, Tailscale's public DERP servers as fallback. No accounts, no relay of yours, but the viewer needs wush, so this one is for laptops rather than phones. **With a shell on the device** ```bash npx scrcpy-webrtc --shell ``` Adds a `>_` button opening a real terminal on the device. This one requires the token that's printed at startup — it's already embedded in the links. ## Everyday things | I want to… | Do this | |---|---| | Type into the device | Click the screen and type, or use the ⌨ button on mobile | | Copy text between device and computer | Just copy — it syncs both ways | | Save bandwidth on a slow link | Pick 流畅/Smooth in the quality selector (or leave it on Auto) | | Hear the device | Click 🔇 (audio only flows in P2P mode) | | Use a different port | `--port 9000` | | Keep the same URL across restarts | The default signaling link already is; among tunnels, `--tunnel tunwg` gives one too | | Reach viewers behind CGNAT / symmetric NAT | Add a TURN relay: `--turn cloudflare` — see [TURN](#turn) | | Lock down viewing too, not just the shell | `--token yoursecret` | | Run without any external site | `--no-page` (serves the UI itself) | ## Options Run `scrcpy-webrtc --help` for the same list. | Flag | Env | Default | | |---|---|---|---| | `--port`, `-p` | `PORT` | `8010` | HTTP/WebSocket listen port | | `--adb-host` | `ADB_HOST` | `127.0.0.1` | adb server host | | `--adb-port` | `ADB_PORT` | `5037` | adb server port | | `--shell` | — | off | enable the device shell (needs a token; generated if absent) | | `--token` | `SCRCPY_WEBRTC_TOKEN` | off | require this token for *everything*, viewing included | | `--video-encoder` | `VIDEO_ENCODER` | device default | pin the video encoder by MediaCodec name (e.g. `c2.android.avc.encoder`); also switchable in the page | | `--audio-encoder` | `AUDIO_ENCODER` | device default | pin the audio encoder by MediaCodec name (e.g. `c2.android.opus.encoder`); only opus is playable over WebRTC | | `--tunnel [backend]` | — | off | expose publicly; `cloudflared`, `tunwg`, `wush`, or auto (prefers cloudflared) | | `--tunnel-api` | `TUNWG_API` | `relay.hapi.run` | tunwg relay server | | `--tunnel-auth` | — | off | basic auth `user:pass` (tunwg only) | | `--turn ` | `TURN_URL` | off | TURN relay for failed P2P: `cloudflare` or a `turn:`/`turns:` URL | | `--turn-auth` | `TURN_AUTH` | — | `user:pass` for a `--turn` URL | | `--signal` | — | on unless `--tunnel`/`--no-page` | remote access via public MQTT brokers + WebRTC; the flag forces it on | | `--no-signal` | — | — | strictly local: never touch a broker | | `--signal-broker` | `SCRCPY_WEBRTC_SIGNAL_BROKERS` | 3 public brokers | comma-separated `wss://` broker list | | — | `CF_TURN_KEY_ID`, `CF_TURN_API_TOKEN` | — | Cloudflare TURN key, used by `--turn cloudflare` | | `--page` | `SCRCPY_WEBRTC_PAGE` | project page | frontend to point remote visitors at | | `--no-page` | — | off | serve the bundled page instead | | `--no-qr` | — | off | don't print the QR code | | — | `TUNWG_BIN` | auto-detect | tunwg binary location | | — | `CLOUDFLARED_PATH` | auto-detect | cloudflared binary location | | — | `WUSH_BIN` | auto-detect | wush binary location | The UI follows your browser language (中文/English); `?lang=en` or `?lang=zh` overrides it. ## Troubleshooting **No devices listed** — run `adb devices` on the host. scrcpy-webrtc only sees what adb sees. For WiFi devices, `adb connect :5555` first. **"port 8010 is already in use"** — another copy is running; use `--port`. **Tunnel won't start** — the error from the tunnel itself is printed below the message. A missing binary means `tunwg`/`cloudflared` isn't installed; an unreachable relay means you should try `--tunnel-api `. Local serving keeps working either way. **The page asks for a backend address** — you opened a `#k` link with a stale cached page (deploys lag pushes by a minute or two, and CDNs cache). Force-reload (`Ctrl+Shift+R`) or try a private window; an up-to-date page never asks on `#k` links. **`P2P failed` on a `#k` link** — WebRTC couldn't punch through (symmetric NAT or CGNAT on both ends), and signaling mode has no WebSocket fallback. Add [`--turn`](#turn) on the host — TURN reaches every NAT — or switch to `--tunnel`. **`Can't reach the host` on a `#k` link** — the host isn't running or has no broker connectivity. Check its console; the link must match the one it currently prints. **Stuck on `WS` instead of `P2P`** (tunnel mode) — WebRTC couldn't establish a direct path. Everything still works through the tunnel, just with more latency and tunnel traffic; [`--turn`](#turn) usually gets such connections onto WebRTC anyway. **Shell button missing** — the server wasn't started with `--shell`, or the page was opened without the token. Use the link printed at startup. **Black screen after connecting** — force-reload the page (`Ctrl+Shift+R`) to clear a stale cached bundle. **`npx`/`bunx` runs an old version** — clear the runner's cache, e.g. `rm -rf /tmp/bunx-*-scrcpy-webrtc@latest`, or pin the version explicitly. ## How it works ``` browser ◀── signaling: brokers or tunnel ──▶ scrcpy-webrtc (Node) ──▶ adb ──▶ device ╰────────────── WebRTC: video, audio, touch, shell ──────────────╯ ``` scrcpy-webrtc pushes the stock scrcpy v3.1 server to the device and starts it through adb, then relays what comes back. The device's hardware H.264 encoder output is **never transcoded** — it's repacketized as RTP and sent over WebRTC, or framed over the WebSocket as a fallback. How browser and host first meet depends on the mode. On a `#k` link (the default) they exchange SDP/ICE through the encrypted broker room and media starts directly on WebRTC. Through a tunnel (or on localhost) the browser connects over WebSocket first, decodes with WebCodecs for instant video, and negotiates WebRTC in parallel — once the peer-to-peer path is up, video switches over and the WebSocket stream pauses; if ICE fails or drops, everything falls back automatically. Touch, keys and the shell ride DataChannels either way, and the status badge shows which path is live. Because the media never touches the tunnel in P2P mode, bandwidth-limited or end-to-end-encrypted relays remain practical. For the same reason the page itself is served from a CDN: visitors arriving through a tunnel get a small notice page linking to the hosted frontend rather than pulling the bundle through your relay. ### Security The shell means full control of the device, so it always requires a token — generated and printed at startup if you don't set one. Auth is tiered: `--shell` gates only the shell, while `--token` locks down viewing as well. The token travels in a WebSocket subprotocol and an `Authorization` header, never in a URL query, and is compared in constant time. With the tunwg backend the relay cannot read any of it — the tunnel is end-to-end encrypted and it only routes TLS bytes by SNI. On `#k` links the key in the fragment is the room secret: it never reaches any server (fragments aren't sent in requests), it derives the broker topic one-way, and it encrypts every signaling message. Possession of the link grants viewing — the same trust model as a tunnel URL — while the shell still demands the token. ### Serverless signaling A remote viewer needs two small things before WebRTC can take over: a URL to open, and a way to exchange a few KB of SDP/ICE. Signaling provides both without any tunnel, and it is the default mode — plain `scrcpy-webrtc` runs it, `--no-signal` turns it off, and asking for a `--tunnel` replaces it (explicit `--signal` keeps both). The printed link is `#k=`. The key is generated locally and kept in `~/.config/scrcpy-webrtc/`, so the link is stable across restarts — and because it travels in the URL fragment, it is never sent to any server. Host and page meet on public MQTT brokers (EMQX intl + CN, HiveMQ by default; override with `--signal-broker`), where every message is AES-256-GCM encrypted with that key and the topic is a one-way hash of it. Brokers see ciphertext and timing, nothing else. Only session control rides the brokers. Media is WebRTC-only on this path — there is no WebSocket video fallback, so pair it with `--turn` if your viewers may sit behind symmetric NAT or CGNAT. Compared to the tunnel backends: no process to install, no relay anyone pays for, a stable link, and end-to-end encryption; the trade-offs are a couple of seconds of extra connect time and the WebRTC requirement. ### TURN `--turn` gives WebRTC a relay for the NAT combinations it can't punch through (symmetric NAT or CGNAT on both ends). It matters twice: on the default signaling path it is the difference between "no video" and "video everywhere", and in tunnel mode it keeps fallback video off the tunnel — which otherwise carries the full stream, on your own bandwidth bill if the relay is yours: ```bash # Cloudflare TURN (1,000 GB/month free): create a TURN key under # Realtime → TURN in the Cloudflare dashboard, then CF_TURN_KEY_ID=… CF_TURN_API_TOKEN=… scrcpy-webrtc --turn cloudflare ``` `--turn` takes two forms: `cloudflare`, which mints short-lived credentials on demand, or a `turn:`/`turns:` URL plus `--turn-auth user:pass` for your own coturn or any provider that hands out static credentials (Metered, ExpressTURN, …). Either way the credentials ride to the browser with the WebRTC offer — nothing to configure on the viewing side. Media through TURN stays DTLS-encrypted end to end; the relay forwards packets it cannot read, so a third-party TURN service gets no eyes on your screen. There is no default TURN server — the once-popular free public relays (Open Relay and friends) are dead. ### Tunnel backends Tunnels are the explicit alternative (`--tunnel`) for when signaling can't serve: networks where WebRTC is blocked outright (the WebSocket fallback shows video through the tunnel), or when you want a plain HTTPS URL. Bare `--tunnel` picks cloudflared when both are installed. - **[cloudflared](https://github.com/cloudflare/cloudflared/releases)** (default) — zero setup, nothing to host and no traffic bill: Cloudflare's free quick tunnels carry everything. In exchange the URL is random per run and TLS terminates at Cloudflare's edge, so they can see the traffic. - **[tunwg](https://github.com/tiann/tunwg)** — pick it for a URL that stays the same across restarts and for end-to-end encryption (the relay can't see your traffic). Traffic flows through a tunwg relay — `relay.hapi.run` by default; use `--tunnel-api` for your own. If a relay issues keys, one is fetched and cached under `~/.config/scrcpy-webrtc/`. - **[wush](https://github.com/coder/wush)** — for viewing from another computer: instead of a public URL, the viewing machine joins a WireGuard overlay (`wush port-forward`) and opens localhost. Direct P2P when possible, Tailscale's public DERP as fallback; end-to-end encrypted, no accounts, nothing hosted. scrcpy-webrtc runs `wush serve` with ssh/cp disabled, so the printed key only grants port-forwarding — still, treat it like a password while the server runs. ### Hosting the frontend yourself The UI is static (`public/`), and the repo ships a GitHub Action that publishes it to `gh-pages` on every push. Deploy it anywhere, then start with `--page https://your-url/`. For offline or LAN-only setups, `--no-page` makes the server deliver the page itself with no external dependency. ## Built on [ya-webadb](https://github.com/yume-chan/ya-webadb) for the ADB and scrcpy protocols, [werift](https://github.com/shinyoshiaki/werift-webrtc) for WebRTC, [xterm.js](https://xtermjs.org/) for the terminal, and the stock [scrcpy](https://github.com/Genymobile/scrcpy) server. ## License MIT. Bundles the scrcpy server binary (Apache-2.0, see `scrcpy-server.LICENSE`). node server.mjs --no-page --no-signal