A free, open-source virtual gamepad for cloud-gaming pages in Chrome — Xbox Remote Play / xCloud, GeForce NOW, Amazon Luna. No physical controller needed. The project has two goals:
- Manual control — a full on-screen controller (draggable analog sticks, every button clickable, touch and mouse) for debugging and for playing cloud streams on the go with no hardware. The free answer to the paid virtual-gamepad extensions on the Chrome Web Store.
- A programmable WebSocket interface — drive the same controller from any language, for accessibility projects (map whatever input device you can use to a controller the game accepts) and cool home projects: bots, agents, custom hardware.
Chrome
MANUAL 🎮 on-screen pad ───────▶ ┌──────────────────────────────────┐
(touch / mouse) │ Qpadbridge extension │
│ overrides navigator.getGamepads │
PROGRAMMATIC (any language) │ → page sees a real controller │
┌─────────────────────────┐ JSON │ │
│ WebSocket server │ ───────▶ │ │
│ (your bot / script / │ ◀─────── │ │
│ custom input device) │ rumble └──────────────────────────────────┘
└─────────────────────────┘
Both paths feed input through the same native Gamepad API path a physical controller uses — so the stream never drops or ignores it, and you get full analog sticks and triggers, not just digital keys like keyboard-remapper extensions give you.
- Play anywhere — a Chromebook, a borrowed laptop, a touchscreen: open your stream, tap 🎮, play.
- Debugging — poke any Gamepad API page by hand and watch how it reacts.
- Accessibility — map any input device you can use (a switch, a head-tracker, a MIDI pedal, your phone) to a controller the game accepts.
- Bots & agents — build AI that plays real console games through Remote Play: screenshot → decide → send controller input. (Qpadbridge was extracted from personal projects for playing Xbox remotely.)
- Automation — script repetitive single-player grinds on your own console.
- Clone this repo.
- Chrome →
chrome://extensions→ enable Developer mode → Load unpacked → select theextension/folder. - Open a matched page (e.g.
xbox.com/playwith your console or Game Pass). A small Qpadbridge pill appears bottom-right: red dot = no program connected, green = connected, yellow flash = input arriving.
To use it on another site that reads the Gamepad API, add the URL to
matches in extension/manifest.json and reload the extension.
Click the 🎮 on the status pill to toggle the on-screen pad. You get the whole controller: two draggable analog sticks (full range, not just 8-way), d-pad, A/B/X/Y, bumpers and triggers, stick clicks (LS/RS), View ⧉, Menu ☰, and Guide ⌂. Works with touch (multi-touch: hold a stick while pressing buttons) and mouse. The pad only captures input on its controls — the stream between them stays clickable — and its visibility persists across reloads.
Manual mode needs no server running; the WebSocket side is entirely optional. When both are active they merge into the same controller state, which makes a nice debugging trick: pause your bot, take over by hand, resume.
Tap the ♿ button at the right end of the pad's center strip to open the
accessibility drawer — five toggles, tucked away since they're not always
needed. Every setting persists across reloads and can also be set over
WebSocket via the pad command; when the drawer is closed with a mode still
active, the ♿ button glows yellow so active state is never invisible.
- ⚡ Flick buttons — adds two arrow clusters that fire one-tap calibrated
stick flicks (160 ms full deflection, same as the
flickcommand). Precise menu/tile navigation with single taps instead of drag gestures. - 🔁 Turbo — while a button is held it auto-repeats (~8 presses/sec) instead of holding. Holding a flick arrow repeats the flick; turbo applies to keyboard keys too.
- 📌 Latch — tap locks a button down, tap again releases it. For sustained inputs (hold-to-accelerate, hold-to-aim) without needing a sustained press. Combine with turbo for tap-on/tap-off auto-fire.
- ⌨️ Keyboard — play on the keyboard. Defaults: WASD left stick,
IJKL right stick (diagonals by combining keys), arrows d-pad,
Space A, F B, E X, R Y, Q/T LB/RB, 1/2 LT/RT,
3/4 LS/RS, Backspace View, Enter Menu, G Guide, Esc
exits. Remap anything by storing JSON at
localStorage["qpadbridge.keymap"](KeyboardEvent.code→["btn", index]or["lstick"|"rstick", dx, dy]). - 🎯 One-switch scanning — for single-input use: control clusters
highlight in turn; your one switch (a tap anywhere on the stream, or
Space/Enter) selects the highlighted cluster, then the highlighted button
inside it. Fires a normal press — or a locked hold if latch is also on.
Dwell time defaults to 1.1 s; tune via
localStorage["qpadbridge.scanms"]. Falls back to cluster level after two idle loops; Esc exits.
Turning latch off, hiding the pad, or any reset releases everything — no
input is ever left stranded pressed.
pip install websockets
cd examples && python demo.py # then open xbox.com/playimport asyncio
from qpadbridge import Qpadbridge
async def main():
async with Qpadbridge() as pad: # ws://localhost:8765
await pad.wait_connected()
await pad.press(Qpadbridge.A) # tap A
await pad.hold(Qpadbridge.RT, value=0.6) # analog trigger
await pad.stick("left", x=0, y=-1, ms=500) # forward half a second
await pad.flick("up") # right-stick flick
asyncio.run(main())Node example: examples/server.mjs. Any WebSocket
server in any language works — the protocol is a handful of JSON messages.
The repo ships a Gamepad API tester so you can verify everything without a
console or cloud-gaming account (http://localhost/* is in the extension's
matches for exactly this):
cd test && python3 -m http.server 8000 # then open http://localhost:8000The page reads navigator.getGamepads() every frame and shows live stick
positions and all 17 button values — poke the manual pad or run
examples/demo.py against it and watch the state change. It also fires one
test rumble so you can confirm the game→server haptics channel.
Your program is the WebSocket server (default ws://localhost:8765 — a
web page can't listen for connections, so the extension dials out to you).
Messages are JSON objects with a type:
| → to extension | fields | effect |
|---|---|---|
press |
button, ms=120, value=1 |
press a button, auto-release after ms |
hold / release |
button, (value) |
press until released; value<1 = analog (triggers) |
stick |
stick (left/right), x, y ∈ [-1,1], ms |
set stick; auto-center after ms, or hold if omitted |
flick |
dir (up/down/left/right), stick=right, ms=160 |
quick directional stick flick |
state |
axes [lx,ly,rx,ry], buttons [17 × 0..1] |
absolute state — stream this at frame rate for full control |
reset |
everything to neutral | |
overlay |
show |
show/hide the status pill |
pad |
show, flicks, turbo, latch, keys, scan |
show/hide the manual controller / set its accessibility options |
ping |
replies pong |
| ← from extension | meaning |
|---|---|
hello |
a page connected (page, version) |
rumble |
the game played a haptic effect (params) — observable game feedback |
pong |
ping reply |
Standard-mapping button indices: 0 A · 1 B · 2 X · 3 Y · 4 LB · 5
RB · 6 LT · 7 RT · 8 Back/View · 9 Start/Menu · 10 LS · 11 RS ·
12–15 d-pad up/down/left/right · 16 Guide.
If a connected program drops, the extension resets all input to neutral and retries every second — a crashed bot never leaves a stick pinned. (Failed retries while no server is running don't touch the state, so manual mode is unaffected.)
This tool is for your own console and account: accessibility, automation of single-player games, and AI/agent research. Don't use it to automate multiplayer or ranked play — that breaks platform rules and ruins games for other people, and platform holders can ban accounts for it.
MIT