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:

WebSocket

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.

window.external

PhotinoX IPC bridge used by the frontend to deliver commands to the backend (window.external.sendMessage).

Content HTTP server

Local HTTP endpoint on http://localhost:2222/ exposing /api/content (video/JSON streaming with byte-range support) and /api/thumbnail (frame extraction via FFmpeg).

Game integration listeners

Per-game local listeners such as the Counter-Strike 2 GSI endpoint on http://127.0.0.1:1340/.

Audio stream

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:

Frontend/src/Utils/MessageUtils.tsTypeScript
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:

Methodstringrequired

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:

Frontend/src/Context/WebSocketContext.tsxTypeScript
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:

methodstringrequired

The event type (e.g., "Settings", "ClipProgress", "ShowModal")

contentobjectrequired

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

Frontend Starts

The WebSocket module loads and opens the connection before the React app renders

WebSocket Connects

Connection established to ws://localhost:44030/

NewConnection Message

Frontend sends NewConnection command to backend

Initial Sync

Backend responds with:

  • Persisted settings (Settings message)
  • Runtime application state (State message)
  • Available games list (GameList message)
  • App version (AppVersion message)
Session Sync

If user is authenticated, frontend sends Login message with tokens

Heartbeat Begins

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 the Settings WebSocket 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 the State WebSocket message.

Whenever settings change, the backend sends a Settings message to sync the frontend:

Backend/App/MessageService.csC#
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"
);

Next Steps