Fix : stopped stream bug
This commit is contained in:
20
README.md
20
README.md
@@ -689,6 +689,26 @@ Plusieurs profils peuvent être en automatisme sur la même VM ; c'est la
|
|||||||
[priorité](#priorité-et-interruption) qui les départage. **Par défaut la capture en place
|
[priorité](#priorité-et-interruption) qui les départage. **Par défaut la capture en place
|
||||||
l'emporte** : un profil qui trouve sa VM occupée passe son tour sans rien écraser.
|
l'emporte** : un profil qui trouve sa VM occupée passe son tour sans rien écraser.
|
||||||
|
|
||||||
|
**Une capture déjà ouverte n'est jamais remplacée par une seconde.** Avant de démarrer, le
|
||||||
|
serveur demande son état réel à l'agent (`obs.refresh`) au lieu de lire le dernier statut reçu.
|
||||||
|
La nuance est décisive : ce statut retombe à « n'enregistre pas » dès qu'une connexion se coupe,
|
||||||
|
et repart vide au redémarrage du serveur. Dans cette fenêtre, une VM occupée passait pour libre
|
||||||
|
— et l'automatisme rouvrait une capture par-dessus une autre, bien vivante, **du même
|
||||||
|
streamer**. Le symptôme : un passage hors-ligne de quelques minutes, la capture mise en pause,
|
||||||
|
puis le retour en direct qui la coupait pour en recommencer une.
|
||||||
|
|
||||||
|
Selon ce que l'agent répond :
|
||||||
|
|
||||||
|
- **capture du même profil en pause** → elle est **reprise**, pas relancée. Le fichier reste le
|
||||||
|
même ; en ouvrir un second aurait perdu le premier ;
|
||||||
|
- **capture du même profil en cours** → rien à faire ;
|
||||||
|
- **capture d'un autre profil** → l'arbitrage de [priorité](#priorité-et-interruption) s'applique ;
|
||||||
|
- **aucune capture** → l'enregistrement démarre, comme attendu.
|
||||||
|
|
||||||
|
Second garde-fou en amont : tant qu'un agent n'a rien rapporté sur sa connexion courante,
|
||||||
|
l'automatisme s'abstient. « On ne sait pas encore » n'est pas « la VM est libre », et attendre
|
||||||
|
le cycle de veille suivant ne coûte rien.
|
||||||
|
|
||||||
**Trois tentatives par diffusion.** Les échecs restants une fois les garde-fous passés (OBS
|
**Trois tentatives par diffusion.** Les échecs restants une fois les garde-fous passés (OBS
|
||||||
injoignable, par exemple) sont surtout persistants ; au-delà, l'automatisme abandonne
|
injoignable, par exemple) sont surtout persistants ; au-delà, l'automatisme abandonne
|
||||||
jusqu'à la diffusion suivante et le dit dans le journal. Passer son tour faute de VM libre
|
jusqu'à la diffusion suivante et le dit dans le journal. Passer son tour faute de VM libre
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ interface AgentConnection {
|
|||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
lastSeenAt: number;
|
lastSeenAt: number;
|
||||||
pending: Map<string, PendingCommand>;
|
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.statuses.set(agentId, this.statuses.get(agentId) ?? emptyStatus());
|
||||||
this.publishAgent(agentId);
|
this.publishAgent(agentId);
|
||||||
}
|
}
|
||||||
@@ -95,6 +109,8 @@ class Hub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(agentId: string, status: AgentStatus): void {
|
updateStatus(agentId: string, status: AgentStatus): void {
|
||||||
|
const connection = this.connections.get(agentId);
|
||||||
|
if (connection) connection.statusReceived = true;
|
||||||
this.statuses.set(agentId, status);
|
this.statuses.set(agentId, status);
|
||||||
this.markSeen(agentId);
|
this.markSeen(agentId);
|
||||||
this.trackRecording(agentId, status);
|
this.trackRecording(agentId, status);
|
||||||
@@ -227,6 +243,18 @@ class Hub {
|
|||||||
return this.statuses.get(agentId) ?? emptyStatus();
|
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 {
|
view(record: AgentRecord): AgentView {
|
||||||
const online = this.isOnline(record.id);
|
const online = this.isOnline(record.id);
|
||||||
return {
|
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 (!record) throw new Error("L'agent assigné n'existe plus");
|
||||||
if (!hub.isOnline(record.id)) throw new Error(`Agent « ${record.name} » hors-ligne`);
|
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
|
// L'état réel se demande à l'agent, il ne se lit pas dans le cache du hub :
|
||||||
// reste refusé par défaut — sauf si ce profil a reçu le droit d'interrompre
|
// celui-ci retombe à « n'enregistre pas » dès que la connexion se coupe (voir
|
||||||
// et qu'il l'emporte en priorité, auquel cas on referme proprement avant de
|
// `detachAgent`) et repart vide au redémarrage du serveur. Dans cette fenêtre,
|
||||||
// relancer plutôt que de laisser les deux se marcher dessus.
|
// une VM occupée passe pour libre — et c'est exactement ce qui écrasait une
|
||||||
if (hub.statusOf(record.id).recording) {
|
// 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);
|
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.
|
* 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;
|
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
|
// 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
|
// 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
|
// recalculé par startTargetRecording — le refaire ici évite seulement de
|
||||||
|
|||||||
Reference in New Issue
Block a user