Fix OBS display and Start button not working
Three issues fixed:
1. Port mismatch: The sidecar reported the OBS port (8080) in the
ready event but the frontend needs the API port (8081). Now reports
the API port so WebSocket/REST connects to the right place.
2. Broadcast from wrong thread: Engine init fires state_changed from
a background thread, but _broadcast_control used get_event_loop()
which returns the wrong loop. Now captures the uvicorn event loop
at startup via on_event("startup").
3. Missed ready state: If the engine finishes before the WebSocket
client connects, the "ready" state_changed was never received.
Added status polling (GET /api/status) on WebSocket connect that
retries every 2s while appState is "initializing".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,11 +99,19 @@ class APIServer:
|
||||
|
||||
self.controller.on_credits_low = on_credits_low
|
||||
|
||||
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
|
||||
"""Set the event loop used for broadcasting (call from uvicorn startup)."""
|
||||
self._event_loop = loop
|
||||
|
||||
def _broadcast_control(self, data: dict):
|
||||
"""Send a message to all connected /ws/control clients."""
|
||||
if not self.control_connections:
|
||||
return
|
||||
|
||||
loop = getattr(self, '_event_loop', None)
|
||||
if loop is None:
|
||||
return
|
||||
|
||||
message = json.dumps(data)
|
||||
disconnected = []
|
||||
|
||||
@@ -111,7 +119,7 @@ class APIServer:
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
ws.send_text(message),
|
||||
asyncio.get_event_loop(),
|
||||
loop,
|
||||
)
|
||||
except Exception:
|
||||
disconnected.append(ws)
|
||||
@@ -124,6 +132,10 @@ class APIServer:
|
||||
app = self.app
|
||||
ctrl = self.controller
|
||||
|
||||
@app.on_event("startup")
|
||||
async def on_startup():
|
||||
self.set_event_loop(asyncio.get_event_loop())
|
||||
|
||||
# ── Status ─────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/status")
|
||||
|
||||
@@ -88,11 +88,16 @@ def main():
|
||||
# Create API server wrapping the controller
|
||||
api_server = APIServer(controller)
|
||||
|
||||
# Determine actual port (web server may have shifted if port was in use)
|
||||
actual_port = controller.actual_web_port or args.port
|
||||
# OBS display runs on the configured port, API server on port+1
|
||||
obs_port = controller.actual_web_port or args.port
|
||||
api_port = obs_port + 1
|
||||
|
||||
# Print ready event so Tauri can discover the port
|
||||
print(json.dumps({"event": "ready", "port": actual_port}), flush=True)
|
||||
# Print ready event so Tauri can discover the API port
|
||||
print(json.dumps({
|
||||
"event": "ready",
|
||||
"port": api_port,
|
||||
"obs_port": obs_port,
|
||||
}), flush=True)
|
||||
|
||||
# Run the API server (blocks)
|
||||
import uvicorn
|
||||
@@ -104,7 +109,7 @@ def main():
|
||||
uvicorn.run(
|
||||
api_server.app,
|
||||
host=args.host,
|
||||
port=actual_port + 1, # API on port+1, OBS display on the main port
|
||||
port=api_port,
|
||||
log_level="error",
|
||||
access_log=False,
|
||||
)
|
||||
|
||||
@@ -54,6 +54,35 @@ async function apiFetch(path: string, options?: RequestInit): Promise<Response>
|
||||
return fetch(url, { ...options, headers });
|
||||
}
|
||||
|
||||
// ── Status polling ──────────────────────────────────────────────────
|
||||
|
||||
let statusPollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
async function pollStatus() {
|
||||
try {
|
||||
const resp = await fetch(apiUrl("/api/status"));
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.state) {
|
||||
state.appState = data.state as AppState;
|
||||
}
|
||||
if (data.engine_device) {
|
||||
state.deviceInfo = data.engine_device;
|
||||
}
|
||||
if (data.version) {
|
||||
state.version = data.version;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// API not ready yet, will retry
|
||||
}
|
||||
|
||||
// Keep polling every 2s while still initializing
|
||||
if (state.appState === "initializing" && state.connectionState === "connected") {
|
||||
statusPollTimer = setTimeout(pollStatus, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebSocket management ─────────────────────────────────────────────
|
||||
|
||||
function connectWebSocket() {
|
||||
@@ -80,6 +109,9 @@ function _openSocket() {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
// Poll status to catch engine ready state that may have been
|
||||
// missed (engine can finish before WebSocket connects)
|
||||
pollStatus();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -132,6 +164,10 @@ function _scheduleReconnect() {
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (statusPollTimer) {
|
||||
clearTimeout(statusPollTimer);
|
||||
statusPollTimer = null;
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user