Skip to content

Browser bridge: WebSocket hub + Chromium extension for web games (Xbox Cloud Gaming, GeForce NOW) - #2

Open
Andrei-Kondrykau wants to merge 8 commits into
Peterksharma:mainfrom
Andrei-Kondrykau:browser-bridge
Open

Andrei-Kondrykau wants to merge 8 commits into
Peterksharma:mainfrom
Andrei-Kondrykau:browser-bridge

Conversation

@Andrei-Kondrykau

@Andrei-Kondrykau Andrei-Kondrykau commented Sep 3, 2026

Copy link
Copy Markdown

What

A third output sink plus a small browser extension, so Switch 2 controllers work in web games (Xbox Cloud Gaming at xbox.com/play, GeForce NOW, Luna, gamepad testers) today, without waiting for the virtual-HID entitlement. Same idea as the SDL bridge, applied to the browser.

  • Output/WebSocketHub.swift — serves controller state as JSON over ws://127.0.0.1:24810 (loopback only, Network.framework's built-in WebSocket server, no dependencies). Rumble comes back the other way; a 15 s ping keeps extension service workers alive.
  • browser/extension — Manifest V3, any Chromium browser, loaded unpacked:
    • background.js owns the socket. Chrome 142+ gates page-initiated loopback connections behind the new Local Network Access permission; extension contexts are not affected, so the socket lives here rather than in the page.
    • bridge.js relays between the service worker and the page (strings only across worlds).
    • shim.js wraps navigator.getGamepads() with standard-mapping virtual pads placed in free indices next to real gamepads, fires gamepadconnected/gamepaddisconnected, and forwards vibrationActuator.playEffect to the hub.
  • browser/README.md — install steps, button table, wire protocol; README gets a short section.

Buttons are mapped by position by default (bottom face button → standard index 0) so Xbox on-screen prompts match the thumb; NINTENDO_LABELS in shim.js switches to label mapping. The app's remapper runs before the sink, so custom layouts carry over.

Why not something else

  • The controllers are BLE with a custom GATT service, not HID, so browsers (which only see IOHIDManager / GameController devices) have no other way in.
  • IOHIDResourceUserClient::initWithTask in IOHIDFamily accepts only the entitlement — there is no root fallback — so CoreHID stays blocked until Apple answers.
  • Wired USB uses the vendor bulk interface on macOS, invisible to browsers.

Testing

  • Hub: handshake, hello, connected replay for late clients and the ping verified with a raw WebSocket client; the sink coexists with UDPHub and VirtualHIDSink unchanged.
  • Shim: verified on hardwaretester.com/gamepad — the pad shows as … (STANDARD GAMEPAD Vendor: 057e Product: 2069), mapping standard, correct axes sign (+Y down), triggers as analog buttons 6/7, rumble events reach the bridge.
  • End to end on xbox.com/play with a Pro Controller 2 (Chromium browser, macOS 27): sticks, buttons, triggers and rumble all work in cloud games.

Ad-hoc build unaffected; no new entitlements or permissions.

Andrei Kondrykau and others added 3 commits September 3, 2026 13:16
…Gaming etc.)

Web games only see gamepads macOS knows about, and a Switch 2 controller
over BLE is not a HID device macOS can pair, so until the virtual-HID
entitlement arrives browsers (Xbox Cloud Gaming, GeForce NOW, Luna) had
no way to use these controllers. This applies the SDL-bridge idea to the
browser:

- Output/WebSocketHub.swift: a third sink serving controller state as
  JSON over ws://127.0.0.1:24810 (loopback only, Network.framework), with
  rumble coming back and a 15 s ping.
- browser/extension: Manifest V3 extension. The service worker owns the
  socket (extension contexts are outside Chrome's Local Network Access
  permission), a content script relays to the page, and shim.js wraps
  navigator.getGamepads() with standard-mapping virtual pads, fires
  gamepadconnected/disconnected and forwards vibrationActuator effects.
- browser/README.md + README: install steps, button table, protocol.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…f checks pass

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ng; note xCloud verification

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Andrei-Kondrykau

Copy link
Copy Markdown
Author

Update: verified end to end on xbox.com/play with a Pro Controller 2 (Chromium, macOS 27) — sticks, buttons, triggers and rumble work in cloud games. Pushed a docs commit with step-by-step install and a troubleshooting section (browser/README.md).

Andrei Kondrykau and others added 5 commits September 3, 2026 15:49
… EXTRA_BUTTONS

Sites diff consecutive getGamepads() results to detect button edges; a
single mutable object made previous and current identical. Extra
indices beyond Share are off by default to match real Xbox pads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Xbox Cloud Gaming parses the vendor id out of gamepad.id and applies
vendor-specific glyphs/handling; with a positional layout the Xbox
identity is the coherent one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ide delivery telemetry

The page now opens the loopback socket itself (shortest path under a
streaming load) and falls back to the service-worker relay only if the
browser refuses. The shim reports 5 s windows of message-arrival
intervals and getGamepads() call counts as {"t":"stats"}, which the hub
logs and echoes to other clients.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t-in

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Andrei-Kondrykau

Copy link
Copy Markdown
Author

Follow-up after real play sessions (GeForce NOW in the browser, Pro Controller 2):

  • Sticks initially felt sticky in GeForce NOW while buttons were fine. The app-side stream was clean (33 Hz, no gaps; rumble traffic does not disturb it), so the cause was on the page side. Two changes fixed it: the shim now hands out a fresh snapshot per getGamepads() call, the way Chrome does (sites diff consecutive snapshots), and the content script opens the loopback WebSocket directly, with the service-worker relay only as a fallback. Page-side telemetry after the fix: 30 ms median arrival interval, p95 31.5 ms, no gaps over 60 ms, while GFN polls ~280 Hz during a stream.
  • Identity was tested both ways and does not matter for GFN; the pad keeps its real Nintendo identity by default, with an Xbox persona available per site.
  • Rumble verified end to end (hardwaretester vibration reaches the controller). Docs in browser/README.md carry the install steps, troubleshooting and the persona note.

@Peterksharma

Copy link
Copy Markdown
Owner

I'm just getting back to this project. Will review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants