Skip to content
giolaqPublic

About

A AI agent window into your Android emulator — npx portholejs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Porthole

Porthole logo

npm version CI license node

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.

porthole-gh.mp4

Quick Start

Prerequisites:

  • Node.js 20 or newer
  • Android Studio or Android SDK command line tools
  • emulator and adb installed 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 portholejs

With 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.mjs

porthole 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_34

For a headless agent workflow:

npx portholejs start Android_TV_1080p --detach -q
npx portholejs remote dpad_down
npx portholejs screenshot -q
npx portholejs kill -q

Things 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 -q
  • Record 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.7
  • Catch 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.02
  • Stream 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_1080p
  • Let 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 pull get_crashes on 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 launch <package> Launch an installed app package
porthole stop-app <package> Force-stop an app
porthole clear-app <package> Clear app data
porthole activity [--follow] Read or follow the shared activity feed
porthole control [shared/human] Read or change who controls the device
porthole ask "<question>" Ask the browser operator and wait for an answer
porthole say "<message>" Post an intent note to the activity feed
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:

porthole start --vega

This 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 with vega on PATH. Porthole uses the active SDK selected by vega sdk use and resolves its device adaptor through vega which vda. ~/vega/bin/vega and legacy SDK installations are also discovered. Set PORTHOLE_VEGA_BIN / PORTHOLE_VDA_BIN to override the binaries.
  • Developer mode enabled on the virtual device (remote input is injected through the on-device inputd-cli, exposed by the developer shell service)

Compatibility checked with Vega SDK 0.24.12112 / CLI 1.4.2 on macOS Apple Silicon: headless boot, attach, PNG screenshots, MJPEG frames, and D-pad focus movement in the SDK 0.24 Hello World sample were verified. The VVD uses ports 5554 and 5555; stop any Android emulator using those ports before starting Vega. Porthole checks the SDK virtual-device status before attaching and rejects conflicting listeners.

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/history/VEGA_EXPLORATION.md.

MCP Setup

.mcp.json:

{
  "mcpServers": {
    "porthole": {
      "command": "npx",
      "args": ["portholejs", "mcp"]
    }
  }
}

Claude Code:

claude mcp add porthole -- npx portholejs mcp

Useful 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, install_apk, get_recent_activity, get_control_state, ask_human, get_annotations, and say.

Humans and agents, one device

The browser, CLI, and MCP share one live device session. The activity feed shows who acted and whether the action succeeded. Agent taps appear briefly over the phone preview; agent remote presses highlight the TV remote button. This makes the agent's input visible to the person watching the emulator.

The browser's control button lets the human take control and later hand it back. While the human holds control, agent input returns PAUSED_BY_HUMAN. The agent can review recent activity, post its intent with say, or ask a question that appears in the browser. The human can answer there and draw a screen annotation with a note; the agent can retrieve the annotation and its captured image through get_annotations.

Human control, agent question, annotation, and shared activity on a TV session

See the shared control plane ADR for the architecture and the demo walkthrough for a scripted example.

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/emu is a raw passthrough to the emulator console (adb emu) — including commands like kill. Treat it as operator-level access; anyone who can reach the (token-protected) API can use it.
  • porthole focused / get_focused reads D-pad focus, which is a TV/leanback concept. On phone profiles it usually returns null unless 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 typecheck

Integration tests that require a booted emulator are gated behind PORTHOLE_EMU=1.

About

A AI agent window into your Android emulator — npx portholejs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages