Skip to content
| Marketplace
Sign in
Visual Studio Code>Testing>Android PanelNew to Visual Studio Code? Get it now.
Android Panel

Android Panel

amophi

| (0) | Free
Mirror a connected Android device into a sidebar panel and drive it with the mouse.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Android Panel

Mirrors a connected Android device into a VS Code sidebar panel and lets you drive it with the mouse.

The panel speaks the scrcpy server protocol directly: it pushes scrcpy-server to the device, opens an adb reverse tunnel, and decodes the H.264 stream in the webview with WebCodecs. Clicks, drags and wheel scrolls go back as input tap / input swipe, so the panel is interactive rather than a read-only preview. No native modules, no bundler, no npm dependencies.

Because it talks to the scrcpy server rather than shelling out to screencap, it can mirror a virtual display — the app runs on a display of its own and the device's real screen stays free for something else.

Why

Existing options either open a separate always-on-top window (scrcpy) or require a full Android Studio device mirror. Neither docks into the editor. This one behaves like any other sidebar view: it lives in the activity bar, follows the editor layout, and stops polling when hidden.

Requirements

The extension bundles neither adb nor scrcpy; it drives the copies already on the machine. A scrcpy release ships both in one folder, so unpacking one anywhere is usually the whole setup.

  • adb, found on PATH or by the discovery below, or an absolute path in androidPanel.adbPath
  • USB debugging enabled on the device, and the host authorised
  • For stream mode: the scrcpy-server file from a scrcpy release, and an editor whose Chromium provides WebCodecs. The version handed to the server must match the server file exactly; leaving androidPanel.scrcpyVersion empty derives it from the scrcpy binary sitting next to the file, so upgrading scrcpy does not silently break the panel.

Install

From the Visual Studio Marketplace, or by searching for Android Panel in the Extensions view:

code --install-extension amophi.android-panel

VS Code forks — Cursor, Windsurf, VSCodium, Antigravity — read Open VSX rather than the Microsoft marketplace. The same release goes to both, so searching the Extensions view works in those editors as well.

From source

Clone into the editor's extensions directory and reload the window:

git clone https://github.com/amophi/android-panel.git \
  ~/.vscode/extensions/android-panel

On VS Code forks, substitute the matching directory — ~/.antigravity-ide/extensions, ~/.cursor/extensions, and so on. git pull in that directory updates the extension; there is nothing to build.

If the machine has no git, build a .vsix where one is available and install that instead:

npx @vscode/vsce package --no-dependencies --out android-panel.vsix
code --install-extension android-panel.vsix

While developing, a directory junction or symlink from the extensions directory to a working copy avoids copying after every edit.

Setting it up on a new machine

stream mode needs adb and the scrcpy-server file. Both ship together in a scrcpy release, so unpacking scrcpy anywhere is usually enough — leave adbPath and scrcpyServerPath empty and the extension looks for them:

  • whatever scrcpy or adb resolve to on PATH
  • %LOCALAPPDATA%\scrcpy, including one level of subdirectories, since scrcpy releases unpack into a versioned folder such as scrcpy-win64-v4.1
  • the Android SDK's platform-tools
  • /usr/local/bin, /usr/bin, /opt/homebrew/bin

Set the two paths explicitly only when discovery misses. scrcpyVersion can stay empty as well; it is read from the scrcpy binary next to the server file.

Settings

Setting Default Meaning
androidPanel.mode stream stream decodes the scrcpy H.264 stream; screencap polls adb exec-out screencap
androidPanel.adbPath (empty) Path to the adb executable. Empty runs the discovery above
androidPanel.scrcpyServerPath (empty) stream mode: path to the scrcpy-server file
androidPanel.scrcpyVersion (empty) Version string the server expects. Empty asks the scrcpy binary next to the server file
androidPanel.newDisplay (empty) stream mode: virtual display to create, e.g. 1440x3120/560. Empty mirrors the real screen
androidPanel.maxSize 1080 stream mode: cap the encoded frame's long edge. 0 encodes at full resolution
androidPanel.maxFps 0 stream mode frame cap; 0 is unlimited
androidPanel.keepStreamWhenHidden true Keep the stream running while the view is hidden
androidPanel.stayAwake false Ask the server to keep the device awake while charging
androidPanel.package (empty) Package launched when the panel opens. Empty mirrors whatever is on screen
androidPanel.intervalMs 600 screencap mode: capture interval in milliseconds
androidPanel.serial (empty) Device serial. Empty picks automatically, preferring physical devices over emulators

When androidPanel.package is set, device selection prefers a device that actually has that package installed — useful when an emulator is running alongside a phone.

Controls

Action Effect
Click input tap
Drag input swipe
Wheel Vertical swipe
← / ⌂ Back / Home key events
▶ Launch the configured package
↻ Re-detect the device
⤢ Toggle between fitting the whole screen and filling the panel width

Stream protocol

Measured against scrcpy 4.1. The server is started as:

CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / \
  com.genymobile.scrcpy.Server 4.1 scid=<8 hex digits> log_level=info \
  audio=false control=false new_display=<WxH/dpi>

scid is parsed with Integer.parseInt(s, 16), so it must fit in a signed 32-bit int — the high bit has to be clear. The server then connects back through the reverse tunnel and writes:

device name   64 bytes, NUL-padded
codec meta    16 bytes: codec id (4) + unknown (4) + width (4) + height (4)
frames        repeated: PTS/flags (8) + length (4) + Annex-B payload

In the PTS word, bit 62 marks a config packet (SPS/PPS) and bit 61 marks a key frame. The config packet is prepended to the following key frame before handing it to VideoDecoder, which is configured from the profile and level found in the SPS.

Input does not use the scrcpy control socket. control=false is passed and taps are sent with adb shell input -d <displayId> tap, which keeps the client to one socket.

Keeping it cheap

A phone screen is far larger than a sidebar. Encoding 1440x3120 and scaling it down in the webview wastes most of the work, so maxSize asks the server to scale before encoding. On a 1440x3120 display maxSize: 1080 yields 498x1080 — still sharper than the panel is wide:

encoded bandwidth pixels per frame
unscaled 1440x3120 ~50 KB/s 4.49 M
maxSize: 1080 498x1080 ~13 KB/s 0.54 M

Scaling separates two coordinate spaces: the video is 498x1080 but input tap still expects display coordinates. The panel asks the device with wm size -d <id> rather than reusing the video size.

Cleaning up after itself

Killing the local adb shell does not kill the server process on the device. A leaked server keeps its virtual display alive, and the next run then launches the app onto a different display while the panel streams the stale one — the symptom is a panel showing an empty secondary launcher.

The client therefore kills its own server by scid on shutdown, and sweeps any orphaned servers and scrcpy_* reverse tunnels on startup. A health check every eight seconds confirms the display still exists and the configured package is still on top of it, restarting or relaunching if not.

Behaviour notes

Tearing the stream down destroys the virtual display, and the activity on it dies with it. Switching to another sidebar view would therefore restart the app, so in stream mode the stream keeps running while the view is hidden and the webview retains its decoder. Set keepStreamWhenHidden to false to trade that for idle battery, at the cost of restarting the app each time the panel is reopened.

In screencap mode there is no such state, so capture stops entirely while the view is hidden.

In screencap mode frames are hashed and only pushed when the screen actually changed, so a static screen costs one screencap per interval and nothing else. That mode can only read physical displays: virtual displays are rejected by screencap and screenrecord alike, which take physical display IDs only. Use stream mode for a virtual display.

Icons

Two different things, easy to confuse. contributes.viewsContainers.activitybar[].icon is the activity bar glyph: a monochrome SVG that must paint with currentColor so it follows the theme and the selected/unselected state. The top-level icon field is the raster image shown in the extensions list and must be a PNG, so currentColor is not available and the artwork carries its own colour.

Both are drawn from the same phone outline, kept at a 24-unit grid so the two stay in step.

Language

The interface is English, and Korean is used instead when the editor's display language is Korean. package.nls.ko.json covers the settings and command titles, l10n/bundle.l10n.ko.json the status messages and the panel's own buttons. Another language is one more file of each and no code changes.

Releasing

Pushing a v* tag packages the extension, publishes it to both marketplaces, and attaches the .vsix to a GitHub release. The tag has to match version in package.json or the workflow stops before publishing anything.

npm version 0.1.0 --no-git-tag-version
git commit -am "release: 0.1.0" && git tag v0.1.0 && git push --follow-tags

That needs two repository secrets: VSCE_PAT, an Azure DevOps personal access token scoped to Marketplace → Manage, and OVSX_PAT, an Open VSX access token. To publish by hand instead:

npx @vscode/vsce publish --no-dependencies
npx ovsx publish android-panel.vsix

License

MIT

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft