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

@@ -181,15 +181,21 @@ Le renversement est là : **l'agent possède le navigateur** au lieu de lui envo
en espérant. Il parle à Firefox, pas au serveur d'affichage — d'où le fonctionnement
identique sous Wayland, où xdotool ne voit rien.
Séquence du rappel de plein écran, dans cet ordre volontaire :
Séquence du rappel de plein écran :
1. `browsingContext.activate` — sans quoi la touche partirait vers un onglet d'arrière-plan ;
2. `input.performActions` — la touche configurée (`f` par défaut), délivrée à la page comme
un vrai évènement (`isTrusted: true`), donc traitée par le lecteur du site ;
3. relecture de `document.fullscreenElement` ;
4. si la page n'a pas bougé : `requestFullscreen()` sur l'élément `<video>`, avec
`userActivation` ;
5. relecture, et verdict rapporté dans l'historique de la VM.
3. relecture de `document.fullscreenElement`, et verdict rapporté dans l'historique de la VM.
Une seule voie, volontairement, sans repli. Une version antérieure appelait
`requestFullscreen()` sur l'élément `<video>` brut quand `document.fullscreenElement` restait
vide après la touche — mais ce champ ne dit rien du mode « théâtre » de Stripchat, qui garde
la barre du haut visible et n'a le plus 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 — l'exact inverse de ce que la touche du lecteur, elle, obtient. `confirmed` reste
donc indéterminé (`undefined`, pas `false`) quand l'API native ne signale rien : ce n'est pas
un échec, juste un mode que ce champ ne peut pas observer.
**Un profil Firefox dédié est obligatoire**, pas cosmétique : le port de pilotage ne
s'ouvre qu'au démarrage du processus, et deux instances ne peuvent pas partager un profil.
@@ -281,19 +287,6 @@ Limite assumée : seuls les cookies sont copiés, pas le `localStorage` ni l'`In
quasi-totalité des sites, Stripchat compris, portent la session dans un cookie ; si un site
s'appuyait uniquement sur un jeton en stockage local, cet import ne suffirait pas.
#### Si le plein écran est refusé
Firefox refuse le plein écran à un document **dont la fenêtre n'a pas le focus**. C'est la
première cause d'échec sur une VM : il suffit qu'OBS ou un terminal l'ait pris. L'agent
relit alors trois conditions depuis la page et nomme celle qui manque, plutôt que de
répercuter un « Fullscreen request denied » qui ne dit rien :
| Message | Cause |
| --- | --- |
| `la fenêtre Firefox n'a pas le focus` | une autre fenêtre est active dans la session |
| `aucun élément vidéo dans la page` | le lecteur n'a pas fini de charger — augmente le délai avant plein écran |
| `l'API plein écran est désactivée dans ce profil` | `full-screen-api.enabled` forcé à faux |
#### Lancement simple
Conservé comme défaut pour ne pas changer le comportement d'un agent existant à la mise à
@@ -370,6 +363,12 @@ puis ajuste la sortie vidéo (`SetVideoSettings`). Trois comportements à conna
qu'au lancement. L'agent le détecte et le signale dans son compte rendu.
- **Un preset n'est jamais appliqué pendant une capture** : cela la corromprait. La
demande est refusée, ou différée jusqu'à l'arrêt de l'enregistrement.
- **Un preset en attente est toujours écrit avant le `StartRecord` suivant.** Changer de
preset juste avant de lancer une capture (la sienne, ou celle d'un autre streamer sur la
même VM) ne dépend pas d'un minuteur en tâche de fond : le démarrage attend explicitement
que l'écriture soit passée. Sans cette garantie, une capture pouvait démarrer sur l'ancien
réglage et y rester verrouillée jusqu'à son propre arrêt — le symptôme perçu était « la
qualité ne se met jamais au maximum, et repart au défaut à chaque nouveau stream ».
Le mode de sortie du profil est forcé sur « Simple » : c'est la section que ces réglages
pilotent. Un paramètre refusé par la version d'OBS installée n'interrompt pas les autres —

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':