{"_id":"serve-avd","_rev":"4-ddd813dcb153a04549b3c004de2e169d","name":"serve-avd","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"serve-avd","version":"0.1.0","keywords":["android","emulator","adb","screen-mirroring","mjpeg","h264","webcodecs","preview","agent","mcp"],"license":"Apache-2.0","_id":"serve-avd@0.1.0","maintainers":[{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"}],"homepage":"https://github.com/hsandhu/serve-avd#readme","bugs":{"url":"https://github.com/hsandhu/serve-avd/issues"},"bin":{"serve-avd":"dist/serve-avd.js"},"dist":{"shasum":"825780f54ba61184ba92344da2ea4bc481404412","tarball":"https://registry.npmjs.org/serve-avd/-/serve-avd-0.1.0.tgz","fileCount":10,"integrity":"sha512-nXG83yuTTl3PZmIFUhUDB1nhFuGCGG51jAZu09eNjCIFe9ZmZjhIyOV7RYg+lPZBa8QSpPoYhF7h1ije9BOoVA==","signatures":[{"sig":"MEUCIQDBRIhNstCWgLOibcLb/VLvOGG8jSHOmAum/CIRpllwLgIgUFaa5Mx+0FZNaMNPMI+9MWJMq9VeO+OI/feo12L7rw4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":331540},"type":"module","engines":{"node":">=18.17"},"exports":{"./state":{"types":"./src/state.ts","import":"./src/state.ts"},"./middleware":{"types":"./src/middleware.ts","import":"./dist/middleware.js","require":"./dist/middleware.cjs"}},"gitHead":"2afa32704e98ed10bd6fd0928862e551b2ae69aa","scripts":{"test":"tsx --test test/*.test.ts","build":"node build.mjs","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"},"repository":{"url":"git+https://github.com/hsandhu/serve-avd.git","type":"git"},"_npmVersion":"11.12.1","description":"The `npx serve` of Android Emulators — stream any emulator to the browser with full interaction, no instrumentation.","directories":{},"_nodeVersion":"24.15.0","dependencies":{"ws":"^8.21.0","commander":"^14.0.1"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","esbuild":"^0.25.0","@types/ws":"^8.18.1","typescript":"^5.7.0","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/serve-avd_0.1.0_1784649838997_0.24520308547812575","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"serve-avd","version":"0.1.1","keywords":["android","emulator","adb","screen-mirroring","mjpeg","h264","webcodecs","preview","agent","mcp"],"license":"Apache-2.0","_id":"serve-avd@0.1.1","maintainers":[{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"}],"homepage":"https://github.com/hsandhu/serve-avd#readme","bugs":{"url":"https://github.com/hsandhu/serve-avd/issues"},"bin":{"serve-avd":"dist/serve-avd.js"},"dist":{"shasum":"71df21a10f0cd60388b899dcfee0f69764bd5053","tarball":"https://registry.npmjs.org/serve-avd/-/serve-avd-0.1.1.tgz","fileCount":10,"integrity":"sha512-ga7kpuWmTPOryLRKPHOMo/Kx6llqe/2P6AiK4GZxG5RYsD5ZNDzUigKvJLzzijcSlsEDQZmONeW0Me/vPd1ABA==","signatures":[{"sig":"MEYCIQCjR8+rDSPoAO9imT6i3NIVpYUO1sfmKhPMifHSRKxroAIhANLWxvTebK033AyAv90eU/3jW90Gyqfx0tGPQD/ugGhi","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":331368},"type":"module","engines":{"node":">=18.17"},"exports":{"./state":{"types":"./src/state.ts","import":"./src/state.ts"},"./middleware":{"types":"./src/middleware.ts","import":"./dist/middleware.js","require":"./dist/middleware.cjs"}},"gitHead":"15a818353b1d2384530b0ea21fb24430180d2b40","scripts":{"test":"tsx --test test/*.test.ts","build":"node build.mjs","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"},"repository":{"url":"git+https://github.com/hsandhu/serve-avd.git","type":"git"},"_npmVersion":"9.6.7","description":"The `npx serve` of Android Emulators — stream any emulator to the browser with full interaction, no instrumentation.","directories":{},"_nodeVersion":"18.17.0","dependencies":{"ws":"^8.21.0","commander":"^14.0.1"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","esbuild":"^0.25.0","@types/ws":"^8.18.1","typescript":"^5.7.0","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/serve-avd_0.1.1_1784693576035_0.9138859233068681","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"serve-avd","version":"0.1.2","keywords":["android","emulator","adb","screen-mirroring","mjpeg","h264","webcodecs","preview","agent","mcp"],"license":"Apache-2.0","_id":"serve-avd@0.1.2","maintainers":[{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"}],"homepage":"https://github.com/hsandhu/serve-avd#readme","bugs":{"url":"https://github.com/hsandhu/serve-avd/issues"},"bin":{"serve-avd":"dist/serve-avd.js"},"dist":{"shasum":"3fd567b90e49e93c063aa809cfed2d7b739b7a69","tarball":"https://registry.npmjs.org/serve-avd/-/serve-avd-0.1.2.tgz","fileCount":10,"integrity":"sha512-bJPwMPm20HjEFjC056yDaDkK5PKVRDJl0wlODuhVkDMVyAXV8aKdHMcjNjV4GGIKqvffagSs5ejn6LEPtIy8Jg==","signatures":[{"sig":"MEUCIQDOfRcPlGmDDTUo8+kK3l9NtcrcXLVcoUAuvhhjY4lSQwIgUa4B+igip3GqWEwawhKLVZz2AtLNBKtO9P9YzdR61Xc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":347913},"type":"module","engines":{"node":">=18.17"},"exports":{"./state":{"types":"./src/state.ts","import":"./src/state.ts"},"./middleware":{"types":"./src/middleware.ts","import":"./dist/middleware.js","require":"./dist/middleware.cjs"}},"gitHead":"39ce85e4db4d963cbafb96a563501ea3c8049ac8","scripts":{"test":"tsx --test test/*.test.ts","build":"node build.mjs","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"},"repository":{"url":"git+https://github.com/hsandhu/serve-avd.git","type":"git"},"_npmVersion":"11.12.1","description":"The `npx serve` of Android Emulators — stream any emulator to the browser with full interaction, no instrumentation.","directories":{},"_nodeVersion":"24.15.0","dependencies":{"ws":"^8.21.0","commander":"^14.0.1"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","esbuild":"^0.25.0","@types/ws":"^8.18.1","typescript":"^5.7.0","@types/node":"^20.11.0"},"_npmOperationalInternal":{"tmp":"tmp/serve-avd_0.1.2_1785125913938_0.9907102346210543","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"serve-avd","version":"0.1.3","description":"The `npx serve` of Android Emulators — stream any emulator to the browser with full interaction, no instrumentation.","type":"module","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/hsandhu/serve-avd.git"},"homepage":"https://github.com/hsandhu/serve-avd#readme","bugs":{"url":"https://github.com/hsandhu/serve-avd/issues"},"keywords":["android","emulator","adb","screen-mirroring","mjpeg","h264","webcodecs","preview","agent","mcp","mcp-server","model-context-protocol","uiautomator","test-automation"],"bin":{"serve-avd":"dist/serve-avd.js"},"engines":{"node":">=18.17"},"exports":{"./middleware":{"types":"./src/middleware.ts","import":"./dist/middleware.js","require":"./dist/middleware.cjs"},"./client":{"types":"./src/sdk.ts","import":"./dist/sdk.js","require":"./dist/sdk.cjs"},"./state":{"types":"./src/state.ts","import":"./src/state.ts"}},"scripts":{"build":"node build.mjs","typecheck":"tsc --noEmit","test":"tsx --test test/*.test.ts","prepublishOnly":"npm run build"},"dependencies":{"commander":"^14.0.1","ws":"^8.21.0"},"devDependencies":{"@types/node":"^20.11.0","@types/ws":"^8.18.1","esbuild":"^0.25.0","tsx":"^4.23.1","typescript":"^5.7.0"},"gitHead":"19b4a096da0239dc7fe42583d57b136ecb1ad922","_id":"serve-avd@0.1.3","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-Ya3eQFp17YHcjXYIzWdLLPG9lNLgaXgnd8D+RDY8xLvGQQtPsMHBCOUyPqtYJ5VeePchQDbkCg6h9ch2q5PrLQ==","shasum":"4ba47e400975dfde820aa94b0f357275aa276031","tarball":"https://registry.npmjs.org/serve-avd/-/serve-avd-0.1.3.tgz","fileCount":17,"unpackedSize":715542,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDCsnPTrSO38VRDHgnuWP8g9R7k9+MWM8bxHLgDrlRs0AIgGc53MU1eYwpC2BoXqNwd1857SqD5ixm/FlfQ9u69OZc="}]},"_npmUser":{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"},"directories":{},"maintainers":[{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/serve-avd_0.1.3_1788671120079_0.0025510977049196804"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-21T16:03:58.914Z","modified":"2026-09-06T05:05:20.445Z","0.1.0":"2026-07-21T16:03:59.135Z","0.1.1":"2026-07-22T04:12:56.192Z","0.1.2":"2026-07-27T04:18:34.116Z","0.1.3":"2026-09-06T05:05:20.265Z"},"bugs":{"url":"https://github.com/hsandhu/serve-avd/issues"},"license":"Apache-2.0","homepage":"https://github.com/hsandhu/serve-avd#readme","keywords":["android","emulator","adb","screen-mirroring","mjpeg","h264","webcodecs","preview","agent","mcp","mcp-server","model-context-protocol","uiautomator","test-automation"],"repository":{"type":"git","url":"git+https://github.com/hsandhu/serve-avd.git"},"description":"The `npx serve` of Android Emulators — stream any emulator to the browser with full interaction, no instrumentation.","maintainers":[{"name":"hsandhu-miami","email":"sandhu.rob@gmail.com"}],"readme":"# serve-avd\n\nThe `npx serve` of Android Emulators.\n\nHost your emulator for use with Agent tools like Codex, Cursor, or Claude Desktop — locally, over your LAN, or host on a remote machine and tunnel anywhere.\n\n```sh\nnpx serve-avd\n# → Preview at http://localhost:3200\n```\n\nhttps://github.com/user-attachments/assets/91ca0811-119e-46f6-8e3e-9b41bd9f9bd7\n\n`serve-avd` captures the emulator's screen via `adb screenrecord`, exposes it as an H.264 WebCodecs stream (with an MJPEG fallback) plus a WebSocket control channel, and serves a browser preview UI on top. It works with any running Android Emulator (and most physical devices over adb) — no root, no plugin, no instrumentation in your app.\n\nIt is a faithful Android port of [serve-sim](https://github.com/EvanBacon/serve-sim) by Evan Bacon: same interface, same streaming design, same wire protocol — rebuilt on what Android and adb provide.\n\n## Features\n\n- Smooth H.264 video stream in the browser (WebCodecs), with instant paint on connect — no waiting for the next frame.\n- Full interaction: tap, drag, and fling with the mouse; scroll with the wheel.\n- Android navigation from the browser: Back, Home, Recents, power, volume, rotate, theme toggle, screenshot.\n- Keyboard forwarding — type into the emulator directly, Escape acts as Back, ⌘⇧H goes Home.\n- logcat is forwarded to the browser (and mirrored into the browser console for browser-use MCP tools to read).\n- Recent actions are available in the browser Tools panel and `serve-avd event-log` — and export as a replayable script (`serve-avd replay`).\n- UI hierarchy dumps for agents: `serve-avd ax` (uiautomator → JSON), plus semantic targeting on top: `serve-avd find \"Sign in\"`, `serve-avd tap --text \"Sign in\"`, `serve-avd wait \"Welcome\"`.\n- **Built-in MCP server** (`serve-avd mcp`) — one config line hands the emulator to Claude Desktop, Cursor, Codex or any MCP client as tools.\n- **Typed client SDK** (`serve-avd/client`) for Playwright-style scripts and agent frameworks — no shelling out.\n- Emulator controls from the CLI, Tools panel, SDK and MCP: location (+ routes), network conditions, battery, fingerprint, calls & SMS, font scale / density / locale / TalkBack, snapshots (save/load to reset state), app install/launch/stop/clear/open-URL, and `shell`/`push`/`pull` passthrough.\n- Multiple emulators at once — boot and attach AVDs straight from the Devices panel.\n- Every command works headless (straight over adb) when no server is running, and through the server (shared event log, live viewers) when one is.\n\n## Why?\n\nHosted emulators can be hard to test. `serve-avd` lets you test the hosted infra locally first for faster iteration. When you're ready to host an emulator remotely, simply tunnel the served URL and users can interact with the emulator as if it were running locally on their device.\n\nIt's also a great way to hand an emulator to an AI agent: everything is driveable over plain HTTP + WebSocket, screenshots and UI dumps are one command away, and the event log tells you what the agent did.\n\n## Install\n\nRequires the Android SDK platform-tools (`adb`; the `emulator` binary is needed to boot AVDs by name) and a [maintained Node.js LTS release](https://nodejs.org/en/about/previous-releases) (Node 18.17+). serve-avd finds your SDK via `$ANDROID_HOME`, `$ANDROID_SDK_ROOT`, or the default SDK locations on macOS and Linux.\n\nThe H.264 stream needs `screenrecord --output-format=h264` (present on every emulator image from the last decade). If it's unavailable, serve-avd automatically falls back to an MJPEG screenshot stream.\n\n## CLI\n\n```\nserve-avd [device...]                 Start preview server (default: localhost:3200)\n                                      device = adb serial or AVD name (boots it if needed)\nserve-avd --no-preview [device...]    Stream in foreground without a preview server\nserve-avd mcp [--serve] [-d serial]   MCP server over stdio (see Connectors)\n\nserve-avd gesture '<json>' [-d serial]\n                                      Send a touch gesture\nserve-avd tap <x> <y> [-d serial]     Tap at normalized 0..1 coords\nserve-avd tap --text \"Sign in\"        …or the first UI element matching\n          --id submit | --desc Search   text / resource id / description\n          [--index n] [--exact] [--long [ms]]\nserve-avd find \"<text>\" [--id|--desc|--class] [--json]\n                                      Find UI elements: bounds, centers, normalized coords\nserve-avd wait \"<text>\" [--timeout 10s] [--gone]\n                                      Poll until an element appears/disappears (exit 2 on timeout)\nserve-avd swipe <x1> <y1> <x2> <y2> [--duration 300ms]\nserve-avd button [name] [-d serial]   Send a button press (default: home)\n                                      home|back|app-switch|power|lock|wake|\n                                      volume-up|volume-down|mute|menu|camera|\n                                      notifications|quick-settings|dpad-*|…\nserve-avd type <text> [-d serial]     Type text via the emulator keyboard\n                                      (ASCII only; also --stdin / --file <path>)\nserve-avd rotate <orientation> [-d serial]\n                                      portrait | portrait_upside_down |\n                                      landscape_left | landscape_right\nserve-avd debug <option> <on|off> [-d serial]\n                                      Toggle an Android render/debug flag\n                                      (overdraw|gpu-profile|layout-bounds|\n                                       show-taps|pointer-location|slow-animations)\nserve-avd memory-warning [-d serial]  Ask the foreground app to trim memory\nserve-avd event-log [-d serial]       Show recent emulator events\nserve-avd event-log --export <file>   …as a replayable JSON script\nserve-avd replay <file> [--speed 2] [--no-wait] [--coords] [--continue]\n                                      Replay a script (exported or hand-written)\nserve-avd screenshot [path] [-d serial]\n                                      Save a screenshot (PNG/JPEG)\nserve-avd ax [-d serial]              Dump the UI hierarchy as JSON (uiautomator)\nserve-avd foreground [-d serial]      Print the foreground app\n\nEmulator controls:\nserve-avd geo <lat> <lon> [alt]       Set the GPS fix\nserve-avd geo --route <file|\"lat,lon lat,lon …\"> [--interval 1s] [--steps n] [--loop]\n                                      Follow a route of fixes\nserve-avd network [speed <gsm|edge|lte|full|up:down>] [delay <gprs|edge|umts|none|min:max>]\n                  [airplane on|off] [wifi on|off] [data on|off]   (no args: status)\nserve-avd battery [<0-100> | unplug | ac | usb | wireless | reset]\nserve-avd fingerprint [id] [--remove] Touch the fingerprint sensor\nserve-avd call <number> | call accept|end|hold <number>\nserve-avd sms <number> <text…>        Deliver an incoming SMS\nserve-avd a11y font-scale <n> | density <dpi|reset> | locale <tag> [--app pkg|--system] | talkback on|off\nserve-avd snapshot save|load|delete <name> | snapshot list\n\nApps:\nserve-avd install <apk> [--launch]    adb install -r -g\nserve-avd launch <package>            Launch by package (or package/.Activity)\nserve-avd stop | clear-data | uninstall <package>\nserve-avd open <url> [--package pkg]  Open a URL / deep link\nserve-avd apps [--all]                List installed packages\nserve-avd shell [cmd…] | push <local> <remote> | pull <remote> [local]\n                                      adb passthrough against the resolved device\n\nOptions:\n  -p, --port <port>   Starting port (preview default: 3200; --no-preview default: 3100)\n      --host <host>   Host to bind (default: 127.0.0.1; use 0.0.0.0 for LAN)\n  -d, --detach        Spawn a background server and exit (daemon mode)\n  -q, --quiet         JSON-only output\n      --no-preview    Skip the web UI; stream in foreground only\n      --panes <panes> Initially open preview panes: devices, tools, logs, or none\n      --fit           Deprecated compatibility flag; previews now fit automatically\n      --theme <theme> Set device appearance before opening the preview:\n                      light or dark\n      --codec <codec> Stream codec for the preview UI: 'auto' (H.264 when the\n                      browser can decode it) or 'mjpeg' (force screenshot\n                      streaming — e.g. in browsers without WebCodecs)\n      --bit-rate <mbps>  H.264 bitrate in Mbps (default: 8)\n      --size <WxH>    Capture at a fixed size (default: display size, capped\n                      at 1920 on the long side)\n      --list [device] List running streams\n      --kill [device] Kill running stream(s)\n```\n\n### Examples\n\n```sh\nserve-avd                              # attach every online device, open preview\n                                       # (boots your first AVD when none are running)\nserve-avd Pixel_9_Pro_XL               # target an AVD by name — boots it if needed\nserve-avd emulator-5554 emulator-5556  # two emulators side by side\nserve-avd --detach                     # start a background server, return JSON\nserve-avd --list                       # show running streams\nserve-avd --kill                       # stop all servers\nserve-avd --panes devices,tools        # start with the devices and tools panes open\nserve-avd --theme dark                 # start the device in Dark Mode\n\n# Type text into the focused field\nserve-avd type \"Hello, world!\"\necho \"from stdin\" | serve-avd type --stdin\nserve-avd type --file ./snippet.txt\n\n# Touch\nserve-avd tap 0.5 0.9                  # near bottom-center\nserve-avd gesture '{\"type\":\"begin\",\"x\":0.5,\"y\":0.8}'\nserve-avd gesture '{\"type\":\"move\",\"x\":0.5,\"y\":0.4}'\nserve-avd gesture '{\"type\":\"end\",\"x\":0.5,\"y\":0.3}'   # swipe up\n\n# Agent helpers\nserve-avd screenshot ./now.png\nserve-avd ax | jq '.root.children[0]'\nserve-avd foreground\nserve-avd event-log --json\n\n# Semantic targeting — no coordinate guessing\nserve-avd find \"Sign in\"                        # matches text *and* content-description\nserve-avd tap --text \"Sign in\"                  # tap the first match (or --id / --desc / --index)\nserve-avd wait \"Welcome\" --timeout 15s          # poll the UI until it appears (exit 2 on timeout)\nserve-avd wait --id progress --gone             # …or until it disappears\nserve-avd tap --text \"Delete\" --long            # long-press\n\n# Emulator controls\nserve-avd geo 37.7749 -122.4194                 # GPS fix\nserve-avd geo --route route.json --interval 2s --steps 10   # drive along a route\nserve-avd network speed lte delay edge          # link conditions (emulator console)\nserve-avd network airplane on                   # airplane / wifi / data toggles\nserve-avd battery 15 && serve-avd battery unplug\nserve-avd fingerprint                           # unblock a biometric prompt (enrol one first)\nserve-avd sms 5551234 \"Your code is 424242\"\nserve-avd call 5551234 && serve-avd call end 5551234\nserve-avd a11y font-scale 1.3\nserve-avd snapshot save clean                   # …run something risky…\nserve-avd snapshot load clean                   # reset to the saved state\n\n# Apps\nserve-avd install ./app-debug.apk --launch\nserve-avd open \"myapp://orders/42\"\nserve-avd launch com.example.app\nserve-avd clear-data com.example.app\n\n# Record what a human (or agent) did in the preview, then reproduce it\nserve-avd event-log --export flow.json\nserve-avd replay flow.json --speed 2\n```\n\nMultiple devices are supported — pass several serials or AVD names, or leave it empty to attach to every online device.\n\nEvery subcommand goes through a running serve-avd server when there is one for the device (so viewers, the Tools pane and the event log all see it) and falls back to driving adb directly when there isn't — `serve-avd tap --text \"OK\"` works with nothing else running.\n\n### Semantic targeting\n\n`find`, `tap --text/--id/--desc/--class` and `wait` are a thin layer over the uiautomator dump. Matching is a case-insensitive substring by default (`--exact` for exact); `--text` also matches content-descriptions so icon buttons resolve too; `--id` accepts either `pkg:id/name` or just `name`; `--index n` picks the nth match. `find` prints normalized centers (what `tap x y` and the SDK take) and pixel bounds:\n\n```\n[0] \"Sign in\" Button com.app:id/submit  @ (0.500, 0.925)  px 500,1850  bounds [100,1800][900,1900]  clickable\n```\n\n`wait` polls (default every 500 ms, 10 s budget) and exits 2 on timeout, so `serve-avd tap --text Next && serve-avd wait \"Done\"` is a one-line assertion. A uiautomator dump takes 1–3 s on a healthy emulator; screens that never go idle (spinners) can take longer, and a dump is capped at 20 s.\n\n### Replay\n\n`event-log --export` distils the event log — taps (with the text/id target when there was one), drags, typing, keys, buttons, rotations, and every emulator control — into a JSON script with relative timing:\n\n```json\n{ \"version\": 1, \"device\": \"emulator-5554\", \"steps\": [\n  { \"t\": 0,    \"action\": \"launch\", \"package\": \"com.example.app\" },\n  { \"t\": 3089, \"action\": \"wait\",   \"text\": \"Sign in\", \"timeoutMs\": 8000 },\n  { \"t\": 5256, \"action\": \"tap\",    \"x\": 0.5, \"y\": 0.925, \"target\": { \"text\": \"Sign in\" } },\n  { \"t\": 7751, \"action\": \"text\",   \"text\": \"hello\" }\n]}\n```\n\n`replay` runs it back through the same actions. Taps prefer the recorded target (survives layout shifts) unless you pass `--coords`; `--speed 2` halves the pauses, `--no-wait` drops them, `--continue` keeps going past failures. Scripts are plain JSON — hand-write or generate them.\n\n### Camera\n\nThe Android Emulator has camera injection built in — no helper needed. Point the AVD's camera at your host webcam or the animated virtual scene when you boot it:\n\n```sh\nemulator -avd Pixel_9_Pro_XL -camera-back virtualscene -camera-front webcam0\n```\n\nInside the virtual scene, custom posters can be placed via the emulator's extended controls. (This replaces serve-sim's dylib-injection camera feature — Android provides the equivalent natively.)\n\n## HTTP + WebSocket API\n\nEverything the preview UI does goes through a small same-origin API you can drive yourself:\n\n```\nGET  /api                                  server + device state\nGET  /api/event-log?device=&limit=         recent events JSON\nGET  /api/event-log/events                 SSE: snapshot + live entries\nGET  /grid/api                             connected devices + configured AVDs\nPOST /grid/api/start {\"device\": \"...\"}     attach a serial / boot an AVD\n\nGET  /helper/<serial>/stream.mjpeg         multipart image stream (?raw=1 → bare frames)\nGET  /helper/<serial>/stream.avcc          H.264 AVCC envelopes (see below)\nGET  /helper/<serial>/config               { width, height, orientation, rotation }\nGET  /helper/<serial>/health               { status: \"ok\" }\nGET  /helper/<serial>/screenshot.png       one-shot screenshot\nGET  /helper/<serial>/logs                 SSE of logcat lines\nGET  /helper/<serial>/ax                   UI hierarchy JSON\nGET  /helper/<serial>/foreground           { packageName, activity, pid }\nPOST /helper/<serial>/action               { \"action\": \"...\", ...params } → { ok, result }\nWS   /helper/<serial>/ws                   binary input protocol\n```\n\n`POST …/action` is the RPC behind every subcommand, the Tools pane, the SDK and the MCP tools. Params can be spread beside `action` or nested under `params`; errors come back as `{ ok: false, error: bad_request | not_found | unsupported | failed, message }` with a matching 4xx/5xx.\n\n| action | params | notes |\n|---|---|---|\n| `tap` | `x, y` **or** `text \\| id \\| desc \\| class [, exact, index]`, `durationMs` | long-press with `durationMs` |\n| `swipe` | `x1, y1, x2, y2, durationMs` | |\n| `text` | `text` | ASCII; `\\n`/`\\t` → Enter/Tab |\n| `key` | `code` (browser `KeyboardEvent.code`) or `keycode`, `longPress` | |\n| `button` | `button` | `home`, `back`, `app-switch`, `power`, `volume-up`, … |\n| `rotate` | `orientation` | resolves once the device has rotated; fails if refused |\n| `debug` / `theme` / `scroll` / `memory-warning` | as the WS messages | |\n| `find` | `text \\| id \\| desc \\| class, exact, index` | `{ matches: [{ node, center, normalized, bounds }], total, screen }` |\n| `wait` | same + `timeoutMs, intervalMs, gone` | `{ ok, match, elapsedMs, attempts }` |\n| `geo` | `lat, lon, alt` | emulator only |\n| `network` | `speed, delay, airplane, wifi, data` | none → status |\n| `battery` | `level, plugged (ac\\|usb\\|wireless\\|none), reset` | returns current state |\n| `fingerprint` | `id, remove` | emulator only |\n| `call` | `number, op (call\\|accept\\|end\\|hold)` | emulator only |\n| `sms` | `number, text` | emulator only |\n| `font-scale` / `density` / `locale` / `talkback` | `scale` / `dpi` / `locale, package, system` / `enabled` | |\n| `install` / `launch` / `stop` / `clear-data` / `uninstall` | `path` / `package` | `install` reads the APK from the server host |\n| `open` | `url, package` | VIEW intent |\n| `apps` | `all` | `{ packages }` |\n| `snapshot` | `op (save\\|load\\|delete\\|list), name` | emulator only; `load` waits for adb to come back |\n| `shell` | `cmd` | `{ code, output }` |\n\n```sh\ncurl -X POST localhost:3200/helper/emulator-5554/action \\\n  -H 'content-type: application/json' -d '{\"action\":\"tap\",\"text\":\"Sign in\"}'\n```\n\nThe `/stream.avcc` wire format matches serve-sim byte-for-byte: each chunk is `[len:u32-be][tag:u8][payload]` where `len` covers the tag + payload. Tags: `0x01` avcC decoder config, `0x02` keyframe, `0x03` delta frame, `0x04` seed image (painted before the first keyframe decodes). The decoder config and the current GOP are replayed to late joiners, so viewers paint instantly even when the screen is static.\n\nThe input WebSocket accepts binary `[tag][JSON]` frames (all coordinates normalized 0..1 in the rotated display space):\n\n```\n0x03 touch     {\"type\":\"begin|move|end\",\"x\":…,\"y\":…}\n0x04 button    {\"button\":\"home\"}\n0x05 pinch     {\"type\":\"begin|move|end\",\"x1\":…,\"y1\":…,\"x2\":…,\"y2\":…}\n0x06 key       {\"type\":\"down|up\",\"code\":\"Enter\"}          (browser KeyboardEvent.code)\n0x07 rotate    {\"orientation\":\"landscape_left\"}\n0x08 debug     {\"option\":\"overdraw\",\"enabled\":true}\n0x09 memory-warning\n0x0b scroll    {\"dx\":…,\"dy\":…,\"x\":…,\"y\":…}\n0x0c toggle software keyboard\n0x0d text      {\"text\":\"hello\"}\n0x0e theme     {\"theme\":\"dark\"}\n0x0f keyframe  (no body — viewer can't decode; re-arm capture for fresh SPS/IDR)\n```\n\nThe server pushes `0x82` + JSON screen config whenever dimensions or orientation change, and `0x83` + `{\"kind\",\"ok\",\"message\"}` for one-off notices (e.g. a rotation the device refused).\n\n## Connectors\n\n### MCP (Claude Desktop, Claude Code, Cursor, Codex, …)\n\n`serve-avd mcp` is a Model Context Protocol server over stdio. It exposes the emulator as tools — `screenshot`, `ui_tree`, `find`, `wait_for`, `tap`, `swipe`, `type_text`, `press_button`, `press_key`, `foreground`, `event_log`, `open_url`, `launch_app`, `install_apk`, `list_apps`, `rotate`, `set_location`, `snapshot`, `list_devices`, and a generic `device_action` for everything else (network, battery, fingerprint, call, sms, locale, …). No extra dependencies; nothing else needs to be running (it drives adb directly, or a running serve-avd when there is one).\n\n```json\n{\n  \"mcpServers\": {\n    \"android\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"serve-avd\", \"mcp\", \"--serve\"]\n    }\n  }\n}\n```\n\nThat's the whole config for Claude Desktop (`claude_desktop_config.json`), Cursor (`.cursor/mcp.json`) and Codex; for Claude Code: `claude mcp add android -- npx -y serve-avd mcp --serve`.\n\n- `--serve` also hosts the preview UI in the same process (default port 3200), so a human can watch the agent work at `http://localhost:3200`. Omit it to run headless.\n- `-d <serial|AVD>` pins every tool call to one device; otherwise tools take an optional `device` and default to the only/first one.\n- `screenshot` returns a JPEG (~250 KB on a 1344×2992 display) plus the screen size; `ui_tree` returns a compact list of labelled/interactive nodes with normalized centers — cheaper and exact, so agents can `find` → `tap` by text without vision at all.\n- Everything an agent does lands in the event log (`serve-avd event-log`, the Tools pane) and can be exported and replayed.\n\n### Client SDK (`serve-avd/client`)\n\nA typed, dependency-free client for a running server — plain `fetch`, so it works from Node 18+, Bun, Deno and browsers:\n\n```ts\nimport { connect } from \"serve-avd/client\";\n\nconst emu = await connect(\"http://localhost:3200\");   // or your mounted base, e.g. http://localhost:8081/.emu\nconst dev = emu.device();                             // first attached device (or emu.device(\"Pixel_9_Pro_XL\"))\n\nawait dev.launch(\"com.example.app\");\nawait dev.waitFor({ text: \"Sign in\" }, { timeoutMs: 15_000 });\nawait dev.tap({ id: \"email\" });\nawait dev.type(\"me@example.com\\n\");\nawait dev.tap({ text: \"Sign in\" });\nawait dev.waitFor({ text: \"Welcome\" });\n\nconst { data, contentType } = await dev.screenshot(); // Uint8Array\nconst nodes = await dev.find({ class: \"Button\" });     // bounds + normalized centers\nawait dev.swipe({ x: 0.5, y: 0.8 }, { x: 0.5, y: 0.3 });\nawait dev.geo(48.8566, 2.3522);\nawait dev.snapshot.save(\"logged-in\");\nawait dev.action(\"network\", { speed: \"edge\" });        // any action by name\n```\n\n`Device` mirrors the action table above (`tap`, `longPress`, `swipe`, `scroll`, `type`, `key`, `button`/`back`/`home`, `rotate`, `find`/`findFirst`/`exists`/`waitFor`, `ax`, `foreground`, `config`, `screenshot`, `eventLog`, `geo`/`followRoute`, `network`, `battery`, `fingerprint`, `call`, `sms`, `fontScale`, `density`, `locale`, `talkback`, `snapshot.*`, `install`, `launch`, `stop`, `clearData`, `uninstall`, `open`, `apps`, `shell`). Failures throw `ServeAvdError` with a `code` (`bad_request | not_found | unsupported | failed | http | network`); `waitFor` throws on timeout. `emu.attach(\"Pixel_9_Pro_XL\")` boots/attaches devices; `emu.grid()` and `emu.eventLog()` mirror the Devices pane and event log.\n\n### Claude Code Desktop\n\nCreate a `.claude/launch.json` and define a server:\n\n```json\n{\n  \"version\": \"0.0.1\",\n  \"configurations\": [\n    {\n      \"name\": \"Android\",\n      \"runtimeExecutable\": \"npx\",\n      \"runtimeArgs\": [\"serve-avd\"],\n      \"port\": 3200\n    }\n  ]\n}\n```\n\n### Expo / Metro\n\nAutomatically start serve-avd with `npx expo start` and access the URL at `http://localhost:8081/.emu`.\n\nFirst, customize the `metro.config.js` file (`bunx expo customize`):\n\n```js\n// Learn more https://docs.expo.io/guides/customizing-metro\nconst { getDefaultConfig } = require(\"expo/metro-config\");\nconst connect = require(\"connect\");\nconst { emuMiddleware } = require(\"serve-avd/middleware\");\n\n/** @type {import('expo/metro-config').MetroConfig} */\nconst config = getDefaultConfig(__dirname);\n\nconfig.server = config.server || {};\nconst originalEnhanceMiddleware = config.server.enhanceMiddleware;\nconfig.server.enhanceMiddleware = (metroMiddleware, server) => {\n  const middleware = originalEnhanceMiddleware\n    ? originalEnhanceMiddleware(metroMiddleware, server)\n    : metroMiddleware;\n  const app = connect();\n  app.use(emuMiddleware({ basePath: \"/.emu\" }));\n  app.use(middleware);\n  return app;\n};\n\nmodule.exports = config;\n```\n\n## Embed in your dev server\n\n`serve-avd/middleware` is a Connect-style middleware that mounts the same preview UI inside your existing dev server (Metro, Vite, Next, plain Express, etc.):\n\n```ts\nimport { emuMiddleware } from \"serve-avd/middleware\";\n\nconst middleware = emuMiddleware({ basePath: \"/.emu\" });\napp.use(middleware);\n// → preview HTML at /.emu\n// → state JSON  at /.emu/api\n\nconst server = app.listen(3000);\nserver.on(\"upgrade\", (req, socket, head) => middleware.handleUpgrade(req, socket, head));\n```\n\nDevice sessions are created in-process and everything (video, input socket, logs) is same-origin behind your one port, so remote proxying/tunnelling needs no extra configuration — just forward the `upgrade` event as above so input and live streams work. When terminating TLS at a reverse proxy, the page uses `wss:` automatically based on the page origin.\n\nOn shutdown, call `closeAllDeviceSessions()` (exported from `serve-avd/middleware`) to stop the adb capture processes.\n\n## How it works\n\n```\n┌──────────────────┐  adb screenrecord  ┌──────────────────┐  AVCC / MJPEG / WS  ┌─────────┐\n│ Android Emulator │ ─────────────────► │ serve-avd server │ ──────────────────► │ Browser │\n│ (or adb device)  │ ◄───────────────── │ (Node,           │                     └─────────┘\n└──────────────────┘   adb shell input  │  in-process)     │\n                                        └──────────────────┘\n                                                ▲\n                                          state files in\n                                        $TMPDIR/serve-avd/\n                                                ▲\n                                        ┌──────────────────┐\n                                        │ serve-avd CLI /  │\n                                        │ middleware       │\n                                        └──────────────────┘\n```\n\n- **Video**: `adb exec-out screenrecord --output-format=h264` produces an Annex B elementary stream. serve-avd re-frames it into length-prefixed AVCC envelopes, extracts SPS/PPS into a decoder config, and caches the current GOP so new viewers decode instantly. screenrecord's 3-minute cap and its inability to follow rotation are handled by transparent restarts (viewers just see a new decoder config). The browser decodes with WebCodecs `VideoDecoder` onto a canvas.\n- **Stills**: `adb exec-out screencap` (JPEG on modern Android, PNG otherwise) backs the MJPEG fallback stream, seed frames, and screenshots.\n- **Input**: a single persistent `adb shell` per device multiplexes `input motionevent/tap/swipe/keyevent/text` commands, skipping the per-command adb handshake for low latency. Devices without `input motionevent` (pre-Android 11) get gestures replayed as `tap`/`swipe` on release.\n- **Pinch**: synthesized as raw multi-touch `sendevent`s where `/dev/input` is writable (non-Play images after `adb root`); unavailable on Play-store images — the UI tells you.\n\nNo native code, no device daemons: the npm package is plain Node + `adb`.\n\n## Caveats\n\n- `type` supports ASCII only (Android's `input text` limitation) — matching serve-sim's \"US keyboard only\".\n- Apps that lock their orientation (launchers do) won't visibly rotate, exactly like hardware. The rotation is confirmed against the device, so the preview stays put and says so instead of pretending.\n- Pinch requires a rootable (non-Play) emulator image.\n- Physical devices work for everything except AVD-specific features (boot-by-name, emulator camera flags, and the emulator-console actions: `geo`, `network speed/delay`, `fingerprint`, `call`, `sms`, `snapshot`); enable USB debugging and expect `screenrecord` limits to vary by OEM.\n- `fingerprint` needs a fingerprint enrolled in Settings first; `a11y talkback` needs an image with TalkBack installed (Google APIs / Play); `a11y locale` sets a per-app locale (Android 13+) unless `--system`, which needs a rooted (non-Play) image and otherwise applies on the next boot.\n- `find`/`wait`/`tap --text` depend on `uiautomator dump`, which is unavailable on secure screens (lock screen, payment sheets) and slow on screens that never go idle.\n- The HTTP action endpoint (and therefore the SDK/MCP) can run `shell` and install APKs — it is meant for `localhost`. Put a proxy with auth in front before binding to `0.0.0.0` or tunnelling.\n\n## Development\n\n```sh\nnpm install\nnpm run build       # bundle CLI + middleware + browser client into dist/\nnpm test            # unit tests (H.264 parser, XML/rotation/keymap parsers, find/wait, replay, MCP handler)\nnpm run typecheck\n```\n\n## Credit & License\n\nApache-2.0\n","readmeFilename":"README.md"}