From d25a482c6b04d894124bf80b10d7dc754dd946c1 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 11 Jan 2026 16:16:24 -0500 Subject: [PATCH 1/6] update docs and envs Added new documentation and ENVs --- docs/Getting-Started/Environment-Variables.md | 2 + docs/romm-sfu.md | 79 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 docs/romm-sfu.md diff --git a/docs/Getting-Started/Environment-Variables.md b/docs/Getting-Started/Environment-Variables.md index 6d8bbd0a..6e1de68c 100644 --- a/docs/Getting-Started/Environment-Variables.md +++ b/docs/Getting-Started/Environment-Variables.md @@ -38,6 +38,8 @@ This is a complete list of available environment variables; required variables a | WEB_SERVER_MAX_REQUESTS_JITTER | Random jitter to add to the maximum number of requests a worker will process before restarting | | `100` | | WEB_SERVER_TIMEOUT | Timeout for web server requests (in seconds) | | `300` | | WEB_SERVER_WORKER_CONNECTIONS | Maximum number of simultaneous clients a single process can handle | | `1000` | +| EMULATORJS_SFU_HOST | Host name of mediasoup SFU instance | | `localhost` | +| EMULATORJS_SFU_PORT | Port number of mediasoup SFU instance | | `3001` | ## Dependencies diff --git a/docs/romm-sfu.md b/docs/romm-sfu.md new file mode 100644 index 00000000..bd24bc01 --- /dev/null +++ b/docs/romm-sfu.md @@ -0,0 +1,79 @@ +IP Leakage: +WebRTC inherently uses STUN/ICE to discover real IP addresses. Using host networking makes your host's public and private IPs directly visible to clients, which is a privacy concern for some users. + +Mediasoup-Specific Considerations +Mediasoup is designed to be a "minimalist" media handler and does not include built-in signaling security or room management. + +Signaling is Your Responsibility: +Mediasoup does not secure your signaling (WebSocket/HTTP). You must ensure your signaling server uses WSS/HTTPS. If this is unencrypted, an attacker can hijack sessions or perform man-in-the-middle (MITM) attacks. + +Media is Secure: +The actual video, audio, and data channels in mediasoup are protected by DTLS and SRTP, providing strong encryption and replay protection for the media itself. + +Input Validation: +Ensure your backend strictly validates all data coming through the data channels. If an attacker can send malicious game state or control signals, they could potentially crash the emulator or exploit vulnerabilities in the emulator's core. + +Optimize Worker Cores: +Run one mediasoup worker per CPU core to prevent worker-level bottlenecks from affecting game input data channels. + +index.js IS HARD CODED TO 3001 BY DEFAULT +This is preferred because romm uses + +notes; environment variables for documentation +index.sj +141-167 - pass env variables for: +listening IP (default 0.0.0.0) +Announced IP () COMPATIBLE WITH URLS -- Support Split Horizon networking configuration +iceServers: + urls: "stun:turn.technicallycomputers.ca:3478", + username: "emulatorjs", + credential: "rCGKgDisoVJcdFRhltm3", + + urls: "turn:turn.technicallycomputers.ca:3478", + username: "emulatorjs", + credential: "rCGKgDisoVJcdFRhltm3", + + // IMPORTANT: We do not currently have explicit client->server signaling + // to close old producers when the client calls producer.close(). + // If the host re-produces (e.g. after pause/resume), the SFU can end up + // with multiple server-side producers of the same kind for the same + // socket, where the older one no longer receives packets. + // Rejoining clients can then consume the stale producer and see + // videoWidth/videoHeight remain 0. + // + // To keep behavior deterministic: enforce at most one producer per kind + // per socket by closing/removing any existing same-kind producers here. + NEED TO REVIEW -- Issue observed in very early version of index.js + Lines 242-273 + + // Netplay system messages: host pause/resume notifications. + // These are simple broadcasts so spectators get an explicit UI cue. + Lines 673-740 + Can we expand on this to address the issues in the block referenced above? + + +documentation +Security + +### UserId Spoofing Protection + +Persistent userid values preserve consistent player assignment in game rooms, and security features server side protect against spoofing userid. + +Userid is a unique identifier that is generated when the player initiates a connection with the SFU server, and is then stored locally client side. EmulatorJS maps this id to the playerid, which identifies which character they control with their inputs. Data packets for input include a userid to identify the player. P2P does not offer input security, and theoretically a userid can be spoofed to manipulate other player characters. The SFU server maps Userid to the data channel, and thus will drop packets with a spoofed userid. Because Sync & Rollback game mode requires deeper synchronicity and management of inputs by utilizing the SFU relay, and because of it's competitive nature, relay channels are enforced for input streams. + +Bound userid to socketid server side and made it immutable per connection. Clients cannot 'switch' userids by sending arbitrary packets. Clients cannot spoof a userid when negotiating a connection in order to hijack use of their socket, connection is only allowed if userid isn't already connected. If a user is connected to a data channel, and tries to send an invalid userid, the data packet is dropped, and they are sent an error "userid mismatch for this connection". + + + +### Token Based Authorization + +New python endpoint in romm/backend/endpoints/sfu.py (/api/sfu/ endpoint was added in /backend/handler/main.py) +* Mints an HS 256 JWT! +* SFU_TOKEN_ISSUER = "romm:sfu" +* SFU_JTI_REDIS_KEY_PREFIX = "sfu:auth:jti" + +* SFU server enforces strict authentiction via token verification. EmulatorJS calls the romm api for an expiring single use token, romm uses the romm_auth_secret_key to sign the token. The token authenticates the user as a member of the romm site, and grants access to the SFU server for that session. The session ends if the user has been disconnected from netplay for more than 30 seconds, and accessing netplay features again requires fetching another token. + +* Romm API fetches netplayUsername, netplay_username, settings.netplayUsername or settings.netplay_username -- after authentication we can authorize also, and associate with a persistent netplay user name for the account. This will be cleaned up but acts as a placeholder for now to build persistent netplay identities for romm users later. + + From 5dca9d1ec814c4a0f9e330fcc17c96c49a4fbb8b Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Tue, 13 Jan 2026 04:28:42 -0500 Subject: [PATCH 2/6] Minor update Many more variable yet to add... Just pushing this update for now. --- docs/Getting-Started/Environment-Variables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Getting-Started/Environment-Variables.md b/docs/Getting-Started/Environment-Variables.md index 6e1de68c..69129d3f 100644 --- a/docs/Getting-Started/Environment-Variables.md +++ b/docs/Getting-Started/Environment-Variables.md @@ -38,8 +38,8 @@ This is a complete list of available environment variables; required variables a | WEB_SERVER_MAX_REQUESTS_JITTER | Random jitter to add to the maximum number of requests a worker will process before restarting | | `100` | | WEB_SERVER_TIMEOUT | Timeout for web server requests (in seconds) | | `300` | | WEB_SERVER_WORKER_CONNECTIONS | Maximum number of simultaneous clients a single process can handle | | `1000` | -| EMULATORJS_SFU_HOST | Host name of mediasoup SFU instance | | `localhost` | -| EMULATORJS_SFU_PORT | Port number of mediasoup SFU instance | | `3001` | +| SFU_HOST | Host name of mediasoup SFU instance | | `localhost` | +| SFU_PORT | Port number of mediasoup SFU instance | | `3001` | ## Dependencies From 01fff91948776acf850d836de11ec264bc3d88b1 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Mon, 19 Jan 2026 17:10:00 -0500 Subject: [PATCH 3/6] Netplay ID Documentation Documentation on how NetplayID works --- NETPLAY_ID_IMPLEMENTATION.md | 286 +++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 NETPLAY_ID_IMPLEMENTATION.md diff --git a/NETPLAY_ID_IMPLEMENTATION.md b/NETPLAY_ID_IMPLEMENTATION.md new file mode 100644 index 00000000..0074d17e --- /dev/null +++ b/NETPLAY_ID_IMPLEMENTATION.md @@ -0,0 +1,286 @@ +# Netplay ID Implementation + +## Overview + +This document describes the implementation of authentic netplay IDs in RomM, which allows users to have unique identifiers for netplay sessions that are separate from their login usernames. + +## Background + +Previously, RomM used user login usernames as identifiers in netplay sessions. This implementation introduces dedicated netplay IDs that: + +- Are separate from login credentials +- Can be customized by users +- Support future federation capabilities +- Maintain full backwards compatibility + +## Implementation Details + +### Database Schema Changes + +#### Migration: `0064_add_netplayid.py` +```sql +ALTER TABLE users ADD COLUMN netplayid VARCHAR(255) NULL UNIQUE; +CREATE INDEX ix_users_netplayid ON users(netplayid); +``` + +**Backwards Compatibility**: The column is nullable, so existing databases continue to work without modification. + +#### Model Updates +```python +# In backend/models/user.py +netplayid: Mapped[str | None] = mapped_column( + String(length=TEXT_FIELD_LENGTH), nullable=True, unique=True, index=True +) + +# Kiosk user also updated +return cls( + id=-1, + username="kiosk", + netplayid="kiosk", # Added for consistency + # ... other fields +) +``` + +### API Changes + +#### User Profile Updates +- **Endpoint**: `PUT /api/users/{id}` +- **New Field**: `netplayid` in UserForm +- **Validation**: + - 3-32 characters + - Alphanumeric + underscore/dash only + - Unique across all users + - Optional (can be null/empty) + +#### Database Handler +```python +# Added method in backend/handler/database/users_handler.py +@begin_session +def get_user_by_netplayid( + self, + netplayid: str, + session: Session = None, +) -> User | None: + query = select(User).filter(User.netplayid == netplayid) + return session.scalar(query.limit(1)) +``` + +### SFU Authentication Updates + +#### Token Generation +```python +# In backend/endpoints/sfu.py mint_sfu_token() +sfu_identifier = user.netplayid or user.username # Fallback for backwards compatibility + +token_data = { + "sub": sfu_identifier, # Uses netplayid if available + # ... other claims +} +``` + +#### Token Verification +```python +# SFU verify endpoint now returns netplay_username from database +user = db_user_handler.get_user_by_netplayid(sub) +if not user: + user = db_user_handler.get_user_by_username(sub) # Backwards compatibility + +netplay_username = user.netplayid if user else None +return SFUVerifyResponse(sub=sub, netplay_username=netplay_username) +``` + +### Frontend Changes + +#### User Profile Page +- **Location**: `frontend/src/views/Settings/UserProfile.vue` +- **Conditional Display**: Only shows when `EJS_NETPLAY_ENABLED = true` +- **Field Position**: Between password and email fields in Account Details section + +#### Validation Rules +```typescript +// In frontend/src/stores/users.ts +const netplayIdLength = (v: string) => + (v.length >= 3 && v.length <= 32) || i18n.global.t("settings.netplay-id-length"); + +const netplayIdChars = (v: string) => + /^[a-zA-Z0-9_-]*$/.test(v) || i18n.global.t("settings.netplay-id-chars"); + +netplayIdRules: [ + (v: string) => !v || netplayIdLength(v), // Optional field + (v: string) => !v || netplayIdChars(v), // Only validate if not empty +] +``` + +#### TypeScript Types +Updated generated types in: +- `frontend/src/__generated__/models/UserSchema.ts` +- `frontend/src/__generated__/models/UserForm.ts` + +## Backwards Compatibility + +### Database Level +- ✅ Existing users have `NULL` netplayid (no migration data loss) +- ✅ Old RomM versions can read the database (unknown column is ignored) +- ✅ No breaking schema changes + +### API Level +- ✅ Existing API calls work unchanged +- ✅ New `netplayid` field is optional in requests +- ✅ SFU tokens work with fallback logic + +### Frontend Level +- ✅ Feature is hidden when `EJS_NETPLAY_ENABLED = false` +- ✅ Existing profile page functionality unchanged +- ✅ TypeScript types are backwards compatible + +### SFU Level +- ✅ Authentication works with username fallback +- ✅ Existing tokens continue to function +- ✅ No changes required to SFU server logic + +## Configuration + +### Environment Variables +```bash +# Enable netplay ID feature in UI +EJS_NETPLAY_ENABLED=true +``` + +### Default Behavior +- **When disabled**: Netplay ID field is hidden, system uses usernames +- **When enabled**: Users can set custom netplay IDs, fallback to username + +## Future Federation Support + +This implementation is designed to support cross-instance netplay federation: + +### Database Schema Ready +```sql +-- Can store federated IDs like "federated-romm.com:user123" +netplayid VARCHAR(255) UNIQUE +``` + +### Authentication Architecture +```python +# Future federated identifier format +federated_id = f"{issuer}:{user_id}" +# Examples: +# "romm:sfu:localuser" (local) +# "federated.com:sfu:remoteuser" (federated) +``` + +### SFU Server Extensions Needed +The SFU server can be extended to: +1. Accept multiple trusted issuers +2. Route federated users to appropriate instances +3. Handle cross-instance communication protocols + +## Deployment Guide + +### 1. Database Migration +```bash +# Run from backend directory +cd backend +alembic upgrade head +``` + +### 2. Environment Configuration +```bash +# Add to your RomM environment +EJS_NETPLAY_ENABLED=true +``` + +### 3. Frontend Rebuild +```bash +# Rebuild frontend to pick up type changes +npm run build +# or +npm run dev +``` + +### 4. Verification +1. Check user profile page shows "Netplay ID" field +2. Test setting and updating netplay IDs +3. Verify SFU authentication still works +4. Confirm backwards compatibility with existing users + +## Security Considerations + +### Input Validation +- Server-side validation prevents injection attacks +- Client-side validation provides immediate feedback +- Length and character restrictions prevent abuse + +### Uniqueness Constraints +- Database-level UNIQUE constraint on netplayid +- Duplicate prevention at API level +- Case-sensitive uniqueness (follows SQL standard) + +### Privacy +- Netplay IDs are public identifiers for netplay sessions +- Separate from private login credentials +- Users can change IDs (with appropriate validation) + +## Testing Scenarios + +### Backwards Compatibility +- ✅ Existing user without netplayid can authenticate +- ✅ Old SFU tokens continue working +- ✅ Database queries work with NULL values + +### New Functionality +- ✅ User can set netplay ID via profile page +- ✅ Validation prevents invalid IDs +- ✅ Uniqueness prevents duplicate IDs +- ✅ SFU uses netplay ID for authentication + +### Edge Cases +- ✅ Empty string clears netplay ID +- ✅ Username fallback when netplay ID not set +- ✅ Migration doesn't affect existing data + +## Troubleshooting + +### Common Issues + +**Migration Fails** +```bash +# Check database permissions +# Ensure no duplicate netplayid values exist +# Verify alembic is properly configured +``` + +**Frontend Doesn't Show Field** +```bash +# Check EJS_NETPLAY_ENABLED=true +# Clear browser cache +# Rebuild frontend +``` + +**SFU Authentication Fails** +```bash +# Check token generation uses correct identifier +# Verify database has netplayid values +# Check SFU server logs for authentication errors +``` + +## Related Files + +### Backend +- `backend/alembic/versions/0064_add_netplayid.py` - Database migration +- `backend/models/user.py` - User model updates +- `backend/endpoints/user.py` - API validation +- `backend/endpoints/sfu.py` - Token generation/verification +- `backend/handler/database/users_handler.py` - Database queries + +### Frontend +- `frontend/src/views/Settings/UserProfile.vue` - Profile page UI +- `frontend/src/stores/users.ts` - Validation rules +- `frontend/src/__generated__/models/UserSchema.ts` - TypeScript types +- `frontend/src/__generated__/models/UserForm.ts` - Form types + +### Configuration +- Environment variable: `EJS_NETPLAY_ENABLED` +- Conditional feature display based on netplay support + +This implementation provides a solid foundation for user-controlled netplay identities while maintaining full backwards compatibility and preparing for future federation capabilities. \ No newline at end of file From 7e5d1fe9169e22f0d968c230b11a5243da14d6f3 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Sun, 25 Jan 2026 01:25:58 -0500 Subject: [PATCH 4/6] Create extensions.json --- .vscode/extensions.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..972ec9fa --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["continue.continue"] +} From 3f4e55f066b7429e9552aef6b99028c9658f1002 Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Fri, 20 Mar 2026 20:11:33 -0400 Subject: [PATCH 5/6] Updated SFU Netplay Documentation --- docs/Getting-Started/Environment-Variables.md | 5 +- .../Multiplayer-Netplay-Setup.md | 183 ++++++++++++++++++ docs/Navigation.md | 1 + .../EmulatorJS-Player.md | 62 +++--- .../snippets/multiplayer.docker-compose.yml | 88 +++++++++ 5 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 docs/Getting-Started/Multiplayer-Netplay-Setup.md create mode 100644 docs/resources/snippets/multiplayer.docker-compose.yml diff --git a/docs/Getting-Started/Environment-Variables.md b/docs/Getting-Started/Environment-Variables.md index 69129d3f..c086492a 100644 --- a/docs/Getting-Started/Environment-Variables.md +++ b/docs/Getting-Started/Environment-Variables.md @@ -38,8 +38,11 @@ This is a complete list of available environment variables; required variables a | WEB_SERVER_MAX_REQUESTS_JITTER | Random jitter to add to the maximum number of requests a worker will process before restarting | | `100` | | WEB_SERVER_TIMEOUT | Timeout for web server requests (in seconds) | | `300` | | WEB_SERVER_WORKER_CONNECTIONS | Maximum number of simultaneous clients a single process can handle | | `1000` | -| SFU_HOST | Host name of mediasoup SFU instance | | `localhost` | +| SFU_HOST | Host name of mediasoup SFU instance (for [multiplayer netplay](Multiplayer-Netplay-Setup.md)) | | `localhost` | | SFU_PORT | Port number of mediasoup SFU instance | | `3001` | +| ROMM_SFU_INTERNAL_SECRET | Shared secret for SFU → RomM internal API auth. Required when using netplay. Generate with `openssl rand -hex 32`. **Must match** the value in the SFU container. | | | +| EMULATORJS_SFU_HOST | Alternative to `SFU_HOST` (backwards-compatible alias) | | | +| EMULATORJS_SFU_PORT | Alternative to `SFU_PORT` (backwards-compatible alias) | | | ## Dependencies diff --git a/docs/Getting-Started/Multiplayer-Netplay-Setup.md b/docs/Getting-Started/Multiplayer-Netplay-Setup.md new file mode 100644 index 00000000..b21d0490 --- /dev/null +++ b/docs/Getting-Started/Multiplayer-Netplay-Setup.md @@ -0,0 +1,183 @@ +# Multiplayer Netplay Setup + +RomM supports **multiplayer netplay** using EmulatorJS: play retro games with friends remotely in real time. This guide covers setting up **SFU-based netplay**, which uses a dedicated mediasoup server for reliable audio, video, and input relay. It is a self hosted dedicated netplay server. This approach scales better than peer-to-peer for multiple players and spectators. + +## Overview + +- **4 Player Lobbies**: One player hosts, the other joins via the netplay menu, and livestream video/audio, while streaming their own inputs to the host. +- **Spectators**: Additional viewers can watch the stream without controlling the game. The SFU server can support ~50-100 livestream users per worker. +- **Supported systems**: Same platforms as [EmulatorJS](Platforms-and-Players/EmulatorJS-Player.md); best for 2-player, co-op, turn-based, and party games. Video latency varies with network setups and host hardware. Streaming is optimized for low video latency but may not always feel playable for certain games with more action. +- **Dedicated Server**: The SFU is currently a self hosted server for your own personal romm domain. The server is fundamentally scalable, and I expect to introduce support for federated mesh networking to connect your romm domain with your trusted friend's domain explicitly for sharing lobby data and connecting over netplay. + +## Requirements + +- RomM full image (includes EmulatorJS-SFU assets) or a custom build with SFU support +- Docker or Podman +- The [romm-sfu](https://github.com/rommapp/romm-sfu-server) server image (mediasoup + Socket.IO) +- TURN/STUN servers for players behind strict NAT (see [ICE servers](#ice-servers)). A STUN/TURN is needed for reliable server connectivity, but TURN will rarely be used if the SFU is configured correctly unless players are behind strict firewalls. + +## Quick setup with Docker Compose + +### 1. Build or obtain the SFU image + +The SFU server is built from the [romm-sfu-server](https://github.com/rommapp/romm-sfu-server) repository: + +```bash +cd romm-sfu-server +docker build -t emulatorjs-sfu:latest . +``` + +Or use a pre-built image if one is published for your architecture. + +### 2. Use a docker-compose stack with RomM + SFU + +Create a `docker-compose.yml` that includes both RomM and the SFU service. The key requirement: **both containers share the same Docker network** so RomM can reach the SFU, and the SFU can call RomM's internal API. + +You can also copy from the [RomM examples](https://github.com/rommapp/romm/blob/master/examples/docker-compose.example.yml). + +### 3. Set required environment variables + +| Variable | Where | Description | +| -------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- | +| `ROMM_AUTH_SECRET_KEY` | RomM | Generate with `openssl rand -hex 32` | +| `ROMM_SFU_INTERNAL_SECRET` | RomM **and** SFU | Shared secret for SFU ↔ RomM API. Generate with `openssl rand -hex 32` — **must match in both services** | +| `SFU_HOST` | RomM | Hostname of the SFU. Use the Docker service name (e.g. `romm-sfu`) when both run in the same stack | +| `SFU_PORT` | RomM | SFU HTTP/WS port (default `3001`) | +| `ROMM_API_BASE_URL` | SFU | URL RomM is reachable at from the SFU (e.g. `http://romm:8080` inside Docker) | + +### 4. Enable netplay in config.yml + +In your RomM config (e.g. `/romm/config/config.yml`): + +```yaml +emulatorjs: + netplay: + enabled: true +``` + +### 5. Expose required ports + +- **RomM**: `8080` (or your chosen port) +- **SFU**: `3001` (TCP), `20000` (WebRTC UDP/TCP) — adjust if using `USE_WEBRTC_SERVER=1` with multiple workers (below). +- **SFU**: More WebRTC ports are required if you use multiple SFU workers (below) + +For internet play, ensure these ports are reachable (firewall, reverse proxy) and consider [host networking](#host-networking-linux) for the SFU on Linux. + +## ICE servers + +For players behind strict NAT or symmetric firewalls, configure TURN/STUN servers in `config.yml`: + +```yaml +emulatorjs: + netplay: + enabled: true + ice_servers: + - urls: "stun:stun.l.google.com:19302" + - urls: "turn:openrelay.metered.ca:80" + username: "openrelayproject" + credential: "openrelayproject" +``` + +NOTE: You must enter your STUN/TURN server settings in the config.yml under emulatorjs here, but also you will see you need to include them under the romm-sfu container environment variables in docker-compose.yml. The reason is to allow support in the future for you to set a "home ICE server config" while allowing you to use ICE servers preferred by the SFU server, if for example, you are roaming, and connect to an SFU node in the mesh hosted by your friend, then you can use their ICE servers once you've negotiated a connection with the SFU. This allows optimal routing as networks get more advanced. + +A free-tier [Metered](https://www.metered.ca/stun-turn) account gives you dedicated TURN credentials for better reliability. + +## Playing a game + +1. Start a game in EmulatorJS. +2. Click the 🌐 icon in the bottom bar. +3. Enter your name (or Netplay ID if configured). +4. **Host**: Create a room (password optional). +5. **Join**: Select the room from the list and join. +6. All players must have access to your RomM instance to connect. + +## Advanced options + +### Host networking (Linux) + +For best WebRTC performance when clients connect over the public internet, run the SFU with `network_mode: host`: + +```yaml +romm-sfu: + image: emulatorjs-sfu:latest + network_mode: host + environment: + - PORT=3001 + - WEBRTC_PORT=20000 + - ANNOUNCED_IP=your-public-ip-or-hostname + - ROMM_API_BASE_URL=http://127.0.0.1:8080 # RomM on host + - ROMM_SFU_INTERNAL_SECRET=${ROMM_SFU_INTERNAL_SECRET} +``` + +Then set `SFU_HOST=host.docker.internal` (or `host.containers.internal` on Podman) in RomM so it can reach the SFU on the host. + +NOTE: The SFU relay server is a WebRTC application, and despite any optimizations I can make with nodejs and mediasoup, WebRTC semantics still require ICE negotiations which have a very large, randomly employed ephemeral range. I cannot support performance or stability optimization on setups with hundreds or thousands of webrtc ports forwarded in docker emulation to support ICE which consumes a lot of resources and overhead, even if it can be forced to work. It is for this reason that I cannot offer a solution for standard bridged container networking on the host machine. + +### SFU on a separate host + +If the SFU runs elsewhere (different machine or data center): + +- Set `SFU_HOST` and `SFU_PORT` in RomM to that host. +- Set `ROMM_API_BASE_URL` in the SFU to the public URL of RomM (e.g. `https://romm.example.com`). +- Set `ANNOUNCED_IP` to the domain name or public IP address of the machine hosting your SFU server node. +- Ensure the SFU can reach RomM’s internal API and that RomM can reach the SFU. You can verify with `docker logs romm-sfu` + +### Multiple SFU workers + +For high concurrency, run multiple mediasoup workers. Each worker should probably support about 100 users, and so on a gigabit connection with 2 cores, given the low bitrate streams employed by netplay, you can support about max 200 users at 720p 60fps across any number of active lobbies. + +```yaml +romm-sfu: + environment: + - USE_WEBRTC_SERVER=1 + - SFU_WORKER_COUNT=2 + - WEBRTC_PORT=20000 +``` + +`USE_WEBRTC_SERVER=1` is required when `SFU_WORKER_COUNT > 1`. Here is why: + +Each worker requires it's own dedicated port open on the docker container (unless you use network mode `host`). + +`WEBRTC_PORT` value determines the initial port # for the first worker, and each other worker incrementally takes the next port. + +Under standard webrtc semantics, each worker might require _hundreds_ of ports dedicated to itself, but `USE_WEBRTC_SERVER=1` enables routing them all through a single port. Even then, each worker needs it's own dedicated port. + +You must have `WEBRTC_PORT=20000` set, and it should use a very high port number. You also must open multiple ports per worker, incrementally. **For example:** If `WEBRTC_PORT=20000` and `SFU_WORKER_COUNT=4` then you must open UDP (and optionally TCP, for TLS TURNS users) on ports 20000, 20001, 20002, and 20003. One for each CPU worker. + +Only the SFU server needs to be aware of what the `WEBRTC_PORT` is set to, because sessions are initiated over port 3001 and the SFU server assigns webrtc ports on initialization of the sessions. + +If you do use multiple workers, you can also optionally enable experimental feature `SFU_FANOUT_ENABLED=1` to support load balancing on the SFU server that enables the ability to balance lobbies that are so large from spectator count, that you span the load for a single lobby across multiple workers. + +## Environment variables reference + +See [Environment Variables](Environment-Variables.md) for the full list. Key ones for netplay: + +| Variable | Service | Description | +| --------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ROMM_SFU_INTERNAL_SECRET` | RomM, SFU | Shared secret for SFU → RomM auth (required for netplay user authentication) | +| `SFU_HOST` | RomM | SFU Hostname (Suggest `host.docker.internal` or `host.containers.internal` on podman, for SFU nodes with host networking on the same machine as romm) | +| `SFU_PORT` | RomM | SFU server TCP port (default `3001`) | +| `EMULATORJS_SFU_HOST` / `EMULATORJS_SFU_PORT` | RomM | Alternative names for SFU_HOST/SFU_PORT | +| `ANNOUNCED_IP` | SFU | Node specific, must match node's public address | +| `LISTEN_IP` | SFU | Listen on any IP (0.0.0.0) or restrict to specific IP | +| `PUBLIC_URL` | SFU | Report what network SFU belongs to for other nodes | +| `PORT` | SFU | SFU port for standard requests. Must be forwarded. | +| `ENABLE_WEBRTC_TCP` | SFU | Allow advanced TCP TURN/S negotiation over port 80/443 that get rerouted to the TURN server over proxy to bypass advanced firewalls. | +| `SFU_FANOUT_ENABLED` | SFU | SFU worker support lobbies that span multiple workers | +| `SFU_WORKER_COUNT` | SFU | SFU workers request dedicated use of a CPU core, "pin" CPU cores in the kernel to optimize worker performance. | +| `USE_WEBRTC_SERVER` | SFU | Route WebRTC traffic through a single port. | +| `ROMM_API_BASE_URL` | SFU | Base URL for API calls required for user authentication and other features. | +| `SFU_STUN_SERVERS` | SFU | A comma delimited list of stun server details. | +| `SFU_TURN_SERVERS` | SFU | A list of dict formatted objects to store TURN credentials. | +| `LOG_LEVEL` | SFU | Set to `debug` for verbose logging. | + +### Split Horizon Networking + +To acheive high performance results on your local network, while still being reachable to the outside, instead of setting your `ANNOUNCED_IP` on the SFU to a local LAN IP, set it to a domain name that your external users can reach (like the domain of romm if hosted on the same network) and use a private DNS service on your own local network to rewrite DNS requests for your own local romm or SFU server's domain name to the local LAN IP of your server. This intercepts local network DNS request for the IP of romm-sfu.coolguy.net, and gives the local LAN IP for optimal routing for local networks and VPN users. Setting this to a domain or IP that isn't publicly reachable effectively prevents outside users from accessing the SFU for netplay. + +## Troubleshooting + +- **404 on loader.js or mediasoup-client-umd.js**: Use the RomM full image (not slim). The slim image does not include EmulatorJS-SFU assets. +- **Connection refused to SFU**: Check `SFU_HOST` and `SFU_PORT`, and that both services share a Docker network (or use `host.docker.internal` for host-mode SFU). +- **Token/auth errors**: Ensure `ROMM_SFU_INTERNAL_SECRET` is identical in RomM and the SFU. +- **WebRTC fails behind NAT**: Add TURN servers in `config.yml` under `emulatorjs.netplay.ice_servers`. diff --git a/docs/Navigation.md b/docs/Navigation.md index 5f4f75d3..ad362432 100644 --- a/docs/Navigation.md +++ b/docs/Navigation.md @@ -12,6 +12,7 @@ search: - [Configuration File](Getting-Started/Configuration-File.md) - [Metadata Providers](Getting-Started/Metadata-Providers.md) - [Environment Variables](Getting-Started/Environment-Variables.md) + - [Multiplayer Netplay Setup](Getting-Started/Multiplayer-Netplay-Setup.md) - [Reverse Proxy](Getting-Started/Reverse-Proxy.md) - Platforms and Players - [Supported Platforms](Platforms-and-Players/Supported-Platforms.md) diff --git a/docs/Platforms-and-Players/EmulatorJS-Player.md b/docs/Platforms-and-Players/EmulatorJS-Player.md index 058b3060..52e9f8ed 100644 --- a/docs/Platforms-and-Players/EmulatorJS-Player.md +++ b/docs/Platforms-and-Players/EmulatorJS-Player.md @@ -17,53 +17,53 @@ Our integration with EmulatorJS automates the process of loading and save files ### Netplay -Netplay lets you play with friends remotely, in realtime with the build-in web player. As it emulates playing on the same console with two controllers while streaming the video to players 2+, it's best for 2-player, co-op, turn based and party games. +Netplay lets you play with friends remotely, in realtime with the build-in web player. As it emulates playing on the same console with two controllers while streaming the video to players 2+, it's best for 2-player, co-op, turn based and party games. -Start by enabling netplay in your `config.yml`: +**SFU-based netplay** (recommended): Uses a dedicated mediasoup server for reliable relay. See [Multiplayer Netplay Setup](../Getting-Started/Multiplayer-Netplay-Setup.md) for Docker Compose setup and configuration. Requires the RomM full image and the [romm-sfu](https://github.com/rommapp/romm-sfu-server) server. ```yaml emulatorjs: - netplay: - enabled: true + netplay: + enabled: true ``` If you require ICE servers for NAT traversal, we recommend a free-tier [Metered](https://www.metered.ca/stun-turn) account. Create new "TURN Credentials" and replace `` and `` with the entries under "Show ICE Server Array": ```yaml emulatorjs: - netplay: - ice_servers: - - urls: "stun:stun.relay.metered.ca:80" - - urls: "stun:stun.relay.metered.ca:80" - - urls: "turn:global.relay.metered.ca:80" - username: "" - credential: "" - - urls: "turn:global.relay.metered.ca:80?transport=tcp" - username: "" - credential: "" - - urls: "turn:global.relay.metered.ca:443" - username: "" - credential: "" - - urls: "turns:global.relay.metered.ca:443?transport=tcp" - username: "" - credential: "" + netplay: + ice_servers: + - urls: "stun:stun.relay.metered.ca:80" + - urls: "stun:stun.relay.metered.ca:80" + - urls: "turn:global.relay.metered.ca:80" + username: "" + credential: "" + - urls: "turn:global.relay.metered.ca:80?transport=tcp" + username: "" + credential: "" + - urls: "turn:global.relay.metered.ca:443" + username: "" + credential: "" + - urls: "turns:global.relay.metered.ca:443?transport=tcp" + username: "" + credential: "" ``` Alternatively, use the free STUN servers from Google and TURN servers via the OpenRelayProject: ```yaml emulatorjs: - netplay: - ice_servers: - - urls: "stun:stun.l.google.com:19302" - - urls: "stun:stun1.l.google.com:19302" - - urls: "stun:stun2.l.google.com:19302" - - urls: "turn:openrelay.metered.ca:80" - username: "openrelayproject" - credential: "openrelayproject" - - urls: "turn:openrelay.metered.ca:443" - username: "openrelayproject" - credential: "openrelayproject" + netplay: + ice_servers: + - urls: "stun:stun.l.google.com:19302" + - urls: "stun:stun1.l.google.com:19302" + - urls: "stun:stun2.l.google.com:19302" + - urls: "turn:openrelay.metered.ca:80" + username: "openrelayproject" + credential: "openrelayproject" + - urls: "turn:openrelay.metered.ca:443" + username: "openrelayproject" + credential: "openrelayproject" ``` To host a game, start it, then hit the 🌐 icon in botton bar. Set your name, create a room (password optional), and other players should be able to see and join your room. **All players need access to your RomM server to join a room and play together.** diff --git a/docs/resources/snippets/multiplayer.docker-compose.yml b/docs/resources/snippets/multiplayer.docker-compose.yml new file mode 100644 index 00000000..ca0493c9 --- /dev/null +++ b/docs/resources/snippets/multiplayer.docker-compose.yml @@ -0,0 +1,88 @@ +version: "3" + +# Required .env values: +# - ROMM_AUTH_SECRET_KEY=... (generate with openssl rand -hex 32) +# - ROMM_SFU_INTERNAL_SECRET=... (generate with openssl rand -hex 32, must match in both romm and romm-sfu) + +volumes: + mysql_data: + romm_resources: + romm_redis_data: + +networks: + romm: + name: romm +services: + romm: + image: rommapp/romm:latest + container_name: romm + restart: unless-stopped + environment: + - EJS_NETPLAY_ENABLED=true + - SFU_HOST=romm-sfu + - SFU_PORT=3001 + + - DB_HOST=romm-db + - DB_NAME=romm + - DB_USER=romm-user + - DB_PASSWD= + + - ROMM_AUTH_SECRET_KEY=${ROMM_AUTH_SECRET_KEY:?set ROMM_AUTH_SECRET_KEY} + - ROMM_SFU_INTERNAL_SECRET=${ROMM_SFU_INTERNAL_SECRET:?set ROMM_SFU_INTERNAL_SECRET} + + - SCREENSCRAPER_USER= + - SCREENSCRAPER_PASSWORD= + - RETROACHIEVEMENTS_API_KEY= + - STEAMGRIDDB_API_KEY= + - HASHEOUS_API_ENABLED=true + volumes: + - romm_resources:/romm/resources + - romm_redis_data:/redis-data + - /path/to/library:/romm/library + - /path/to/assets:/romm/assets + - /path/to/config:/romm/config + ports: + - 80:8080 + depends_on: + romm-db: + condition: service_healthy + restart: true + networks: + - romm + + romm-sfu: + image: emulatorjs-sfu:latest + container_name: romm-sfu + restart: unless-stopped + network_mode: host + environment: + - ROMM_API_BASE_URL=http://romm:8080 + - ROMM_SFU_INTERNAL_SECRET=${ROMM_SFU_INTERNAL_SECRET:?set ROMM_SFU_INTERNAL_SECRET} + - PORT=3001 + - WEBRTC_PORT=20000 + - ANNOUNCED_IP=your-public-ip-or-hostname + - PUBLIC_URL=https://your-public-url.com + - ENABLE_WEBRTC_TCP=1 + - SFU_STUN_SERVERS=stun.your-stun-server.com:3478 + - SFU_TURN_SERVERS=[{"urls":["turn:turn.your-turn-server.com:3478?transport=udp"],"username":"your-username","credential":"your-password"}] + + romm-db: + image: docker.io/mariadb:latest + container_name: romm-db + restart: unless-stopped + environment: + - MARIADB_ROOT_PASSWORD= + - MARIADB_DATABASE=romm + - MARIADB_USER=romm-user + - MARIADB_PASSWORD= + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 30s + start_interval: 10s + interval: 10s + timeout: 5s + retries: 5 + networks: + - romm From e942aff6a00767676df36992e62bed771952129d Mon Sep 17 00:00:00 2001 From: Alex Vanderveen Date: Fri, 20 Mar 2026 22:54:37 -0400 Subject: [PATCH 6/6] minor update to notes --- docs/Getting-Started/Environment-Variables.md | 1 + docs/Getting-Started/Multiplayer-Netplay-Setup.md | 14 +++++++++++--- docs/Platforms-and-Players/EmulatorJS-Player.md | 4 +++- .../snippets/multiplayer.docker-compose.yml | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/Getting-Started/Environment-Variables.md b/docs/Getting-Started/Environment-Variables.md index c086492a..de7c431f 100644 --- a/docs/Getting-Started/Environment-Variables.md +++ b/docs/Getting-Started/Environment-Variables.md @@ -38,6 +38,7 @@ This is a complete list of available environment variables; required variables a | WEB_SERVER_MAX_REQUESTS_JITTER | Random jitter to add to the maximum number of requests a worker will process before restarting | | `100` | | WEB_SERVER_TIMEOUT | Timeout for web server requests (in seconds) | | `300` | | WEB_SERVER_WORKER_CONNECTIONS | Maximum number of simultaneous clients a single process can handle | | `1000` | +| SFU_NETPLAY_ENABLED | Enable SFU-based netplay. `false` = OG EmulatorJS from cdn.emulatorjs.org; `true` = EmulatorJS-SFU from jsDelivr (requires SFU server). Overrides config.yml `emulatorjs.netplay.enabled`. `EJS_NETPLAY_ENABLED` is accepted as a fallback. | | `false` | | SFU_HOST | Host name of mediasoup SFU instance (for [multiplayer netplay](Multiplayer-Netplay-Setup.md)) | | `localhost` | | SFU_PORT | Port number of mediasoup SFU instance | | `3001` | | ROMM_SFU_INTERNAL_SECRET | Shared secret for SFU → RomM internal API auth. Required when using netplay. Generate with `openssl rand -hex 32`. **Must match** the value in the SFU container. | | | diff --git a/docs/Getting-Started/Multiplayer-Netplay-Setup.md b/docs/Getting-Started/Multiplayer-Netplay-Setup.md index b21d0490..88efb654 100644 --- a/docs/Getting-Started/Multiplayer-Netplay-Setup.md +++ b/docs/Getting-Started/Multiplayer-Netplay-Setup.md @@ -45,9 +45,9 @@ You can also copy from the [RomM examples](https://github.com/rommapp/romm/blob/ | `SFU_PORT` | RomM | SFU HTTP/WS port (default `3001`) | | `ROMM_API_BASE_URL` | SFU | URL RomM is reachable at from the SFU (e.g. `http://romm:8080` inside Docker) | -### 4. Enable netplay in config.yml +### 4. Enable netplay via config or environment -In your RomM config (e.g. `/romm/config/config.yml`): +Enable netplay either in RomM config (e.g. `/romm/config/config.yml`): ```yaml emulatorjs: @@ -55,6 +55,14 @@ emulatorjs: enabled: true ``` +Or via environment variable (overrides config): + +```yaml +SFU_NETPLAY_ENABLED=true +``` + +When `SFU_NETPLAY_ENABLED=true`, RomM loads EmulatorJS-SFU from jsDelivr. When `false`, it uses the original EmulatorJS from cdn.emulatorjs.org (no netplay). + ### 5. Expose required ports - **RomM**: `8080` (or your chosen port) @@ -177,7 +185,7 @@ To acheive high performance results on your local network, while still being rea ## Troubleshooting -- **404 on loader.js or mediasoup-client-umd.js**: Use the RomM full image (not slim). The slim image does not include EmulatorJS-SFU assets. +- **404 on loader.js or mediasoup-client-umd.js**: With SFU netplay enabled, EmulatorJS-SFU and mediasoup are loaded from jsDelivr CDN by default. If mediasoup fails from CDN, RomM falls back to `/assets/emulatorjs-sfu/data/vendor/` (requires RomM full image). The slim image does not include EmulatorJS-SFU assets. - **Connection refused to SFU**: Check `SFU_HOST` and `SFU_PORT`, and that both services share a Docker network (or use `host.docker.internal` for host-mode SFU). - **Token/auth errors**: Ensure `ROMM_SFU_INTERNAL_SECRET` is identical in RomM and the SFU. - **WebRTC fails behind NAT**: Add TURN servers in `config.yml` under `emulatorjs.netplay.ice_servers`. diff --git a/docs/Platforms-and-Players/EmulatorJS-Player.md b/docs/Platforms-and-Players/EmulatorJS-Player.md index 52e9f8ed..8f74fdb5 100644 --- a/docs/Platforms-and-Players/EmulatorJS-Player.md +++ b/docs/Platforms-and-Players/EmulatorJS-Player.md @@ -19,7 +19,7 @@ Our integration with EmulatorJS automates the process of loading and save files Netplay lets you play with friends remotely, in realtime with the build-in web player. As it emulates playing on the same console with two controllers while streaming the video to players 2+, it's best for 2-player, co-op, turn based and party games. -**SFU-based netplay** (recommended): Uses a dedicated mediasoup server for reliable relay. See [Multiplayer Netplay Setup](../Getting-Started/Multiplayer-Netplay-Setup.md) for Docker Compose setup and configuration. Requires the RomM full image and the [romm-sfu](https://github.com/rommapp/romm-sfu-server) server. +**SFU-based netplay** (recommended): Uses a dedicated mediasoup server for reliable relay. See [Multiplayer Netplay Setup](../Getting-Started/Multiplayer-Netplay-Setup.md) for Docker Compose setup and configuration. Requires the RomM full image and the [romm-sfu](https://github.com/rommapp/romm-sfu-server) server. When enabled, RomM loads EmulatorJS-SFU from jsDelivr. When disabled, it uses the original EmulatorJS from cdn.emulatorjs.org. ```yaml emulatorjs: @@ -27,6 +27,8 @@ emulatorjs: enabled: true ``` +Or set `SFU_NETPLAY_ENABLED=true` (env var overrides config). + If you require ICE servers for NAT traversal, we recommend a free-tier [Metered](https://www.metered.ca/stun-turn) account. Create new "TURN Credentials" and replace `` and `` with the entries under "Show ICE Server Array": ```yaml diff --git a/docs/resources/snippets/multiplayer.docker-compose.yml b/docs/resources/snippets/multiplayer.docker-compose.yml index ca0493c9..b18f93e8 100644 --- a/docs/resources/snippets/multiplayer.docker-compose.yml +++ b/docs/resources/snippets/multiplayer.docker-compose.yml @@ -18,7 +18,7 @@ services: container_name: romm restart: unless-stopped environment: - - EJS_NETPLAY_ENABLED=true + - SFU_NETPLAY_ENABLED=true - SFU_HOST=romm-sfu - SFU_PORT=3001