jeanotx32 8315e6a065
All checks were successful
release / build (push) Successful in 16s
release / verify-windows (push) Successful in 1m16s
fix : Firefox handling 3
2026-08-12 02:57:56 +02:00
2026-08-11 22:48:15 +02:00
2026-08-12 02:57:56 +02:00
2026-08-12 02:45:09 +02:00
2026-08-11 20:04:08 +02:00
2026-08-11 17:18:54 +02:00
2026-08-11 18:13:50 +02:00
2026-08-11 17:10:08 +02:00
2026-08-11 19:04:32 +02:00
2026-08-11 17:10:08 +02:00
2026-08-11 18:04:28 +02:00
2026-08-11 17:10:08 +02:00
FP
2026-08-11 00:26:56 +02:00
2026-08-12 02:45:09 +02:00
FP
2026-08-11 00:26:56 +02:00

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 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 (Stripchat), avec reprise et rappel du plein écran au retour du flux public.
  • Clôture différée sur passage hors-ligne : une coupure brève ne découpe pas le fichier, une vraie fin de diffusion le termine.
  • Enregistrement automatique d'un streamer sur une VM dédiée, dès qu'il est en direct depuis assez longtemps.
  • Presets d'enregistrement appliqués à OBS depuis l'interface, réglables VM par VM : de « Économe » à « Sans perte ».

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). Le mode historique exige en plus xdotool et une session X11.

Démarrage rapide (développement)

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 :

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 : interface en Docker Compose, agents en une commande, artefacts construits et publiés par Gitea Actions.

En résumé :

# 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://<IP_DU_CONTROLE>: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://<IP_DU_CONTROLE>: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 :

node /opt/stream-control-agent/agent.cjs --check

Configuration

Serveur — .env (voir .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.

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, dans cet ordre volontaire :

  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 ;
  4. si la page n'a pas bougé : requestFullscreen() sur l'élément <video>, avec userActivation ;
  5. relecture, et verdict rapporté dans l'historique de la VM.

Un profil Firefox dédié est obligatoire, pas cosmétique : le port de pilotage ne s'ouvre qu'au démarrage du processus, et deux instances ne peuvent pas partager un profil. L'agent en gère un et y réécrit un user.js à chaque lancement — chaque préférence y supprime quelque chose qui finirait dans le fichier enregistré, ou qui empêcherait un démarrage sans surveillance :

Préférence Pourquoi
full-screen-api.warning.timeout = 0 le bandeau « … est maintenant en plein écran » se retrouvait dans les premières secondes de chaque fichier
media.autoplay.default = 0 sans lecture automatique, l'agent enregistre une image fixe sans que rien ne le signale
browser.aboutwelcome.enabled = false, browser.startup.page = 0, … tout onglet d'accueil passerait devant la page du streamer
browser.sessionstore.resume_from_crash = false un dialogue modal bloquerait toute commande
app.update.auto = false une mise à jour fermerait la fenêtre que capture OBS, en plein enregistrement

Firefox en snap (Ubuntu)

Sur Ubuntu, firefox est un snap, et cela impose deux choses.

L'interface home d'un snap exclut délibérément les fichiers et répertoires cachés — ceux commençant par un point, réputés contenir des données sensibles. Un profil sous ~/.stream-control/ y est donc inaccessible, et Firefox meurt sans jamais ouvrir son port. L'agent détecte le paquet snap en suivant la chaîne de liens de la commande (/usr/bin/firefox/snap/bin/firefox/usr/bin/snap) et place alors son profil sous ~/snap/firefox/common/stream-control-profile. Un profil explicitement configuré dans un répertoire caché est refusé avant le lancement, avec l'explication.

Par ailleurs, firefox n'est ici qu'un lanceur : il passe la main au snap et sort aussitôt en code 0 pendant que le vrai navigateur démarre derrière. L'agent ne traite donc qu'une sortie non nulle comme un échec ; sinon il abandonnerait le cas nominal au bout de quelques centaines de millisecondes.

La sortie de Firefox est redirigée vers <profil>/firefox.log, et ses dernières lignes utiles accompagnent tout échec de démarrage — c'est le seul endroit où il explique son refus. Les lignes update.go:193: cannot change mount namespace … sont du bavardage systématique de snapd, sans rapport avec Firefox : elles sont comptées puis écartées de l'extrait, sans quoi elles chasseraient la vraie erreur.

Le démarrage laisse 150 s au navigateur. Ce n'est pas de la prudence : un snap lancé à froid sur une VM, avec un profil neuf à construire, dépasse couramment la minute. L'agent signale sa patience toutes les 20 s dans l'historique de la VM.

Verrou de profil bloqué

Le profil est exclusif à l'agent : personne n'y travaille à la main en temps normal. Un lancement qui échoue sans que le processus Firefox s'arrête (l'ancien comportement, avant correction) y laisse un verrou — et chaque tentative suivante se heurte alors au dialogue « Firefox est déjà ouvert », qui n'ouvre jamais le port de pilotage et bloque indéfiniment, y compris un lancement manuel de diagnostic sur ce même profil.

Deux garde-fous, l'un préventif et l'un correctif :

  • un lancement qui échoue termine désormais le processus qu'il venait de créer, plutôt que de le laisser tourner comme verrou pour la prochaine tentative ;
  • avant chaque lancement, l'agent identifie (sous Linux, via /proc/<pid>/cmdline) tout processus dont la ligne de commande référence exactement ce profil, le termine, puis efface le verrou. La correspondance porte sur le chemin exact du profil : rien d'autre ne peut être atteint par erreur.

Au premier passage en BiDi, une nouvelle fenêtre Firefox s'ouvre : pointe la source « capture de fenêtre » d'OBS dessus une fois. Ensuite elle survit aux enregistrements comme aux redémarrages de l'agent — le processus est lancé détaché, et l'agent se rattache au port plutôt que de relancer.

Si le plein écran est refusé

Firefox refuse le plein écran à un document dont la fenêtre n'a pas le focus. C'est la première cause d'échec sur une VM : il suffit qu'OBS ou un terminal l'ait pris. L'agent relit alors trois conditions depuis la page et nomme celle qui manque, plutôt que de répercuter un « Fullscreen request denied » qui ne dit rien :

Message Cause
la fenêtre Firefox n'a pas le focus une autre fenêtre est active dans la session
aucun élément vidéo dans la page le lecteur n'a pas fini de charger — augmente le délai avant plein écran
l'API plein écran est désactivée dans ce profil full-screen-api.enabled forcé à faux

Lancement simple

Conservé comme défaut pour ne pas changer le comportement d'un agent existant à la mise à jour. 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
Le cookie X est résolu à l'exécution : $XAUTHORITY, puis /run/user/<uid>/gdm/Xauthority, les cookies Xwayland, puis ~/.Xauthority
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)

La fenêtre du lecteur passe au premier plan : les navigateurs ignorent les évènements clavier synthétiques envoyés sans focus (XSendEvent).

Le titre de fenêtre à renseigner est celui de la fenêtre, pas le nom du processus. Sous Firefox il vaut <titre de la page> — Mozilla Firefox, et le titre d'une page Stripchat se termine par | Stripchat : Stripchat comme Firefox conviennent donc. En cas d'échec, l'agent liste les fenêtres qu'il voit réellement, et agent.cjs --check fait de même sans rien déclencher.

Symptôme Cause
Authorization required / Invalid MIT-MAGIC-COOKIE-1 key cookie X introuvable ou périmé
Session Wayland : seule la fenêtre technique du compositeur est visible voir ci-dessous
Aucune fenêtre visible sur DISPLAY=:0 navigateur lancé hors de la session de l'agent
Liste des fenêtres ouvertes titre mal renseigné — recopier un fragment de la liste

Le bouton ⛶ sur la fiche de l'agent renvoie la touche à la demande, et ⟳ force une sonde immédiate.

Wayland

Ubuntu démarre en session Wayland par défaut, et Firefox y tourne en client Wayland natif. Une telle fenêtre est invisible à xdotool : ni activation, ni envoi de touche. Le symptôme est net — côté X11, seule mutter guard window apparaît, la fenêtre technique du compositeur. L'agent reconnaît cette signature et le dit explicitement.

La réponse est le mode WebDriver BiDi, qui ne passe pas par le serveur d'affichage.

Les contournements d'avant sont conservés ici pour mémoire, mais ils coûtent tous quelque chose et aucun n'est à préférer :

  • forcer Firefox sous XWayland (MOZ_ENABLE_WAYLAND=0) ajoute une copie d'image par trame ;
  • basculer la session en Xorg fait chuter les performances de capture (voir « Xorg, Wayland et le coût de la capture »).

Presets d'enregistrement

Chaque VM peut être réglée sur un compromis qualité / charge CPU différent, depuis la fiche de l'agent (⚙ → « Preset d'enregistrement »). Tant que la case n'est pas cochée, l'agent ne touche à rien et OBS garde sa configuration manuelle.

Preset Sortie Qualité Conteneur CPU
Économe 720p 30 fps haute (CRF ~23) MP4
Équilibré 1080p 30 fps très haute (CRF ~16) MP4 ●●
Qualité maximale définition de la scène, 60 fps très haute, encodage lent MKV ●●●
Sans perte définition de la scène, 60 fps aucune perte MKV ●●●●

L'encodeur se choisit séparément : x264 (le seul disponible sur un VPS sans GPU), NVENC, Quick Sync, AMF ou VideoToolbox. Le preset fournit la valeur de vitesse adaptée à la famille retenue — veryfast pour x264, p5 pour NVENC, etc.

Ce qui se passe à l'application

L'agent écrit dans le profil OBS courant (SetProfileParameter, section SimpleOutput) puis ajuste la sortie vidéo (SetVideoSettings). Trois comportements à connaître :

  • La qualité prend effet au prochain démarrage d'enregistrement. OBS relit ces paramètres à ce moment-là ; l'interface d'OBS, elle, ne les rafraîchit qu'au changement de profil.
  • Changer d'encodeur exige un redémarrage d'OBS. L'objet encodeur n'est instancié qu'au lancement. L'agent le détecte et le signale dans son compte rendu.
  • Un preset n'est jamais appliqué pendant une capture : cela la corromprait. La demande est refusée, ou différée jusqu'à l'arrêt de l'enregistrement.

Le mode de sortie du profil est forcé sur « Simple » : c'est la section que ces réglages pilotent. Un paramètre refusé par la version d'OBS installée n'interrompt pas les autres — il apparaît dans le compte rendu affiché sous le bouton « Appliquer maintenant ».

Diagnostiquer une chute de performance

La fiche de l'agent sépare les deux goulots qu'OBS mesure distinctement. Les confondre mène à régler le mauvais paramètre.

Métrique Ce qu'elle dit Leviers
Perdu · rendu La scène n'a pas été composée à temps Méthode de capture, définition de la scène, accélération 3D du bureau
Perdu · encodage La scène était prête, l'encodeur n'a pas suivi Preset plus léger, encodeur matériel
Rendu (ms) Durée moyenne de composition d'une image Doit rester bien sous 1000 / fps (33 ms à 30 fps)

Les compteurs cumulent depuis le lancement d'OBS, d'où l'affichage en pourcentage : sous 1 %, il n'y a rien à faire ; au-delà de quelques pour cent, le poste indiqué est saturé.

Changer de preset ou d'encodeur ne corrige que la colonne encodage. Si c'est le rendu qui décroche, le problème est en amont d'OBS.

Xorg, Wayland et le coût de la capture

Sur une VM sans accélération 3D, la méthode de capture pèse lourd :

  • Wayland : OBS passe par PipeWire, le compositeur lui remet les images déjà composées.
  • Xorg : la capture d'écran XSHM recopie tout le tampon à chaque image. La capture de fenêtre XComposite est nettement plus économe — à préférer systématiquement.

Xorg n'était nécessaire que pour le rappel du plein écran par xdotool. Le mode de pilotage « WebDriver BiDi » supprime cette contrainte : Wayland est le bon choix, en performance comme en simplicité.

Un preset trop lourd pour la VM fait chuter les images par seconde : la qualité perçue baisse alors malgré un meilleur CRF. Après un changement, surveille « FPS » et les deux colonnes de frames perdues sur la fiche de l'agent.

Historique par VM

Le journal en bas de page mélange toutes les machines. Le bouton 🕘 de la fiche d'un agent ouvre son historique à lui : mêmes entrées, filtrées sur cette VM, groupées par jour et du plus récent au plus ancien.

Chaque entrée porte un type d'évènementrecord.paused, obs.disconnected, fullscreen.restored, preset.applied… — qui lui donne son pictogramme et sa couleur, et qui alimente les filtres : Enregistrement, OBS, Surveillance, Problèmes.

Ce champ est facultatif dans le protocole. Une entrée écrite avant cette version, ou envoyée par un agent qui n'a pas encore été mis à jour, s'affiche sans pictogramme plutôt que de disparaître.

Les pauses détectées valent d'être soulignées : elles proviennent de l'évènement RecordStateChanged d'OBS, pas de la commande envoyée. L'historique montre donc aussi les pauses déclenchées depuis l'interface d'OBS sur la VM, que le dashboard n'aurait aucun autre moyen de connaître.

La rétention est celle du journal global (LOG_RETENTION) : les entrées les plus anciennes sont purgées, toutes VM confondues.

Statuts Stripchat

Le champ autoritatif est user.user.status. Deux familles, deux traitements :

Statut brut Signification Traitement
public diffusion publique enregistré
private, p2p, groupShow, virtualPrivate, ticketShow show payant pause, sans clôture
idle « revient bientôt » — connecté mais ne diffuse pas pause, sans clôture
off, notFound, deleted déconnecté pause puis clôture différée

idle se distingue de off par isLive: false mais isOnline: true : le streamer est toujours là, il s'est simplement absenté. Le flux revient, donc l'enregistrement se met en pause et attend, exactement comme pendant un show privé — il ne se clôt pas.

Ne pas confondre avec offlineStatus, un champ voisin : c'est le message d'absence libre du modèle (« I'll be back soon »), qui reste renseigné pendant qu'il diffuse. Il ne dit rien de l'état courant et n'est pas utilisé.

La liste des statuts mis en pause est modifiable par agent, dans « Surveillance du stream ». Retirer groupShow pour continuer à enregistrer les shows de groupe, par exemple. Les agents créés avant l'ajout d'idle sont migrés au démarrage du serveur, sauf si leur liste a été personnalisée.

Passage hors-ligne

Quand le statut brut passe à off, deux temps distincts :

  1. Pause immédiate. L'écran d'attente n'a rien à faire dans le fichier. Aucun délai, aucune confirmation : reprendre ne coûte rien si la lecture était fausse.
  2. Clôture différée, après le délai réglé sur la fiche de l'agent (1 h par défaut). Une coupure de quelques minutes est fréquente ; clore tout de suite découperait le fichier en deux. Le compte à rebours est annulé dès que le flux revient, public ou privé, et l'enregistrement reprend dans le même fichier.

Le décompte s'affiche sur la fiche de l'agent. La clôture ferme aussi la fenêtre du navigateur si son pilotage est activé, contrairement à une simple pause.

Régler le délai à 0 clôt dès la première lecture hors-ligne ; décocher « Clore l'enregistrement quand le flux passe hors-ligne » désactive les deux temps, y compris la pause.

Enregistrement automatique

Sur la fiche d'un streamer (onglet Streamers), assigne un agent puis coche Enregistrer automatiquement. Dès que le profil est en direct depuis AUTO_RECORD_DELAY_MS (60 s par défaut), le serveur lance la capture sur cet agent, exactement comme le bouton « Enregistrer » — surveillance des shows privés comprise.

Trois règles cadrent l'automatisme :

  • Un streamer par VM. Activer l'automatisme sur un agent déjà pris est refusé, avec le nom du profil qui l'occupe.
  • Aucune préemption. Si la VM enregistre déjà quoi que ce soit, l'automatisme passe son tour plutôt que d'écraser la capture en cours.
  • Trois tentatives par diffusion. Les échecs restants une fois ces garde-fous passés (OBS injoignable, par exemple) sont surtout persistants ; au-delà, l'automatisme abandonne jusqu'à la diffusion suivante et le dit dans le journal.

Le délai d'amorçage n'est pas une précaution de style : un modèle qui sort d'un show privé repasse « public » quelques secondes avant de se remettre en place. Déclencher sur la première lecture produirait des fichiers de dix secondes.

Capture de fenêtre OBS et réutilisation

La source « capture de fenêtre » d'OBS mémorise un identifiant de fenêtre X11. Fermer la fenêtre du navigateur, ou en ouvrir une nouvelle, invalide cet identifiant : la source devient noire et il faut la repointer à la main.

En mode WebDriver BiDi le problème disparaît : l'agent ne relance jamais de processus, il navigue dans l'onglet qu'il pilote. La fenêtre est ouverte une fois et survit à tout, y compris à un redémarrage de l'agent — celui-ci se rattache au port au lieu de relancer. La seule fois où il faut repointer la source OBS est le passage initial en BiDi.

En mode lancement simple, l'agent évite les deux pièges :

  • aucun argument par défaut (--new-window est proscrit) : Firefox confie l'URL à la fenêtre déjà ouverte ;
  • à l'arrêt, page vide plutôt que fermeture : la fenêtre survit, et le lecteur cesse de décoder la vidéo.

Les agents configurés avant ce correctif sont migrés au démarrage du serveur, sauf si leurs arguments ou leur comportement d'arrêt ont été personnalisés.

« Firefox est déjà ouvert »

Propre au mode lancement simple : ce dialogue signifie que le processus lancé par l'agent n'a pas trouvé l'instance déjà en cours, et bute sur le verrou de profil au lieu de lui confier l'URL. Le mode BiDi ne peut pas le rencontrer — il n'y a qu'une instance, sur un profil qui n'appartient qu'à l'agent.

Le passage de relais se fait par le bus de session D-Bus — le seul mécanisme disponible sous Wayland, le protocole X de remoting n'y existant pas. Or un service systemd « system » n'hérite pas de ce bus, pas plus qu'il n'hérite du cookie X. L'agent résout donc lui-même, à chaque lancement :

Variable Origine
DISPLAY valeur héritée, sinon la socket X présente dans /tmp/.X11-unix
XAUTHORITY $XAUTHORITY s'il existe, puis GDM, Xwayland, ~/.Xauthority
XDG_RUNTIME_DIR valeur héritée, sinon /run/user/<uid>
DBUS_SESSION_BUS_ADDRESS valeur héritée, sinon la socket $XDG_RUNTIME_DIR/bus
WAYLAND_DISPLAY valeur héritée, sinon la socket wayland-<n> du répertoire d'exécution

agent.cjs --check les affiche toutes. Si le bus ressort en avertissement, vérifie que l'agent tourne bien sous le même compte que la session graphique : l'utilisateur est choisi par --user à l'installation.

Éviter l'accumulation d'onglets

Propre lui aussi au mode lancement simple ; en BiDi l'agent navigue toujours dans le même onglet.

Avec les réglages d'usine de Firefox, un lien venu de l'extérieur ouvre un nouvel onglet : chaque cycle d'enregistrement en laisse donc derrière lui, et les anciens continuent de décoder leur page. Sur une VM d'enregistrement, règle une fois pour toutes dans about:config :

browser.link.open_newwindow = 1

L'URL remplace alors le contenu de l'onglet courant. Ouverture et page vide réutilisent le même onglet, dans la même fenêtre — rien ne s'accumule et OBS ne perd jamais sa cible.

Dans OBS, règle aussi la priorité de correspondance de la source sur « Faire correspondre le titre, sinon trouver une fenêtre du même type » : le titre suit l'onglet actif et change à chaque streamer.

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, surveillance, navigateur, preset
PATCH /api/watchlist/:id Libellé, agent assigné, notifications, automatisme
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, toutes VM confondues
GET /api/agents/:id/logs?limit= Historique d'une VM, ordre chronologique
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, browser.open, browser.close, capture.start, capture.stop, preset.apply, agent.update, agent.ping.

Structure

Paquet Rôle
packages/shared/ Types du protocole, partagés par les trois autres
packages/server/ API REST, passerelle agents, diffusion dashboard, SQLite
packages/agent/ Binaire à déployer sur chaque VM, pilote OBS
packages/web/ Dashboard React/Vite

Points d'entrée utiles : packages/shared/src/index.ts (le protocole), packages/server/src/hub.ts (état central et dispatch), packages/agent/src/obs.ts (traduction action → obs-websocket), packages/agent/src/watcher.ts (sonde de statut et machine à états pause/reprise), 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.

Description
No description provided
Readme 2.4 MiB
Languages
TypeScript 82.7%
JavaScript 8%
CSS 4.6%
Shell 2.3%
PowerShell 1.8%
Other 0.5%