diff --git a/README.md b/README.md index 2e34947..2e2bf95 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,23 @@ disparu — l'exact inverse de ce que la touche du lecteur, elle, obtient. `conf 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 seul envoi à la fois, et pas plus d'un toutes les 10 s.** Deux mécanismes indépendants +peuvent réclamer ce rappel : la séquence d'ouverture (une fois, à l'arrivée sur la page) et +la surveillance de l'agent, qui le redemande de lui-même après un show privé. Sans +coordination, un flux qui vacille juste après le démarrage (bref retour en privé pendant que +la séquence d'ouverture patiente encore) fait partir les deux presque en même temps — et la +touche du lecteur *basculant* l'affichage plutôt que le forçant, le second envoi annule le +premier au lieu de le confirmer : la fenêtre, au lieu de s'agrandir, finit par rétrécir. Les +deux mécanismes convergent vers un même point de passage côté agent, qui partage le résultat +d'un envoi déjà en cours plutôt que d'en déclencher un second, et ignore toute nouvelle +demande dans les 10 s suivant la précédente. + +**La qualité vidéo se règle après le plein écran, jamais avant.** Le rappel de plein écran +envoie une touche au lecteur, et la plupart des lecteurs vidéo traitent n'importe quelle +touche — pas seulement la souris — comme une activité qui réaffiche leurs contrôles. Réglée +avant, la sélection de qualité (qui termine en écartant le curseur pour laisser les +contrôles disparaître — voir plus bas) se ferait aussitôt annuler par cette touche. + **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. L'agent en gère un et y réécrit un `user.js` à chaque lancement — chaque préférence y supprime quelque chose qui finirait dans le fichier @@ -325,6 +342,17 @@ inspectant la page en direct. Sans effet, mais sans erreur non plus (juste un av dans l'historique), si Stripchat change ce balisage — l'avertissement inclut alors un extrait du menu tel qu'ouvert, pour diagnostiquer sans repasser par les DevTools. +**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. + #### Lancement simple Conservé comme défaut pour ne pas changer le comportement d'un agent existant à la mise à diff --git a/packages/agent/src/firefox.ts b/packages/agent/src/firefox.ts index 2a65627..168a9cc 100644 --- a/packages/agent/src/firefox.ts +++ b/packages/agent/src/firefox.ts @@ -266,9 +266,7 @@ export class FirefoxController { // 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. Un coin sûr : la barre - // du haut du site (pas celle du lecteur), toujours visible d'après le - // réglage plein écran plus haut, donc jamais partie du lecteur. + // permanence ici puisque rien ne bouge plus ensuite. await this.moveAway(); await this.send('input.releaseActions', { context: this.context }, 5000).catch(() => undefined); @@ -653,23 +651,107 @@ export class FirefoxController { await delay(150); } - /** Déplace le curseur vers un coin hors du lecteur, sans cliquer. */ + /** + * Déplace le curseur vers un point réellement hors du lecteur, sans cliquer. + * + * 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. + */ private async moveAway(): Promise { - await this.send( - 'input.performActions', - { - context: this.context, - actions: [ - { - type: 'pointer', - id: 'stream-control-pointer', - parameters: { pointerType: 'mouse' }, - actions: [{ type: 'pointerMove', x: 2, y: 2, origin: 'viewport' }], - }, - ], - }, - 5000, - ).catch(() => undefined); + const point = await this.safePointOutsidePlayer(); + if (point) { + await this.send( + 'input.performActions', + { + context: this.context, + actions: [ + { + type: 'pointer', + id: 'stream-control-pointer', + parameters: { pointerType: 'mouse' }, + actions: [ + { type: 'pointerMove', x: Math.round(point.x), y: Math.round(point.y), origin: 'viewport' }, + ], + }, + ], + }, + 5000, + ).catch(() => undefined); + return; + } + + // 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 start = document.querySelector('video') + || document.querySelector('.player-resolution')?.closest('[class*="player"]') + || document.querySelector('.player-resolution'); + if (!start) return; + // 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 + // plutôt que sur le seul point de départ. + let node = start; + for (let i = 0; i < 4 && node; i++) { + node.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true })); + node.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + node = node.parentElement; + } + })()`).catch(() => undefined); + } + + /** + * 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). + * + * 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 `