Skip to content

Sélecteur de médias dans un plugin streamer ​

Exemple de choix pendant l’exécution : une appli liste jusqu’à 30 images du channel, le plugin streamer propose un menu et l’appli affiche le média choisi. Pour un réglage persistant, réutilisé d’une session à l’autre, utiliser plutôt le champ mediaFile. Le FileManager du dashboard n’est pas une API publique du helper.

Conserver le bootstrap et ses configurations ; fusionner cet extrait de manifeste et intégrer les deux scripts dans leurs contextes. Les noms d’images servent ici de clés ; suppression/renommage du fichier demande une nouvelle exécution pour rafraîchir le catalogue. Aucun stockage persistant, upload, message Twitch ni récompense.

json
{
  "streamer-plugin": "streamer",
  "streamer-plugin-index": "index.html",
  "scopes": ["file:read", "app-plugin-streamer:write"]
}

config/default_triggers.config.json ​

Ce default propose de garder l'app ouverte pour recevoir les demandes du plugin. Le streamer peut le modifier ou le retirer ; voir les déclencheurs d'installation.

json
[{"type":"PROCESSUS"}]

script.js de l’appli ​

js
(async () => {
    let closed = false;
    let busy = false;
    let playing;
    let catalog = [];
    const onPlugin = ({event, data, reply}) => {
        if (!reply || closed) return;
        void (async () => {
            if (event === 'media:list') {
                await reply({ok: true, items: catalog.map(file => ({name: file.name}))});
                return;
            }
            if (event !== 'media:choose') return;
            if (busy) { await reply({ok: false, reason: 'busy'}); return; }
            if (!catalog.some(file => file.name === data?.name)) {
                await reply({ok: false, reason: 'invalid_choice'}); return;
            }
            busy = true;
            try {
                const file = await kapp.medias.get(data.name);
                if (closed) return;
                if (!file || file.type !== 'images') {
                    await reply({ok: false, reason: 'missing_media'}); return;
                }
                playing?.remove();
                const next = await kapp.medias.playMedia({file, volume: 0,
                    position: {x: 100, y: 100, w: 640, h: 360}, timeline: {}});
                if (closed) { next.remove(); return; }
                playing = next;
                await reply({ok: true, name: file.name});
            } finally { busy = false; }
        })().catch(error => console.error('Choix média non confirmé', error));
    };
    kapp.onBeforeExit('*', () => {
        closed = true;
        kapp.plugin.events.off(kapp.plugin.EVENTS.PLUGIN_STREAMER, onPlugin);
        playing?.remove();
    });
    catalog = (await kapp.medias.all()).filter(file => file.type === 'images').slice(0, 30);
    if (closed) return;
    kapp.plugin.events.on(kapp.plugin.EVENTS.PLUGIN_STREAMER, onPlugin);
    kapp.display();
})().catch(async error => {
    console.error('Catalogue indisponible', error);
    await kapp.abort('Catalogue indisponible');
});

streamer/index.html ​

html
<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Choisir une image</title>
<script src="https://kappapps.app/assets/app-plugin/helper-v3.js" defer></script>
<script src="./script.js" defer></script></head>
<body>
  <label for="media">Image du channel</label><select id="media" disabled></select>
  <button id="choose" disabled>Afficher</button><button id="reload">Actualiser</button>
  <p id="status" role="status">Chargement…</p>
</body>
</html>

streamer/script.js ​

js
const select = document.getElementById('media');
const choose = document.getElementById('choose');
const reload = document.getElementById('reload');
const status = document.getElementById('status');
let busy = false;
async function request(event, data = {}) {
    if (busy) return;
    busy = true;
    choose.disabled = reload.disabled = select.disabled = true;
    status.textContent = 'En attente…';
    try {
        const result = await KappappsHost.emitWithReply(event, data, {timeoutMs: 10000});
        if (!result.ok) throw new Error(result.reason || 'Refus');
        if (event === 'media:list') {
            select.replaceChildren();
            for (const item of result.items) {
                const option = document.createElement('option');
                option.value = item.name;
                option.textContent = item.name;
                select.appendChild(option);
            }
            status.textContent = result.items.length ? 'Choisir une image' : 'Aucune image disponible';
        } else status.textContent = 'Image affichée';
    } catch (error) {
        status.textContent = 'Confirmation indisponible ou choix refusé. Vérifier l’appli avant de réessayer.';
        console.error('Sélecteur média', error);
    } finally {
        busy = false;
        reload.disabled = false;
        choose.disabled = select.disabled = select.options.length === 0;
    }
}
choose.addEventListener('click', () => { void request('media:choose', {name: select.value}); });
reload.addEventListener('click', () => { void request('media:list'); });
void request('media:list');

Le menu n’envoie aucune URL ni identité de viewer. L’appli contrôle le nom contre son catalogue puis relit le média ; les opérateurs streamer/admin sont ceux du shell Kappapps. Ne pas transformer cette recette en accès viewers à toute la médiathèque du channel. Pour des viewers, préparer un catalogue public limité et vérifier l’acteur, les règles et les quotas côté appli.

La position est horizontale et fixe pour cet exemple : adapter via layout avant d’annoncer un support vertical. Le média reste affiché jusqu’au choix suivant ou à l’arrêt de l’appli. Une réponse perdue peut laisser l’image affichée ; aucun renvoi automatique n’est prévu. En cas d’échec de lecture, la console de l’appli porte l’erreur et le plugin peut expirer sans confirmation.

Actualiser recharge le catalogue, pas l’état de l’image affichée. Recharger le plugin conserve le média dans l’appli active, mais reconstruit le menu avec sa première option. Cette option n’indique donc pas l’image courante. Changer la sélection sans cliquer sur Afficher n’émet aucune action : cet exemple n’ouvre pas un sélecteur système avec une commande d’annulation. Pour plusieurs opérateurs ou une reprise de l’état visuel, ajoute un instantané de l’image courante et sa révision selon le protocole appli–plugins.

Pour vérifier la reprise, ouvre d’abord le plugin seul : l’absence de réponse doit laisser Actualiser utilisable. Démarre ensuite l’appli et actualise ; affiche une image, recharge uniquement le plugin, puis affiche une autre image. Vérifie le même identifiant d’exécution et une seule image dans l’overlay. Cela valide le rechargement de la page, pas une coupure réseau.

Recette : shell streamer via prepare_preview, catalogue vide, choix valide, nom forgé, média supprimé après lecture, double clic, arrêt pendant chargement et rechargement du plugin. L’appli doit être active pour répondre ; un menu ouvert seul ne lance pas le fournisseur. Les abonnements DOM du plugin disparaissent avec sa page ; une UI remontée sans rechargement doit les retirer explicitement.