Fix : stopped stream bug
This commit is contained in:
@@ -35,6 +35,15 @@ interface AgentConnection {
|
||||
socket: WebSocket;
|
||||
lastSeenAt: number;
|
||||
pending: Map<string, PendingCommand>;
|
||||
/**
|
||||
* Un statut a été reçu depuis l'établissement de *cette* connexion.
|
||||
*
|
||||
* Le dernier statut connu survit à une coupure, mais `detachAgent` y force
|
||||
* « n'enregistre pas » — et au redémarrage du serveur il n'y en a aucun. Une
|
||||
* VM occupée passe alors pour libre le temps que l'agent se manifeste. Ce
|
||||
* drapeau distingue « la VM n'enregistre pas » de « on n'en sait encore rien ».
|
||||
*/
|
||||
statusReceived: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +69,12 @@ class Hub {
|
||||
}
|
||||
}
|
||||
|
||||
this.connections.set(agentId, { socket, lastSeenAt: Date.now(), pending: new Map() });
|
||||
this.connections.set(agentId, {
|
||||
socket,
|
||||
lastSeenAt: Date.now(),
|
||||
pending: new Map(),
|
||||
statusReceived: false,
|
||||
});
|
||||
this.statuses.set(agentId, this.statuses.get(agentId) ?? emptyStatus());
|
||||
this.publishAgent(agentId);
|
||||
}
|
||||
@@ -95,6 +109,8 @@ class Hub {
|
||||
}
|
||||
|
||||
updateStatus(agentId: string, status: AgentStatus): void {
|
||||
const connection = this.connections.get(agentId);
|
||||
if (connection) connection.statusReceived = true;
|
||||
this.statuses.set(agentId, status);
|
||||
this.markSeen(agentId);
|
||||
this.trackRecording(agentId, status);
|
||||
@@ -227,6 +243,18 @@ class Hub {
|
||||
return this.statuses.get(agentId) ?? emptyStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Le statut connu de cet agent est-il digne de confiance ?
|
||||
*
|
||||
* Faux tant que l'agent n'a rien rapporté sur sa connexion courante. Le
|
||||
* distinguer importe : après une coupure ou un redémarrage du serveur, le
|
||||
* statut retenu annonce « n'enregistre pas » sans que personne ne l'ait
|
||||
* vérifié — et un automatisme qui s'y fie écrase la capture en cours.
|
||||
*/
|
||||
hasFreshStatus(agentId: string): boolean {
|
||||
return this.connections.get(agentId)?.statusReceived === true;
|
||||
}
|
||||
|
||||
view(record: AgentRecord): AgentView {
|
||||
const online = this.isOnline(record.id);
|
||||
return {
|
||||
|
||||
@@ -29,11 +29,46 @@ export async function startTargetRecording(target: WatchTarget): Promise<void> {
|
||||
if (!record) throw new Error("L'agent assigné n'existe plus");
|
||||
if (!hub.isOnline(record.id)) throw new Error(`Agent « ${record.name} » hors-ligne`);
|
||||
|
||||
// Une VM n'enregistre qu'un flux à la fois. Écraser une capture en cours
|
||||
// reste refusé par défaut — sauf si ce profil a reçu le droit d'interrompre
|
||||
// et qu'il l'emporte en priorité, auquel cas on referme proprement avant de
|
||||
// relancer plutôt que de laisser les deux se marcher dessus.
|
||||
if (hub.statusOf(record.id).recording) {
|
||||
// L'état réel se demande à l'agent, il ne se lit pas dans le cache du hub :
|
||||
// celui-ci retombe à « n'enregistre pas » dès que la connexion se coupe (voir
|
||||
// `detachAgent`) et repart vide au redémarrage du serveur. Dans cette fenêtre,
|
||||
// une VM occupée passe pour libre — et c'est exactement ce qui écrasait une
|
||||
// capture en pause pour en rouvrir une seconde sur le même streamer.
|
||||
const live = (await liveRecordState(record)) ?? {
|
||||
// Sans réponse exploitable, on retombe sur le dernier statut connu : il vaut
|
||||
// mieux qu'aucune information, et le garde-fou côté veille empêche déjà
|
||||
// l'automatisme d'agir sur un statut qu'il sait périmé.
|
||||
active: hub.statusOf(record.id).recording,
|
||||
paused: hub.statusOf(record.id).recordPaused === true,
|
||||
};
|
||||
|
||||
if (live.active) {
|
||||
// Cette VM capture-t-elle déjà *ce* profil ? Sa veille porte le seul lien
|
||||
// fiable entre une capture en cours et un profil suivi.
|
||||
if (record.watch.username === target.username) {
|
||||
if (live.paused) {
|
||||
// Le cas visé : la capture existe toujours, seulement suspendue le temps
|
||||
// d'un show privé ou d'un passage hors-ligne. La reprendre garde le même
|
||||
// fichier ; la relancer en ouvrirait un second et perdrait le premier.
|
||||
await hub.sendCommand(record.id, 'record.resume');
|
||||
hub.log(
|
||||
record.id,
|
||||
'info',
|
||||
`Capture de « ${target.label ?? target.username} » déjà ouverte sur ${record.name} : ` +
|
||||
'reprise de la pause plutôt qu\'un nouvel enregistrement',
|
||||
Date.now(),
|
||||
'record.resumed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Déjà en train de capturer ce profil, et pas en pause : il n'y a
|
||||
// strictement rien à faire.
|
||||
return;
|
||||
}
|
||||
|
||||
// Une autre capture occupe la VM. L'écraser reste refusé par défaut — sauf
|
||||
// si ce profil a reçu le droit d'interrompre et qu'il l'emporte en priorité,
|
||||
// auquel cas on referme proprement avant de relancer.
|
||||
await preemptRecording(record, target);
|
||||
}
|
||||
|
||||
@@ -83,6 +118,30 @@ export async function startTargetRecording(target: WatchTarget): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* État d'enregistrement demandé à l'agent lui-même, ou `null` s'il n'a pas su
|
||||
* répondre.
|
||||
*
|
||||
* `obs.refresh` relit OBS et renvoie un instantané complet : c'est la seule
|
||||
* source qui ne peut pas être périmée, contrairement au dernier statut reçu.
|
||||
* Un échec (OBS injoignable, agent muet) n'est pas un « n'enregistre pas » —
|
||||
* d'où le `null`, que l'appelant traite comme une absence d'information.
|
||||
*/
|
||||
async function liveRecordState(
|
||||
record: AgentRecord,
|
||||
): Promise<{ active: boolean; paused: boolean } | null> {
|
||||
try {
|
||||
const snapshot = (await hub.sendCommand(record.id, 'obs.refresh')) as {
|
||||
recording?: boolean;
|
||||
recordPaused?: boolean;
|
||||
} | null;
|
||||
if (!snapshot || typeof snapshot.recording !== 'boolean') return null;
|
||||
return { active: snapshot.recording, paused: snapshot.recordPaused === true };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Referme la capture en cours au profit de `target`, ou refuse en expliquant.
|
||||
*
|
||||
|
||||
@@ -190,6 +190,14 @@ class Watchlist {
|
||||
|
||||
if (!hub.isOnline(target.agentId)) return;
|
||||
|
||||
// Tant que l'agent n'a rien rapporté sur sa connexion courante, on ne sait
|
||||
// pas ce qu'il fait — et « on ne sait pas » n'est pas « il est libre ». Le
|
||||
// statut retenu annonce pourtant « n'enregistre pas » après une coupure ou
|
||||
// un redémarrage du serveur : s'y fier faisait relancer une capture par
|
||||
// dessus une autre, bien vivante, du même streamer. Attendre le cycle
|
||||
// suivant ne coûte rien.
|
||||
if (!hub.hasFreshStatus(target.agentId)) return;
|
||||
|
||||
// VM occupée : par défaut la capture en place l'emporte, sauf si ce profil
|
||||
// a reçu le droit d'interrompre et le rang pour le faire. Le verdict est
|
||||
// recalculé par startTargetRecording — le refaire ici évite seulement de
|
||||
|
||||
Reference in New Issue
Block a user