portholejs
v0.4.0
Published
A window into your Android emulator — stream and control AVDs from the browser
Maintainers
Readme
Porthole
A window into your Android emulator — npx portholejs.
Porthole boots or attaches to Android phone and TV AVDs, streams them to a browser, and forwards touch, keyboard, D-pad remote, screenshots, logcat, and file drops through one local tool. It also has experimental support for the Amazon Fire TV Vega simulator. It is the Android sibling of serve-sim.
Quick Start
Prerequisites:
- Node.js 20 or newer
- Android Studio or Android SDK command line tools
emulatorandadbinstalled under the SDK- Chrome, Edge, or another browser with WebCodecs for H.264 playback
Porthole finds the SDK from ANDROID_HOME, then ANDROID_SDK_ROOT, then the
platform defaults:
- macOS:
~/Library/Android/sdk - Linux:
~/Android/Sdk - Windows:
%LOCALAPPDATA%\Android\Sdk
npx portholejsWith no running emulator, Porthole prompts for an AVD. To boot a specific AVD:
The scrcpy-server component
Porthole mirrors and controls the emulator by speaking the
scrcpy protocol. At runtime it pushes a
small server jar (scrcpy-server, Apache-2.0, by Genymobile) to the emulator
over adb and connects to its video and control sockets — no scrcpy CLI or
native binary is involved.
The jar is not bundled in this repository or in the npm package. It is
downloaded automatically during npm install (a postinstall step) from the
official Genymobile/scrcpy GitHub release, pinned to v3.1 and verified
against a SHA-256 checksum. If the download was skipped (for example, an
offline install), fetch it later with:
node node_modules/portholejs/scripts/download-scrcpy-server.mjsporthole doctor reports whether the jar is present. To bump the pinned
version, update SCRCPY_VERSION and SCRCPY_SHA256 in
scripts/download-scrcpy-server.mjs and keep the engine's scrcpy options class
in sync (see src/engine/scrcpy-engine.ts).
npx portholejs start Pixel_8_Pro_API_34
npx portholejs start Pixel_8_Pro_API_34 Television_1080p_API_34For a headless agent workflow:
npx portholejs start Android_TV_1080p --detach -q
npx portholejs remote dpad_down
npx portholejs screenshot -q
npx portholejs kill -qThings to try
Test a TV app without touching a remote. Walk the D-pad focus to any element by its text and press OK, then read where leanback focus landed as JSON:
porthole focus-on "Library" --select porthole focused -qRecord a bug repro as a real video while you drive the phone from the CLI. The swipe below opens the notification shade:
porthole record repro.mp4 --duration 30s porthole scroll down porthole swipe 0.5 0.05 0.5 0.7Catch visual regressions in one line. Save a baseline, then the assert exits non-zero and writes a diff image the moment the screen drifts:
porthole screenshot -o good.png porthole assert-screen good.png --threshold 0.02Stream a phone and a TV side by side from one command. Both devices share the same browser tab with a device picker:
porthole start Pixel_8 Tv_1080pLet an agent run the whole loop. Add the MCP server to Claude Code (below) and ask it to "open my app on the TV emulator, navigate to Settings, and check nothing crashed". It will drive the D-pad, read the UI as JSON with
dump_ui, and pullget_crasheson its own.If anything misbehaves, diagnose your setup in seconds:
porthole doctor
CLI Reference
| Command | Purpose |
| ------------------------------------ | --------------------------------------------------- |
| porthole start [avd...] | Boot or attach one or more emulators and serve them |
| porthole list | List AVDs, running emulators, and known sessions |
| porthole kill [avd] | Stop emulators Porthole booted and detached servers |
| porthole tap <x> <y> | Touch normalized phone coordinates from 0..1 |
| porthole swipe <x1> <y1> <x2> <y2> | Swipe between normalized phone coordinates |
| porthole longpress <x> <y> | Long-press normalized phone coordinates |
| porthole scroll <direction> | Scroll phone content up, down, left, or right |
| porthole key <keycode> | Send an Android keycode down/up pair |
| porthole remote <button> | Press a TV D-pad/media button |
| porthole text "<string>" | Type text into the active session |
| porthole screenshot [-o file.png] | Save a PNG screenshot |
| porthole assert-screen <png> | Compare the current screen with a PNG baseline |
| porthole record <out.mp4> | Record the current H.264 stream to MP4 |
| porthole focused | Print the currently focused UI node |
| porthole focus-on <text> | Move TV D-pad focus to matching text |
| porthole dump-ui [--filter text] | Dump the Android UI hierarchy |
| porthole wait-for <text> | Wait until UI text appears |
| porthole open-url <url> | Open a URL or Android deep link |
| porthole stop-app <package> | Force-stop an app |
| porthole clear-app <package> | Clear app data |
| porthole rotate <orientation> | Rotate a phone emulator |
| porthole emu <args...> | Pass through to adb emu |
| porthole doctor | Diagnose Node/SDK/adb/AVD/session problems |
| porthole mcp | Run the MCP server over stdio |
Common options:
| Option | Meaning |
| ----------------------- | ------------------------------------------------------- |
| -p, --port <port> | Preview/control port, default 3200 |
| -d, --device <serial> | Target a running emulator serial; comma-list on start |
| --host <host> | Bind address, default 127.0.0.1 |
| --no-preview | Do not open the browser automatically |
| --detach | Start the preview server in the background |
| --mjpeg | Force MJPEG screenshot polling |
| --vega | Attach to the Amazon Fire TV Vega virtual device |
| -q, --quiet | Emit one JSON object/array on stdout |
| --max-size <px> | Maximum scrcpy stream dimension |
| --max-fps <fps> | Maximum scrcpy FPS |
| --bitrate <bps> | Scrcpy video bitrate |
| --wipe-data | Wipe emulator data before boot |
| --no-snapshot | Disable loading/saving emulator snapshots |
| --cold-boot | Alias for --no-snapshot |
| --gpu <mode> | Pass emulator GPU mode |
| --keep-alive | Leave a Porthole-booted emulator running on exit |
Quiet schemas are intentionally simple: list -q returns
{ "devices": [...], "sessions": [...] }; single-device start --detach -q
returns the session record with url, pid, serial, avdName, port, and
profile; multi-device detached starts return { "sessions": [...] }; input
commands return { "ok": true, "session": ... }; screenshot -q returns
{ "ok": true, "path": "...", "session": ... }. Pass -d <serial> to
port-scoped commands when a server is streaming more than one emulator.
Fire TV Vega (experimental)
Porthole can stream and control Amazon's Vega OS (Fire TV) virtual device — likely the only browser window into the Vega simulator today:
porthole start --vegaThis attaches to a running Vega Virtual Device (or boots one headless via the
vega CLI), streams it to the browser at ~3 fps (screenshot polling — Vega
has no H.264 stream), and drives the full Fire TV remote (D-pad, OK, Back,
Home, Menu, media keys) plus porthole remote, screenshot, and
assert-screen headlessly.
Requirements:
- The Vega SDK
installed (
~/vega/sdk/..., or pointPORTHOLE_VEGA_BIN/PORTHOLE_VDA_BINat thevegaandvdabinaries) - Developer mode enabled on the virtual device (remote input is injected
through the on-device
inputd-cli, exposed by the developer shell service)
Not yet available on Vega: H.264/WebCodecs streaming, touch/gesture input
(TV profile), dump-ui/focused/focus-on, and MP4 recording. The
exploration notes live in docs/VEGA_EXPLORATION.md.
MCP Setup
.mcp.json:
{
"mcpServers": {
"porthole": {
"command": "npx",
"args": ["portholejs", "mcp"]
}
}
}Claude Code:
claude mcp add porthole -- npx portholejs mcpUseful MCP tools: list_devices, boot_device, wait_for_boot,
attach_device, tap, swipe, long_press, scroll, key, remote,
type_text, screenshot, assert_screen, start_recording, stop_recording,
dump_ui, get_focused, focus_on, find_element, wait_for, open_url,
stop_app, clear_app, get_crashes, read_logcat, and install_apk.
Agent Skill
Porthole ships an agent skill (skills/porthole/) that teaches agents the full workflow: doctor triage,
boot/attach, semantic UI assertions, TV navigation, and cleanup.
Install it by copying the skill directory into your skills folder — for the current project:
mkdir -p .claude/skills
cp -r node_modules/portholejs/skills/porthole .claude/skills/or for all your projects:
mkdir -p ~/.claude/skills
cp -r node_modules/portholejs/skills/porthole ~/.claude/skills/From a clone of this repository, copy skills/porthole instead. Claude Code
picks the skill up automatically; ask it to "test my app on the Android
emulator" and it will follow the Porthole workflow.
GitHub Action
Porthole ships an in-repo GitHub Action for CI smoke tests. It wraps
reactivecircus/android-emulator-runner, starts npx --yes portholejs against
the booted emulator, and runs your script while the emulator is alive.
jobs:
tv-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: porthole
uses: giolaq/porthole@main
with:
avd-name: porthole-tv
api-level: "35"
profile: tv
script: |
mkdir -p artifacts
"$PORTHOLE_CLI" focus-on "Library" --select -q -p "$PORTHOLE_PORT"
"$PORTHOLE_CLI" screenshot -o artifacts/library.png -q -p "$PORTHOLE_PORT"The action exposes url, serial, and port outputs, and also sets
PORTHOLE_URL, PORTHOLE_SERIAL, PORTHOLE_PORT, and PORTHOLE_CLI for the
smoke script. See docs/github-action-example.yml for a fuller focus-on /
assert-screen workflow that uploads screenshot artifacts on failure.
Browser UI
The preview shows device tabs, stream status, screenshots, copy-to-clipboard, stream stats, logcat, drag-and-drop APK install/file push, phone hardware keys, and a TV D-pad remote. Switching tabs re-subscribes to that device's stream without reloading the page. TV sessions reject touch input server-side.
Video uses WebCodecs by default. Browsers without VideoDecoder automatically
fall back to /stream.mjpeg, and --mjpeg or ?video=mjpeg forces that mode.
MJPEG is implemented with shared adb screencap polling at roughly 3 fps,
re-encoded server-side to downscaled JPEG (pure JS, max 800 px) to keep
per-frame payloads small; it is a compatibility fallback, not a
high-frame-rate stream.
When serving on a LAN with --host 0.0.0.0, Porthole prints a tokenized URL.
Non-local requests must present that token.
Two capability notes:
porthole emu/POST /api/emuis a raw passthrough to the emulator console (adb emu) — including commands likekill. Treat it as operator-level access; anyone who can reach the (token-protected) API can use it.porthole focused/get_focusedreads D-pad focus, which is a TV/leanback concept. On phone profiles it usually returnsnullunless a view holds keyboard focus — that is expected, not an error.
Troubleshooting
| Problem | Fix |
| ----------------------------- | --------------------------------------------------------------------------- |
| SDK not found | Set ANDROID_HOME or ANDROID_SDK_ROOT to your Android SDK |
| adb devices shows offline | Porthole runs adb reconnect offline; if it persists, restart the emulator |
| Port 3200 is taken | Pass -p 3201 or another free port |
| Blank video | Use a WebCodecs-capable browser and check DevTools for decoder errors |
| TV taps do nothing | Use porthole remote <button>; TV profiles reject touch |
Development
npm install
npm run build
npm run test
npm run lint
npm run typecheckIntegration tests that require a booted emulator are gated behind
PORTHOLE_EMU=1.
