fix bla
This commit is contained in:
35
README.md
35
README.md
@@ -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 —
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user