Files
stream-control/README.md
jeanotx32 105d07b82b
Some checks failed
release / build (push) Failing after 45s
release / verify-windows (push) Has been skipped
CI : Deployment
2026-08-11 17:10:08 +02:00

228 lines
11 KiB
Markdown

# 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.
- 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.
## 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 : `xdotool` et une session X11
(voir [Prérequis pour le rappel du plein écran](#prérequis-pour-le-rappel-du-plein-écran)).
## 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 <http://localhost:5173>.
## 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 <gitea>/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
| sudo bash -s -- --registry <gitea> --server ws://control.lan:8080/ws/agent --token <JETON>
# Agent Windows (PowerShell administrateur)
& ([scriptblock]::Create((irm '<gitea>/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) `
-Registry '<gitea>' -Server 'ws://control.lan:8080/ws/agent' -Token '<JETON>'
```
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 |
| `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.
### Prérequis pour le rappel du plein écran
L'envoi de touche se fait au niveau du système, pas via OBS.
| OS | Mécanisme | À prévoir |
| --- | --- | --- |
| Ubuntu | `xdotool windowactivate` + XTEST | `apt install xdotool`, session **X11** (pas Wayland), `DISPLAY` accessible à l'agent |
| Windows | `SetForegroundWindow` + `SendKeys` | L'agent doit tourner dans la session interactive — d'où la tâche planifiée plutôt qu'un service |
| macOS | AppleScript System Events | Autorisation Accessibilité (prévu pour le développement) |
Dans les deux cas la fenêtre du lecteur passe **au premier plan** : les navigateurs
ignorent les évènements clavier synthétiques envoyés sans focus (`XSendEvent`). Sans
conséquence sur une VM d'enregistrement dédiée, gênant si quelqu'un s'en sert en même
temps.
Le bouton ⛶ sur la fiche de l'agent renvoie la touche à la demande, et ⟳ force une sonde
immédiate — les deux servent à valider le titre de fenêtre sans attendre un vrai show privé.
Si le rappel du plein écran s'avère fragile sur ta VM, l'alternative sans clavier est de
lancer le navigateur en mode kiosque (`chromium --kiosk`) : il n'y a alors plus de plein
écran à restaurer.
## API HTTP
Toutes les routes hors `/api/login` exigent `Authorization: Bearer <jeton de session>`.
| Méthode | Route | Rôle |
| --- | --- | --- |
| `POST` | `/api/login` | Ouvre une session (`{ password }`) |
| `GET` | `/api/agents` | Liste des agents et de leur état |
| `POST` | `/api/agents` | Provisionne un agent, renvoie son jeton **une seule fois** |
| `PATCH` | `/api/agents/:id` | Nom, notes, paramètres OBS, auto-connexion |
| `POST` | `/api/agents/:id/token` | Régénère le jeton (coupe la session en cours) |
| `DELETE` | `/api/agents/:id` | Supprime l'agent |
| `POST` | `/api/agents/:id/command` | `{ action, params }`, attend le résultat de l'agent |
| `POST` | `/api/commands/bulk` | Même action sur plusieurs agents, résultat par agent |
| `GET` | `/api/logs?limit=` | Journal récent |
| `GET` | `/healthz` | Sonde de vie (non authentifiée) |
Actions disponibles : `obs.connect`, `obs.disconnect`, `obs.refresh`, `record.start`,
`record.stop`, `record.pause`, `record.resume`, `record.split`, `stream.start`,
`stream.stop`, `scene.set`, `profile.set`, `collection.set`, `recordDirectory.set`,
`watch.check`, `hotkey.fullscreen`, `agent.ping`.
## Structure
| Paquet | Rôle |
| --- | --- |
| [packages/shared/](packages/shared/) | Types du protocole, partagés par les trois autres |
| [packages/server/](packages/server/) | API REST, passerelle agents, diffusion dashboard, SQLite |
| [packages/agent/](packages/agent/) | Binaire à déployer sur chaque VM, pilote OBS |
| [packages/web/](packages/web/) | Dashboard React/Vite |
Points d'entrée utiles : [packages/shared/src/index.ts](packages/shared/src/index.ts)
(le protocole), [packages/server/src/hub.ts](packages/server/src/hub.ts) (état central et
dispatch), [packages/agent/src/obs.ts](packages/agent/src/obs.ts) (traduction
action → obs-websocket), [packages/agent/src/watcher.ts](packages/agent/src/watcher.ts)
(sonde de statut et machine à états pause/reprise),
[packages/agent/src/hotkey.ts](packages/agent/src/hotkey.ts) (envoi de touche par OS).
## Sécurité
- Les jetons d'agent ne sont stockés qu'en SHA-256 ; le clair n'est affiché qu'à la création.
- Le mot de passe obs-websocket n'est jamais renvoyé au navigateur (masqué en `********`).
- Les sessions dashboard sont des jetons HMAC à durée limitée, sans état serveur.
- **Sers l'application en HTTPS/WSS** : jetons d'agent et de session circulent dans les
en-têtes et les URL de WebSocket.
- Après un `POST /api/agents/:id/token`, l'agent est déconnecté jusqu'à ce que son
`agent.config.json` soit mis à jour.
## Pistes d'évolution
Enregistrements programmés (cron par agent), rapatriement automatique des fichiers
(rclone/S3 déclenché à `record.stop`), alertes sur seuil d'espace disque ou de frames
perdues, comptes utilisateurs multiples, groupes d'agents.