Skip to content

Plugins viewers et streamer : service côté appli ​

Une appli peut avoir un plugin viewers (interface publique jouée par les spectateurs) et/ou un plugin streamer, en plus de l'overlay. kapp.plugin est le pont temps réel entre ces contextes.

Voir le helper V3 côté page, les confirmations et la reprise, ou la carte des transports. Les plugins appartiennent aux applis ; ils ne constituent pas un contrat widget.

Bien distinguer les deux plugins ​

  • Le plugin viewers est public et potentiellement ouvert par beaucoup de personnes en même temps. Tout événement reçu est une intention non fiable : l'appli doit revalider l'état, les droits, le coût et les règles du jeu avant d'agir.
  • Le plugin streamer est privé, mais son nom est historique : le streamer et les admins autorisés du channel peuvent l'ouvrir, avec plusieurs onglets. Ne suppose jamais qu'il n'existe qu'une seule instance ou un seul opérateur.
  • Les deux flux sont limités séparément. Le plugin streamer/admin dispose d'une marge plus large ; un afflux viewers ne consomme pas sa réserve.

Déclaration (infos.json) ​

  • viewers-plugin est le dossier du plugin, relatif à la racine publiée ; viewers-plugin-index est son fichier HTML, relatif à ce dossier. Par exemple, "viewers-plugin": "viewers" et "viewers-plugin-index": "index.html" servent viewers/index.html. viewers-plugin-height configure sa hauteur.
  • streamer-plugin / streamer-plugin-index suivent la même règle pour la page streamer et admins.
  • L'appli déclare app-plugin-viewers:write pour émettre vers les viewers ou répondre à leurs emitWithReply, et app-plugin-streamer:write pour les échanges correspondants vers streamer/admins. callStreamer() nécessite séparément app-plugin-streamer:call. Ces scopes applicatifs sont distincts des autorisations OAuth Twitch.

Service ​

  • await emitViewers(users: User[] | null, event: string, data = {}): Promise<void> — émet vers le plugin viewers. users = null → tous ; users = [...] → ciblés.
  • await emitStreamer(event: string, data = {}): Promise<void> — émet vers toutes les instances ouvertes du plugin streamer/admin.
  • await callStreamer(): Promise<void> — notifie le streamer et ses admins (sans données).
  • getViewersPluginUrl(): string — URL publique du plugin viewers ({host}/viewers/{channel}/{app_slug}).

Événements reçus ​

  • kapp.plugin.EVENTS.PLUGIN_VIEWERS — message d'un viewer : { viewer: User, event: string, data?: Record<string, any> }.
  • kapp.plugin.EVENTS.PLUGIN_STREAMER — message d'une instance streamer/admin : { event: string, data?: Record<string, any> }.

Abonne-toi avec kapp.plugin.events.on(...) ou once(...) ; off(...) retire un callback enregistré avec on(...) en passant la même référence de fonction. Le nom d'événement détermine le type du callback. viewer est un modèle User du SDK, pas les données brutes du transport. L'émetteur et les gestionnaires de messages restent internes.

Pour une demande emitWithReply, l'événement expose aussi requestId et reply(data): Promise<void>. Ces champs sont absents pour emit. Le bus n'attend pas les Promises des callbacks : traite leurs rejets dans le handler. Voir confirmations et concurrence.

Erreurs du relais dans le sens appli → plugins ​

kapp.plugin.emitViewers, emitStreamer, callStreamer et le callback reply rejettent leur Promise si le relais refuse l'envoi. L'erreur conserve le contrat du SDK app (status, reason) et ajoute code :

CodeHTTPSignification
PAYLOAD_TOO_LARGE413Le relais a refusé la taille (reason: plugin_payload_too_large). Réduire le message.
RELAY_UNAVAILABLE503L'envoi vers le relais a échoué (reason: plugin_relay_unavailable). Un timeout réseau peut laisser sa livraison incertaine.

Ces erreurs valent aussi pour les anciennes applis. Un succès reste une acceptation par le relais, sans garantie qu'un plugin est ouvert ou qu'il a traité le message. Un envoi ciblé à plusieurs viewers peut échouer après avoir atteint certains destinataires : l'API renvoie l'erreur, arrête les envois restants et ne rejoue rien. Si l'envoi de la confirmation échoue, l'appli reçoit cette erreur ; le plugin demandeur peut seulement constater l'absence de réponse.

Développement en localhost ​

Le serveur serve du Kappapps DEV HUB couvre aussi les deux plugins d'un brouillon. Il suffit de conserver leurs dossiers dans le runtime publié et de les déclarer dans infos.json : les pages viewers et streamer de Kappapps chargent automatiquement leurs fichiers depuis localhost, tandis que le helper officiel et les API event / data continuent de passer par Kappapps.

  • Aucun serveur, port ou réglage localhost supplémentaire n'est nécessaire pour les plugins.
  • Utilise des chemins relatifs pour les fichiers propres au plugin, mais conserve l'URL Kappapps absolue du helper V3 pour le helper officiel. Elle fonctionne aussi lorsque le document du plugin est servi par le DEV HUB en localhost.
  • En localhost, KappappsHost.publicAssetsUrl pointe directement sur le dossier public-assets du runtime servi par le DEV HUB.
  • Le plugin viewers local n'est testable que depuis la machine du développeur : chez une autre personne, localhost désigne sa propre machine.
  • Une version soumise ignore toujours localhost_path et retombe sur les fichiers publiés.

Valider le vrai shell avec un agent ​

  1. Lance serve, puis appelle prepare_preview avec surface: viewers_plugin ou surface: streamer_plugin.
  2. Ouvre immédiatement la preview_url dans le navigateur/Playwright de l'agent. La page porte un badge SESSION DEV et expose data-dev-preview-ready="true" une fois le helper du plugin prêt.
  3. Inspecte la coque et l'iframe indiquée par frame.selector / frame.origin : console, réseau, responsive et interactions doivent être vérifiés dans ce contexte.
  4. Utilise open_managed_preview seulement si ce navigateur ne peut pas atteindre l'iframe localhost.

Cette preview n'est ni le Dashboard viewers, ni le Dashboard streamer. C'est un shell minimal dédié qui reconstitue le même bridge postMessage, les mêmes API JWT et les mêmes canaux socket sur le channel DEV. N'ouvre jamais directement l'URL localhost du plugin : elle peut sembler correcte tout en masquant une erreur d'API, de bridge ou de résolution d'URL.

La preview DEV viewers utilise l'identité du propriétaire du channel DEV, pas celle du compte connecté dans le navigateur. Deux previews DEV, même dans deux navigateurs, ne prouvent donc pas deux joueurs distincts. Pour une recette multiutilisateur locale, ouvre l'URL publique retournée par getViewersPluginUrl() dans deux profils connectés à des comptes différents, sur la machine qui sert le brouillon. Vérifie les identités réellement reçues par l'appli via viewer, puis distingue ce cas de deux onglets du même compte. Le propriétaire du channel peut lui-même participer par cette page viewers ; son plugin streamer reste un autre contexte.

Le ticket présent dans la première URL est one-shot et valable deux minutes. Il est échangé contre un cookie HttpOnly, puis disparaît lors de la redirection vers une URL propre. La session complète reste courte et est révoquée quand la preview est déchargée ou la session locale arrêtée.

Confidentialité ​

Kappapps compte les échanges sans journaliser le nom des événements, les payloads ni les identités. Ajoute tes propres compteurs fonctionnels si nécessaire, sans données sensibles.

Pièges ​

  • Vérifie la forme exacte des events et de EVENTS dans app-sdk.d.ts.
  • Toutes les instances streamer/admin ouvertes peuvent recevoir le même événement : leur affichage doit être idempotent.
  • Ne construis pas un chemin partagé avec ../ depuis le plugin : utilise KappappsHost.publicAssetsUrl et vérifie qu'il n'est pas null.
  • Un viewer qui a interdit ton appli (viewers-optout, cf. modes de lancement) n'a plus accès à son plugin et ne reçoit plus les emitViewers : ne compte pas sur une réponse de sa part.

Stockage des plugins ​

KappappsHost.data() du helper V3 lit les stockages V2 et déclenche leur copie initiale si nécessaire. Migrer ensemble les plugins et les écritures kapp.*StorageV2, y compris les anciens alias et getters User. Le même nom data() dans les helpers V1/V2 reste sur V1 : la version du helper détermine le stockage. Voir le guide de migration.