🌱 Par où commencer si tu débarques
Pas de panique. Tu n'as pas besoin d'être informaticien·ne pour créer un module OpenOverlay. Un module c'est simplement un petit programme qui dit à ton overlay quoi faire quand quelque chose se passe sur ta chaîne. Et ce "petit programme", ça ressemble à des instructions écrites en anglais simplifié — pas si différent d'une recette de cuisine.
L'idée en une phrase : tu vas écrire des fichiers texte qui disent "quand un viewer s'abonne, affiche ce message et joue ce son". OpenOverlay s'occupe de tout le reste.
Ce dont tu as besoin
Un éditeur de texte
VS Code est gratuit et fait tout ce qu'il faut. Ça ressemble à un Bloc-notes très intelligent.
IndispensableNotions de base en HTML
Savoir que <div> crée une boîte et <p> un paragraphe, c'est suffisant pour commencer.
Un peu de JavaScript
Comprendre if / else, les fonctions, et ce qu'est une variable. Tout s'apprend en quelques heures sur YouTube.
Si tu n'as jamais touché à JavaScript, fais d'abord l'exemple complet en bas de page. Copie-colle le code, fais fonctionner quelque chose, puis reviens lire les explications. C'est souvent plus efficace que d'apprendre dans l'ordre.
🗺️ Les concepts clés
Avant de plonger dans le code, voici comment fonctionne OpenOverlay en quelques mots.
L'overlay et l'admin : deux mondes séparés
OpenOverlay tourne sur deux pages différentes en même temps dans ton navigateur. La page overlay est celle que tu captures dans OBS — c'est ton stream. La page admin est le tableau de bord où tu configures tout. Ces deux pages communiquent ensemble en temps réel.
Un module, c'est quoi concrètement ?
Un module est un dossier avec 4 fichiers minimum. Chaque fichier a un rôle précis :
- manifest.json — la carte d'identité du module. Son nom, sa description, ses réglages par défaut. Pas de code ici, juste des données.
- overlay.html — ce que tu veux afficher sur le stream. Comme construire une maison vide que tu vas ensuite meubler avec le code.
- overlay.js — le code qui s'exécute sur le stream. C'est lui qui "écoute" les événements Twitch et anime l'overlay.
- admin.js — le code qui construit l'interface de configuration dans le tableau de bord.
Les "hooks" : brancher sur ce qui se passe
Un hook, c'est une fonction que tu déclares une fois, et OpenOverlay l'appelle automatiquement quand quelque chose se passe. C'est comme brancher une prise — tu prépares le câble, et l'électricité coule quand il y en a.
Analogie : c'est comme les notifications sur ton téléphone. Tu n'attends pas en permanence — tu as paramétré "quand quelqu'un m'envoie un message, sonne". Les hooks c'est pareil mais pour ton overlay.
📁 Structure d'un module
Un module est un dossier dans mods/. Son nom de dossier correspond à son identifiant unique.
mon_module/
manifest.json ← carte d'identité + valeurs par défaut
overlay.js ← ce qui se passe sur le stream
overlay.html ← éléments visuels injectés dans l'overlay
admin.js ← interface de configuration dans le dashboard
overlay.css ← styles visuels de l'overlay (optionnel)
admin.css ← styles du panneau admin (optionnel)
manifest.json — la carte d'identité
JSON est un format texte pour écrire des données. Les règles sont simples : les textes sont entre guillemets, les nombres non, les listes entre crochets, les objets entre accolades. Si tu as une erreur, un validateur JSON en ligne te dira exactement où elle est.
| Champ | Type | Requis | Description |
|---|---|---|---|
id | string | requis | Identifiant unique en snake_case. Lettres minuscules, chiffres et underscores uniquement. Doit être identique au nom du dossier et à configKey. |
name | string | requis | Nom lisible affiché dans la navigation du dashboard. |
description | string | requis | Courte description affichée sous le nom dans le dashboard. |
icon | string | requis | Un emoji qui représente le module visuellement. |
types | string[] | requis | Catégorie(s) du module, parmi display, chat, events, integration. Utilisé pour le filtrage dans le Store. |
configKey | string | requis | La clé sous laquelle ta config est rangée dans la sauvegarde de l'utilisateur. Souvent identique à id. |
defaultConfig | object | requis | Les réglages par défaut. Doit contenir "enabled": true. |
version | string | optionnel | Numéro de version (ex : "1.0.0"). Affiché dans le Store. |
author | string | optionnel | Ton pseudo. Affiché dans la fiche Store. |
longDescription | string | optionnel | Description détaillée pour la fiche Store. |
tags | string[] | optionnel | Mots-clés pour la recherche dans le Store. |
files | string[] | optionnel | Liste de tous les fichiers du module. Utilisée lors de l'installation depuis le Store. |
changelog | array | optionnel | Historique des versions. Chaque entrée : { "version", "date", "notes" }. |
minSystemVersion | string | optionnel | Version minimale d'OpenOverlay requise (ex : "1.24.0"). Si l'instance de l'utilisateur est plus ancienne, le Store masque le bouton d'installation/mise à jour et affiche « Requiert OpenOverlay ≥ x.y.z » — et le serveur refuse l'installation. À renseigner dès que ton module dépend d'une API introduite dans une version précise du système. |
{
"id": "mon_module",
"name": "Mon Module",
"description": "Ce que fait mon module en une phrase",
"icon": "✨",
"types": ["display"],
"configKey": "mon_module",
"defaultConfig": {
"enabled": true,
"message": "Bonjour !",
"volume": 1.0
},
"version": "1.0.0",
"author": "TonPseudo",
"files": ["manifest.json", "overlay.js", "overlay.html", "admin.js"]
}
🎬 Module overlay (overlay.js)
C'est le cœur de ton module — le code qui tourne sur la page stream et qui réagit à ce qui se passe sur Twitch. Tu déclares ton module avec OO.Mods.register() en lui passant un objet qui contient toutes tes réponses aux événements.
Pour les débutants : imagine que tu remplis un formulaire avec les cases "que faire quand quelqu'un follow ?", "que faire quand quelqu'un s'abonne ?", etc. Chaque case est une fonction. Tu n'es pas obligé·e de remplir toutes les cases — laisse de côté celles dont tu n'as pas besoin.
OO.Mods.register({
id: 'mon_module',
// Appelé avant init() avec l'état précédemment sauvegardé (ou null)
onStateRestore(data, api) { },
// Appelé une fois au démarrage
init(api) { },
// Appelé quand le streamer sauvegarde ses réglages
onConfigReload(api) { },
// Appelé à chaque événement Twitch (follow, sub, raid…)
onTwitchEvent(type, event, api) { },
// Appelé quand un message commence par ! dans le chat
onChatCommand(cmd, args, api, sender) { },
// Appelé quand un viewer envoie son premier message
onFirstChatter(username, isFirstEver, api) { },
// Appelé quand l'admin envoie un message à l'overlay
onAdminMessage(data, api) { },
});
Hooks d'événements — le détail
onStateRestore(data, api) ≥ 1.45.0
Appelé automatiquement par le core avant init(), avec les données précédemment sauvegardées via api.saveState() — ou null si aucune donnée n'existe. Utilise ce hook pour restaurer l'état d'affichage (scène active, file, score…) sans appel réseau bloquant dans init().
onStateRestore(data, api) {
if (!data) return;
this._score = data.score ?? 0;
this._active = data.active ?? null;
// Ne pas déclencher d'effets visuels ici — init() s'en charge
},
init(api)
Appelé une seule fois, après que tout est chargé et connecté. C'est le bon endroit pour démarrer un timer, écouter des événements DOM, ou initialiser des variables d'état.
onConfigReload(api)
Déclenché quand le streamer clique sur "Sauvegarder" dans le dashboard. Utilise ce hook pour relire la configuration et adapter l'overlay sans tout recharger.
onTwitchEvent(type, event, api)
C'est l'événement le plus important. Il est appelé pour chaque notification Twitch : un follow, un sub, un raid, des bits, un échange de points de chaîne… Le paramètre type est une chaîne de caractères qui dit quel événement c'est. Le paramètre event est l'objet avec toutes les données.
onTwitchEvent(type, event, api) {
if (api.config.mon_module?.enabled === false) return;
if (type !== 'channel.follow') return;
console.log('Nouveau follow de', event.user_name);
},
onChatCommand(cmd, args, api, sender)
Appelé pour chaque message du chat qui commence par !. cmd est la commande en minuscules (ex : '!salut'), args est un tableau avec les mots qui suivent. sender décrit qui a envoyé le message.
| Propriété | Type | Description |
|---|---|---|
login | string | Identifiant en minuscules. À utiliser pour les commandes IRC (/timeout, /ban…). |
displayName | string | Pseudo avec la casse d'origine. À utiliser pour l'affichage. |
userId | string | Identifiant Twitch unique du viewer. |
isBroadcaster | boolean | true si c'est le streamer lui-même. |
isMod | boolean | true si la personne est modératrice. |
isVip | boolean | true si la personne est VIP. |
isSubscriber | boolean | true si la personne est abonnée. |
onFirstChatter(username, isFirstEver, api)
Déclenché quand un viewer envoie un message pour la première fois. isFirstEver est true si c'est son tout premier message depuis la création de la chaîne.
onAdminMessage(data, api)
Appelé quand l'admin envoie quelque chose à l'overlay via tools.sendToOverlay(). Utilise data.type pour distinguer tes différents messages.
L'objet api
| Propriété / méthode | Description |
|---|---|
api.config | La configuration complète de l'utilisateur. En lecture seule depuis l'overlay. |
api.channelName | Le login Twitch du streamer. |
api.broadcasterId | L'identifiant Twitch numérique du streamer. |
api.token | Le token OAuth Twitch. Nécessaire pour les appels authentifiés. |
api.sendChat(text) | Envoie un message dans le chat Twitch. Protection anti-doublon intégrée (4 secondes). |
api.playSound(src, volume) | Joue un fichier audio. volume entre 0 et 1. Relaie aussi automatiquement le son vers /overlay-audio (miroir audio pour usage externe type TTS, système ≥ 1.57.0) — pas d'action supplémentaire requise côté module. |
api.resolveAsset(path) | Convertit un chemin user://fichier.mp3 en URL complète. |
api.updateStreamTitle(title) | Change le titre du stream via l'API Twitch. Fonction async. |
api.checkPermission(sender, level) | Vérifie si un sender a le niveau requis, de façon sync. level : 'streamer' | 'moderator' | 'vip' | 'subscriber' | 'follower' | 'everyone'. Traite 'follower' comme 'everyone' (les tags IRC ne portent pas le statut follower réel) — utiliser checkPermissionAsync si le niveau exact importe. |
api.checkPermissionAsync(sender, level) | async → boolean (système ≥ 1.31.0). Résout le vrai statut follower via l'API Helix avant de trancher — plus lent que la version sync mais fiable pour le niveau 'follower'. |
api.isFollower(userId) | async → boolean (système ≥ 1.31.0). Interroge Helix directement, avec cache 5 minutes et déduplication des appels concurrents pour un même userId. |
api.twitchFetch(endpoint, opts?) | async. Appel authentifié vers l'API Helix de Twitch (préfixe l'URL, ajoute les headers Authorization/Client-Id). À utiliser plutôt qu'un fetch() manuel pour toute requête Helix depuis un mod. |
api.getMod(id) | Retourne un mod enregistré par son ID. Permet la communication inter-mods sans accéder à OO.Mods directement. |
api.emit(method, ...args) | Dispatch une méthode sur tous les mods enregistrés. Sert à notifier l'écosystème d'un événement custom. |
api.trigger(cmd, args?, sender?) | Déclenche programmatiquement une commande chat sur tous les mods. Le sender par défaut est le streamer (isBroadcaster: true). |
api.logEvent(entry) | Écrit un événement dans l'onglet Historique du dashboard (système ≥ 1.26.0). entry : { modId, label, icon?, user?, detail? } — modId et label obligatoires. À réserver aux événements signifiants pour le streamer (achat, montée de rang…) : l'historique est plafonné à 100 entrées. Appelle-le avec api.logEvent?.() (optional chaining) pour rester compatible avec les systèmes plus anciens. |
api.ai(task, input, opts?) | async, appelle le proxy IA sécurisé de l'instance (système ≥ 1.32.0). La clé fournisseur reste côté Netlify (OPENOVERLAY_AI_API_KEY), jamais dans la config front du module. Fournisseur/modèle/base URL sont résolus côté serveur — réglage du dashboard (Outils → Check-up, système ≥ 1.55.0) en priorité, puis variables d'environnement OPENOVERLAY_AI_*, puis défaut (openai / gpt-4o-mini). Le module n'a rien à configurer. Première tâche supportée : 'stream_summary'. Appelle-le avec api.ai?.() et prévois toujours un fallback local si l'IA n'est pas configurée. Réservé au propriétaire de l'instance depuis le système ≥ 1.59.1 (voir encadré ci-dessous) — via api.ai, appelé automatiquement avec le token du streamer, ça ne change rien pour ton module. |
api.tts(voiceId, text, opts?) | async → Blob, appelle le proxy vocal sécurisé de l'instance — synthèse ElevenLabs (système ≥ 1.53.0). La clé fournisseur reste côté Netlify (ELEVENLABS_API_KEY), jamais dans la config front du module. Le modèle est résolu côté serveur — réglage du dashboard (Outils → Check-up, système ≥ 1.55.0) en priorité, puis ELEVENLABS_MODEL_ID, puis eleven_multilingual_v2 par défaut. Le module n'a pas à choisir de modèle lui-même. Retourne un Blob audio (audio/mpeg) à convertir en URL lisible via URL.createObjectURL() puis api.playSound() (pense à révoquer l'URL après lecture). Appelle-le avec api.tts?.() et prévois un fallback silencieux si la voix n'est pas configurée. Réservé au propriétaire de l'instance depuis le système ≥ 1.59.1 (voir encadré ci-dessous) — via api.tts, appelé automatiquement avec le token du streamer, ça ne change rien pour ton module. |
api.sendWhisper(toUserId, message) | async, retourne true/false (système ≥ 1.30.0). Envoie un chuchotement Twitch depuis le compte du streamer (toUserId = sender.userId, message tronqué à 450 caractères). Nécessite le scope user:manage:whispers (reconnexion pour les sessions antérieures) et un téléphone vérifié sur le compte Twitch. Prévois toujours un fallback api.sendChat() si l'envoi échoue. Appelle-le avec api.sendWhisper?.() pour la rétro-compatibilité. |
api.sendToAdmin(modId, payload) | Pousse un état de mod vers le dashboard admin en temps réel via BroadcastChannel (système ≥ 1.37.0). Le dashboard doit implémenter onOverlayMessage(payload) sur le module admin pour recevoir ces messages. Permet d'afficher un état live dans le panel admin sans polling ni localStorage. Le panel admin reçoit payload uniquement si son module est la section active. Appelle-le avec api.sendToAdmin?.() pour la rétro-compatibilité. |
api.saveState(modId, data) | Sauvegarde l'état du module côté serveur via /api/memory (Netlify Blobs). Fire-and-forget — aucun retour. data doit être sérialisable en JSON. Le mod est identifié par modId (son id). Système ≥ 1.45.0. Appelle-le avec api.saveState?.() pour la rétro-compatibilité. |
api.loadState(modId) | async → data | null. Charge l'état précédemment sauvegardé pour ce mod. Préférer onStateRestore pour la restauration au démarrage (appelé automatiquement avant init()). Utile pour un rechargement ponctuel ou un polling cross-device (ex : dashboard sur iPhone, overlay sur iPad). Système ≥ 1.45.0. |
api.ai / api.tts — réservés au propriétaire de l'instance (système ≥ 1.59.1) : ces deux endpoits consomment une clé tierce payante (OPENOVERLAY_AI_API_KEY / ELEVENLABS_API_KEY) partagée par toute l'instance. Le serveur retient automatiquement le login du premier compte Twitch qui sauvegarde une configuration (POST /api/config) comme propriétaire, et rejette tout appel direct à /api/ai / /api/tts venant d'un autre login (403), ou tant qu'aucune configuration n'a encore été sauvegardée (503). Aucune variable d'environnement à ajouter : ça se met en place tout seul dès que le streamer ouvre son dashboard. Passer par api.ai()/api.tts() depuis un module ne change rien — l'overlay appelle toujours ces endpoints avec le token du streamer.
getCommands côté overlay : expose aussi getCommands(config) sur ton enregistrement OO.Mods.register() (même contrat que côté admin — au minimum { cmd, permission }). C'est cette version qui alimente la commande d'aide (système ≥ 1.30.0) : quand un viewer tape la commande configurée, le core lui envoie en chuchotement la liste des commandes auxquelles son niveau d'accès donne droit, puis publie un court message chat confirmant l'envoi (ou signalant l'échec) — la liste elle-même n'est jamais visible publiquement. Si c'est le streamer qui tape la commande, aucun effet (Twitch interdit de se chuchoter à soi-même) ; utiliser l'outil Commandes du dashboard à la place. Sans getCommands, les commandes du module n'apparaissent pas dans cette liste.
Réflexe à prendre : commence toujours tes hooks par if (api.config.mon_module?.enabled === false) return;. Ça respecte la bascule d'activation que l'utilisateur voit dans son dashboard.
overlay.html — construire la scène
Ce fichier est inséré dans la page stream au chargement. Il contient les éléments HTML que ton module va animer. Ils sont invisibles au départ (classe hidden) et ton JavaScript les rend visibles au bon moment.
Règle d'or : préfixe tous tes identifiants avec le nom de ton module pour éviter les conflits.
<!-- overlay.html -->
<div id="mon-module-panel" class="hidden">
<div id="mon-module-texte"></div>
</div>
⚙️ Module admin (admin.js)
Ce fichier construit l'interface de configuration qui apparaît dans le tableau de bord. C'est là que le streamer règle les messages, les sons, les couleurs de ton module.
OO.Admin.register({
id: 'mon_module',
render(config, container, tools) {
if (!config.mon_module) config.mon_module = {};
const cfg = config.mon_module;
container.innerHTML = `
<div class="card">
<div class="card-body">
<div class="field">
<label>Message</label>
<input type="text" id="mm-msg" value="${cfg.message ?? ''}">
</div>
</div>
</div>`;
container.querySelector('#mm-msg').oninput = e => {
cfg.message = e.target.value;
tools.markDirty();
};
},
getCommands(config) {
const permission = config.commandPermission ?? 'streamer';
return [
{ cmd: '!macommande', args: '[texte]', desc: 'Affiche quelque chose sur le stream', permission },
];
},
});
getCommands(config) — chaque objet retourné a quatre champs : cmd (commande avec préfixe !), args (arguments attendus), desc (description affichée) et permission ('streamer', 'moderator', 'vip', 'subscriber', 'follower' ou 'everyone' — correspond à commandPermission en config). Toujours retourner [] si config.enabled === false — c'est une convention obligatoire pour tous les mods.
render() est appelée à chaque navigation vers la section. Commence toujours par container.innerHTML = ... pour repartir d'une page blanche.
L'objet tools
| Méthode / propriété | À quoi ça sert |
|---|---|
tools.markDirty() | Signale qu'il y a des modifications non sauvegardées. Fait apparaître la barre de sauvegarde. |
tools.sendToOverlay(data) | Envoie un message à l'overlay en temps réel. Reçu dans onAdminMessage(). |
tools.showToast(message, type?) | Affiche une notification. type : 'success' ou 'error'. |
tools.testUsername | Le pseudo configuré pour les tests (défaut : 'TestUser'). |
tools.username | Le login Twitch de l'utilisateur connecté. Utile pour les appels API contextualisés. |
tools.broadcasterId | L'identifiant Twitch du streamer. |
tools.createAssetField({ type, value, onChange }) | Crée et retourne un HTMLElement — un sélecteur de fichier avec deux catégories (Intégrés / Mes fichiers) et un bouton d'upload intégré. type : 'gif', 'image', 'sound' ou 'video'. onChange(path) est appelé à chaque changement de sélection ou après un upload. |
tools.resolveAsset(path) | Convertit un chemin user:// en URL complète. |
tools.uploadAsset(file) | async. Uploade un fichier File. Retourne user://nom-du-fichier. Préférer createAssetField pour les champs de sélection dans l'admin. |
tools.addDropZone(element, accept, callback) | Active le glisser-déposer. accept : tableau de préfixes MIME. |
tools.twitchFetch(endpoint, options?) | async. Appelle l'API Twitch Helix avec l'auth déjà gérée. |
tools.permissionSelect(id, value) | Génère un <select> HTML pour choisir un niveau de permission (streamer, moderator…). Retourne la chaîne HTML à injecter. |
tools.posPicker(id, current?) | Génère une grille 3×3 HTML pour choisir la position overlay ('top-left', 'middle-center'…). Le binding des clics reste à écrire dans le mod. |
tools.cmdField(id, value, opts?) | Génère le bloc HTML complet d'un champ commande : préfixe !, input-mono, hint optionnel. La validation en temps réel (format, conflit) est automatique. opts : { placeholder?, hint? } — le hint accepte du HTML brut. |
tools.saveModState(modId, data) | Sauvegarde l'état d'un mod depuis le dashboard via /api/memory. Équivalent de api.saveState côté overlay. Fire-and-forget. Système ≥ 1.45.0. |
tools.loadModState(modId) | async → data | null. Charge l'état d'un mod depuis le dashboard. Utile pour synchroniser l'admin avec un état modifié par l'overlay (ex : polling toutes les N secondes pour un dashboard sur un autre appareil). Système ≥ 1.45.0. |
onOverlayMessage(payload) ≥ 1.37.0
Hook optionnel sur le module admin. Appelé quand l'overlay envoie un état via api.sendToAdmin(modId, payload). Uniquement déclenché si le mod est la section active du dashboard. Permet d'afficher un panel live (file d'attente, score, état…) sans polling ni localStorage.
onOverlayMessage(payload) {
// payload = ce que l'overlay a passé dans api.sendToAdmin('mon-mod', payload)
const panel = this._container?.querySelector('#mon-panel');
if (panel) this._render(panel, payload);
},
Côté overlay, déclenche api.sendToAdmin(modId, payload) à chaque changement d'état. Envoie aussi une réponse à 'queue_request_state' pour initialiser le panel quand l'admin ouvre la section.
💾 Persistance d'état
Par défaut, l'état d'un module (file active, scène en cours, score…) est perdu quand l'overlay se recharge. Pour les modules avec un état métier significatif, OpenOverlay propose deux mécanismes : la sauvegarde explicite et la restauration automatique.
La persistance s'appuie sur /api/memory — un endpoint Netlify Blobs générique partagé par tous les modules. Chaque module dispose d'un espace isolé identifié par son id. Aucune configuration serveur n'est nécessaire pour l'utiliser.
Sauvegarder depuis l'overlay
// Fire-and-forget — aucun retour à attendre
api.saveState('mon_module', { score: 42, activeSlot: 'intro' });
// Chargement explicite (async) — rare, préférer onStateRestore
const data = await api.loadState('mon_module');
Sauvegarder depuis l'admin
// Fire-and-forget
tools.saveModState('mon_module', { mode: 'gaming', queue: [] });
// Chargement (async) — ex : polling cross-device
const data = await tools.loadModState('mon_module');
Restauration automatique — hook onStateRestore
Le hook onStateRestore(data, api) est appelé par le core avant init(), avec les données précédemment sauvegardées — ou null. C'est l'endroit idéal pour restaurer l'état sans appel réseau bloquant.
OO.Mods.register({
id: 'mon_module',
_score: 0,
_active: null,
onStateRestore(data, api) {
if (!data) return;
this._score = data.score ?? 0;
this._active = data.active ?? null;
// Ne pas déclencher d'effets visuels ici — init() s'en charge
},
init(api) {
// À ce stade, this._score et this._active sont déjà restaurés
this._render(api);
},
_someAction(api) {
this._score++;
this._render(api);
api.saveState('mon_module', { score: this._score, active: this._active });
},
});
Si ton module utilise onStateRestore ou api.saveState, ajoute "minSystemVersion": "1.45.0" dans son manifest.json. Appelle ces méthodes avec optional chaining (api.saveState?.()) pour rester compatible avec les instances plus anciennes.
N'utilise la persistance que pour un état métier significatif — ce que le streamer voudrait retrouver après un rechargement accidentel. Évite de sauvegarder des données éphémères ou qui se périment rapidement.
⚙️ Système de configuration
La configuration de l'utilisateur est un grand objet JSON stocké côté serveur. Chaque module occupe une clé, définie par configKey dans le manifest.
Au démarrage, OpenOverlay fusionne intelligemment trois sources (du moins au plus prioritaire) :
- Le fichier
config.jsonglobal - Le
defaultConfigdu manifest - La configuration sauvegardée par l'utilisateur
La fusion est profonde : si l'utilisateur n'a changé que le volume, les autres champs viennent de ton defaultConfig. Exception : les tableaux sont remplacés entièrement.
Conséquence pratique : si tu ajoutes un nouveau champ dans defaultConfig, les utilisateurs existants l'obtiendront automatiquement avec la valeur par défaut.
onTwitchEvent(type, event, api) {
const cfg = api.config.mon_module;
if (cfg?.enabled === false) return;
const message = cfg?.message ?? 'Valeur par défaut';
const volume = cfg?.volume ?? 1.0;
},
📡 Référence des événements Twitch
OpenOverlay s'abonne automatiquement à ces événements dès le démarrage. Tu les reçois tous dans onTwitchEvent(type, event, api).
channel.followQuelqu'un vient de suivre la chaîne.
Données utiles : event.user_name, event.user_login, event.user_id
channel.subscribeNouvel abonnement (hors cadeaux). Vérifie event.is_gift === false pour éviter un doublon.
Données utiles : event.user_name, event.tier ("1000" / "2000" / "3000"), event.is_gift
channel.subscription.giftUn viewer offre des abonnements à la communauté. Peut être anonyme.
Données utiles : event.user_name, event.total, event.is_anonymous
channel.subscription.messageRé-abonnement avec message.
Données utiles : event.user_name, event.cumulative_months, event.message.text
channel.cheerEnvoi de Bits dans le chat. user_name peut être null si anonyme.
Données utiles : event.user_name, event.bits, event.message
channel.raidRaid entrant.
Données utiles : event.from_broadcaster_user_name, event.viewers
channel.channel_points_custom_reward_redemption.addUn viewer échange une récompense de points de chaîne personnalisée.
Données utiles : event.user_name, event.reward.id, event.reward.title, event.user_input
Événements étendus
Ces événements sont disponibles via onTwitchEvent() pour les mods tiers. Les mods intégrés ne les utilisent pas.
stream.online / stream.offlineLe stream démarre ou s'arrête.
Données utiles : event.broadcaster_user_name, event.type ("live" / "rerun"…)
channel.prediction.begin / channel.prediction.endUne prédiction démarre ou se termine.
Données utiles : event.title, event.outcomes (tableau avec title, users, channel_points), event.winning_outcome_id
channel.poll.begin / channel.poll.endUn sondage démarre ou se termine.
Données utiles : event.title, event.choices (tableau avec title, votes)
channel.hype_train.begin / channel.hype_train.endUn Hype Train démarre ou se termine.
Données utiles : event.level, event.total, event.top_contributions
channel.shoutout.createLe streamer (ou un modérateur) envoie un shoutout.
Données utiles : event.to_broadcaster_user_name, event.viewer_count
channel.banUn utilisateur est banni ou mis en timeout.
Données utiles : event.user_name, event.reason, event.is_permanent, event.ends_at
OpenOverlay déduplique automatiquement les notifications Twitch. Même si Twitch renvoie deux fois le même événement, ton hook ne sera appelé qu'une fois.
💬 Commandes chat
Chaque message qui commence par ! déclenche onChatCommand() sur tous les modules. Il n'y a pas de routage exclusif — plusieurs modules peuvent répondre à la même commande.
onChatCommand(cmd, args, api, sender) {
if (api.config.mon_module?.enabled === false) return;
if (!api.checkPermission(sender, 'streamer')) return;
if (cmd !== '!macommande') return;
const texte = args.join(' ');
api.sendChat(`Commande reçue : ${texte}`);
},
Niveaux d'accès
Utilise api.checkPermission(sender, level) — ne lis jamais les propriétés de sender directement.
Valeur de level | Qui peut déclencher |
|---|---|
'streamer' | Streamer uniquement |
'moderator' | Modérateurs et streamer |
'vip' | VIP, modérateurs et streamer |
'subscriber' | Abonnés, VIP, modérateurs et streamer |
'follower' | Tout le monde avec checkPermission() (IRC ne fournit pas ce statut). Utiliser checkPermissionAsync() ou isFollower() pour un vrai contrôle Helix. |
'everyone' | Tout le monde |
Le flag sender._viaChannelPoints
Quand une commande est déclenchée par une récompense de points de chaîne (et non par un message dans le chat), le sender reçu contient _viaChannelPoints: true. Cela te permet de traiter ces deux origines différemment.
onChatCommand(cmd, args, api, sender) {
if (sender._viaChannelPoints) {
// Déclenché par un échange de points — login du viewer dans sender.login
}
},
Déclencher une commande programmatiquement
Depuis un mod overlay, tu peux déclencher une commande sans passer par le chat :
// Via l'objet api (recommandé)
api.trigger('!alerte', ['follow', 'Alice']);
// Via événement DOM (depuis n'importe quel script de la page)
document.dispatchEvent(new CustomEvent('oo:cmd', {
detail: { cmd: '!alerte', args: ['follow', 'Alice'] }
}));
api.sendChat() intègre une déduplication : si tu envoies exactement le même texte deux fois en moins de 4 secondes, le deuxième envoi est ignoré.
🗂️ Assets utilisateur
Chaque utilisateur dispose d'un espace de stockage personnel pour ses fichiers (GIFs, sons, images, vidéos), accessible depuis la section Outils → Mes fichiers du dashboard. Ces fichiers sont référencés avec le préfixe user:// dans la config, et résolus vers leur URL complète au moment de l'utilisation.
Limites : 4 Mo par fichier et 50 fichiers par utilisateur (l'écrasement d'un fichier existant reste toujours possible). Un upload au-delà du quota est refusé avec une erreur explicite — gère ce cas dans tes mods en affichant le message d'erreur retourné par uploadAsset.
Pour les champs de sélection dans l'admin, utilise toujours createAssetField — il combine le sélecteur (assets intégrés + fichiers uploadés), le bouton d'upload et un aperçu automatique (miniature pour les images/GIFs, lecteur audio pour les sons, aperçu vidéo pour les vidéos).
// Dans le template HTML :
<div class="field">
<label>Son</label>
<div id="mon-sound-field"></div>
</div>
// Après container.innerHTML :
container.querySelector('#mon-sound-field').replaceWith(
createAssetField({
type: 'sound', // 'image', 'gif' ou 'sound'
value: cfg.sound ?? '',
onChange: (path) => { cfg.sound = path; markDirty(); },
})
);
overlay.js — lire et jouer un son
const soundPath = cfg?.sound;
if (soundPath) {
const url = api.resolveAsset(soundPath);
api.playSound(url, cfg.volume ?? 1);
}
🔗 Communication inter-modules
Les modules overlay peuvent se parler via l'objet api, sans jamais accéder à OO.Mods directement. Deux méthodes sont disponibles :
| Méthode | Usage |
|---|---|
api.emit(method, ...args) | Appelle method sur tous les mods enregistrés qui l'implémentent. Sert à diffuser un événement custom. |
api.trigger(cmd, args?, sender?) | Déclenche une commande chat (onChatCommand) programmatiquement. Le sender par défaut est le broadcaster. |
// Diffuser un événement custom à tous les mods
api.emit('onMyCustomEvent', { data: 'valeur' });
// Déclencher une commande chat depuis un autre mod ou depuis l'admin
api.trigger('macommande', ['arg1']);
api.trigger('macommande', [], { isBroadcaster: false, isMod: true });
Pour les débutants : api.emit() c'est comme appuyer sur un bouton qui fait sonner toutes les sonnettes du bâtiment. Chaque module entend la sonnette et décide s'il doit répondre ou pas.
Cibler un mod spécifique
Pour appeler une méthode publique d'un autre mod directement, sans passer par un broadcast :
// Accès direct au mod alerts via api.getMod()
const alerts = api.getMod('alerts');
alerts?.queue('follow', { username: 'Alice' });
🧰 Utilitaires partagés (OO.*)
L'objet global OO expose des utilitaires partagés entre tous les mods. Ils sont disponibles dans overlay.js et admin.js sans import.
Conventions de nommage du système : les clés localStorage du core sont préfixées oo_, les événements DOM oo: (ex. oo:cmd), et le canal admin ↔ overlay s'obtient via la constante OO.CHANNEL.
OO.CHANNEL — canal admin ↔ overlay
Nom du BroadcastChannel reliant le dashboard et l'overlay. Si ton module doit instancier le canal lui-même (cas rare — préfère tools.sendToOverlay côté admin et onAdminMessage côté overlay), utilise toujours cette constante, jamais une chaîne en dur.
const bc = new BroadcastChannel(OO.CHANNEL);
OO.Mods.all() — accès à tous les mods
Retourne le tableau de tous les mods enregistrés. À utiliser uniquement quand api.getMod(id) ou api.emit() ne suffisent pas (cas avancés : introspection, méta-commandes…).
const mods = OO.Mods.all(); // Array des mods enregistrés
Pour la grande majorité des cas, préfère api.getMod(id) (accès ciblé) ou api.emit() (broadcast). Réserve OO.Mods.all() aux rares cas où tu dois itérer sur tous les mods sans exception.
OO.fmt(tpl, vars) — templating
Remplace les tokens {clé} dans un template par les valeurs de l'objet vars. Les tokens inconnus sont remplacés par une chaîne vide.
OO.fmt('{username} a envoyé {amount} bits !', {
username: 'Alice',
amount: 100,
});
// → "Alice a envoyé 100 bits !"
OO.semverCompare(a, b) — comparaison de versions
Compare deux versions semver ("1.24.0"). Retourne -1 si a < b, 0 si égales, 1 si a > b. C'est la fonction utilisée par le Store pour vérifier minSystemVersion.
OO.semverCompare('1.9.3', '1.24.0'); // → -1 (tri numérique, pas alphabétique)
OO.escHtml(str) — sécurité DOM
Échappe les caractères spéciaux HTML avant une insertion dans innerHTML. Gère null et undefined (retourne une chaîne vide).
container.innerHTML = `<span>${OO.escHtml(userName)}</span>`;
OO.anim(el, keyframes, options) — animations
Wrapper Promise autour de Element.animate(). Se résout quand l'animation se termine ou est annulée.
await OO.anim(box, [
{ transform: 'scale(0)' },
{ transform: 'scale(1)' },
], { duration: 300, easing: 'cubic-bezier(0.22,1,0.36,1)' });
OO.setVolume(volume) — volume global du son ≥ 1.58.0
Multiplicateur de volume (0-1) appliqué à tous les sons joués via OO.playSound/api.playSound, y compris ceux déjà en cours de lecture. N'est pas destiné aux mods — prévu pour un hôte natif qui embarque une page OpenOverlay dans une WebView (ex. l'app ChatCast embarquant /overlay-audio) et veut piloter le volume du mix depuis son UI native, via un pont JS (window.OO.setVolume(0.5)). N'affecte pas le volume système.
OO.setVolume(0.5); // tous les sons désormais joués à 50% de leur volume configuré
iOS ≥ 1.58.1 : WebKit iOS ignore silencieusement HTMLMediaElement.volume (confirmé en test réel). Le volume y passe donc par un graphe WebAudio (GainNode) construit pendant le geste de déverrouillage, plutôt que par .volume — transparent pour les mods, aucune action requise. Seuls les sons hébergés sur le même domaine (assets intégrés, fichiers uploadés) bénéficient de ce graphe ; un jingle externe en URL HTTPS reste sur l'ancien comportement (.volume natif, ignoré par iOS) pour éviter qu'il devienne silencieux en l'absence d'en-têtes CORS.
OO.AlertBox — thèmes, animations et logo ≥ 1.49.0
Utilitaire partagé par tous les mods utilisant une boîte d'alerte. Lit le style configuré dans Préférences → Styles graphiques (animation, thème, couleur d'accent, animation logo) et l'applique automatiquement. Depuis la v1.50.0, gère aussi l'animation du logo d'overlay.
| Méthode | Description |
|---|---|
OO.AlertBox.configure(style) | Appelé par le core après chaque chargement de config. style = { animation, theme, accent, logoThrough }. |
OO.AlertBox.applyTheme(el) | Applique la classe .oo-theme-* et la variable CSS --oo-alert-accent sur l'élément. |
OO.AlertBox.applyAccent(el) | ≥ 1.56.0 Applique uniquement --oo-alert-accent, sans classe .oo-theme-*. Pour un widget qui veut s'harmoniser avec la couleur choisie dans Styles graphiques tout en gardant sa propre forme/mise en page (ex : queue, voix_ia) — utilise ensuite var(--oo-alert-accent) dans ton overlay.css à la place de couleurs en dur. |
OO.AlertBox.animIn(el, prefix) | Animation d'entrée de la box (async). |
OO.AlertBox.animOut(el) | Fondu de sortie de la box (async) — cache avec la classe .hidden. |
OO.AlertBox.animLogoArc() | ≥ 1.50.0 Arc du logo vers le centre (async). Si logoThrough actif, le logo grossit jusqu'à remplir l'écran (−20 px). Stocke l'état logo pour les méthodes suivantes. |
OO.AlertBox.animLogoExit() | ≥ 1.50.0 Sortie du logo (async) : traversée vers le spectateur (zoom + fondu) si logoThrough, glissement à droite sinon. |
OO.AlertBox.animLogoFadeIn() | ≥ 1.50.0 Logo réapparaît en fondu après l'alerte (async). Nettoie l'état logo interne. |
OO.AlertBox.cancelLogo() | ≥ 1.50.0 Annule les animations logo en cours et restaure sa position d'origine. Appelé automatiquement par AlertQueue.clear(). |
Thèmes disponibles : glass (défaut, arrondi 14px), solid (angles vifs 4px), neon (arrondi 22px), minimal (bande avec bordure gauche), bd (bulle BD arrondie 48px avec queue).
Animations : pop (défaut), slide-top, slide-left, slide-right.
Animation logo : activée via logoThrough: true dans config.alert_style (configurable dans Préférences).
OO.AlertQueue — file d'alertes séquentielles
Classe qui gère une file d'attente d'alertes : chaque alerte attend la fin de la précédente avant de s'afficher. Inclut un counter de génération pour annuler proprement les alertes en cours si clear() est appelé.
Depuis la v1.49.0, accepte un second argument opts optionnel. Depuis la v1.50.0, opts.withLogo active la séquence logo complète : arc → animIn box → sortie logo → showFn → animOut box → fondu logo. Le showFn n'a alors besoin que de peupler le contenu et d'attendre la durée. clear() gère automatiquement l'annulation de la box et du logo.
// Mode automatique avec logo (recommandé — alerts, channel_points)
init(api) {
this._alertQueue = new OO.AlertQueue(
async (data, isCurrentGen) => {
// Peupler le contenu et attendre la durée — animations gérées par la queue
document.getElementById('mon-titre').textContent = data.title;
await new Promise(r => setTimeout(r, data.duration ?? 5000));
},
{
box: document.getElementById('ma-box'),
getPrefix: () => this._getPositionPrefix(api),
withLogo: true, // active l'animation logo partagée
}
);
},
isCurrentGen() est une fonction passée à ton callback — appelle-la après chaque await long pour détecter si la file a été vidée (via clear()) pendant l'attente. Si elle retourne false, arrête-toi immédiatement.
api.placeInZone(zoneId, element) — zones d'emplacement
Place (ou déplace) un élément DOM dans l'un des 9 conteneurs de zone de l'overlay. Le conteneur se charge du positionnement ; si plusieurs modules occupent la même zone, ils s'empilent automatiquement en colonne ou en ligne selon la configuration du dashboard.
// overlay.js — placer le widget dans la zone configurée
init(api) {
const cfg = api.config?.mon_module ?? {};
api.placeInZone(cfg.position ?? 'top-right', document.getElementById('mon-widget'));
},
onConfigReload(api) {
const cfg = api.config?.mon_module ?? {};
// appendChild déplace l'élément si la position a changé
api.placeInZone(cfg.position ?? 'top-right', document.getElementById('mon-widget'));
}
| Zone ID | Position |
|---|---|
top-left | Haut gauche |
top-center | Haut centre |
top-right | Haut droite |
middle-left | Milieu gauche |
middle-center | Milieu centre |
middle-right | Milieu droite |
bottom-left | Bas gauche |
bottom-center | Bas centre |
bottom-right | Bas droite |
Un module qui utilise les zones ne doit pas déclarer position: fixed dans son overlay.css — le conteneur de zone s'en charge. Conserve uniquement les propriétés visuelles (background, border-radius, font…).
Variables CSS système
OpenOverlay expose des variables CSS globales sur :root que les modules peuvent lire — ou écrire — pour se coordonner sans passer par des événements JS.
| Variable | Valeur | Qui l'écrit |
|---|---|---|
--oo-safe-bottom |
0px par défaut. 40px quand une barre système occupe le bas de l'overlay (ex : ticker actif en position basse). |
Le mod qui occupe la zone basse (ticker, barre d'infos personnalisée…) |
--mod-z-{id} |
Z-index calculé automatiquement par le système selon l'ordre des modules dans le dashboard. Pas à définir manuellement. | Le core (overlay.js) |
Pour qu'un élément positionné en bas s'écarte automatiquement quand une barre système est active, utilise calc() avec la variable :
/* overlay.css */
#mon-widget {
position: fixed;
bottom: calc(16px + var(--oo-safe-bottom));
right: 16px;
}
Si ton module crée lui-même une barre en bas, il doit écrire la variable pour que les autres s'adaptent :
// overlay.js — quand ta barre devient visible en bas
document.documentElement.style.setProperty('--oo-safe-bottom', '50px');
// quand elle disparaît
document.documentElement.style.setProperty('--oo-safe-bottom', '0px');
Pour le z-index, utilise var(--mod-z-{id}) dans ton overlay.css (sans fallback) — toutes les valeurs par défaut sont déclarées dans :root d'overlay/style.css et le système les remplace automatiquement selon l'ordre défini par le streamer dans le dashboard.
🚀 Exemple complet et commenté
Un module "Annonce de sub" — quand quelqu'un s'abonne, un panneau apparaît sur le stream avec un son. Le streamer peut configurer le message, la durée et le son depuis le dashboard.
Chaque ligne de code est commentée pour que tu comprennes exactement ce qui se passe.
1 / 4 — manifest.json{
"id": "sub-announce",
"name": "Annonce Abonnement",
"description": "Affiche un panneau quand quelqu'un s'abonne",
"icon": "⭐",
"configKey": "sub_announce",
"defaultConfig": {
"enabled": true,
"message": "⭐ {username} vient de s'abonner !",
"sound": "",
"volume": 1.0,
"duration": 5000
},
"version": "1.0.0",
"files": ["manifest.json", "overlay.js", "overlay.html", "admin.js"]
}
2 / 4 — overlay.html
<div id="sa-panel" class="hidden" style="
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: #1a1a2e; border: 2px solid #9147ff;
border-radius: 16px; padding: 32px 40px;
font-family: sans-serif; color: white;
font-size: 28px; text-align: center; z-index: 9999;">
<div id="sa-text"></div>
</div>
3 / 4 — overlay.js
OO.Mods.register({
id: 'sub-announce',
_timer: null,
onTwitchEvent(type, event, api) {
const cfg = api.config.sub_announce;
if (cfg?.enabled === false) return;
if (type !== 'channel.subscribe' || event.is_gift) return;
const text = (cfg?.message ?? '⭐ {username} s\'abonne !')
.replace('{username}', event.user_name);
if (cfg?.sound) {
api.playSound(api.resolveAsset(cfg.sound), cfg.volume ?? 1);
}
this._show(text, cfg?.duration ?? 5000);
},
onAdminMessage(data, api) {
if (data.type === 'sa_test') {
this._show('⭐ TestUser vient de s\'abonner !', 3000);
}
},
_show(text, duration) {
clearTimeout(this._timer);
const panel = document.getElementById('sa-panel');
document.getElementById('sa-text').textContent = text;
panel.classList.remove('hidden');
this._timer = setTimeout(() => {
panel.classList.add('hidden');
}, duration);
},
});
4 / 4 — admin.js
OO.Admin.register({
id: 'sub-announce',
render(config, container, { markDirty, sendToOverlay, showToast, createAssetField }) {
if (!config.sub_announce) config.sub_announce = {};
const cfg = config.sub_announce;
container.innerHTML = `
<div class="card">
<div class="card-header no-border">
<span class="card-title">⭐ Annonce Abonnement</span>
<button class="btn btn-ghost btn-sm" id="sa-test">▶ Tester</button>
</div>
<div class="card-body grid">
<div class="field">
<label>Message <span class="hint">({username} = pseudo)</span></label>
<input type="text" id="sa-msg" value="${cfg.message ?? ''}">
</div>
<div class="field">
<label>Durée (ms)</label>
<input type="number" id="sa-dur" min="1000" max="30000" step="500" value="${cfg.duration ?? 5000}">
</div>
<div class="field">
<label>Son (optionnel)</label>
<div id="sa-snd-field"></div>
</div>
</div>
</div>`;
container.querySelector('#sa-msg').oninput = e => { cfg.message = e.target.value; markDirty(); };
container.querySelector('#sa-dur').oninput = e => { cfg.duration = parseInt(e.target.value); markDirty(); };
container.querySelector('#sa-test').onclick = () => {
sendToOverlay({ type: 'sa_test' });
showToast('Test envoyé — l\'overlay doit être ouvert');
};
container.querySelector('#sa-snd-field').replaceWith(
createAssetField({
type: 'sound',
value: cfg.sound ?? '',
onChange: (path) => { cfg.sound = path; markDirty(); },
})
);
},
getCommands() { return []; },
});
En 4 fichiers et moins de 120 lignes de code, tu as un module fonctionnel avec configuration admin, upload de son, bouton de test et réaction aux événements Twitch. Copie ce code, change les textes, et fais-le tourner — c'est la meilleure façon d'apprendre.
🎨 Framework CSS modules
Le dashboard d'OpenOverlay charge automatiquement admin/admin.css pour chaque panneau admin. Ce fichier contient un ensemble de classes prêtes à l'emploi — un framework CSS léger que tu peux utiliser directement dans ton admin.js sans importer quoi que ce soit.
Règle d'or : n'ajoute dans ton mods/{id}/admin.css que ce qui est spécifique à ton mod. Tout le reste est déjà là.
Trois interdictions absolues dans le CSS de ton mod :
1. Pas de style="..." dans le HTML généré par admin.js — aucune propriété visuelle par attribut inline.
2. Pas de fallback dans var() dans admin.css — écrire var(--x) uniquement. Pour les couleurs issues de la config, définir la valeur par défaut via el.style.setProperty('--x', valeur) en JS avant le premier rendu.
3. Pas de fallback dans var() dans overlay.css non plus — toutes les variables système (--mod-z-{id}, --oo-safe-bottom, --oo-alert-radius…) ont leurs valeurs par défaut déclarées dans :root d'overlay/style.css.
Layout & listes d'items
Pour afficher une liste d'éléments configurables (commandes, séries, récompenses…) :
<div class="mod-list-header">
<h3>Mes éléments (3)</h3>
<button class="btn btn-primary btn-sm">+ Ajouter</button>
</div>
<div class="mod-list">
<div class="mod-item">
<div class="mod-item-header">
<span class="mod-item-icon">🎯</span>
<span class="mod-cmd-code">!bonjour</span>
<span class="mod-item-label">Répond bonjour</span>
</div>
<div class="mod-item-body">
<!-- champs du formulaire ici -->
</div>
</div>
</div>
| Classe | Rôle |
|---|---|
.mod-list-header | En-tête de section — titre à gauche, bouton à droite |
.mod-list | Conteneur flex-column, gap 12px |
.mod-item | Carte item — fond, bordure, border-radius 8px |
.mod-item-header | En-tête d'item — flex row, gap 8px |
.mod-item-body | Corps d'item — grille 2 colonnes, gap 12px 20px |
.mod-item-icon | Icône/emoji 16px dans l'en-tête |
.mod-item-label | Libellé secondaire gris, tronqué |
.mod-cmd-code | Code de commande violet 13px |
.mod-cmd-empty | Texte "vide" gris italique |
.span-full | Force grid-column: 1 / -1 dans .mod-item-body |
Contrôles de formulaire
Patterns courants pour les champs dans .card-body.grid ou .mod-item-body :
const { cmdField, markDirty } = tools;
// Dans le HTML template :
<div class="field">
<label>Commande</label>
${cmdField('mon-cmd', cfg.commandName ?? 'bonjour', {
placeholder: 'bonjour',
hint: 'Description de la commande.',
})}
</div>
// Après container.innerHTML :
container.querySelector('#mon-cmd').oninput = e => {
cfg.commandName = e.target.value.trim().replace(/^!/, '') || 'bonjour';
markDirty();
};
Sélecteur d'asset (image, gif ou son)
// Dans le HTML template :
<div class="field">
<label>Image</label>
<div id="mon-image-field"></div>
</div>
// Après container.innerHTML, remplacer le placeholder :
container.querySelector('#mon-image-field').replaceWith(
createAssetField({
type: 'image', // 'image', 'gif' ou 'sound'
value: cfg.image ?? '',
onChange: (path) => { cfg.image = path; markDirty(); },
})
);
Toggle avec label descriptif
<div class="field field-inline">
<div class="field-inline-label">
<strong>Saisie requise</strong>
<small>Le viewer doit taper un message</small>
</div>
<label class="toggle">
<input type="checkbox">
<span class="toggle-track"></span>
</label>
</div>
| Classe | Rôle |
|---|---|
.cmd-prefix-row | Ligne ! + input — flex row, gap 6px. Le dashboard y injecte automatiquement un .cmd-hint avec validation en temps réel (format, unicité). |
.cmd-prefix | Le symbole ! — gris, non-sélectionnable |
.cmd-hint | Hint de validation auto-injecté dans .cmd-prefix-row. États : .valid (vert, → !nom), .invalid (rouge, format incorrect), .conflict (orange, commande déjà utilisée par un autre mod). |
.input-mono | Font monospace pour les champs de commandes |
.input-narrow | Input court 72px (durées, nombres…) |
.upload-row | Input texte + bouton upload côte à côte |
.num-range-row | Ligne deux inputs avec séparateur textuel |
.num-range-sep | Séparateur entre inputs (à, –, min…) |
.field-inline | Champ horizontal label + toggle |
.field-inline-label | Label structuré avec strong + small |
.asset-preview | Aperçu image générique — max-height 64px |
.asset-preview-sm | Aperçu image compact — max-height 48px |
.input-narrow | Input numérique étroit (72px) — pour les secondes, compteurs… |
Largeurs personnalisées : les inputs font width: 100% par défaut, mais ce défaut est déclaré en spécificité zéro (:where(), système ≥ 1.27.1) — une simple classe dans le CSS de ton module (.mon-input { width: 80px; }) suffit à le surcharger. Pour rester compatible avec les systèmes plus anciens, préfixe le sélecteur avec l'élément : input.mon-input { width: 80px; }.
Sélecteur de position et taille (overlay)
Pour les mods qui ont une position configurable sur le canvas stream :
Sélecteur de position — via tools.posPicker()const { posPicker, markDirty } = tools;
// Dans le HTML template :
${posPicker('mon-pos', cfg.position ?? 'top-left')}
// Après container.innerHTML, binding des clics :
container.querySelectorAll('#mon-pos .pos-btn').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('#mon-pos .pos-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
cfg.position = btn.dataset.pos;
markDirty();
});
});
| Classe | Rôle |
|---|---|
.pos-picker | Grille 3×3 de 32px — gap 4px |
.pos-btn | Bouton de position 32×32px avec états hover/active |
.pos-btn.active | Position sélectionnée — fond violet |
.size-row | Ligne slider + valeur numérique |
.size-val | Valeur monospace violet clair, min-width 44px |
Ne pas redéfinir ces classes dans mods/{id}/admin.css. Ajoute uniquement des classes propres à ton mod en complément (ex: .mon-mod-badge en plus de .badge).
📡 Intégration audio externe — /api/sound-relay ≥ 1.59.0
Contrat d'API stable pour un client natif tiers (ex. ChatCast) qui veut recevoir les sons joués par l'overlay et les rejouer lui-même avec un moteur audio natif — sans passer par une WebView. C'est le même endpoint que consomme /overlay-audio (voir guide d'utilisation) ; rien n'empêche un client natif de l'interroger directement.
Stabilité : la forme des requêtes/réponses ci-dessous est stable à partir de la v1.59.0. Ce n'est pas un détail interne de /overlay-audio — toute évolution incompatible sera documentée ici avec un numéro de version.
Authentification
En-tête Authorization: Bearer <token> — le token Twitch du streamer, le même que celui obtenu via le lien généré depuis le dashboard (Guide d'utilisation → 🔊 Copie audio). C'est un access token OAuth Twitch classique, valide ~4 heures.
GET /api/sound-relay — recevoir les événements (long-polling)
| Paramètre | Description |
|---|---|
since | Entier — id du dernier événement déjà traité. 0 au tout premier appel. Le serveur ne renvoie que les événements avec id > since. |
sync=1 | Optionnel, pour le tout premier appel uniquement : renvoie immédiatement le lastId courant sans événement, pour amorcer le curseur sans rejouer les sons déjà passés (utile si le client se connecte en plein live). |
La requête reste ouverte côté serveur jusqu'à ~8 secondes en attendant un nouvel événement avant de répondre — c'est voulu (long-polling), pas un timeout d'erreur. Configure un timeout client ≥ 15 s, et relance immédiatement une nouvelle requête dès que la réponse précédente arrive, qu'elle contienne un événement ou non.
Réponse sans événement (fin du délai d'attente, rien de nouveau) :
200 OK
{ "events": [], "lastId": 42 }
Réponse avec un ou plusieurs événements :
200 OK
{
"events": [
{ "id": 43, "src": "https://ton-instance.netlify.app/overlay/assets/sounds/defaut.mp3", "volume": 0.8, "ts": 1720000000000 }
],
"lastId": 43
}
id— entier croissant, unique par instance. Sert de curseur : réutilise lelastIdde la réponse commesincede la requête suivante.src— URL absolue (≥ 1.59.0), prête à être téléchargée/jouée telle quelle.volume— flottant0-1, déjà résolu et borné côté serveur. Le client joue le son à ce volume, éventuellement multiplié par son propre volume maître.ts— epoch en millisecondes, moment où le serveur a reçu l'événement (pas nécessairement l'instant exact de lecture côté overlay — écart négligeable).
Historique borné : seuls les 20 derniers événements sont conservés côté serveur. Un client resté déconnecté longtemps peut rater des événements plus anciens que son since — ce n'est pas signalé explicitement dans la réponse ; à traiter comme acceptable (mix audio en direct, pas une file à rattraper).
POST /api/sound-relay
Réservé à l'overlay lui-même (source de vérité — c'est lui qui pousse chaque son réellement joué). Un client tiers comme ChatCast n'a besoin que du GET.
Erreurs
| Code | Cause | À faire |
|---|---|---|
401 | Token invalide ou expiré. | Rafraîchir le token (voir plus bas) puis relancer. |
400 | Requête malformée (ne devrait pas arriver avec un client conforme à ce contrat). | Vérifier les paramètres envoyés. |
405 | Méthode HTTP non supportée. | Seuls GET/POST/OPTIONS existent. |
5xx | Erreur serveur (rare). | Backoff (~2 s) puis réessayer. |
403 n'existe pas sur ce endpoint : l'utilisateur concerné est déterminé uniquement par le token, il n'y a pas de paramètre user à usurper. Pas de 429 non plus pour l'instant — le long-polling borne déjà naturellement la fréquence des requêtes ; ne polle pas plus vite que la réponse précédente ne revient.
Rafraîchir le token
Le lien généré par le dashboard contient token et refresh en query params. Quand GET /api/sound-relay renvoie 401, échange le refresh token :
POST /api/token-exchange
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<refresh>
Réponse : { "access_token", "refresh_token", "expires_in" }. Le refresh token est à usage unique (rotation Twitch) — stocke systématiquement le nouveau avant de réessayer, et évite d'appeler ce endpoint deux fois en parallèle avec le même refresh token (le second échouerait).
🔐 Modèle de confiance des modules
Un module OpenOverlay s'exécute dans la même page que l'overlay et le dashboard, avec le même niveau d'accès que le code du système. C'est ce qui rend les modules puissants — mais il faut en comprendre les implications.
Un module installé est du code de confiance. Il peut :
- lire le token Twitch du streamer (et donc agir en son nom sur l'API Twitch) ;
- lire et écrire la configuration, les fichiers et l'historique de l'instance ;
- communiquer avec n'importe quel serveur externe.
Conséquences concrètes :
- Pour le streamer — installer un module revient à lui faire confiance sur son instance, comme une extension de navigateur. Les modules du Store officiel sont relus avant publication ; le Mode Développeur n'installe que du code que tu fournis toi-même (un avertissement explicite le rappelle à l'activation). N'installe en mode développeur que du code que tu as écrit ou relu.
- Pour l'auteur de module — limite-toi aux APIs documentées (
api.*,tools.*) et n'envoie jamais le token ni les données du streamer vers un serveur tiers. Un module qui exfiltre des données sera refusé du Store. - Tokens — ils sont stockés dans le
localStoragedu navigateur, sur l'instance personnelle du streamer (jamais sur un serveur central). Les scopes Twitch demandés sont limités au strict nécessaire au fonctionnement du système.
🚀 Publier dans le Store
Tu as créé et testé un module qui te semble prêt à être partagé ? Voici le chemin complet, de la première ligne de code jusqu'à la disponibilité dans le Store officiel.
Tester en Mode Développeur
Avant de soumettre, tu dois valider ton module sur ta propre installation. Le Mode Développeur est accessible dans les Préférences du dashboard. Une fois activé, une section "Installer un module local" apparaît. Sélectionne le dossier de ton module — le dashboard le lit, le valide et l'envoie au serveur. Recharge ensuite la page.
Le mode développeur est réservé aux tests. N'active jamais ce mode pour installer un module dont tu n'as pas lu le code — un module malveillant pourrait accéder à ton token Twitch ou envoyer tes données à un serveur tiers.
Critères de validation
- Absence d'exfiltration de données — pas d'envoi du token, de la config ou de données personnelles vers des serveurs tiers
- Absence d'injection de code — pas d'exécution de scripts externes non déclarés
- Respect des APIs OpenOverlay — usage des hooks et méthodes documentés ici
- Fonctionnement stable — pas d'erreurs JS non catchées, pas de boucles infinies
Délai de réponse habituel : 5 à 10 jours ouvrés