Feat : added and tweaked state machine
All checks were successful
release / build (push) Successful in 39s
release / verify-windows (push) Successful in 1m17s

This commit is contained in:
jeanotx32
2026-08-11 23:21:35 +02:00
parent a5777ad920
commit 2380196169
14 changed files with 469 additions and 53 deletions

View File

@@ -3,6 +3,15 @@ import { DEFAULT_WATCH_SETTINGS, fetchStripchatStatus } from '@stream-control/sh
import { config } from './config.ts';
import { targetsRepo } from './db.ts';
import { hub } from './hub.ts';
import { startTargetRecording } from './recorder.ts';
/**
* Tentatives d'amorçage automatique par diffusion. Les causes d'échec restantes
* une fois les garde-fous passés (OBS injoignable, par exemple) sont surtout
* persistantes : réessayer indéfiniment toutes les dix secondes noierait le
* journal sans rien changer.
*/
const MAX_AUTO_ATTEMPTS = 3;
/**
* Sonde périodiquement les profils surveillés et signale les passages en direct.
@@ -14,6 +23,8 @@ import { hub } from './hub.ts';
class Watchlist {
private timer: NodeJS.Timeout | null = null;
private running = false;
/** Tentatives déjà faites pour la diffusion en cours, par profil. */
private readonly autoAttempts = new Map<string, number>();
start(): void {
if (this.timer) return;
@@ -102,6 +113,7 @@ class Watchlist {
if (!updated) return;
hub.publishTarget(updated);
this.maybeAutoRecord(updated);
if (changed) {
const name = updated.label ?? updated.username;
@@ -113,6 +125,67 @@ class Watchlist {
}
}
}
/**
* Déclenche l'enregistrement d'un profil en automatisme, une fois qu'il est en
* direct depuis assez longtemps.
*
* 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, et un flux qui redémarre alterne parfois plusieurs fois. Enregistrer
* sur la première lecture produirait des fichiers de dix secondes.
*/
private maybeAutoRecord(target: WatchTarget): void {
if (target.state !== 'public') {
// Diffusion terminée : la suivante aura droit à ses propres tentatives.
this.autoAttempts.delete(target.id);
return;
}
if (!target.autoRecord || !target.agentId) return;
if ((this.autoAttempts.get(target.id) ?? 0) >= MAX_AUTO_ATTEMPTS) return;
// `statusChangedAt` vient de la plateforme et vaut mieux que notre première
// observation : il survit à un redémarrage du serveur.
const liveSince = target.statusChangedAt ?? target.stateSince;
if (Date.now() - liveSince < config.autoRecordDelayMs) return;
if (!hub.isOnline(target.agentId)) return;
// Une VM déjà occupée n'est jamais préemptée : l'enregistrement en cours
// prime sur celui qu'on allait lancer.
if (hub.statusOf(target.agentId).recording) return;
const agentId = target.agentId;
const name = target.label ?? target.username;
// Marqué avant l'appel : la séquence de capture dure plusieurs secondes, et
// le cycle suivant ne doit pas en lancer une seconde en parallèle.
const attempt = (this.autoAttempts.get(target.id) ?? 0) + 1;
this.autoAttempts.set(target.id, attempt);
void startTargetRecording(target)
.then(() => {
this.autoAttempts.set(target.id, MAX_AUTO_ATTEMPTS);
hub.log(
agentId,
'info',
`Automatisme : enregistrement de ${name} démarré`,
Date.now(),
'capture.started',
);
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
const giveUp = attempt >= MAX_AUTO_ATTEMPTS;
hub.log(
agentId,
'warn',
`Automatisme : démarrage de ${name} en échec (${attempt}/${MAX_AUTO_ATTEMPTS}) — ${message}` +
(giveUp ? '. Abandon jusqu\'à la prochaine diffusion.' : ''),
Date.now(),
'command.failed',
);
});
}
}
function labelOf(state: StreamState): string {