From cd3380d8d39713ce170197fcc912ee7829878989 Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Sat, 15 Aug 2026 12:06:50 -0400 Subject: [PATCH] Fix : overlay showing some times --- README.md | 59 ++++++++-- packages/agent/src/firefox.ts | 200 ++++++++++++++++++++++++++-------- packages/agent/src/index.ts | 27 +++++ 3 files changed, 233 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 3fb3b62..a4de351 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ IP publique nécessaire, et obs-websocket reste sur `127.0.0.1`. - Reconnexion automatique de bout en bout (agent → serveur, agent → OBS, dashboard → serveur). - [Pause automatique pendant les shows privés](#pause-automatique-pendant-les-shows-privés) (Stripchat), avec reprise et rappel du plein écran au retour du flux public. +- Le curseur est [écarté du lecteur](#écarter-le-curseur-du-lecteur) à la fin de chaque + interaction, pour que la barre de contrôles ne finisse pas dans le fichier enregistré. - Le plein écran est [mesuré, pas supposé](#mesurer-plutôt-que-demander), relu pendant la capture et rétabli s'il se perd ; un [lecteur figé](#lecteur-figé--rechargement-automatique) est détecté et la page rechargée. @@ -448,16 +450,53 @@ la fenêtre semblait rétrécir juste après le réglage de qualité, comme un s plein écran qui aurait annulé le premier — alors qu'il ne s'agissait que d'un Échap qui sortait du plein écran natif que la touche du lecteur venait d'activer. -**Le curseur est écarté du lecteur une fois la qualité réglée**, sans quoi la barre de -contrôles du lecteur — qui se garde affichée tant qu'une souris réelle la survole — resterait -visible en permanence, faute de mouvement ultérieur. Le point de sortie est **calculé, pas -deviné** : une marge est cherchée autour de la position réelle de l'élément vidéo (en -dessous en priorité, sinon au-dessus, à droite ou à gauche), confirmée par -`elementFromPoint` plutôt que supposée à un endroit fixe de la page. Si le lecteur couvre -tout le viewport sans la moindre marge, un évènement `mouseleave` non fiable est émis en -dernier recours sur l'élément vidéo et ses parents proches — sans garantie si le site vérifie -`isTrusted` dessus comme il le fait sur le clic du bouton de qualité, mais sans risque non -plus à tenter. +Voir aussi [Écarter le curseur du lecteur](#écarter-le-curseur-du-lecteur), qui referme la +séquence. + +#### Écarter le curseur du lecteur + +**Le curseur est écarté à la fin de chaque interaction avec la page** : rappel de plein écran, +réglage de qualité, et jusqu'aux abandons de ce réglage. Un lecteur vidéo garde sa barre de +contrôles affichée tant qu'une souris réelle la survole — comportement voulu pour un vrai +spectateur, mais qui la fige à l'écran ici, puisque plus rien ne bouge ensuite. Et cette barre +est enregistrée avec le flux : c'est l'incrustation que l'on voit en travers de certains +replays. + +Le cas qui échappait : une capture qui **démarre** passe par le réglage de qualité, qui se +terminait déjà par cet écart — mais une **reprise** (retour du flux public après un show privé +ou un passage hors-ligne) ne fait qu'envoyer la touche du lecteur, sans repasser par la +qualité. Le curseur restait donc exactement là où la dernière interaction l'avait laissé, +c'est-à-dire sur un bouton du lecteur. D'où une incrustation présente sur certains replays et +pas sur d'autres, sans qu'aucun réglage ne les distingue. + +Trois recours, du plus fiable au moins : + +1. **Hors du lecteur.** Le point est **calculé, pas deviné** : une marge est cherchée autour de + la position réelle de l'élément vidéo (en dessous en priorité, sinon au-dessus, à droite ou + à gauche), confirmée par `elementFromPoint` plutôt que supposée à un endroit fixe de la + page. Une version antérieure visait un coin fixe, en pariant qu'une barre du site y restait + affichée — un coin qui s'avère faire *partie* du lecteur produit exactement l'inverse de + l'effet recherché. +2. **Dans le lecteur, à l'écart de ses commandes.** Le cas du plein écran, justement : le + lecteur couvre tout, il n'existe aucun « dehors ». Le balayage part du haut de l'image (la + barre de contrôles occupe le bas chez tous les lecteurs) et **préfère la surface vidéo nue**, + seul endroit dont on sache qu'il n'épingle rien ; un habillage sans nom parlant (voile + cliquable, dégradé) fait un repli acceptable, mais n'est jamais préféré à la vidéo quand + celle-ci est atteignable. Tout ce qui ressemble à une commande — `button`, `[role=button]`, + lien, champ, ou une classe contenant `control`/`menu`/`btn`/`bar` — est écarté. Un point + posé sur l'image relance la minuterie d'auto-masquage du lecteur, qui fait alors disparaître + la barre tout seul : ce qui n'arrive jamais tant que le curseur reste sur un bouton. +3. **Évènements de sortie simulés**, en dernier recours, sur l'élément vidéo et ses parents + proches — sans garantie si le site vérifie `isTrusted` dessus comme il le fait sur le clic du + bouton de qualité, mais sans risque non plus à tenter. + +Le résultat part au journal (`curseur écarté hors du lecteur`, `lecteur plein cadre : curseur +posé à l'écart des commandes`…) : c'est ce qui permet, devant un replay incrusté, de savoir si +le curseur avait été déplacé et où il avait atterri. + +L'écart a lieu **après** la mesure du plein écran, jamais avant : déplacer le curseur réaffiche +les contrôles le temps de la minuterie, et le relevé porterait alors sur une page en train de +changer. #### Lancement simple diff --git a/packages/agent/src/firefox.ts b/packages/agent/src/firefox.ts index 54b92c3..c235c59 100644 --- a/packages/agent/src/firefox.ts +++ b/packages/agent/src/firefox.ts @@ -79,6 +79,50 @@ export function describeMetrics( return parts.join(', '); } +/** + * Ce qu'a pu faire {@link FirefoxDriver.parkPointer} du curseur. + * + * Relevé plutôt que supposé : c'est la seule façon de savoir, après coup et + * depuis le journal, pourquoi une incrustation est restée à l'écran sur un + * enregistrement — le curseur posé sur un bouton, ou pas déplacé du tout. + */ +/** Curseur effectivement déplacé, avec le point retenu. */ +export interface ParkPoint { + where: 'outside' | 'inside'; + x: number; + y: number; + over: string; +} + +export type ParkOutcome = + /** + * Curseur posé quelque part : `outside`, hors du lecteur — le cas idéal, le + * survol cesse aussitôt ; `inside`, dans le lecteur mais à l'écart de ses + * commandes (le cas du plein écran, où il n'existe aucun dehors), la minuterie + * d'auto-masquage du lecteur faisant le reste. `over` nomme l'élément + * réellement sous le curseur à l'arrivée. + */ + | ParkPoint + /** + * Aucun point utilisable : sortie de survol simulée en dernier ressort + * (`events`), ou rien à faire faute de lecteur dans la page (`none`). + */ + | { where: 'events' | 'none' }; + +/** Résume un éloignement de curseur pour le journal. */ +export function describePark(outcome: ParkOutcome): string { + switch (outcome.where) { + case 'outside': + return `curseur écarté hors du lecteur (sur ${outcome.over})`; + case 'inside': + return `lecteur plein cadre : curseur posé à l'écart des commandes (sur ${outcome.over})`; + case 'events': + return 'aucun point hors des commandes : sortie de survol simulée'; + default: + return 'curseur non déplacé : aucun lecteur repéré dans la page'; + } +} + /** * Instantané de la lecture, tel que le lecteur le rapporte. * @@ -274,6 +318,20 @@ export class FirefoxController { } this.fullscreen = state; + + // Après la mesure, jamais avant : déplacer le curseur réaffiche les + // contrôles du lecteur le temps de sa minuterie, et le relevé porterait + // alors sur une page en train de changer. + // + // C'est ici que se jouait l'incrustation restée en travers de certaines + // reprises. Une capture qui démarre passe par la sélection de qualité, qui + // se terminait déjà par cet écart ; un simple rappel de plein écran — le + // chemin de toute reprise après un show privé ou un passage hors-ligne — + // n'y passait pas, et laissait le curseur exactement là où la dernière + // interaction l'avait posé. + const parked = await this.parkPointer(); + this.log('info', describePark(parked)); + return { method: attempts > 1 ? 'webdriver (touche, 2 essais)' : 'webdriver (touche)', target: this.url ?? undefined, @@ -351,8 +409,15 @@ export class FirefoxController { await delay(150); } - if (!tooltip?.found) return { ok: false, reason: 'menu de qualité non ouvert' }; + // Un abandon laisse le curseur sur le bouton de qualité, donc les contrôles + // épinglés à l'écran : la qualité n'aura pas été réglée, mais au moins + // l'incrustation ne sera pas enregistrée par-dessus le flux. + if (!tooltip?.found) { + await this.parkPointer(); + return { ok: false, reason: 'menu de qualité non ouvert' }; + } if (tooltip.options.length === 0) { + await this.parkPointer(); return { ok: false, reason: 'menu ouvert mais aucune option de résolution trouvée', @@ -388,12 +453,10 @@ export class FirefoxController { await this.click(buttonPoint.x, buttonPoint.y); } - // Le curseur reste sinon posé sur le bouton ou l'option cliquée : la barre - // de contrôles du lecteur, elle, se garde visible tant qu'une souris réelle - // la survole — un comportement voulu pour ne pas la faire disparaître sous - // le curseur d'un vrai spectateur, mais qui la laisse affichée en - // permanence ici puisque rien ne bouge plus ensuite. - await this.moveAway(); + // Le curseur reste sinon posé sur le bouton ou l'option cliquée, ce qui + // maintient la barre de contrôles affichée (voir {@link parkPointer}). + const parked = await this.parkPointer(); + this.log('info', describePark(parked)); await this.send('input.releaseActions', { context: this.context }, 5000).catch(() => undefined); @@ -895,20 +958,34 @@ export class FirefoxController { } /** - * Déplace le curseur vers un point réellement hors du lecteur, sans cliquer. + * Écarte le curseur de tout ce qui maintient les contrôles du lecteur + * affichés, sans cliquer. À appeler à la fin de chaque interaction. * - * Calculé plutôt que deviné. Une version antérieure visait un coin fixe - * (haut gauche de la page), en supposant qu'une barre du site y restait - * toujours affichée au-dessus du lecteur. Rien ne garantissait que ça - * tienne — et un coin qui s'avère plutôt FAIRE PARTIE du lecteur produit - * l'inverse de l'effet recherché : la souris y reste posée, les contrôles - * restent affichés. Ici, la marge est cherchée autour de la position réelle - * du lecteur, confirmée par `elementFromPoint` plutôt que supposée. + * Un lecteur vidéo garde sa barre de contrôles visible tant qu'une souris la + * survole — comportement voulu pour un vrai spectateur, mais qui la fige à + * l'écran ici, puisque plus rien ne bouge ensuite. Et cette barre est + * enregistrée avec le flux : c'est l'incrustation que l'on voit en travers de + * certains replays. + * + * Trois recours, du plus fiable au moins : + * + * 1. **Hors du lecteur.** Calculé plutôt que deviné : une version antérieure + * visait un coin fixe de la page, en supposant qu'une barre du site y + * restait affichée. Un coin qui s'avère FAIRE PARTIE du lecteur produit + * exactement l'inverse de l'effet recherché. + * 2. **Dans le lecteur, à l'écart des contrôles.** Le cas du plein écran : + * le lecteur couvre tout, il n'existe aucun « dehors ». Un point posé sur + * la surface vidéo nue relance quand même la minuterie d'auto-masquage du + * lecteur, qui fait alors disparaître la barre toute seule — ce qui + * n'arrive jamais tant que le curseur reste sur un bouton. + * 3. **Évènements de sortie simulés**, en dernier ressort. Un site qui + * vérifie `isTrusted` les ignorera, mais ils ne coûtent rien à tenter. */ - private async moveAway(): Promise { - const point = await this.safePointOutsidePlayer(); - if (point) { - await this.send( + async parkPointer(): Promise { + const target = await this.pointerParkTarget(); + + if (target) { + const moved = await this.send( 'input.performActions', { context: this.context, @@ -918,26 +995,24 @@ export class FirefoxController { id: 'stream-control-pointer', parameters: { pointerType: 'mouse' }, actions: [ - { type: 'pointerMove', x: Math.round(point.x), y: Math.round(point.y), origin: 'viewport' }, + { type: 'pointerMove', x: Math.round(target.x), y: Math.round(target.y), origin: 'viewport' }, ], }, ], }, 5000, - ).catch(() => undefined); - return; + ).then( + () => true, + () => false, + ); + if (moved) return target; } - // Aucune marge trouvée : le lecteur couvre tout le viewport, sans point - // où poser le curseur en dehors de lui. Un évènement non fiable ne fera - // peut-être rien si le site vérifie `isTrusted` sur celui-ci comme il le - // fait sur le clic du bouton de qualité, mais ne coûte rien à tenter — - // mieux vaut essayer que renoncer complètement à masquer les contrôles. - await this.evaluate(`(() => { + const dispatched = await this.evaluate(`(() => { const start = document.querySelector('video') || document.querySelector('.player-resolution')?.closest('[class*="player"]') || document.querySelector('.player-resolution'); - if (!start) return; + if (!start) return false; // L'écouteur qui cache les contrôles vit le plus souvent sur le // conteneur du lecteur (survol de la zone entière), pas sur la vidéo // elle-même — inconnu d'ici, donc émis sur quelques niveaux de parents @@ -948,20 +1023,21 @@ export class FirefoxController { node.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); node = node.parentElement; } - })()`).catch(() => undefined); + return true; + })()`).catch(() => false); + + return { where: dispatched === true ? 'events' : 'none' }; } /** - * Point de viewport garanti hors du lecteur, ou `null` si aucun n'a pu être - * trouvé (lecteur couvrant tout l'écran, sans marge disponible dans aucune - * direction). + * Point de viewport où poser le curseur, ou `null` si la page n'offre rien. * * L'ancre est l'élément vidéo lui-même — le repère le plus universel d'un * lecteur, quelle que soit la façon dont le site habille ses contrôles * autour. À défaut (site sans balise `