🌱 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.

Indispensable
🌐

Notions de base en HTML

Savoir que <div> crée une boîte et <p> un paragraphe, c'est suffisant pour commencer.

Utile
⚙️

Un peu de JavaScript

Comprendre if / else, les fonctions, et ce qu'est une variable. Tout s'apprend en quelques heures sur YouTube.

Utile
🎓

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.

📺Twitchenvoie un événement
🧩overlay.jsle reçoit et réagit
⚙️admin.jsconfigure le comportement
👁️Streamce que le viewer voit

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.

mods/
  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.

ChampTypeRequisDescription
idstringrequisIdentifiant unique. Lettres minuscules, chiffres et tirets uniquement. Doit être identique au nom du dossier.
namestringrequisNom lisible affiché dans la navigation du dashboard.
descriptionstringrequisCourte description affichée sous le nom dans le dashboard.
iconstringrequisUn emoji qui représente le module visuellement.
configKeystringrequisLa clé sous laquelle ta config est rangée dans la sauvegarde de l'utilisateur. Souvent identique à id.
defaultConfigobjectrequisLes réglages par défaut. Doit contenir "enabled": true.
versionstringoptionnelNuméro de version (ex : "1.0.0"). Affiché dans le Store.
authorstringoptionnelTon pseudo. Affiché dans la fiche Store.
longDescriptionstringoptionnelDescription détaillée pour la fiche Store.
tagsstring[]optionnelMots-clés pour la recherche dans le Store.
filesstring[]optionnelListe de tous les fichiers du module. Utilisée lors de l'installation depuis le Store.
changelogarrayoptionnelHistorique des versions. Chaque entrée : { "version", "date", "notes" }.
Exemple — manifest.json
{
  "id":          "mon-module",
  "name":        "Mon Module",
  "description": "Ce que fait mon module en une phrase",
  "icon":        "✨",
  "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 OverlayMods.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.

OverlayMods.register({
  id: 'mon-module',

  // 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

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éTypeDescription
loginstringIdentifiant en minuscules. À utiliser pour les commandes IRC (/timeout, /ban…).
displayNamestringPseudo avec la casse d'origine. À utiliser pour l'affichage.
userIdstringIdentifiant Twitch unique du viewer.
isBroadcasterbooleantrue si c'est le streamer lui-même.
isModbooleantrue si la personne est modératrice.
isVipbooleantrue si la personne est VIP.
isSubscriberbooleantrue 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éthodeDescription
api.configLa configuration complète de l'utilisateur. En lecture seule depuis l'overlay.
api.channelNameLe login Twitch du streamer.
api.broadcasterIdL'identifiant Twitch numérique du streamer.
api.tokenLe 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.
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.

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.

AdminMods.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) {
    return [
      { cmd: '!macommande', args: '[texte]', desc: 'Affiche quelque chose sur le stream' },
    ];
  },
});
⚠️

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.testUsernameLe pseudo configuré pour les tests (défaut : 'TestUser').
tools.broadcasterIdL'identifiant Twitch du streamer.
tools.resolveAsset(path)Convertit un chemin user:// en URL complète.
tools.uploadAsset(file)async. Uploade un fichier File. Retourne user://nom-du-fichier.
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.

⚙️ 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) :

  1. Le fichier config.json global
  2. Le defaultConfig du manifest
  3. 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.

Pattern d'accès recommandé
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.follow

Quelqu'un vient de suivre la chaîne.

Données utiles : event.user_name, event.user_login, event.user_id

channel.subscribe

Nouvel 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.gift

Un viewer offre des abonnements à la communauté. Peut être anonyme.

Données utiles : event.user_name, event.total, event.is_anonymous

🔁 channel.subscription.message

Ré-abonnement avec message.

Données utiles : event.user_name, event.cumulative_months, event.message.text

💎 channel.cheer

Envoi de Bits dans le chat. user_name peut être null si anonyme.

Données utiles : event.user_name, event.bits, event.message

🚨 channel.raid

Raid entrant.

Données utiles : event.from_broadcaster_user_name, event.viewers

🏆 channel.channel_points_custom_reward_redemption.add

Un 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

ℹ️

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 (!sender?.isBroadcaster) return;
  if (cmd !== '!macommande') return;
  const texte = args.join(' ');
  api.sendChat(`Commande reçue : ${texte}`);
},

Niveaux d'accès

NiveauCondition sur sender
Streamer uniquementsender.isBroadcaster
Modérateur et +sender.isBroadcaster || sender.isMod
VIP et +sender.isBroadcaster || sender.isMod || sender.isVip
Abonné et +... || sender.isSubscriber
Tout le mondetrue
⚠️

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). 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.

admin.js — bouton d'upload
const uploadEl = container.querySelector('#mon-upload');
uploadEl.onchange = async (e) => {
  const file = e.target.files[0];
  if (!file) return;
  e.target.value = '';
  tools.showToast('Upload en cours…');
  try {
    const path = await tools.uploadAsset(file);
    cfg.sound = path;
    markDirty();
    tools.showToast('Fichier uploadé !');
  } catch (err) {
    tools.showToast(`Erreur : ${err.message}`, 'error');
  }
};
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 OverlayMods.dispatch(). Cette fonction appelle une méthode sur tous les modules enregistrés qui l'implémentent.

OverlayMods.dispatch(
  'onChatCommand',
  '!macommande',
  ['arg1'],
  api,
  { isBroadcaster: true, login: 'viewer123', displayName: 'Viewer123' }
);
💡

Pour les débutants : dispatch() 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.

🚀 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
OverlayMods.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
AdminMods.register({
  id: 'sub-announce',

  render(config, container, { markDirty, sendToOverlay, showToast, uploadAsset, addDropZone }) {
    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 style="display:flex;gap:8px">
              <input type="text" id="sa-snd" value="${cfg.sound ?? ''}" placeholder="user://...">
              <label class="btn btn-secondary btn-sm" style="cursor:pointer">⬆
                <input type="file" id="sa-snd-upload" accept="audio/*" style="display:none">
              </label>
            </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-snd').oninput = e => { cfg.sound    = e.target.value.trim();      markDirty(); };

    container.querySelector('#sa-test').onclick = () => {
      sendToOverlay({ type: 'sa_test' });
      showToast('Test envoyé — l\'overlay doit être ouvert');
    };

    const uploadEl = container.querySelector('#sa-snd-upload');
    const label    = uploadEl.closest('label');
    const doUpload = async (file) => {
      showToast('Upload en cours…');
      try {
        const path = await uploadAsset(file);
        cfg.sound = path;
        container.querySelector('#sa-snd').value = path;
        markDirty();
        showToast('Son uploadé !');
      } catch (err) {
        showToast(`Erreur : ${err.message}`, 'error');
      }
    };
    uploadEl.onchange = async (e) => {
      const f = e.target.files[0];
      if (!f) return;
      e.target.value = '';
      await doUpload(f);
    };
    addDropZone(label, ['audio/'], doUpload);
  },

  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.

🚀 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.

🧑‍💻
1. Tu crées
Tu développes ton module avec les APIs décrites dans cette documentation.
🧪
2. Tu testes
Tu l'installes via le Mode Développeur dans les Préférences du dashboard.
📬
3. Tu soumets
Tu envoies le formulaire avec le code de ton module.
🔍
4. Vérification
Le code est relu pour détecter tout problème de sécurité.
5. Validation
Tu es notifié·e par e-mail. Le module est ajouté au Store.
🌍
6. Disponible
Tous les utilisateurs OpenOverlay peuvent installer ton module en un clic.

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
📬 Soumettre mon module →

Délai de réponse habituel : 5 à 10 jours ouvrés