Self-hosted Switch 2 cloud play
Stream the dock over WebRTC and play from any browser on the LAN, up to eight pads through a Pico.
The console stays on the capture PC. Browsers see the picture over WebRTC. One browser can drive several gamepads at once. Inputs go to a Raspberry Pico that shows up on the Switch as 8 USB pads.
The s2pipe launcher (Windows and Linux) downloads FFmpeg and MediaMTX, starts Node and the browser UI, and talks to the Pico. No auth. Trusted LAN only. Do not expose this on the Internet.
Switch 2 --HDMI--> capture --USB--> launcher (FFmpeg + MediaMTX + node + client)
|-- ICE --> browsers
Browsers --WHEP + WebSocket--> node --UART--> Pico --USB--> Switch 2
| Piece | Path | Role |
|---|---|---|
| Launcher | launcher |
GUI / CLI: configure and run the stack |
| Node | apps/node |
HTTP, WebSocket, Pico serial, WHEP proxy |
| Client | apps/client |
Browser UI |
| Firmware | firmware |
Pico: UART in, 8 USB pads out |
Equivalents are fine.
- Raspberry Pico 2 W, board
pico2_w - CP2102 UART adapter HW-598, jumper 3.3 V, no VCC on the Pico (TX / RX / GND only)
- XIIXMASK HDMI USB 3.0 capture, UVC webcam, HDMI in + loop
- Switch 2 + dock, a Windows or Linux PC
Download the Windows or Linux build (x86_64 or aarch64) from Releases. Run it. First start
fetches FFmpeg, MediaMTX, and Deno into ~/.s2pipe/bins/.
On Windows, install the CP2102 UART driver first or the Pico port never appears: Silicon Labs VCP drivers.
- Download
s2pipe-pico-<version>.uf2from Releases (no SDK needed). - Hold BOOTSEL, plug the Pico into the PC, drop the
.uf2. - Unplug. Pico USB -> Switch dock. UART -> PC.
Local build: firmware/README.md.
One native USB: flash from the PC, then plug into the dock. Not a COM port and pads at the same time.
Node --UART 921600--> Pico GP5 RX / GP4 TX
Switch dock USB --> Pico USB
| Pico | Goes to |
|---|---|
| USB | Switch 2 dock (USB-A or USB-C OTG) |
| GP4 (TX) | UART adapter RX |
| GP5 (RX) | UART adapter TX |
| GND | GND |
Power from Switch USB. Leave adapter VCC disconnected (5 V on a GPIO kills the Pico). CP2102 jumper on 3.3 V. UART 921600 8N1.
Open the launcher.
- Pick the HDMI capture card, or Test (color bars, no hardware).
- Pick HDMI audio if you want sound.
- Pick the Pico serial port, or leave None for video only.
- This PC only or LAN (set this PC's address so other devices can reach it).
- Click Start s2pipe.
Open http://localhost:5000. On another LAN device, use the node address you set.
Optional: Launch at startup starts s2pipe in the background on login (no window). Closing the launcher does not stop
the stack; use Stop or s2pipe stop.
No window: use the CLI. Flags overlay ~/.s2pipe/config.json for that run (defaults if there is no saved file).
s2pipe start
s2pipe start --capture-source test --pico-serial COM3
s2pipe stop
s2pipe start --helpClick a gamepad at the bottom to take a Pico seat (max 8 in total, several per browser). Click again to release it. Esc = settings. The HUD stays on until fullscreen.
No gamepad: Use virtual controller (keyboard and mouse).
n/8 playing is Pico seats taken, not Switch player numbers. Pills: capture, Pico, WebSocket.
Home is Xbox Guide / PS. Turn off Windows Game Bar's controller shortcut (Settings, Gaming, Xbox Game Bar) and Steam's "Guide button focuses Steam". Last resort: hold Minus and Plus together (View + Menu).
After updating s2pipe, reflash the Pico: the node and the firmware must agree on the same packet format.
The Pico 2 W can pull the Switch 2 out of sleep (not a full power-off) while a browser is on the play page. Switch 1 Joy-Con / Pro Controller cannot wake a Switch 2. You need a Joy-Con 2 / Pro 2 / NSO GameCube already paired with that console. The console does not show its BT MAC in settings; the pad broadcasts it.
- Pair the pad with the Switch 2 once (normal Nintendo pairing).
- Put the Switch to Sleep. Detach the Joy-Con, or press a button on the Pro 2.
- In the launcher, click Listen for controller.
- Flash a
pico2_wbuild. Open the play page (Watch is enough).
If the scan finds no Switch MAC, the pad was not advertising the console address. Sleep the Switch, detach the pad, stand closer, retry.
The Pico LED stays on, and goes dark for a second each time a wake advert fires.
| Port | What |
|---|---|
| 5000 | Client |
| 5050 | Node: health, WebSocket, WHEP proxy |
| 8189 UDP | WebRTC ICE / RTP |
Deno workspace: apps/node, apps/client, shared. Firmware is CMake. Launcher is Tauri 2 (launcher/).
deno fmt
deno check apps/node/src/index.ts
deno check apps/client/src/index.tsLauncher: cd launcher && deno task dev
Node: cd apps/node && deno task dev (5050). PICO_SERIAL=/dev/ttyUSB0 or COM3.
Client: cd apps/client && deno task dev (5000). No NODE_BASE_URL -> connect page.
Firmware and packet: keep apps/node/src/utils/packet.ts in sync with firmware/packet.h.
| Route | What |
|---|---|
GET /health |
Capture, Pico, occupied seats |
GET /socket |
WebSocket |
POST /switch/whep |
Video WHEP; PATCH / DELETE /switch/whep/:session |
POST /switch-audio/whep |
Audio WHEP; PATCH / DELETE /switch-audio/whep/:session |
- Client:
{ op: "play", data: { count } }/{ op: "watch" }/{ op: "pad", data: PadState, seat } - Server:
{ op: "status", data: { capture, pico, occupied } }on join and when seats change - Server:
{ op: "play", data: { seats: number[] } }after aplay(empty if none left)
A socket starts with no seat. play claims count seats on that socket (or fewer if the lobby is full), so clicking a
gamepad sends the new total. watch or closing the socket releases every seat held by that browser.
MIT Copyright (c) 2026-present, Borane
