Package SDK · apiVersion 1

Build your own widgets and themes — without rebuilding the app

OSE picks packages up from a folder: a widget is a widget.json, a JS class and some CSS; a theme is tokens and CSS. The SDK scaffolds a working package, validates it with the same rules as the app loader, and ships window.OSE types for autocompletion.

  • No rebuild — just a folder in the user data
  • Plain JavaScript and CSS, like any web page
  • Manifest validation before install
npx ose-plugin create hello
npx ose-plugin check .

Quick start

Node 18+ is required. The SDK lives in npm as ose-plugin — no need to clone the repository.

npx ose-plugin create hello                  # widget package in ./hello
npx ose-plugin create plain --theme --name Plain
npx ose-plugin check .                       # validate before install
npx ose-plugin check config/plugins --json   # every package, CI-friendly
npx ose-plugin pack hello                    # hello.zip for Install from ZIP

The same commands live in the OSE repository without npm: node sdk/create.mjs, node sdk/validate.mjs and node sdk/pack.mjs.

  1. 1

    Create a package

    The scaffold already works: manifest, widget class, styles and a README with install steps. Replace the contents with your own.

  2. 2

    Drop it into the packages folder

    Development — config/plugins/<id>/; installed app — %APPDATA%\com.openstreamenvironment.app\plugins\ (on other systems, the app data directory). You can skip the manual folder: build an archive with ose-plugin pack and use Install from ZIP in Settings → Packages.

  3. 3

    Reload the page

    No restart is needed: manifests are re-read when a page loads. The panel reloads itself when you return focus to it; refresh the OBS source manually. Code and CSS are served from disk, so edits show up right away.

  4. 4

    Add the widget from the library

    The card appears in the widget column; a theme shows up in Settings → Appearance marked as a package.

What is inside a package

One folder — one package. The manifest picks the kind: widget.json for a widget, theme.json for a theme.

Widget

The class extends OSE.BaseWidget: the engine creates the element, applies geometry and calls render() on mount and when settings change.

widget.json · widget.js · widget.css · README.md

Theme

--md-*, --font-* and --panel-* tokens plus optional CSS scoped to your data-theme. The theme applies to the overlay, scenes and splash.

theme.json · theme.css · README.md

Widget

The settings schema draws the inspector itself — no need to hand-build fields. Values arrive in this.config.

widget.json

{
  "apiVersion": 1,
  "kind": "widget",
  "id": "hello",
  "name": "Hello",
  "entry": "widget.js",
  "styles": ["widget.css"],
  "defaultGeometry": { "x": 40, "y": 42, "w": 20, "h": 10 },
  "settings": {
    "text": {
      "type": "string",
      "label": "Text",
      "default": "Hello, stream!"
    }
  }
}

widget.js

(function () {
  const OSE = window.OSE;
  if (!OSE) return;

  class HelloWidget extends OSE.BaseWidget {
    render() {
      if (!this.element) return;
      this.element.textContent =
        String(this.config.text || "Hello");
    }
  }

  OSE.defineWidget("hello", HelloWidget);
})();

Settings

The inspector understands seven field types; the default comes from default, and for select it must be one of options — otherwise the app silently falls back to the first option.

  • string — single-line text;
  • text — multi-line text;
  • number — a number;
  • range — a slider with min, max and step;
  • color — a colour;
  • boolean — a toggle;
  • select — a list of { "value", "label" } options.

Lifecycle and events

The engine unsubscribes and clears timers when the widget is removed. Events are the same frames the overlay receives: ALERT, CHAT_MESSAGE, RECENT_EVENT, GOAL_UPDATE, STAT_UPDATE, SESSION_STATS, THEME_UPDATE.

onMount() {
  this.subscribe(OSE.EVENT_TYPES.ALERT, (alert) => {
    this.element.textContent = `${alert.kind}: ${alert.user}`;
  });
  this.every(() => this.render(), 1000);
}

Also available: this.geometry, this.element, this.canvas and this.ctx, this.startRenderLoop(fps), this.context.readCssVar("--md-primary") and OSE.asset(id, file) for package files.

Theme

A theme appears in the list as plugin:<id>. It cannot be edited from the panel — the package is the source of truth.

theme.json

{
  "apiVersion": 1,
  "kind": "theme",
  "id": "plain",
  "name": "Plain",
  "tokens": {
    "--md-primary": "#7aa2f7",
    "--md-surface": "#0b1020",
    "--md-on-surface": "#e6edf7"
  },
  "css": "theme.css"
}

theme.css

Scope rules to your own data-theme, or they will leak into other themes and the control panel.

[data-theme="plugin:plain"] .widget-instance {
  border-radius: 4px;
}

Fonts end up on stream, so use only free licences that allow commercial use (SIL OFL and similar) and keep the licence text next to the file.

Validation and types

A manifest mistake is better seen in the terminal than after an app restart with an empty card.

ose-plugin check mirrors the loader rules: id matches the folder, entry and styles exist and stay inside the package, settings use known types, a theme has tokens starting with --. Warnings (for example, default missing from options) do not block the install but usually mean a typo.

npx ose-plugin check .
npx ose-plugin check config/plugins
npx ose-plugin check . --json   # for scripts and CI

The SDK ships index.d.ts — global types for window.OSE, BaseWidget and events. Add this to the top of your JS:

/// <reference path="node_modules/ose-plugin/index.d.ts" />
// @ts-check

The editor starts suggesting this.config, hooks and event payloads. Manifests are also described by JSON schemas in sdk/schema/ — add a $schema link and the editor validates widget.json too.