cd /news/developer-tools/show-hn-stream-android-emulators-to-… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-69283] src=github.com β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

Show HN: Stream Android Emulators to a Browser Window

A new open-source tool called serve-avd lets developers stream Android emulators to a browser window for use with AI agent tools like Codex, Cursor, or Claude Desktop, capturing the emulator's screen via adb screenrecord and exposing it as an H.264 WebCodecs stream with full interaction support. Created by an unnamed developer, serve-avd is a port of Evan Bacon's serve-sim for Android, requiring the Android SDK platform-tools and Node.js 18.17+, and works with any running Android emulator or physical device over adb without root or plugins.

read9 min views1 publishedJul 22, 2026
Show HN: Stream Android Emulators to a Browser Window
Image: source

The npx serve

of Android Emulators.

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

npx serve-avd

serve-emu.mov #

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.

It is a faithful Android port of serve-sim by Evan Bacon: same interface, same streaming design, same wire protocol β€” rebuilt on what Android and adb provide.

  • Smooth H.264 video stream in the browser (WebCodecs), with instant paint on connect β€” no waiting for the next frame.
  • Full interaction: tap, drag, and fling with the mouse; scroll with the wheel.
  • Android navigation from the browser: Back, Home, Recents, power, volume, theme toggle, screenshot.
  • Keyboard forwarding β€” type into the emulator directly, Escape acts as Back, βŒ˜β‡§H goes Home.
  • logcat is forwarded to the browser (and mirrored into the browser console for browser-use MCP tools to read).
  • Recent actions are available in the browser Tools panel and serve-avd event-log

. - UI hierarchy dumps for agents: serve-avd ax

(uiautomator β†’ JSON). - Multiple emulators at once β€” boot and attach AVDs straight from the Devices panel.

  • Works headless over the same HTTP API when no browser is attached.

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

It'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.

Requires the Android SDK platform-tools (adb

; the emulator

binary is needed to boot AVDs by name) and a maintained Node.js LTS release (Node 18.17+). serve-avd finds your SDK via $ANDROID_HOME

, $ANDROID_SDK_ROOT

, or the default SDK locations on macOS and Linux.

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

serve-avd [device...]                 Start preview server (default: localhost:3200)
                                      device = adb serial or AVD name (boots it if needed)
serve-avd --no-preview [device...]    Stream in foreground without a preview server
serve-avd gesture '<json>' [-d serial]
                                      Send a touch gesture
serve-avd tap <x> <y> [-d serial]     Tap at normalized 0..1 coords
serve-avd button [name] [-d serial]   Send a button press (default: home)
                                      home|back|app-switch|power|lock|wake|
                                      volume-up|volume-down|mute|menu|camera|
                                      notifications|quick-settings|dpad-*|…
serve-avd type <text> [-d serial]     Type text via the emulator keyboard
                                      (ASCII only; also --stdin / --file <path>)
serve-avd rotate <orientation> [-d serial]
                                      portrait | portrait_upside_down |
                                      landscape_left | landscape_right
serve-avd debug <option> <on|off> [-d serial]
                                      Toggle an Android render/debug flag
                                      (overdraw|gpu-profile|layout-bounds|
                                       show-taps|pointer-location|slow-animations)
serve-avd memory-warning [-d serial]  Ask the foreground app to trim memory
serve-avd event-log [-d serial]       Show recent emulator events
serve-avd screenshot [path] [-d serial]
                                      Save a screenshot (PNG/JPEG)
serve-avd ax [-d serial]              Dump the UI hierarchy as JSON (uiautomator)
serve-avd foreground [-d serial]      Print the foreground app

Options:
  -p, --port <port>   Starting port (preview default: 3200; --no-preview default: 3100)
      --host <host>   Host to bind (default: 127.0.0.1; use 0.0.0.0 for LAN)
  -d, --detach        Spawn a background server and exit (daemon mode)
  -q, --quiet         JSON-only output
      --no-preview    Skip the web UI; stream in foreground only
      --panes <panes> Initially open preview panes: devices, tools, logs, or none
      --fit           Initially size the emulator to fit the preview viewport
      --theme <theme> Set device appearance before opening the preview:
                      light or dark
      --codec <codec> Stream codec for the preview UI: 'auto' (H.264 when the
                      browser can decode it) or 'mjpeg' (force screenshot
                      streaming β€” e.g. in browsers without WebCodecs)
      --bit-rate <mbps>  H.264 bitrate in Mbps (default: 8)
      --size <WxH>    Capture at a fixed size (default: display size, capped
                      at 1920 on the long side)
      --list [device] List running streams
      --kill [device] Kill running stream(s)
serve-avd                              # attach every online device, open preview
serve-avd Pixel_9_Pro_XL               # target an AVD by name β€” boots it if needed
serve-avd emulator-5554 emulator-5556  # two emulators side by side
serve-avd --detach                     # start a background server, return JSON
serve-avd --list                       # show running streams
serve-avd --kill                       # stop all servers
serve-avd --panes devices,tools --fit  # start with panes open and fit the emulator
serve-avd --theme dark                 # start the device in Dark Mode

serve-avd type "Hello, world!"
echo "from stdin" | serve-avd type --stdin
serve-avd type --file ./snippet.txt

serve-avd tap 0.5 0.9                  # near bottom-center
serve-avd gesture '{"type":"begin","x":0.5,"y":0.8}'
serve-avd gesture '{"type":"move","x":0.5,"y":0.4}'
serve-avd gesture '{"type":"end","x":0.5,"y":0.3}'   # swipe up

serve-avd screenshot ./now.png
serve-avd ax | jq '.root.children[0]'
serve-avd foreground
serve-avd event-log --json

Multiple devices are supported β€” pass several serials or AVD names, or leave it empty to attach to every online device.

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

emulator -avd Pixel_9_Pro_XL -camera-back virtualscene -camera-front webcam0

Inside 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.)

Everything the preview UI does goes through a small same-origin API you can drive yourself:

GET  /api                                  server + device state
GET  /api/event-log?device=&limit=         recent events JSON
GET  /api/event-log/events                 SSE: snapshot + live entries
GET  /grid/api                             connected devices + configured AVDs
POST /grid/api/start {"device": "..."}     attach a serial / boot an AVD

GET  /helper/<serial>/stream.mjpeg         multipart image stream (?raw=1 β†’ bare frames)
GET  /helper/<serial>/stream.avcc          H.264 AVCC envelopes (see below)
GET  /helper/<serial>/config               { width, height, orientation, rotation }
GET  /helper/<serial>/health               { status: "ok" }
GET  /helper/<serial>/screenshot.png       one-shot screenshot
GET  /helper/<serial>/logs                 SSE of logcat lines
GET  /helper/<serial>/ax                   UI hierarchy JSON
GET  /helper/<serial>/foreground           { packageName, activity, pid }
WS   /helper/<serial>/ws                   binary input protocol

The /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.

The input WebSocket accepts binary [tag][JSON]

frames (all coordinates normalized 0..1 in the rotated display space):

0x03 touch     {"type":"begin|move|end","x":…,"y":…}
0x04 button    {"button":"home"}
0x05 pinch     {"type":"begin|move|end","x1":…,"y1":…,"x2":…,"y2":…}
0x06 key       {"type":"down|up","code":"Enter"}          (browser KeyboardEvent.code)
0x07 rotate    {"orientation":"landscape_left"}
0x08 debug     {"option":"overdraw","enabled":true}
0x09 memory-warning
0x0b scroll    {"dx":…,"dy":…,"x":…,"y":…}
0x0c toggle software keyboard
0x0d text      {"text":"hello"}
0x0e theme     {"theme":"dark"}

The server pushes 0x82

  • JSON screen config whenever dimensions or orientation change.

Create a .claude/launch.json

and define a server:

{
  "version": "0.0.1",
  "configurations": [
    {
      "name": "Android",
      "runtimeExecutable": "npx",
      "runtimeArgs": ["serve-avd"],
      "port": 3200
    }
  ]
}

Automatically start serve-avd with npx expo start

and access the URL at http://localhost:8081/.emu

.

First, customize the metro.config.js

file (bunx expo customize

):

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const connect = require("connect");
const { emuMiddleware } = require("serve-avd/middleware");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

config.server = config.server || {};
const originalEnhanceMiddleware = config.server.enhanceMiddleware;
config.server.enhanceMiddleware = (metroMiddleware, server) => {
  const middleware = originalEnhanceMiddleware
    ? originalEnhanceMiddleware(metroMiddleware, server)
    : metroMiddleware;
  const app = connect();
  app.use(emuMiddleware({ basePath: "/.emu" }));
  app.use(middleware);
  return app;
};

module.exports = config;

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.):

import { emuMiddleware } from "serve-avd/middleware";

const middleware = emuMiddleware({ basePath: "/.emu" });
app.use(middleware);
// β†’ preview HTML at /.emu
// β†’ state JSON  at /.emu/api

const server = app.listen(3000);
server.on("upgrade", (req, socket, head) => middleware.handleUpgrade(req, socket, head));

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

On shutdown, call closeAllDeviceSessions()

(exported from serve-avd/middleware

) to stop the adb capture processes.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  adb screenrecord  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  AVCC / MJPEG / WS  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Android Emulator β”‚ ─────────────────► β”‚ serve-avd server β”‚ ──────────────────► β”‚ Browser β”‚
β”‚ (or adb device)  β”‚ ◄───────────────── β”‚ (Node,           β”‚                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   adb shell input  β”‚  in-process)     β”‚
                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                β–²
                                          state files in
                                        $TMPDIR/serve-avd/
                                                β–²
                                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                        β”‚ serve-avd CLI /  β”‚
                                        β”‚ middleware       β”‚
                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

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 WebCodecsVideoDecoder

onto a canvas.Stills:adb exec-out screencap

(JPEG on modern Android, PNG otherwise) backs the MJPEG fallback stream, seed frames, and screenshots.Input: a single persistentadb shell

per device multiplexesinput motionevent/tap/swipe/keyevent/text

commands, skipping the per-command adb handshake for low latency. Devices withoutinput motionevent

(pre-Android 11) get gestures replayed astap

/swipe

on release.Pinch: synthesized as raw multi-touchsendevent

s where/dev/input

is writable (non-Play images afteradb root

); unavailable on Play-store images β€” the UI tells you.

No native code, no device daemons: the npm package is plain Node + adb

.

type

supports ASCII only (Android'sinput text

limitation) β€” matching serve-sim's "US keyboard only".- Apps that lock their orientation (launchers do) won't visibly rotate, exactly like hardware.

  • Pinch requires a rootable (non-Play) emulator image.
  • Physical devices work for everything except AVD-specific features (boot-by-name, emulator camera flags); enable USB debugging and expect screenrecord

limits to vary by OEM.

npm install
npm run build       # bundle CLI + middleware + browser client into dist/
npm test            # unit tests (H.264 parser, XML/rotation/keymap parsers)
npm run typecheck

Apache-2.0

── more in #developer-tools 4 stories Β· sorted by recency
── more on @serve-avd 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/show-hn-stream-andro…] indexed:0 read:9min 2026-07-22 Β· β€”