About Projects Skills Blog Contact Resume
// Jun 2026  ·  OBS  ·  Electron

Getting Started with OBS WebSocket in an Electron App

OBS WebSocket is more capable than most people realize. Here's how I integrated it into K3lPoke OBS Tools and what I learned along the way.

← All posts

When I started building K3lPoke OBS Tools, I assumed controlling OBS from an external app would be complicated. It turned out OBS has had a WebSocket server built in since version 28, and the protocol is surprisingly well-documented. What took time wasn't the connection — it was understanding the event model and figuring out the right architecture for an Electron app talking to a local server.

What OBS WebSocket Actually Is

OBS WebSocket is a plugin (now bundled with OBS Studio) that starts a local WebSocket server when OBS is running. Your application connects to it like any other WebSocket client, sends request messages in JSON, and receives responses and events back. There's an official JavaScript library — obs-websocket-js — that wraps the protocol and handles authentication.

The protocol supports two main things: requests (ask OBS to do something and get a response) and events (OBS pushes updates to you when things change). Scene switches, source visibility, recording state, stream status — it's all available.

Setting Up the Connection

In the Electron main process, the connection is straightforward:

import OBSWebSocket from 'obs-websocket-js';

const obs = new OBSWebSocket();

async function connectToOBS(host = 'localhost', port = 4455, password = '') {
  await obs.connect(`ws://${host}:${port}`, password);
  console.log('Connected to OBS');
}

obs.on('ConnectionClosed', () => {
  // retry logic here
});

One thing that tripped me up early: the connection needs to live in the main process, not the renderer. If you try to open a WebSocket from the renderer, you'll run into context isolation issues and it becomes a headache to manage. Keep OBS communication in main, expose it to the renderer through IPC.

The IPC Bridge

Electron's ipcMain and ipcRenderer are how you pass data between the main process and your UI. For OBS control, I set up a simple handler pattern:

// main process
ipcMain.handle('obs:switchScene', async (_, sceneName) => {
  await obs.call('SetCurrentProgramScene', { sceneName });
});

// renderer (via preload)
const result = await window.api.obs.switchScene('Gameplay');

The preload script is the boundary — it's where you expose only what the renderer needs, keeping the main process internals isolated.

Listening to Events

Events are where things get interesting. Rather than polling OBS for state, you subscribe to events and react. For the clip queue, I listen for MediaInputPlaybackEnded to know when to advance to the next clip:

obs.on('MediaInputPlaybackEnded', ({ inputName }) => {
  if (inputName === CLIP_SOURCE_NAME) {
    playNextInQueue();
  }
});

The event documentation is thorough, but some events behave slightly differently than the docs suggest depending on OBS version. Worth testing against your specific version before building logic around edge cases.

What I'd Do Differently

The main thing I'd change is how I handle reconnection. My initial implementation had a simple retry loop, but it didn't account for OBS closing and reopening (which changes the WebSocket session). A proper exponential backoff with session validation would have saved me debugging time later.

I'd also abstract the OBS calls earlier. Because I wrote direct obs.call() invocations spread across the codebase, refactoring when the protocol updated required touching a lot of files. A single service layer with typed wrappers is the right pattern from the start.

Resources