SDK Overlay
Construis des overlays OBS personnalises qui reagissent a StreamooS en temps reel -- comptes a rebours, bannieres "en cours de lecture", animations d'alertes, ou tout ce que tu peux exprimer en HTML/CSS/JS.
Qu'est-ce qu'un overlay personnalise ?
Un overlay personnalise est une petite page web (HTML + CSS + JS) que StreamooS sert a OBS comme source navigateur. Il se connecte au serveur WebSocket local sur ws://127.0.0.1:8765 et recoit les mises a jour d'etat en direct (tics du timer, changements de piste, evenements Twitch...). Tu peux aussi envoyer des commandes pour controler les fonctionnalites depuis l'overlay.
Les overlays personnalises sont packages sous forme de repertoires .streamoos et importes via le tableau de bord StreamooS.
Ton premier overlay
Construisons un afficheur de timer minimal en moins de 20 lignes.
Structure des fichiers
my-timer.streamoos/
manifest.json
index.htmlmanifest.json
Chaque overlay .streamoos a besoin d'un manifest :
{
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"name": "Mon Timer",
"version": "1.0",
"htmlPath": "index.html",
"createdAt": "2026-01-01T00:00:00.000Z"
}| Champ | Type | Description |
|---|---|---|
id | UUID string | Un identifiant unique. Genere-en un sur uuidgenerator.net ou avec crypto.randomUUID() dans la console du navigateur. |
name | string (1--100 car.) | Nom affiche dans le gestionnaire d'overlays du tableau de bord. |
version | "1.0" | Version du schema. Toujours "1.0" pour l'instant. |
htmlPath | string | Chemin relatif vers le fichier HTML d'entree. Doit se terminer par .html, ne doit pas commencer par / ni contenir ... |
createdAt | date ISO 8601 | Horodatage de creation. |
index.html
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<style>
body {
background: transparent;
margin: 0;
font-family: monospace;
color: white;
font-size: 48px;
}
</style>
</head>
<body>
<div id="display">--:--</div>
<script src="https://127.0.0.1:8766/shared/websocket-client.js"></script>
<script>
const client = new StreamooSClient("ws://127.0.0.1:8765");
client.onUpdate((msg) => {
if (msg.feature !== "timer") return;
const s = msg.data.remainingSeconds;
const mm = String(Math.floor(s / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
document.getElementById("display").textContent = mm + ":" + ss;
});
client.onConnected(() => {
client.sendCommand("timer", "get_state");
});
</script>
</body>
</html>C'est tout. Quand StreamooS tourne et qu'un timer est actif, cet overlay affiche un compte a rebours en direct.
Connexion au SDK
Inclure le client
Le client WebSocket partage est disponible depuis le serveur d'assets StreamooS :
<script src="https://127.0.0.1:8766/shared/websocket-client.js"></script>Si ton overlay est place directement dans le dossier app/overlays/ pendant le developpement, tu peux aussi utiliser un chemin relatif :
<script src="../shared/websocket-client.js"></script>Creer un client
const client = new StreamooSClient("ws://127.0.0.1:8765");Le constructeur accepte un second argument optionnel pour les options avancees :
| Option | Defaut | Description |
|---|---|---|
reconnectBaseDelayMs | 1000 | Delai initial avant la premiere tentative de reconnexion. |
reconnectMaxDelayMs | 15000 | Delai maximum entre les tentatives de reconnexion. |
reconnectJitterMs | 250 | Jitter aleatoire ajoute a chaque delai pour eviter le thundering herd. |
connectTimeoutMs | 5000 | Temps d'attente pour le handshake initial avant abandon. |
autoConnect | true | Mettre a false pour se connecter manuellement avec client.connect(). |
debug | false | Si true, le client log les evenements (cycle de vie + messages) avec le prefixe [StreamooS]. Off par defaut pour garder les overlays silencieux en production. |
Ecouter les mises a jour
const off = client.onUpdate((msg) => {
// msg.type === 'update'
// msg.feature === 'timer' | 'khinsider_radio' | 'smtc' | ...
// msg.data === objet d'etat specifique a la fonctionnalite
});
// Detache plus tard quand tu n'as plus besoin de ce handler
off();Chaque enregistrement on* retourne une fonction d'unsubscribe. L'appeler retire ce handler unique - pas besoin de garder une reference au callback original.
Les mises a jour sont diffusees a chaque changement d'etat d'une fonctionnalite. Filtre par msg.feature pour ne reagir qu'aux donnees qui t'interessent.
Helper en un appel : bindOverlay()
Pour le cas frequent "je me connecte, je filtre une fonctionnalite, je reagis a son etat", bindOverlay() supprime le boilerplate :
const client = bindOverlay({
feature: "timer",
onConnected: () => client.sendCommand("timer", "get_state"),
onUpdate: (data) => {
// `data` est deja deballe (msg.data pour la fonctionnalite filtree)
document.getElementById("display").textContent = data.remainingSeconds;
},
});bindOverlay() retourne le StreamooSClient sous-jacent, donc tu peux toujours appeler sendCommand(), disconnect(), etc. Options disponibles :
| Option | Type | Description |
|---|---|---|
feature | string | Si renseigne, onUpdate ne se declenche que pour les messages dont feature correspond. |
url | string | URL WebSocket personnalisee. Defaut : ws://127.0.0.1:8765. |
debug | boolean | Active le logging verbose. Defaut : false. |
onUpdate | function | (data, msg) => void - recoit la data deballee plus le message brut. |
onResponse | function | Reponse serveur a une commande envoyee. |
onError | function | Notification d'erreur cote serveur. |
onConnected | function | Appele une fois le handshake WebSocket etabli. |
onDisconnected | function | Appele a la fermeture, avec { willReconnect }. |
Envoyer des commandes
client.sendCommand("timer", "start", { minutes: 5, seconds: 0 });Parametres : sendCommand(feature, action, data). L'argument data est optionnel et vaut {} par defaut.
Evenements du cycle de vie
client.onConnected(() => {
// Connexion WebSocket etablie
});
client.onDisconnected(({ willReconnect }) => {
// willReconnect est true sauf si tu as appele client.disconnect()
});
client.onResponse((msg) => {
// Reponse a une commande envoyee (msg.ok, msg.data, msg.code)
});
client.onError((msg) => {
// Erreur cote serveur
});Controle manuel
client.disconnect(); // Fermer et arreter la reconnexion
client.reconnectNow(); // Forcer une tentative de reconnexion immediateMode composite
Un overlay peut aussi tourner embarque dans une source composite (http://127.0.0.1:8766/overlays/composite/?preset=<id>), qui empile plusieurs overlays derriere une seule source OBS. Rien a faire pour un overlay normal - StreamooSClient detecte le parametre ?composite=1 que la page composite ajoute a ton src et bascule automatiquement en mode proxy (aucune connexion propre, relaie via l'unique connexion du parent). Ton code d'overlay et son propre reglage de position fonctionnent sans changement dans les deux cas.
Si ton overlay affiche un item transitoire, evenementiel (une alerte qui apparait, pas un widget permanent comme une horloge ou une carte now-playing), attends un slot de position avant de l'afficher, pour qu'il ne se superpose jamais a un autre overlay evenementiel place a la meme position dans le preset composite de quelqu'un :
await requestOverlaySlot(maPositionConfiguree, durationMs);
// affichage sur sans risque maintenant ; hors mode composite, resout immediatementDeclare-le aussi dans ton manifest.json builtin, pour qu'un preset composite sache l'arbitrer :
{ "kind": "event" }Omets kind (ou mets "permanent") pour tout ce qui n'est pas une popup transitoire - jamais mis en attente. Design complet et limites actuelles : doc/internal/adr/ADR-010-overlay-composite-mode.md.
Reference des fonctionnalites et evenements
Chaque fonctionnalite diffuse des messages update. Voici les fonctionnalites disponibles et leurs actions :
timer
| Action | Parametres | Description |
|---|---|---|
start | { minutes, seconds } | Demarrer (ou reprendre) le timer. |
stop | -- | Mettre en pause le timer. |
reset | -- | Remettre a zero. |
get_state | -- | Demander l'etat actuel (declenche un update). |
Forme des donnees update :
{
running: true, // boolean
durationSeconds: 300, // duree totale
remainingSeconds: 120, // secondes restantes
endAtMs: 1760000000000 // epoch ms quand le timer atteint zero (null si en pause)
}khinsider_radio
| Action | Parametres | Description |
|---|---|---|
play | -- | Reprendre la lecture. |
pause | -- | Mettre en pause. |
next | -- | Piste suivante. |
shuffle | { enabled } (optionnel) | Activer/desactiver le mode aleatoire. |
volume | { value } (0--100) | Regler le volume. |
get_state | -- | Demander l'etat actuel. |
activate | -- | Activer le module radio. |
deactivate | -- | Desactiver le module radio. |
Forme des donnees update :
{
active: true,
isPlaying: true,
isShuffleEnabled: false,
volume: 75,
title: "Main Theme",
game: "Celeste",
coverUrl: "https://...",
currentTimeSeconds: 42,
durationSeconds: 180,
history: [{ title, game, coverUrl }, ...]
}smtc
| Action | Parametres | Description |
|---|---|---|
play | -- | Reprendre le media systeme. |
pause | -- | Mettre en pause. |
next | -- | Piste suivante. |
previous | -- | Piste precedente. |
get_state | -- | Demander l'etat actuel. |
Forme des donnees update :
{
connected: true,
isPlaying: true,
title: "Titre du morceau",
artist: "Artiste",
albumTitle: "Album",
artworkUrl: "https://...",
progressMs: 30000,
durationMs: 240000,
radioSource: "smtc", // 'khinsider' | 'smtc'
mediaApp: "spotify", // app id detecte, "" si inconnu
mediaAppLabel: "Spotify"
}twitch
| Action | Parametres | Description |
|---|---|---|
status | -- | Obtenir le statut de connexion. |
get_profile | -- | Obtenir le profil du streamer. |
Forme des donnees update :
{
active: true,
connected: true,
displayName: "NomDuStreamer",
avatarUrl: "https://..."
}Configuration d'overlay
Chaque overlay integre supporte une action set_overlay_config qui controle la visibilite, la position et le theme. Tu peux reutiliser ce pattern dans tes propres overlays pour persister les preferences utilisateur :
client.sendCommand("timer", "set_overlay_config", {
enabled: true,
position: "bottom-right",
theme: "streamoos",
});Positions disponibles :
top-left top-center top-right
middle-left center middle-right
bottom-left bottom-center bottom-rightTester en local
- Lancer StreamooS en mode developpement :
pnpm dev - Placer ton dossier overlay dans
app/overlays/pendant le developpement (ex :app/overlays/my-timer/) - Ouvrir dans un navigateur a
http://127.0.0.1:8766/my-timer/index.html(le serveur d'assets embarque) ou directement via le cheminfile:// - Ajouter comme source navigateur OBS :
- Cliquer sur + puis Navigateur
- Decocher "Fichier local" et entrer l'URL ou le chemin absolu
file:// - Resolution : 1920x1080 (ou adapter a ta scene)
- Activer "Actualiser le navigateur quand la scene devient active"
- Iterer : modifier ton HTML/CSS/JS, puis rafraichir la source navigateur dans OBS (clic droit sur la source, "Actualiser")
Astuce : utilise les DevTools du navigateur (F12) pour debuguer avant de passer a OBS. La connexion WebSocket fonctionne de maniere identique dans les deux environnements.
Packager en .streamoos
Quand ton overlay est pret pour la distribution ou l'import :
- Verifie que ton dossier contient un
manifest.jsonvalide a la racine - Nomme le dossier avec l'extension
.streamoos:my-timer.streamoos/ - Importe via le tableau de bord StreamooS : ouvre la page Gestionnaire d'overlays et utilise la fonction d'import
Le tableau de bord lit le manifest, le valide et copie l'overlay dans le repertoire userData de l'application. A partir de la, il apparait dans la liste des overlays et peut etre ouvert dans OBS.
Regles de validation du manifest
iddoit etre un UUID v4 validenamedoit contenir entre 1 et 100 caracteresversiondoit etre exactement"1.0"htmlPathdoit se terminer par.html, ne doit pas contenir..et ne doit pas commencer par/createdAtdoit etre une date ISO 8601 valide
Si un champ est invalide, l'import echouera avec une erreur de validation affichee dans le tableau de bord.
Depannage
L'overlay affiche "Connecting..." indefiniment
- Verifie que StreamooS tourne et que le serveur WebSocket est actif sur le port
8765 - Consulte la console du navigateur (F12) pour les erreurs de connexion
- Assure-toi que l'URL est
ws://127.0.0.1:8765(paslocalhost, paswss://)
L'overlay se connecte mais ne recoit aucune mise a jour
Envoie une commande
get_stateapres la connexion pour demander l'etat actuel :jsclient.onConnected(() => { client.sendCommand("timer", "get_state"); });Verifie que tu filtres correctement
msg.featuredans ton handleronUpdateAssure-toi que la fonctionnalite est bien active (ex : un timer doit etre demarre, un media doit etre en cours de lecture)
Erreurs CORS ou d'origine
Le serveur WebSocket n'accepte que les connexions provenant de ces origines :
| Origine | Contexte |
|---|---|
null | Source navigateur OBS (fichier local) |
file:// | Electron en production |
http://127.0.0.1:5173 | Serveur de dev Vite |
http://127.0.0.1:8766 | Serveur d'assets embarque |
Si tu testes depuis une autre origine (ex : http://localhost:3000), la connexion sera rejetee avec HTTP 403.
Fond transparent non fonctionnel dans OBS
Assure-toi que ton CSS contient :
body {
background: transparent;
}Et que le champ "CSS personnalise" de la source navigateur OBS ne surcharge pas cette propriete.
Nom de fonctionnalite non reconnu
Les noms de fonctionnalites sont sensibles a la casse et doivent correspondre exactement :
timer -- khinsider_radio -- smtc -- twitch -- discord_rpc -- system
Script non charge
Si tu utilises le client partage depuis le serveur d'assets, assure-toi d'utiliser https://127.0.0.1:8766 (HTTPS, port 8766), pas HTTP. Si tu testes via file://, utilise un chemin relatif a la place.
Pour aller plus loin
- Consulte la reference de l'API WebSocket pour la matrice complete des commandes et les contrats d'etat
- Voir Overlays OBS pour les details sur les themes, positions et la configuration des overlays integres
- Explore les overlays integres dans
app/overlays/pour des exemples concrets (timer, KHInsider, SMTC, dock)
