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 -X POST http://localhost:3000/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 Calibur 2", "platform": "ps2", "path": "/romm/ps2/SC2.iso" }
}'You get back {"status": "launching", "session_id": ..., "url": "/streaming/?token=<controller token>"}.
Open https://<your RomM address>/streaming/?token=... in a browser (through your reverse proxy) to land in the
room, where the game is already streaming. If a session is already active,
you get back an HTTP error, 409, instead.
A RetroArch launch also answers with core, the libretro core it booted,
core_tier, that core's tier, and
library_name, the folder RetroArch sorts that core's saves and states into.
Every RetroArch client files the core's saves and states under that folder
name, so RomM files the session's there too. For an untested or blocked
core, the name is a guess: the folder the restored archive's manifest
recorded for that core, else the core-info catalog's name. RetroArch can
turn out to write somewhere else; the state GET's X-State-Library header
names the folder each state actually came from.
rom.path can point to a single file or to a whole game folder. When it's a
folder, the broker automatically picks the best disc image to boot: it
prefers the lowest disc number first, then a fixed format order, such as chd
over iso, and so on. Either way, the path has to resolve inside ROM_ROOT,
or activate answers with 400.
The payload fields:
| Field | Required | Purpose |
|---|---|---|
emulator | yes | The emulator's name, from the Supported emulators list. |
rom | unless the emulator is desktop | path is the container path (the file's location inside the container, not on your host machine). id, name and platform come back in the exit upload, and for retroarch, platform picks the core (the plugin that lets RetroArch emulate that particular system). The optional language is the rom's own language, for an emulator whose games ship several of them in one folder: ScummVM sets up one entry, called a target, per language it detects, and that target is what actually boots. Without language, a multilingual game starts in whichever language happens to sort first. A code the broker doesn't recognise is ignored rather than failing the launch. The optional title_id, save_target and save_target_layout help an emulator place a declared import. The optional core (RetroArch only) boots that libretro core instead of the platform's default, and must match ^[a-z0-9_]+$ or the launch fails with a 422. experimental_cores: true allows a core the broker lists as blocked. See Choosing a core. |
gui_language | no | The player's own interface language, not the rom's. If an emulator has a translated interface, it follows this setting (ScummVM writes it into scummvm.ini, which is also where the broker reads the keyboard shortcuts it uses to drive ScummVM's in-game menu automatically). A multilingual game folder falls back to gui_language when rom.language is missing: RomM rarely knows a game's language, so the player's own setting is the only clue left. This is sent for the desktop session too, even though it has no rom at all. An unrecognised code is ignored. |
user | no | id, username and display_name for the controller (the person launching the game, who gets full control by default). If they join or rejoin, the broker matches them by id first, then by username. |
session_id | no | An id you supply, cleaned up so it's safe to use in a filename. The broker generates one itself if you leave this out. |
save | no | See below. |
callback | no | base_url and token for the exit upload; see Exit. |
multiplayer | no | Set once on RomM's launch screen. It decides whether RomM advertises the session so others can join, and whether the room shows its chat, webcam and mic controls while the host is playing alone. |
Launch a game with save data
Add a save object to restore save data. archive is a zip file restored
before the game launches, and resume_slot loads a save state once the
emulator is up and running. Both are optional, and you can use either one on
its own. As with the state routes, resume_slot doesn't load whatever number
you send: it always loads from the emulator's own working slot, so any value
you give ends up loading whatever that slot currently holds.
Most emulators clear their working slot at the start of activate, before the archive is restored: PCSX2, PPSSPP, Dolphin, DuckStation, Flycast, RPCS3 and RetroArch all do this. A save-state file is named after the game it was taken from, which says which game it belongs to, but never which session created it, so a leftover from the last person to play that same game can't be told apart from your own. RomM already keeps a copy of those states, and clearing them here is what stops the last player's save from being served as yours. Both the archive restore and the later resume push happen after this clearing step, so neither one is affected by it.
curl -X POST http://localhost:3000/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 Calibur 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, then using the path
that route sends back.
Setting save.memory_card_synced: true tells the broker that the whole
memory card is being synced separately, through the
memory card routes. The card then travels
as its own image, so it's left out of both this restore and the later exit
dump.
A resume state can also show up after activate has already answered: the state-file PUT needs a session that's already live, so RomM sends its chosen state once the game has started booting, and the broker's deferred resume load waits for it.
An archive can also carry saves the broker did not write, declared under
.import/; see Declared imports. Each is checked before
the working slot is cleared, and a refused import leaves the slot as it was.
Launch the desktop
Setting emulator to "desktop" streams the full webstation desktop, so you
can configure emulators the normal way, through their own interface. This
mode takes no rom or save. Everything else works the same: the same room,
the same exit teardown.
curl -X POST http://localhost:3000/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
Some emulators (currently RetroArch) report supports_disc_swap, meaning
they can change the mounted disc without restarting the game:
curl -X POST http://localhost:3000/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 checked against ROM_ROOT the same way activate checks its rom
path. Returns {"status": "ok", "path": "<path to the disc>"}. You get 400 if the running
emulator has no disc tray to swap into, 404 if the disc path doesn't exist,
409 if there's no active session, and 502 if the emulator refused the swap. Disc swaps can't happen at the same time as each other, or at the
same time as a deferred resume load that's already running. A swap only
counts as finished once the core confirms it's running a game again. On
RetroArch, a swap also makes sure the emulator's menu is closed first, since
an open menu drops the tray commands. See
RetroArch for how.
What activate does
Checks the shared secret and makes sure no session is already active
(answers 409 if one is), looks up the emulator by name (answers 422 if
it's not recognised), and works out which file under ROM_ROOT should
actually boot. A rom path that doesn't exist answers 404. A folder with
nothing bootable in it, or a missing rom for an emulator that needs one,
answers 422.
Shuts down any emulator left running by an earlier broker process, using the process ID (PID) it recorded at the time, and clears the emulator's working state slot of anything left over from the previous session.
Restores save.archive, if one was sent, into the emulator's save folders,
but never rolls back a file that's newer on disk than what's in the archive.
A file in the archive that falls outside those folders refuses the whole
restore, as do an encrypted file, one with a compression method the broker
cannot read, one with an invalid timestamp, and one whose data is corrupt.
The broker reads every file it is about to restore in full before step 2
clears anything, so a refused activate leaves the slot as it was: an
archive without imports answers 422 save restore failed: <message>, and
one with imports lists the file as unrecognised_layout, unsafe_path or
unreadable_member. A corrupt file refuses the restore even when a newer
copy on disk means it would not have been written. Only a synced memory
card sitting in an older archive gets skipped.
A save.archive path that doesn't exist answers 404. If checking the
imports against the emulator crashes outright, activate answers 500 with
{"error": "import_preflight_failed"} instead of launching.
Launches the emulator in its own process group, with its output captured to its log file, and records its PID. It also mints the controller token and pushes the updated token map to selkies (the video/input streaming layer), so the stream will accept input from the controller.
If you sent resume_slot, a background watchdog waits for the game to
report it's running, gives it a moment to settle, then loads the state.
Answers right away with the session id and the room URL, without waiting for
any of that to finish. If the emulator comes up but never actually reaches a
running game, the status route reports boot_failed.