mediafuse loads overlays into OBS from a manifest file. The manifest describes which plugins to run, what permissions they have, and how they're configured. Everything starts here.
A manifest is a JSON document that tells mediafuse what to display:
{
"v": 1,
"plugins": [
{
"src": "https://example.com/my-data-plugin.js",
"allowTypes": ["data"],
"config": {
"pollInterval": 5000
}
},
{
"src": "https://example.com/my-overlay.js",
"allowTypes": ["overlay"],
"name": "My Overlay"
}
],
"blocks": [],
"config": {
"brandName": "my-stream"
}
}
Top-level fields:
1)Plugin entries:
"data", "overlay", or both (optional, allows all if omitted)config (optional)The manifest is encrypted into the overlay URL so it can be loaded without authentication. You can generate an overlay URL from the dashboard's Setup page.
src URLPlugins are standalone ES modules loaded at runtime. Each plugin exports a default function that receives definePlugin:
export default (definePlugin) => definePlugin("my-plugin", setup);
definePlugin(name, setup) declares the plugin name and calls your setup function with a PluginContext.
A plugin registers as one of two types:
The setup function receives { register, manifest }:
function setup({ register, manifest }) {
const registered = register("overlay", {
onCreate(ctx) {
// ctx.container - HTMLDivElement (overlay) or null (data)
// ctx.config - merged manifest config + plugin entry config
// ctx.emit - function to broadcast events (data plugins only)
},
onMessage(msg) {
// msg.type - string or null
// msg.data - the message payload
// msg.timestamp - when the message was created
// msg.expiresAt - when it expires, or null
},
onCommand(cmd) {
// cmd.name - command name
// cmd.data - command payload
},
onTick(dt) {
// dt - milliseconds since last frame (requestAnimationFrame)
},
onResize({ width, height }) {
// overlay container resized
},
onConfig(manifest) {
// manifest updated
},
onDestroy() {
// clean up timers, DOM, connections
},
});
if (!registered) return; // type was denied by manifest
}
register returns false if the manifest's allowTypes array doesn't include the requested type.
Messages flow through the system as StoredMessage objects:
{
type: "music", // freeform string, or null
data: { // arbitrary payload
title: "Track Name",
artist: "Artist"
},
timestamp: 1709900000, // set by the server
expiresAt: 1709900015 // or null for persistent messages
}
Data plugins post messages via the HTTP API:
await fetch(`/api/messages?channel=${channel}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "music",
data: { title: "Now Playing", artist: "Someone" },
duration: 15,
}),
});
Overlay plugins receive messages through their onMessage handler. The type field is freeform -- use it to distinguish between different kinds of messages (e.g. "music", "alert", "chat").
src URLdefinePlugindefinePlugin calls your setup function with register and manifestregister is called synchronously during setup -- the onCreate handler fires immediatelyonTick handleronMessage handlersonDestroy is called for each registrationA data plugin that polls an API and posts messages:
function setup({ register: reg }) {
let intervalId = null;
let apiUrl = null;
const registered = reg("data", {
onCreate(ctx) {
apiUrl = ctx.config.apiUrl;
intervalId = setInterval(() => poll(ctx.config.channel), 5000);
},
onDestroy() {
if (intervalId) clearInterval(intervalId);
},
});
if (!registered) return;
async function poll(channel) {
const res = await fetch(apiUrl);
if (!res.ok) return;
const data = await res.json();
await fetch(`/api/messages?channel=${encodeURIComponent(channel)}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "status", data, duration: 10 }),
});
}
}
export default (definePlugin) => definePlugin("my-poller", setup);
An overlay plugin that displays incoming messages:
function setup({ register: reg }) {
let el = null;
const registered = reg("overlay", {
onCreate(ctx) {
el = document.createElement("div");
el.style.cssText = "position:fixed;bottom:1rem;right:1rem;color:#fff;";
ctx.container.appendChild(el);
},
onMessage(msg) {
if (!msg || !el) return;
el.textContent = msg.data.title || "";
},
onDestroy() {
el?.remove();
},
});
if (!registered) return;
}
export default (definePlugin) => definePlugin("toast", setup);
Install the SDK for type definitions:
npm install mediafuse
Types are exported from the package:
import type {
StoredMessage,
MessageOf,
PluginType,
PluginContext,
PluginHandlers,
CreateContext,
PluginManifest,
} from "mediafuse";
MessageOf<T> is a helper that narrows StoredMessage to require a non-null type and typed data:
type MusicData = { artist: string; title: string };
type MusicMessage = MessageOf<MusicData>;
// { type: string; data: MusicData; timestamp: number; expiresAt: number | null }