# Stream Control Plan de contrôle web pour piloter des enregistrements OBS répartis sur plusieurs VM/VPS (Windows et Ubuntu) depuis une seule interface. ``` ┌────────────┐ HTTPS + WS ┌──────────────────┐ WS sortant ┌──────────────────┐ │ Navigateur │ ───────────────►│ Serveur de │◄────────────────│ Agent (VM) │ │ dashboard │◄─── temps réel ─│ contrôle │──── commandes ─►│ └► OBS (4455) │ └────────────┘ │ Express + SQLite│ └──────────────────┘ └──────────────────┘ ┌──────────────────┐ ◄────────────────│ Agent (VM) … │ └──────────────────┘ ``` **Le sens des connexions est le point clé** : ce sont les agents qui se connectent au serveur, pas l'inverse. Aucun port entrant à ouvrir sur les VM d'enregistrement, aucune IP publique nécessaire, et obs-websocket reste sur `127.0.0.1`. ## Fonctionnalités - Découverte automatique des agents par jeton d'enrôlement, ou provisionnement manuel. - État temps réel par VM : OBS connecté, enregistrement en cours, durée, taille du fichier, scène active, FPS, frames perdues, CPU, espace disque restant. - Commandes unitaires ou groupées : démarrer/arrêter/mettre en pause/découper un enregistrement, démarrer/arrêter un stream, changer de scène, de profil, de collection, changer le dossier d'enregistrement. - Journal d'évènements horodaté, persistant et diffusé en direct, plus un [historique par VM](#historique-par-vm) filtrable (🕘 sur la fiche de l'agent). - Reconnexion automatique de bout en bout (agent → serveur, agent → OBS, dashboard → serveur). - [Pause automatique pendant les shows privés](#pause-automatique-pendant-les-shows-privés) (Stripchat), avec reprise et rappel du plein écran au retour du flux public. - [Clôture différée sur passage hors-ligne](#passage-hors-ligne) : une coupure brève ne découpe pas le fichier, une vraie fin de diffusion le termine. - [Enregistrement automatique](#enregistrement-automatique) d'un streamer sur une VM dédiée, dès qu'il est en direct depuis assez longtemps, avec [priorité et interruption](#priorité-et-interruption) quand plusieurs profils se disputent la même machine. - [Presets d'enregistrement](#presets-denregistrement) appliqués à OBS depuis l'interface, réglables VM par VM : de « Économe » à « Sans perte ». - [Vignette du flux en direct](#vignette-du-flux-en-direct) sur la fiche compacte de chaque profil en direct, agrandie au survol. - [Étiquettes](#étiquettes) libres sur les streamers, créées à la volée, avec filtrage de la liste par étiquette. - [Frise des diffusions](#frise-des-diffusions) : l'historique des streams de chaque profil suivi, avec en surimpression ce qui en a été capturé — par profil sur sa fiche, ou tous ensemble dans l'onglet « Timeline ». Une [vignette est conservée par diffusion](#une-vignette-par-diffusion-conservée), visible au survol des mois après. ## Prérequis - Node.js **22+** sur le serveur et sur chaque VM (le serveur utilise `node:sqlite`). - OBS **28+** sur chaque VM, avec *Outils → Paramètres du serveur WebSocket* activé. Note le port (4455 par défaut) et le mot de passe. - Pour le rappel du plein écran sous Ubuntu : Firefox, en mode de pilotage « WebDriver BiDi » (voir [Deux façons de piloter le navigateur](#deux-façons-de-piloter-le-navigateur)). Le mode historique exige en plus `xdotool` et une session X11. ## Démarrage rapide (développement) ```bash npm install cp .env.example .env # renseigne ADMIN_PASSWORD, SESSION_SECRET, ENROLLMENT_TOKEN npm run dev # serveur :8080 + dashboard Vite :5173 ``` Puis, dans un autre terminal, un agent local : ```bash cd packages/agent cp agent.config.example.json agent.config.json # colle le ENROLLMENT_TOKEN dans "token" npm run dev ``` L'agent s'enrôle, reçoit son jeton permanent, le réécrit dans `agent.config.json`, et apparaît dans le dashboard sur . ## Déploiement La procédure complète est dans **[DEPLOY.md](DEPLOY.md)** : interface en Docker Compose, agents en une commande, artefacts construits et publiés par Gitea Actions. En résumé : ```bash # Interface (machine du plan de contrôle) docker compose up -d # Agent Ubuntu curl -fsSL /api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \ | sudo bash -s -- --registry --server ws://:8080/ws/agent --token # Agent Windows (PowerShell administrateur) & ([scriptblock]::Create((irm '/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) ` -Registry '' -Server 'ws://:8080/ws/agent' -Token '' ``` Les agents ne sont volontairement pas conteneurisés : ils doivent voir l'OBS local et la fenêtre du navigateur. Pour vérifier une installation à tout moment : ```bash node /opt/stream-control-agent/agent.cjs --check ``` ## Configuration ### Serveur — `.env` (voir [.env.example](.env.example)) | Variable | Rôle | | --- | --- | | `PORT`, `HOST` | Écoute HTTP/WebSocket | | `ADMIN_PASSWORD` | Mot de passe du dashboard | | `SESSION_SECRET` | Clé HMAC des sessions (32+ octets aléatoires) | | `ENROLLMENT_TOKEN` | Jeton d'auto-enregistrement ; vide = désactivé | | `DB_PATH` | Fichier SQLite | | `PREVIEWS_DIR` | Vignettes de diffusion conservées, une par stream | | `PREVIEW_DELAY_MS` | Attente avant de copier la vignette d'un stream qui démarre | | `STATUS_INTERVAL_MS` | Fréquence de remontée d'état des agents | | `AGENT_TIMEOUT_MS` | Délai avant de déclarer un agent hors-ligne | ### Agent — `agent.config.json` (ou variables d'environnement) | Clé | Variable | Rôle | | --- | --- | --- | | `serverUrl` | `SERVER_URL` | `wss://control.exemple.com/ws/agent` | | `token` | `AGENT_TOKEN` | Jeton d'enrôlement puis jeton propre à l'agent | | `name` | `AGENT_NAME` | Nom affiché (défaut : hostname) | | `obs.host/port/password` | `OBS_HOST` / `OBS_PORT` / `OBS_PASSWORD` | Repli local ; le serveur pousse la config de référence | | `insecureTls` | `INSECURE_TLS=1` | Accepter un certificat auto-signé | Les paramètres OBS édités dans le dashboard sont poussés à chaud vers l'agent : pas besoin de se connecter à la VM pour changer un mot de passe obs-websocket. ## Pause automatique pendant les shows privés Quand un streamer bascule en show privé, le flux public est remplacé par un écran d'attente : l'enregistrement continue mais ne capte plus rien d'utile, et le lecteur sort du plein écran. L'agent peut surveiller le statut du streamer et réagir seul. Configuration par agent, dans *Configuration → Surveillance du stream* : | Réglage | Effet | | --- | --- | | Pseudo du streamer | Celui de l'URL de sa page | | Intervalle de sonde | Fréquence d'interrogation de l'API (10 s par défaut, plancher 3 s) | | Lectures avant pause | Lectures « privé » consécutives exigées avant d'agir (2 par défaut) | | Statuts « privé » | Statuts déclenchant la pause — retire `groupShow` pour continuer à enregistrer les shows de groupe | | Touche / fenêtre / délai | Raccourci plein écran à renvoyer au lecteur après le show | ### Ce que fait l'agent 1. Il interroge `GET /api/front/v2/models/username/{pseudo}/cam` et lit `user.user.status`. Valeurs relevées en production : `public`, `private`, `p2p`, `groupShow`, `idle`. 2. Statut privé confirmé → `PauseRecord`, en mémorisant que **c'est lui** qui a mis en pause. 3. Retour au public → `ResumeRecord`, puis envoi de la touche plein écran après le délai configuré, le temps que le lecteur ait rechargé le flux. Trois garde-fous, parce qu'une automatisation qui coupe un enregistrement au mauvais moment coûte plus cher que quelques secondes d'écran d'attente enregistrées : - **La pause exige plusieurs lectures consécutives, la reprise agit immédiatement.** Une fausse pause perd du contenu réel ; une fausse reprise ne coûte rien. - **Une sonde en échec ne déclenche jamais rien.** API injoignable ou réponse inattendue : l'agent conserve le dernier état connu et ne touche pas à l'enregistrement. - **Une pause manuelle n'est jamais reprise automatiquement.** L'agent ne reprend que ce qu'il a lui-même mis en pause. Un passage `hors-ligne` (`idle`) ne provoque ni pause ni reprise : seul le retour effectif du flux public relance l'enregistrement. ### Deux façons de piloter le navigateur Le rappel du plein écran dépend entièrement de ce choix, qui se règle par agent dans « Pilotage du navigateur ». | | **WebDriver BiDi** (recommandé) | **Lancement simple** (historique) | | --- | --- | --- | | Ouverture d'une page | commande dans l'instance pilotée | un processus lancé par page | | Plein écran | commande WebDriver adressée à Firefox | touche envoyée au serveur d'affichage | | Effet vérifié ? | oui — `document.fullscreenElement` est relu | non | | Session Wayland | **fonctionne** | impossible (voir plus bas) | | Dépendances | Firefox | xdotool + session X11 | #### WebDriver BiDi Firefox expose ce protocole dès qu'on le lance avec `--remote-debugging-port` : ni geckodriver, ni Selenium, une simple WebSocket JSON sur la boucle locale. L'agent implémente le strict nécessaire (`bidi.ts`, `firefox.ts`). Le renversement est là : **l'agent possède le navigateur** au lieu de lui envoyer des URL en espérant. Il parle à Firefox, pas au serveur d'affichage — d'où le fonctionnement identique sous Wayland, où xdotool ne voit rien. Séquence du rappel de plein écran : 1. `browsingContext.activate` — sans quoi la touche partirait vers un onglet d'arrière-plan ; 2. `input.performActions` — la touche configurée (`f` par défaut), délivrée à la page comme un vrai évènement (`isTrusted: true`), donc traitée par le lecteur du site ; 3. relecture de `document.fullscreenElement`, et verdict rapporté dans l'historique de la VM. Une seule voie, volontairement, sans repli. Une version antérieure appelait `requestFullscreen()` sur l'élément `