Feat : detection de fullscreen
All checks were successful
release / build (push) Successful in 30s
release / verify-windows (push) Successful in 2m57s

This commit is contained in:
jeanotx32
2026-08-13 14:05:49 -04:00
parent a1295003fd
commit 00535d5e19
4 changed files with 366 additions and 25 deletions

View File

@@ -33,7 +33,7 @@ import { ObsController } from './obs.ts';
import { StreamWatcher } from './watcher.ts';
import { currentBuildId, runningBundlePath, selfUpdate } from './updater.ts';
import { blankPage, closeWindow, delay as sleep, openUrl } from './browser.ts';
import { FirefoxController, type StreamQualityOutcome } from './firefox.ts';
import { FirefoxController, describeMetrics, type StreamQualityOutcome } from './firefox.ts';
import { describeFullscreen, type FullscreenOutcome } from './fullscreen.ts';
import { sendHotkey } from './hotkey.ts';
import { cpuUsagePercent, diskUsage, memoryUsage } from './system.ts';
@@ -252,6 +252,9 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
}
const url = requireUrl(params);
// Une nouvelle capture repart avec ses propres chances : un abandon décidé sur
// la précédente ne doit pas laisser celle-ci sans surveillance.
fullscreenMisses = 0;
const opened = await openPage(url);
const wait = Number(params.readyDelayMs ?? browserSettings.readyDelayMs);
@@ -473,6 +476,93 @@ async function restoreFullscreen(settings: FullscreenSettings): Promise<Fullscre
return fullscreenInFlight;
}
// --- Surveillance du plein écran ---------------------------------------------
/**
* Fréquence de relecture du plein écran pendant une capture.
*
* Espacée : la mesure est une évaluation dans la page, et rien ne justifie de
* la refaire au rythme du statut (2 s). Une perte de plein écran gâche au pire
* cette demi-minute de vidéo, là où une relecture serrée pèserait en continu.
*/
const FULLSCREEN_WATCH_MS = 30_000;
/**
* Rappels de surveillance restés sans effet, à la suite. Au-delà, on cesse
* d'insister : réappuyer toutes les 30 s sur une touche qui ne produit rien
* noierait l'historique sans rien corriger. Le compteur repart à chaque
* nouvelle capture, et à chaque rappel qui aboutit.
*/
let fullscreenMisses = 0;
const MAX_FULLSCREEN_MISSES = 3;
let fullscreenTimer: NodeJS.Timeout | null = null;
/**
* Vérifie que le lecteur occupe toujours la fenêtre, et le rétablit sinon.
*
* C'est le pendant du rappel programmé par {@link StreamWatcher} : celui-ci ne
* réagit qu'aux interruptions du flux, alors que le plein écran peut se perdre
* sans que le statut bouge — un clic malheureux, un ré-affichage du lecteur, une
* publicité qui reprend la main. Rien ne le signalait jusqu'ici, d'où des
* captures qui en sortaient sans qu'on sache ni quand ni pourquoi ; la mesure
* est maintenant relue régulièrement, et l'écart journalisé.
*/
async function watchFullscreen(): Promise<void> {
const bidi = driver();
const settings = watcher.snapshotSettings.fullscreen;
if (!bidi || !settings.enabled || fullscreenMisses >= MAX_FULLSCREEN_MISSES) return;
// Uniquement pendant une capture réellement en cours. Hors capture il n'y a
// rien à cadrer ; en pause (show privé), l'overlay du site occupe le lecteur
// et insister entrerait en conflit avec le rappel que la surveillance
// programme déjà pour le retour du flux public.
const record = await obs.recordState().catch(() => null);
if (!record?.active || record.paused) {
fullscreenMisses = 0;
return;
}
// `undefined` couvre aussi bien « pas de lecteur dans la page » que « rien ne
// permet de trancher » : dans les deux cas, agir se ferait à l'aveugle.
const { fullscreen, metrics, baseline } = await bidi.checkFullscreen();
if (fullscreen !== false) {
fullscreenMisses = 0;
return;
}
fullscreenMisses += 1;
// La mesure part avec le constat : c'est elle qui dit ce que le lecteur occupe
// réellement au moment où on le déclare sorti du plein écran.
report(
'warn',
`Plein écran perdu en cours de capture (${describeMetrics(metrics, baseline)}) — rappel envoyé`,
'fullscreen.failed',
);
const outcome = await restoreFullscreen(settings).catch((err: Error) => {
report('warn', `Rappel du plein écran impossible : ${err.message}`, 'fullscreen.failed');
return null;
});
if (!outcome) return;
// Un envoi ignoré (rappel trop rapproché) ne prouve rien : il ne compte ni
// comme réussite ni comme échec, et la relecture suivante tranchera.
if (outcome.skipped) return;
if (outcome.confirmed === true) {
fullscreenMisses = 0;
report('info', describeFullscreen(outcome, settings.key), 'fullscreen.restored');
return;
}
if (fullscreenMisses >= MAX_FULLSCREEN_MISSES) {
report(
'warn',
`Plein écran non rétabli après ${MAX_FULLSCREEN_MISSES} tentatives : surveillance ` +
'suspendue jusqu\'au prochain enregistrement.',
'fullscreen.failed',
);
}
}
/**
* Sélectionne la meilleure qualité offerte par le lecteur Stripchat.
*
@@ -619,11 +709,23 @@ function startStatusLoop(): void {
void pushStatus();
statusTimer = setInterval(() => void pushStatus(), statusIntervalMs);
statusTimer.unref?.();
// Horloge distincte de celle du statut : la relecture du plein écran est bien
// plus espacée, et la caler sur un cycle de 2 s reviendrait à évaluer du code
// dans la page en permanence.
fullscreenTimer = setInterval(() => {
void watchFullscreen().catch((err: unknown) => {
console.error('Surveillance du plein écran en échec :', err);
});
}, FULLSCREEN_WATCH_MS);
fullscreenTimer.unref?.();
}
function stopStatusLoop(): void {
if (statusTimer) clearInterval(statusTimer);
statusTimer = null;
if (fullscreenTimer) clearInterval(fullscreenTimer);
fullscreenTimer = null;
}
// --- Cycle de vie ------------------------------------------------------------