add config entry to enable netplay

This commit is contained in:
Georges-Antoine Assi
2025-12-08 22:58:48 -05:00
parent 86098ef1d9
commit 27c83e4736
14 changed files with 90 additions and 41 deletions

View File

@@ -84,6 +84,7 @@ class Config:
HIGH_PRIO_STRUCTURE_PATH: str
EJS_DEBUG: bool
EJS_CACHE_LIMIT: int | None
EJS_NETPLAY_ENABLED: bool
EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer]
EJS_SETTINGS: dict[str, EjsOption] # core_name -> EjsOption
EJS_CONTROLS: dict[str, EjsControls] # core_name -> EjsControls
@@ -227,6 +228,9 @@ class ConfigManager:
EJS_CACHE_LIMIT=pydash.get(
self._raw_config, "emulatorjs.cache_limit", None
),
EJS_NETPLAY_ENABLED=pydash.get(
self._raw_config, "emulatorjs.netplay.enabled", None
),
EJS_NETPLAY_ICE_SERVERS=pydash.get(
self._raw_config, "emulatorjs.netplay.ice_servers", []
),
@@ -401,6 +405,11 @@ class ConfigManager:
log.critical("Invalid config.yml: emulatorjs.debug must be a boolean")
sys.exit(3)
if not isinstance(self.config.EJS_NETPLAY_ENABLED, bool):
log.critical(
"Invalid config.yml: emulatorjs.netplay.enabled must be a boolean"
)
if self.config.EJS_CACHE_LIMIT is not None and not isinstance(
self.config.EJS_CACHE_LIMIT, int
):
@@ -524,6 +533,7 @@ class ConfigManager:
"debug": self.config.EJS_DEBUG,
"cache_limit": self.config.EJS_CACHE_LIMIT,
"netplay": {
"enabled": self.config.EJS_NETPLAY_ENABLED,
"ice_servers": self.config.EJS_NETPLAY_ICE_SERVERS,
},
"settings": self.config.EJS_SETTINGS,

View File

@@ -37,6 +37,7 @@ def get_config() -> ConfigResponse:
SKIP_HASH_CALCULATION=cfg.SKIP_HASH_CALCULATION,
EJS_DEBUG=cfg.EJS_DEBUG,
EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT,
EJS_NETPLAY_ENABLED=cfg.EJS_NETPLAY_ENABLED,
EJS_NETPLAY_ICE_SERVERS=cfg.EJS_NETPLAY_ICE_SERVERS,
EJS_CONTROLS=cfg.EJS_CONTROLS,
EJS_SETTINGS=cfg.EJS_SETTINGS,

View File

@@ -17,6 +17,7 @@ class ConfigResponse(TypedDict):
SKIP_HASH_CALCULATION: bool
EJS_DEBUG: bool
EJS_CACHE_LIMIT: int | None
EJS_NETPLAY_ENABLED: bool
EJS_NETPLAY_ICE_SERVERS: list[NetplayICEServer]
EJS_SETTINGS: dict[str, dict[str, str]]
EJS_CONTROLS: dict[str, EjsControls]

View File

@@ -56,6 +56,7 @@ emulatorjs:
debug: true
cache_limit: 1000
netplay:
enabled: true
ice_servers:
- urls: "stun:stun.relay.metered.ca:80"
- urls: "turn:global.relay.metered.ca:80"

View File

@@ -22,6 +22,7 @@ def test_config_loader():
assert loader.config.SKIP_HASH_CALCULATION
assert loader.config.EJS_DEBUG
assert loader.config.EJS_CACHE_LIMIT == 1000
assert loader.config.EJS_NETPLAY_ENABLED
assert loader.config.EJS_NETPLAY_ICE_SERVERS == [
{"urls": "stun:stun.relay.metered.ca:80"},
{
@@ -68,6 +69,7 @@ def test_empty_config_loader():
assert not loader.config.SKIP_HASH_CALCULATION
assert not loader.config.EJS_DEBUG
assert loader.config.EJS_CACHE_LIMIT is None
assert not loader.config.EJS_NETPLAY_ENABLED
assert loader.config.EJS_NETPLAY_ICE_SERVERS == []
assert loader.config.EJS_SETTINGS == {}
assert loader.config.EJS_CONTROLS == {}

View File

@@ -104,6 +104,14 @@ RUN apk add --no-cache \
wget \
ca-certificates
ARG EMULATORJS_VERSION=4.2.3
ARG EMULATORJS_SHA256=07d451bc06fa3ad04ab30d9b94eb63ac34ad0babee52d60357b002bde8f3850b
RUN wget "https://github.com/EmulatorJS/EmulatorJS/releases/download/v${EMULATORJS_VERSION}/${EMULATORJS_VERSION}.7z" && \
echo "${EMULATORJS_SHA256} ${EMULATORJS_VERSION}.7z" | sha256sum -c - && \
7z x -y "${EMULATORJS_VERSION}.7z" -o/emulatorjs && \
rm -f "${EMULATORJS_VERSION}.7z"
ARG RUFFLE_VERSION=nightly-2025-08-14
ARG RUFFLE_FILE=ruffle-nightly-2025_08_14-web-selfhosted.zip
ARG RUFFLE_SHA256=178870c5e7dd825a8df35920dfc5328d83e53f3c4d5d95f70b1ea9cd13494151
@@ -225,6 +233,7 @@ CMD ["/init"]
# FULL IMAGE
FROM slim-image AS full-image
ARG WEBSERVER_FOLDER=/var/www/html
COPY --from=emulator-stage /emulatorjs ${WEBSERVER_FOLDER}/assets/emulatorjs
COPY --from=emulator-stage /ruffle ${WEBSERVER_FOLDER}/assets/ruffle

View File

@@ -116,6 +116,7 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
# default: # These settings apply to all cores
# fps: show
# netplay:
# enabled: true
# ice_servers:
# - urls: "stun:stun.relay.metered.ca:80"
# - urls: "turn:global.relay.metered.ca:80"

View File

@@ -18,6 +18,7 @@ export type ConfigResponse = {
SKIP_HASH_CALCULATION: boolean;
EJS_DEBUG: boolean;
EJS_CACHE_LIMIT: (number | null);
EJS_NETPLAY_ENABLED: boolean;
EJS_NETPLAY_ICE_SERVERS: Array<NetplayICEServer>;
EJS_SETTINGS: Record<string, Record<string, string>>;
EJS_CONTROLS: Record<string, EjsControls>;

View File

@@ -84,8 +84,8 @@ export type DetailedRomSchema = {
missing_from_fs: boolean;
siblings: Array<SiblingRomSchema>;
rom_user: RomUserSchema;
merged_ra_metadata: (RomRAMetadata | null);
merged_screenshots: Array<string>;
merged_ra_metadata: (RomRAMetadata | null);
user_saves: Array<SaveSchema>;
user_states: Array<StateSchema>;
user_screenshots: Array<ScreenshotSchema>;

View File

@@ -79,7 +79,7 @@ export type SimpleRomSchema = {
missing_from_fs: boolean;
siblings: Array<SiblingRomSchema>;
rom_user: RomUserSchema;
merged_ra_metadata: (RomRAMetadata | null);
merged_screenshots: Array<string>;
merged_ra_metadata: (RomRAMetadata | null);
};

View File

@@ -607,9 +607,11 @@ async function boot() {
// Allow route transition animation to settle
await new Promise((r) => setTimeout(r, 50));
const EMULATORJS_VERSION = "4.2.3";
const LOCAL_PATH = "/assets/emulatorjs/data/";
const CDN_PATH = `https://cdn.emulatorjs.org/${EMULATORJS_VERSION}/data/`;
const EMULATORJS_VERSION = configStore.config.EJS_NETPLAY_ENABLED
? "nightly"
: "4.2.3";
const LOCAL_PATH = "/assets/emulatorjs/data";
const CDN_PATH = `https://cdn.emulatorjs.org/${EMULATORJS_VERSION}/data`;
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
@@ -625,15 +627,19 @@ async function boot() {
async function attemptLoad(path: string, label: "local" | "cdn") {
loaderStatus.value = label === "local" ? "loading-local" : "loading-cdn";
window.EJS_pathtodata = path;
await loadScript(`${path}loader.js`);
await loadScript(`${path}/loader.js`);
}
try {
try {
await attemptLoad(LOCAL_PATH, "local");
(await configStore.config.EJS_NETPLAY_ENABLED)
? attemptLoad(CDN_PATH, "cdn")
: attemptLoad(LOCAL_PATH, "local");
} catch (e) {
console.warn("[Play] Local loader failed, trying CDN", e);
await attemptLoad(CDN_PATH, "cdn");
(await configStore.config.EJS_NETPLAY_ENABLED)
? attemptLoad(LOCAL_PATH, "local")
: attemptLoad(CDN_PATH, "cdn");
}
// Wait for emulator bootstrap
const startDeadline = Date.now() + 8000; // 8s

View File

@@ -24,6 +24,7 @@ const defaultConfig = {
PLATFORMS_VERSIONS: {},
SKIP_HASH_CALCULATION: false,
EJS_DEBUG: false,
EJS_NETPLAY_ENABLED: false,
EJS_CACHE_LIMIT: null,
EJS_NETPLAY_ICE_SERVERS: [],
EJS_SETTINGS: {},

View File

@@ -14,6 +14,7 @@ import { ROUTES } from "@/plugins/router";
import firmwareApi from "@/services/api/firmware";
import romApi from "@/services/api/rom";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
import storePlaying from "@/stores/playing";
import { type DetailedRom } from "@/stores/roms";
import { formatTimestamp, getSupportedEJSCores } from "@/utils";
@@ -21,13 +22,12 @@ import { getEmptyCoverImage } from "@/utils/covers";
import CacheDialog from "@/views/Player/EmulatorJS/CacheDialog.vue";
import Player from "@/views/Player/EmulatorJS/Player.vue";
const EMULATORJS_VERSION = "nightly";
const { t, locale } = useI18n();
const { smAndDown } = useDisplay();
const route = useRoute();
const auth = storeAuth();
const playingStore = storePlaying();
const configStore = storeConfig();
const { playing, fullScreen } = storeToRefs(playingStore);
const rom = ref<DetailedRom | null>(null);
const firmwareOptions = ref<FirmwareSchema[]>([]);
@@ -42,7 +42,7 @@ const supportedCores = ref<string[]>([]);
const gameRunning = ref(false);
const fullScreenOnPlay = useLocalStorage("emulation.fullScreenOnPlay", true);
function onPlay() {
async function onPlay() {
if (rom.value && auth.scopes.includes("roms.user.write")) {
romApi.updateUserRomProps({
romId: rom.value.id,
@@ -56,33 +56,44 @@ function onPlay() {
fullScreen.value = fullScreenOnPlay.value;
playing.value = true;
const LOCAL_PATH = "/assets/emulatorjs/data/";
const CDN_PATH = `https://cdn.emulatorjs.org/${EMULATORJS_VERSION}/data/`;
const EMULATORJS_VERSION = configStore.config.EJS_NETPLAY_ENABLED
? "nightly"
: "4.2.3";
const LOCAL_PATH = "/assets/emulatorjs/data";
const CDN_PATH = `https://cdn.emulatorjs.org/${EMULATORJS_VERSION}/data`;
// Try loading local loader.js via fetch to validate it's real JS
fetch(`${LOCAL_PATH}loader.js`)
.then((res) => {
const type = res.headers.get("content-type") || "";
if (!res.ok || !type.includes("javascript")) {
throw new Error("Invalid local loader.js");
}
window.EJS_pathtodata = LOCAL_PATH;
return res.text();
})
.then((jsCode) => {
playing.value = true;
fullScreen.value = fullScreenOnPlay.value;
const script = document.createElement("script");
script.textContent = jsCode;
document.body.appendChild(script);
})
.catch(() => {
console.warn("Local EmulatorJS failed, falling back to CDN");
window.EJS_pathtodata = CDN_PATH;
const fallbackScript = document.createElement("script");
fallbackScript.src = `${CDN_PATH}loader.js`;
document.body.appendChild(fallbackScript);
function loadScript(src: string): Promise<void> {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error("Failed loading " + src));
document.body.appendChild(s);
});
}
async function attemptLoad(path: string) {
window.EJS_pathtodata = path;
await loadScript(`${path}/loader.js`);
}
try {
try {
await attemptLoad(
configStore.config.EJS_NETPLAY_ENABLED ? CDN_PATH : LOCAL_PATH,
);
} catch (e) {
console.warn("[Play] Local loader failed, trying CDN", e);
await attemptLoad(
configStore.config.EJS_NETPLAY_ENABLED ? LOCAL_PATH : CDN_PATH,
);
}
playing.value = true;
fullScreen.value = fullScreenOnPlay.value;
} catch (err) {
console.error("[Play] Emulator load failure:", err);
}
}
function onFullScreenChange() {

View File

@@ -144,11 +144,16 @@ window.EJS_gameName = romRef.value.fs_name_no_tags
window.EJS_language = selectedLanguage.value.value.replace("_", "-");
window.EJS_disableAutoLang = true;
const { EJS_DEBUG, EJS_CACHE_LIMIT, EJS_NETPLAY_ICE_SERVERS } =
configStore.config;
window.EJS_netplayServer = window.location.host;
window.EJS_netplayICEServers = EJS_NETPLAY_ICE_SERVERS;
const {
EJS_DEBUG,
EJS_CACHE_LIMIT,
EJS_NETPLAY_ICE_SERVERS,
EJS_NETPLAY_ENABLED,
} = configStore.config;
window.EJS_netplayServer = EJS_NETPLAY_ENABLED ? window.location.host : "";
window.EJS_netplayICEServers = EJS_NETPLAY_ENABLED
? EJS_NETPLAY_ICE_SERVERS
: [];
if (EJS_CACHE_LIMIT !== null) window.EJS_CacheLimit = EJS_CACHE_LIMIT;
window.EJS_DEBUG_XX = EJS_DEBUG;