fix bla
All checks were successful
release / build (push) Successful in 25s
release / verify-windows (push) Successful in 1m6s

This commit is contained in:
jeanotx32
2026-08-12 04:18:44 +02:00
parent 8fef1aacb0
commit ea8eeec129
4 changed files with 58 additions and 103 deletions

View File

@@ -103,14 +103,17 @@ export class FirefoxController {
}
/**
* Rappelle le plein écran, puis le vérifie.
* Rappelle le plein écran en envoyant la touche du lecteur, et seulement elle.
*
* Deux tentatives, dans cet ordre volontaire. La touche d'abord : c'est le
* geste de l'opérateur, elle passe par le gestionnaire du lecteur et donne
* exactement le cadrage attendu par le site. L'appel direct à
* `requestFullscreen` ensuite, seulement si la première n'a rien produit —
* il fonctionne toujours mais met en plein écran l'élément vidéo brut, sans
* l'habillage du lecteur.
* Volontairement une seule voie, sans repli. Une version antérieure appelait
* `requestFullscreen()` sur l'élément vidéo brut quand la touche ne semblait
* pas avoir agi — mais « ne semblait pas » se mesurait à
* `document.fullscreenElement`, qui ne dit rien du bouton « théâtre » de
* Stripchat : ce mode-là garde la barre du haut visible et n'a souvent rien
* à voir avec l'API plein écran native du navigateur. Le repli déclenchait
* alors un vrai plein écran natif — vidéo seule, tout le reste disparu —
* précisément l'effet que la touche du lecteur, elle, évite. Mieux vaut
* envoyer le geste attendu par le site et ne pas le corriger à l'aveugle.
*/
async restoreFullscreen(settings: FullscreenSettings): Promise<FullscreenOutcome> {
await this.ensureReady();
@@ -139,57 +142,18 @@ export class FirefoxController {
// Le passage en plein écran est animé : relire trop tôt renverrait faux
// alors que la touche a bien été prise en compte.
await delay(1200);
if (await this.isFullscreen()) {
this.fullscreen = true;
return { method: 'webdriver (touche)', target: this.url ?? undefined, confirmed: true };
}
this.log(
'info',
`La touche « ${settings.key} » n'a pas basculé la page ; tentative directe sur l'élément vidéo`,
);
const detail = await this.requestFullscreenOnVideo();
await delay(800);
const confirmed = await this.isFullscreen();
this.fullscreen = confirmed;
if (confirmed) {
return { method: 'webdriver (élément vidéo)', target: this.url ?? undefined, confirmed };
}
// « Fullscreen request denied » ne dit rien à personne. Les trois conditions
// que Gecko exige sont lisibles depuis la page : on les relit pour nommer
// celle qui manque, plutôt que de laisser l'opérateur deviner.
const why = await this.explainRefusal(detail);
this.lastError = why;
return { method: `webdriver ${why}`, target: this.url ?? undefined, confirmed: false };
}
/**
* Nomme la condition manquante après un refus de plein écran.
*
* Le focus est de loin la première cause : Firefox refuse le plein écran à un
* document dont la fenêtre n'est pas celle qu'a sélectionnée le gestionnaire
* de fenêtres — et sur une VM, il suffit qu'OBS ou un terminal l'ait pris.
*/
private async explainRefusal(detail: string): Promise<string> {
const raw = await this.evaluate(
'JSON.stringify({ focus: document.hasFocus(), api: document.fullscreenEnabled, video: !!document.querySelector("video") })',
).catch(() => null);
const page = typeof raw === 'string' ? (JSON.parse(raw) as Record<string, boolean>) : null;
if (!page) return detail;
if (!page.video) return 'aucun élément vidéo dans la page — le lecteur a-t-il fini de charger ?';
if (!page.focus) {
return (
"la fenêtre Firefox n'a pas le focus : Firefox refuse le plein écran à un onglet " +
"en arrière-plan. Ferme les autres fenêtres de la session, ou laisse Firefox seul " +
"au premier plan sur cette VM"
);
}
if (!page.api) return "l'API plein écran est désactivée dans ce profil Firefox";
return detail;
// `confirmed` reste `undefined`, pas `false`, quand l'API native ne signale
// rien : c'est le cas attendu pour un mode « théâtre » propre au site, pas
// un échec. Le distinguer évite de journaliser en avertissement une touche
// qui a très bien pu faire son effet.
return {
method: 'webdriver (touche)',
target: this.url ?? undefined,
confirmed: confirmed ? true : undefined,
};
}
/** Décharge le lecteur sans détruire la fenêtre — donc sans casser la source OBS. */
@@ -494,30 +458,6 @@ export class FirefoxController {
return result === true;
}
/**
* Demande le plein écran sur l'élément vidéo.
*
* `userActivation` fait croire à Firefox à un geste utilisateur : sans lui,
* l'API refuse l'appel. Le profil pose aussi
* `full-screen-api.allow-trusted-requests-only=false`, pour les versions de
* Firefox qui ne connaissent pas encore ce paramètre de commande.
*/
private async requestFullscreenOnVideo(): Promise<string> {
const expression = `(async () => {
const video = document.querySelector('video');
if (!video) return 'aucune vidéo dans la page';
try {
await (video.requestFullscreen ? video.requestFullscreen() : Promise.reject(new Error('API absente')));
return 'ok';
} catch (err) {
return 'refus : ' + (err && err.message ? err.message : err);
}
})()`;
const result = await this.evaluate(expression, true).catch((err: Error) => err.message);
return typeof result === 'string' ? result : 'ok';
}
private async evaluate(expression: string, userActivation = false): Promise<unknown> {
const response = await this.send<{
type: string;

View File

@@ -1,18 +1,22 @@
/**
* Résultat d'un rappel de plein écran, quelle que soit la méthode employée.
*
* `confirmed` est le champ qui compte. xdotool ne peut jamais le renseigner :
* il envoie une touche et n'a aucun moyen de savoir ce que la page en a fait.
* BiDi, lui, relit `document.fullscreenElement` — donc un `false` ici est une
* information, pas une incertitude, et c'est ce qui permet d'enchaîner sur une
* seconde tentative plutôt que de laisser l'enregistrement en fenêtré.
* `confirmed` distingue trois cas, pas deux. xdotool ne peut jamais le
* renseigner (`undefined`) : il envoie une touche et n'a aucun moyen de
* savoir ce que la page en a fait. BiDi relit `document.fullscreenElement`,
* mais son absence ne vaut pas `false` pour autant : beaucoup de lecteurs —
* celui de Stripchat compris — implémentent leur propre mode « théâtre » en
* CSS, barre du haut toujours visible, sans jamais toucher à l'API plein
* écran native. `undefined` couvre donc aussi bien « aucun moyen de vérifier »
* que « vérifié négatif, mais rien ne prouve un échec » ; `false` ne
* s'emploie que là où l'échec est positivement établi.
*/
export interface FullscreenOutcome {
/** `webdriver`, `xdotool`, `SendKeys`, `osascript`… */
method: string;
/** Titre de fenêtre ou URL, selon ce que la méthode a pu identifier. */
target?: string;
/** Vérifié dans la page. `undefined` = la méthode ne sait pas vérifier. */
/** Vrai si confirmé, faux si l'échec est positivement établi, sinon indéterminé. */
confirmed?: boolean;
}

View File

@@ -276,6 +276,12 @@ async function startCapture(params: Record<string, unknown>): Promise<unknown> {
});
}
// Dernière chance d'écrire un preset en attente : une fois l'enregistrement
// commencé, OBS refuse toute modification jusqu'à l'arrêt. Sans cet appel
// explicite, l'écriture dépendait du hasard d'un cycle de statut (toutes les
// 2 s) tombant avant le démarrage — perdu, le preset restait en attente
// jusqu'au prochain arrêt, et cet enregistrement tournait sur l'ancien réglage.
await drainPresetApply();
await obs.execute('record.start');
report('info', `Capture démarrée pour ${url}`, 'capture.started');
@@ -348,6 +354,12 @@ async function runAction(action: AgentAction, params: Record<string, unknown>):
return startCapture(params);
case 'capture.stop':
return stopCapture();
case 'record.start':
// Même raison que dans startCapture() : sans ce passage explicite, un
// preset changé juste avant restait en attente jusqu'au prochain arrêt,
// et cette capture démarrait sur l'ancien réglage.
await drainPresetApply();
return obs.execute('record.start');
case 'preset.apply':
return applyPresetNow(params);
case 'agent.update':