webstation-broker
REST API

Launch a game

POST /api/session/activate, with save data to restore, a state to resume, the desktop session, and disc swaps.

Launch a game

curl -k -X POST https://localhost:3001/streaming/api/session/activate \
  -H 'X-Broker-Secret: <shared secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "user": { "id": 1, "username": "ryan", "display_name": "Ryan" },
    "emulator": "pcsx2",
    "rom": { "name": "Soul Calibre 2", "platform": "ps2", "path": "/romm/ps2/SC2.iso" }
  }'

Returns {"status": "launching", "session_id": ..., "url": "/streaming/?token=<controller token>"}. Open https://<host>:3001/streaming/?token=... for the room with the game streaming. Returns 409 if a session is already active.

rom.path may be a file or a game folder; the broker picks the best bootable disc image (disc number first, then format ranking, chd > iso > ...). The path must resolve inside ROM_ROOT or activate answers 400.

The payload fields:

FieldRequiredPurpose
emulatoryesA registry key from Supported emulators.
romunless the emulator is desktoppath is the container path; id, name and platform are echoed in the exit upload and, for retroarch, platform picks the core.
usernoid, username, display_name of the controller. Joins and re-joins match on id, then username.
session_idnoA caller-supplied id, reduced to what is safe in a filename; the broker mints one otherwise.
savenoSee below.
callbacknobase_url and token for the exit upload; see Exit.
multiplayernoDecided once on RomM's launch screen: whether RomM advertises the session for joining and whether the room shows its comms surface while the host is alone.

Launch a game with save data

Add a save object: archive is a zip restored before launch, resume_slot loads a state once the VM is up. Both are optional and independent. Like the state routes, resume_slot resolves to the emulator's working slot rather than being honoured literally, so any value loads whatever that slot holds.

PCSX2 empties its working slot at the start of activate, before the archive is restored. A .p2s is named for the disc it was taken from and the serial only comes off the running disc, so a file left over from an earlier session cannot be told apart from the current game's; RomM already holds those states, and clearing them is what stops the last player's save being served as this one's. Both the restore and the later resume push land afterwards, so neither is touched. The other emulators name a state after the loaded content and can tell a stale one apart on sight, so they need no equivalent.

curl -k -X POST https://localhost:3001/streaming/api/session/activate \
  -H 'X-Broker-Secret: <shared secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "user": { "id": 1, "username": "ryan", "display_name": "Ryan" },
    "emulator": "pcsx2",
    "rom": { "name": "Soul Calibre 2", "platform": "ps2", "path": "/romm/ps2/SC2.iso" },
    "save": { "archive": "/config/broker-imports/4471.zip", "resume_slot": 10 }
  }'

The archive path is a container path. RomM gets one by uploading the zip through the import route first and feeding back the path it returns.

save.memory_card_synced: true tells the broker the whole memory card is being synced over the memory card routes. The card then travels as its own image, so it is left out of both the restore and the exit dump.

A resume state can also arrive after activate has returned: the state-file PUT needs a live session, so RomM pushes its pick once the game is already booting, and the deferred resume load waits for it.

Launch the desktop

emulator: "desktop" streams the full webstation desktop so emulators can be configured through the GUI. No rom or save; same room interface, same exit teardown.

curl -k -X POST https://localhost:3001/streaming/api/session/activate \
  -H 'X-Broker-Secret: <shared secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "emulator": "desktop", "user": { "id": 1, "username": "ryan", "display_name": "Ryan" } }'

Swap discs

Emulators that report supports_disc_swap (RetroArch) can change the mounted disc without restarting:

curl -k -X POST https://localhost:3001/streaming/api/session/swap-disc \
  -H 'X-Broker-Secret: <shared secret>' \
  -H 'Content-Type: application/json' \
  -d '{ "path": "/romm/psx/Final Fantasy VII (Disc 2).chd" }'

path is validated against ROM_ROOT the same way activate validates its rom path. Returns {"status": "swapped", ...}, 400 when the running emulator has no tray, 409 without a session. Swaps are serialised against each other and against the deferred resume load, and a swap is only committed once the core reports a running game again.

What activate does

Checks the secret and that no session is active (409 otherwise), looks the emulator up in the registry (422 for an unknown one), and resolves the bootable file under ROM_ROOT.

Clears any emulator left behind by an earlier broker process (the pid record), and clears the emulator's working state slot of leftovers from the previous session.

Restores save.archive into the emulator's save subtrees without rolling back files that are newer on disk. A member outside the subtrees, or a synced memory card inside an older archive, is skipped rather than failing the restore.

Launches the emulator in its own process group with output captured to its log file, records its pid, and mints the controller token. The token map is pushed to selkies so the stream accepts the controller.

If resume_slot was given, a watchdog waits for the game to report running, lets it settle, and fires the load. Answers immediately with the session id and the room URL; the status route reports boot_failed if the emulator comes up but never reaches a running game.

On this page