Skip to content

Commit a6e8cd7

Browse files
committed
feat(streaming): add emulator streaming backend
Add /api/streaming endpoints for session claim/release, ROM launch, save states, volume control, and save-and-exit against per-platform emulator broker containers. Sessions are stored in Redis with atomic SET NX claims, owner-bound, and admin force-releasable. Config comes from config.yml (streaming.enabled + containers list) with a STREAMING_SAVE_TIMEOUT env override for slow-saving brokers. Includes 19 endpoint tests covering auth scopes, claim races, ownership, broker failure paths, and force-release.
1 parent 1c9c405 commit a6e8cd7

6 files changed

Lines changed: 1098 additions & 0 deletions

File tree

backend/config/config_manager.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ class NetplayICEServer(TypedDict):
104104
credential: NotRequired[str]
105105

106106

107+
class StreamingContainer(TypedDict):
108+
platform: str
109+
host: str
110+
broker_host: str
111+
label: str
112+
113+
107114
class Config:
108115
CONFIG_FILE_MOUNTED: bool
109116
CONFIG_FILE_WRITABLE: bool
@@ -135,6 +142,8 @@ class Config:
135142
SCAN_MEDIA: list[str]
136143
GAMELIST_MEDIA_THUMBNAIL: MetadataMediaType
137144
GAMELIST_MEDIA_IMAGE: MetadataMediaType
145+
STREAMING_ENABLED: bool
146+
STREAMING_CONTAINERS: list[StreamingContainer]
138147

139148
def __init__(self, **entries):
140149
self.__dict__.update(entries)
@@ -447,6 +456,10 @@ def _parse_config(self):
447456
PEGASUS_AUTO_EXPORT_ON_SCAN=pydash.get(
448457
self._raw_config, "scan.pegasus.export", False
449458
),
459+
STREAMING_ENABLED=pydash.get(self._raw_config, "streaming.enabled", False),
460+
STREAMING_CONTAINERS=pydash.get(
461+
self._raw_config, "streaming.containers", []
462+
),
450463
)
451464

452465
def _get_ejs_controls(self) -> dict[str, EjsControls]:
@@ -717,6 +730,14 @@ def _validate_config(self):
717730
)
718731
self.config.GAMELIST_MEDIA_IMAGE = MetadataMediaType.SCREENSHOT
719732

733+
if not isinstance(self.config.STREAMING_ENABLED, bool):
734+
log.critical("Invalid config.yml: streaming.enabled must be a boolean")
735+
sys.exit(3)
736+
737+
if not isinstance(self.config.STREAMING_CONTAINERS, list):
738+
log.critical("Invalid config.yml: streaming.containers must be a list")
739+
sys.exit(3)
740+
720741
def get_config(self) -> Config:
721742
try:
722743
with open(self.config_file, "r") as config_file:

0 commit comments

Comments
 (0)