Overview
API Overview
Understanding Segra internal architecture and frontend-backend communication
Architecture
Segra uses a dual-process architecture to separate concerns and maximize performance:
- Backend Process (C#/.NET): Built on OBS, handles recording, video processing, file operations, and system integration
- Frontend Process (React/TypeScript): Provides the user interface through a WebView
Communication Layer
The frontend and backend communicate through several channels:
Bidirectional real-time channel on ws://localhost:44030/ for state updates, progress notifications, and system events. Frontend-to-backend messages can also be sent over the same socket.
PhotinoX IPC bridge used by the frontend to deliver commands to the backend (window.external.sendMessage).
Local HTTP endpoint on http://localhost:2222/ exposing /api/content (video/JSON streaming with byte-range support) and /api/thumbnail (frame extraction via FFmpeg).
Per-game local listeners such as the Counter-Strike 2 GSI endpoint on http://127.0.0.1:1340/.
Separate WebSocket endpoint on ws://localhost:44031/ (Windows only, AudioStreamServer) where the frontend streams in-app playback audio as raw PCM so it plays through the Segra process and is picked up by application-audio capture.
The local endpoints only serve local pages. The WebSocket on port 44030 rejects upgrade requests whose Origin host is not localhost or 127.0.0.1 with HTTP 403. The content server on port 2222 sends Access-Control-Allow-Origin only for those same origins, echoing the request's Origin instead of *. Its input query parameter must be a fully qualified file path.
Frontend to Backend
The frontend sends commands to the backend using the window.external.sendMessage() interface, wrapped by the sendMessageToBackend utility:
export const sendMessageToBackend = (method: string, parameters?: any) => {
const message = { Method: method, Parameters: parameters };
if ((window as any).external && typeof (window as any).external.sendMessage === 'function') {
const messageString = JSON.stringify(message);
(window as any).external.sendMessage(messageString);
} else {
console.error('window.external.sendMessage is not available.');
}
};Message Format
All messages sent to the backend follow this structure:
The command/action to execute (e.g., "StartRecording", "CreateClip")
Optional parameters specific to the method being called
Example: Starting a Recording
import { sendMessageToBackend } from '../Utils/MessageUtils';
// Start recording manually
sendMessageToBackend('StartRecording');
// Login with authentication tokens
sendMessageToBackend('Login', {
accessToken: session.access_token,
refreshToken: session.refresh_token,
});Backend to Frontend
The backend sends real-time updates to the frontend via WebSocket on ws://localhost:44030/.
WebSocket Connection
The frontend opens the WebSocket connection when WebSocketContext.tsx loads, before the first render, so the backend's initial messages arrive while the UI is still mounting. Messages that arrive before the providers subscribe are held and dispatched once they do. The connection uses the browser's native WebSocket API:
const SOCKET_URL = 'ws://localhost:44030/';
const RECONNECT_INTERVAL_MS = 3000;
const HEARTBEAT_INTERVAL_MS = 30000;
const HEARTBEAT_TIMEOUT_MS = 120000;
function connect() {
const ws = new WebSocket(SOCKET_URL);
ws.onopen = () => {
sendMessageToBackend('NewConnection');
};
ws.onmessage = (event) => {
const data: WebSocketMessage = JSON.parse(event.data);
// Dispatch to listeners
window.dispatchEvent(new CustomEvent('websocket-message', { detail: data }));
};
ws.onclose = () => {
setTimeout(connect, RECONNECT_INTERVAL_MS);
};
}Message Format
All WebSocket messages from the backend follow this structure:
The event type (e.g., "Settings", "ClipProgress", "ShowModal")
The payload data specific to the method
Heartbeat Protocol
The WebSocket connection maintains a heartbeat to detect disconnections:
- Frontend → Backend: Sends
"ping"every 30 seconds - Backend → Frontend: Responds with
{"method": "pong", "content": {}} - Timeout: The frontend closes and reconnects the socket after 120 seconds without any message from the backend
While the Segra window is in the background, WebView2 throttles timers to about once every 60 seconds. The 30-second interval keeps each throttled tick sending a ping, and the 120-second timeout keeps one slow tick from closing the connection.
Connection Lifecycle
The WebSocket module loads and opens the connection before the React app renders
Connection established to ws://localhost:44030/
Frontend sends NewConnection command to backend
Backend responds with:
- Persisted settings (
Settingsmessage) - Runtime application state (
Statemessage) - Available games list (
GameListmessage) - App version (
AppVersionmessage)
If user is authenticated, frontend sends Login message with tokens
Frontend starts sending ping every 30 seconds
The backend automatically closes any existing WebSocket connection when a new one is established, ensuring only one active connection at a time.
State Management
The backend splits application state across two singletons:
Settings.Instance(Backend/Core/Models/Settings.cs) — persisted user preferences (video quality, storage locations, keybindings, whitelists/blacklists, etc.). Synced to the frontend via theSettingsWebSocket message.AppState.Instance(Backend/Core/Models/AppState.cs) — runtime, non-persisted state (active recording, pre-recording, content library, detected devices/displays/codecs, available OBS versions, current folder size, GPU vendor). Synced to the frontend via theStateWebSocket message.
Whenever settings change, the backend sends a Settings message to sync the frontend:
public static async Task SendSettingsToFrontend(string cause)
{
if (!Program.hasLoadedInitialSettings || Settings.Instance._isBulkUpdating)
return;
Log.Information("Sending settings to frontend ({Cause})", cause);
await SendFrontendMessage("Settings", Settings.Instance);
}
public static async Task SendStateToFrontend(string cause)
{
if (!Program.hasLoadedInitialSettings || Settings.Instance._isBulkUpdating)
return;
Log.Information("Sending state to frontend ({Cause})", cause);
await SendFrontendMessage("State", AppState.Instance);
}Bulk operations set _isBulkUpdating = true to prevent spamming the frontend with updates. A single update is sent after the operation completes.
Error Handling
Both communication channels implement error handling:
Frontend Errors
if ((window as any).external && typeof (window as any).external.sendMessage === 'function') {
// Send message
} else {
console.error('window.external.sendMessage is not available.');
}Backend Errors
The backend catches and logs exceptions in the message handler:
try {
var jsonDoc = JsonDocument.Parse(message);
// Handle message
}
catch (JsonException ex) {
Log.Error($"Failed to parse message as JSON: {ex.Message}");
}
catch (Exception ex) {
Log.Error($"Unhandled exception in message handler: {ex.Message}");
}Critical errors are displayed to users via the ShowModal message:
await MessageService.ShowModal(
"Error",
$"Failed to create clip: {ex.Message}",
"error"
);