serve-emul
v0.0.4
Published
The `npx serve` of Android devices — stream a booted emulator or attached device over a WebSocket H.264 feed decoded in the browser with WebCodecs.
Maintainers
Readme
serve-emul
The npx serve of Android devices.
Host your Android emulator (or real device) for use with agent tools like Codex, Cursor, or Claude Desktop — locally, over your LAN, or tunnel anywhere.
bunx serve-emul@latest
# or: npx serve-emul@latest
# → Preview at http://localhost:3300Use @latest for one-off runs so Bun/npm fetches the newest published version instead of reusing a cached or locally installed copy.
serve-emul spawns the scrcpy server on the device, opens an adb forward tunnel, pipes H.264 frames over a WebSocket, and decodes them in the browser with WebCodecs. Input events flow back over the same socket to scrcpy's control channel.
Package Rename
The npm package name is serve-emul. npm package names cannot be renamed in place, so releases under this name should be published as a new package:
npm publish --workspace packages/serve-emul
npm deprecate serve-emu "Package renamed to serve-emul. Use: npm install serve-emul"Status
Current package version: see package.json and CHANGELOG.md.
Working:
- Live H.264 video stream from device → WebCodecs canvas
- Taps, swipes, hardware buttons (Back / Home / Recents / Power)
- Text injection, keyevents
- Multi-client (multiple browser tabs share one stream)
- Auto-replay of SPS/PPS to clients joining mid-stream
- Emulator GPS location control from the browser UI and
POST /api/location - Route playback from GPX, GeoJSON, KML, or waypoint JSON
- Logcat forwarding over SSE with browser-side filter, pause, and copy controls
- Network on/off control from the browser UI and
POST /api/network - Agent-friendly REST input APIs plus session event replay
- App management controls for APK install, launch, clear data, force stop, and permission grants
Planned:
- Multi-device routing
- Embeddable Connect-style middleware (
serve-emul/middleware) - Compiled single binary
Requirements
- Node.js 18+ or Bun 1.1+
adbon PATH (Android platform-tools)- A booted device/emulator (
adb devicesshows it), or an AVD name passed with--avd - Chrome / Edge / Safari 16.4+ (for WebCodecs)
Quick start
bun install
bun run --filter serve-emul setup # downloads scrcpy-server-v4.0 into vendor/
bun run packages/serve-emul/src/cli.ts
# → http://localhost:3300The setup step is also run lazily on first start, so you can skip it.
CLI
serve-emul [-p <port>] [-s <serial>] [--max-fps N] [--bit-rate N] [--max-size N] [--key-frame-interval sec]
serve-emul --avd <name> [--restart-avd]
serve-emul --avd-list
serve-emul --running-avds| flag | default | meaning |
|---|---|---|
| -p, --port | 3300 | HTTP port for the preview server |
| -s, --serial | auto | adb device serial (only required when multiple devices are attached) |
| --max-fps | 60 | cap source frame rate |
| --bit-rate | 8000000 | H.264 bit rate in bps |
| --max-size | 1920 | downscale longest edge to N pixels; 0 = native (encoders on many emulators reject above ~2560, so the default trims) |
| --key-frame-interval | 1 | ask the encoder for regular keyframes so clients can recover without resetting video capture; 0 disables this codec option |
| --avd | none | launch this Android Virtual Device before streaming |
| --restart-avd | false | stop a running matching AVD before launching it |
| --avd-list | false | list available Android Virtual Device names |
| --running-avds | false | list currently running emulator serials and AVD names |
| --emulator | auto | Android Emulator binary path; defaults to PATH or Android SDK env vars |
| --emulator-port | auto | emulator console port for --avd; must be an even port from 5554 through 5682 |
HTTP API
Set an Android Emulator GPS fix:
curl -X POST http://localhost:3300/api/location \
-H 'Content-Type: application/json' \
-d '{"latitude":37.5665,"longitude":126.978}'Location control uses the Android Emulator geo fix command and is currently emulator-only.
Start route playback from waypoints:
curl -X POST http://localhost:3300/api/route \
-H 'Content-Type: application/json' \
-d '{"speedKph":30,"multiplier":1,"loop":false,"waypoints":[{"latitude":37.5665,"longitude":126.978},{"latitude":37.5651,"longitude":126.98955}]}'Pause, resume, or stop playback:
curl -X POST http://localhost:3300/api/route/control \
-H 'Content-Type: application/json' \
-d '{"action":"pause"}'Drive the device with REST:
curl -X POST http://localhost:3300/api/tap \
-H 'Content-Type: application/json' \
-d '{"x":0.5,"y":0.5}'
curl -X POST http://localhost:3300/api/text \
-H 'Content-Type: application/json' \
-d '{"text":"hello"}'
curl http://localhost:3300/api/screenshot --output screen.pngStream logcat over SSE:
curl -N 'http://localhost:3300/api/logcat?package=com.example.app&search=error'Toggle device network:
curl http://localhost:3300/api/network
curl -X POST http://localhost:3300/api/network \
-H 'Content-Type: application/json' \
-d '{"enabled":false}'Replay recorded input and location events:
curl -X POST http://localhost:3300/api/session/replay \
-H 'Content-Type: application/json' \
-d '{"multiplier":2}'Manage apps:
curl -X POST http://localhost:3300/api/apps/install \
-F apk=@/path/to/app.apk
curl -X POST http://localhost:3300/api/apps/launch \
-H 'Content-Type: application/json' \
-d '{"packageName":"com.example.app","activity":".MainActivity"}'
curl -X POST http://localhost:3300/api/apps/clear \
-H 'Content-Type: application/json' \
-d '{"packageName":"com.example.app"}'
curl -X POST http://localhost:3300/api/apps/grant \
-H 'Content-Type: application/json' \
-d '{"packageName":"com.example.app","permission":"android.permission.POST_NOTIFICATIONS"}'How it works
┌──────────────────┐ adb forward ┌─────────────┐ H264 / WS ┌─────────┐
│ scrcpy-server.jar│ ◄──────────► │ serve-emul │ ────────────► │ Browser │
│ on device │ TCP tunnel │ (Bun) │ WebCodecs │ <canvas>│
│ • video socket │ │ │ ◄──────────── │ │
│ • control socket│ │ │ input JSON │ │
└──────────────────┘ └─────────────┘ └─────────┘- The CLI pushes
scrcpy-server-v4.0to/data/local/tmp/scrcpy-server.jar. - It opens
adb forward tcp:<localPort> localabstract:scrcpy_<scid>. - It spawns
app_processwith the scrcpy server class on the device, then connects two sockets through the tunnel: video and control. - The Bun server reads scrcpy's framed H.264 stream (12-byte header + Annex-B payload) and forwards each Access Unit as a binary WebSocket message. Raw
/wsclients receive the Annex-B payload unchanged; the built-in browser UI opts into a 16-byte frame metadata header with keyframe and PTS data. - The browser configures a
VideoDecoderfrom the SPS, uses server-provided frame metadata to avoid per-frame NAL scans, and draws frames to a<canvas>. Pointer events are normalized to device coordinates and written back to scrcpy's control socket as 32-byte touch packets.
License
Apache-2.0. Bundles the upstream scrcpy server binary (Apache-2.0) at runtime.
