Fix : overlay showing some times
This commit is contained in:
59
README.md
59
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).
|
- 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)
|
- [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.
|
(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
|
- Le plein écran est [mesuré, pas supposé](#mesurer-plutôt-que-demander), relu pendant la
|
||||||
capture et rétabli s'il se perd ; un
|
capture et rétabli s'il se perd ; un
|
||||||
[lecteur figé](#lecteur-figé--rechargement-automatique) est détecté et la page rechargée.
|
[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
|
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.
|
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
|
Voir aussi [Écarter le curseur du lecteur](#écarter-le-curseur-du-lecteur), qui referme la
|
||||||
contrôles du lecteur — qui se garde affichée tant qu'une souris réelle la survole — resterait
|
séquence.
|
||||||
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
|
#### Écarter le curseur du lecteur
|
||||||
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
|
**Le curseur est écarté à la fin de chaque interaction avec la page** : rappel de plein écran,
|
||||||
tout le viewport sans la moindre marge, un évènement `mouseleave` non fiable est émis en
|
réglage de qualité, et jusqu'aux abandons de ce réglage. Un lecteur vidéo garde sa barre de
|
||||||
dernier recours sur l'élément vidéo et ses parents proches — sans garantie si le site vérifie
|
contrôles affichée tant qu'une souris réelle la survole — comportement voulu pour un vrai
|
||||||
`isTrusted` dessus comme il le fait sur le clic du bouton de qualité, mais sans risque non
|
spectateur, mais qui la fige à l'écran ici, puisque plus rien ne bouge ensuite. Et cette barre
|
||||||
plus à tenter.
|
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
|
#### Lancement simple
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,50 @@ export function describeMetrics(
|
|||||||
return parts.join(', ');
|
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.
|
* Instantané de la lecture, tel que le lecteur le rapporte.
|
||||||
*
|
*
|
||||||
@@ -274,6 +318,20 @@ export class FirefoxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fullscreen = state;
|
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 {
|
return {
|
||||||
method: attempts > 1 ? 'webdriver (touche, 2 essais)' : 'webdriver (touche)',
|
method: attempts > 1 ? 'webdriver (touche, 2 essais)' : 'webdriver (touche)',
|
||||||
target: this.url ?? undefined,
|
target: this.url ?? undefined,
|
||||||
@@ -351,8 +409,15 @@ export class FirefoxController {
|
|||||||
await delay(150);
|
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) {
|
if (tooltip.options.length === 0) {
|
||||||
|
await this.parkPointer();
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: 'menu ouvert mais aucune option de résolution trouvée',
|
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);
|
await this.click(buttonPoint.x, buttonPoint.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le curseur reste sinon posé sur le bouton ou l'option cliquée : la barre
|
// Le curseur reste sinon posé sur le bouton ou l'option cliquée, ce qui
|
||||||
// de contrôles du lecteur, elle, se garde visible tant qu'une souris réelle
|
// maintient la barre de contrôles affichée (voir {@link parkPointer}).
|
||||||
// la survole — un comportement voulu pour ne pas la faire disparaître sous
|
const parked = await this.parkPointer();
|
||||||
// le curseur d'un vrai spectateur, mais qui la laisse affichée en
|
this.log('info', describePark(parked));
|
||||||
// permanence ici puisque rien ne bouge plus ensuite.
|
|
||||||
await this.moveAway();
|
|
||||||
|
|
||||||
await this.send('input.releaseActions', { context: this.context }, 5000).catch(() => undefined);
|
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
|
* Un lecteur vidéo garde sa barre de contrôles visible tant qu'une souris la
|
||||||
* (haut gauche de la page), en supposant qu'une barre du site y restait
|
* survole — comportement voulu pour un vrai spectateur, mais qui la fige à
|
||||||
* toujours affichée au-dessus du lecteur. Rien ne garantissait que ça
|
* l'écran ici, puisque plus rien ne bouge ensuite. Et cette barre est
|
||||||
* tienne — et un coin qui s'avère plutôt FAIRE PARTIE du lecteur produit
|
* enregistrée avec le flux : c'est l'incrustation que l'on voit en travers de
|
||||||
* l'inverse de l'effet recherché : la souris y reste posée, les contrôles
|
* certains replays.
|
||||||
* 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.
|
* 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<void> {
|
async parkPointer(): Promise<ParkOutcome> {
|
||||||
const point = await this.safePointOutsidePlayer();
|
const target = await this.pointerParkTarget();
|
||||||
if (point) {
|
|
||||||
await this.send(
|
if (target) {
|
||||||
|
const moved = await this.send(
|
||||||
'input.performActions',
|
'input.performActions',
|
||||||
{
|
{
|
||||||
context: this.context,
|
context: this.context,
|
||||||
@@ -918,26 +995,24 @@ export class FirefoxController {
|
|||||||
id: 'stream-control-pointer',
|
id: 'stream-control-pointer',
|
||||||
parameters: { pointerType: 'mouse' },
|
parameters: { pointerType: 'mouse' },
|
||||||
actions: [
|
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,
|
5000,
|
||||||
).catch(() => undefined);
|
).then(
|
||||||
return;
|
() => true,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
if (moved) return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aucune marge trouvée : le lecteur couvre tout le viewport, sans point
|
const dispatched = await this.evaluate(`(() => {
|
||||||
// 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')
|
const start = document.querySelector('video')
|
||||||
|| document.querySelector('.player-resolution')?.closest('[class*="player"]')
|
|| document.querySelector('.player-resolution')?.closest('[class*="player"]')
|
||||||
|| document.querySelector('.player-resolution');
|
|| document.querySelector('.player-resolution');
|
||||||
if (!start) return;
|
if (!start) return false;
|
||||||
// L'écouteur qui cache les contrôles vit le plus souvent sur le
|
// 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
|
// 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
|
// 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.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
|
||||||
node = node.parentElement;
|
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
|
* Point de viewport où poser le curseur, ou `null` si la page n'offre rien.
|
||||||
* 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
|
* 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
|
* lecteur, quelle que soit la façon dont le site habille ses contrôles
|
||||||
* autour. À défaut (site sans balise `<video>` classique), on retombe sur
|
* autour. À défaut (site sans balise `<video>` classique), on retombe sur
|
||||||
* le conteneur du bouton de qualité déjà repéré plus haut.
|
* le conteneur du bouton de qualité déjà repéré plus haut.
|
||||||
*/
|
*/
|
||||||
private async safePointOutsidePlayer(): Promise<{ x: number; y: number } | null> {
|
private async pointerParkTarget(): Promise<ParkPoint | null> {
|
||||||
const raw = await this.evaluate(`JSON.stringify((() => {
|
const raw = await this.evaluate(`JSON.stringify((() => {
|
||||||
const anchor = document.querySelector('video')
|
const anchor = document.querySelector('video')
|
||||||
|| document.querySelector('.player-resolution')?.closest('[class*="player"]')
|
|| document.querySelector('.player-resolution')?.closest('[class*="player"]')
|
||||||
@@ -972,29 +1048,67 @@ export class FirefoxController {
|
|||||||
const vw = window.innerWidth;
|
const vw = window.innerWidth;
|
||||||
const vh = window.innerHeight;
|
const vh = window.innerHeight;
|
||||||
const margin = 20;
|
const margin = 20;
|
||||||
|
const inside = (p) => p.x >= 0 && p.x <= vw && p.y >= 0 && p.y <= vh;
|
||||||
|
const name = (el) => {
|
||||||
|
if (!el) return '?';
|
||||||
|
const cls = typeof el.className === 'string' ? el.className.trim().split(/\\s+/)[0] : '';
|
||||||
|
return el.tagName.toLowerCase() + (cls ? '.' + cls : '');
|
||||||
|
};
|
||||||
|
|
||||||
// Dans cet ordre : en dessous, au-dessus, à droite, à gauche — sous le
|
// 1. Hors du lecteur. Dans cet ordre : en dessous, au-dessus, à droite, à
|
||||||
// lecteur est le plus souvent une zone de page ordinaire (chat,
|
// gauche — sous le lecteur est le plus souvent une zone de page ordinaire
|
||||||
// commentaires), donc le candidat le plus sûr en premier.
|
// (chat, commentaires), donc le candidat le plus sûr en premier.
|
||||||
const candidates = [
|
const outside = [
|
||||||
{ x: rect.left + rect.width / 2, y: rect.bottom + margin },
|
{ x: rect.left + rect.width / 2, y: rect.bottom + margin },
|
||||||
{ x: rect.left + rect.width / 2, y: rect.top - margin },
|
{ x: rect.left + rect.width / 2, y: rect.top - margin },
|
||||||
{ x: rect.right + margin, y: rect.top + rect.height / 2 },
|
{ x: rect.right + margin, y: rect.top + rect.height / 2 },
|
||||||
{ x: rect.left - margin, y: rect.top + rect.height / 2 },
|
{ x: rect.left - margin, y: rect.top + rect.height / 2 },
|
||||||
];
|
];
|
||||||
|
for (const point of outside) {
|
||||||
for (const point of candidates) {
|
if (!inside(point)) continue;
|
||||||
if (point.x < 0 || point.x > vw || point.y < 0 || point.y > vh) continue;
|
|
||||||
// Confirmé par l'élément réellement affiché à ce point, pas seulement
|
// Confirmé par l'élément réellement affiché à ce point, pas seulement
|
||||||
// par la géométrie : un habillage du site peut déborder du rectangle
|
// par la géométrie : un habillage du site peut déborder du rectangle
|
||||||
// de l'ancre sans en faire partie au sens du DOM.
|
// de l'ancre sans en faire partie au sens du DOM.
|
||||||
const el = document.elementFromPoint(point.x, point.y);
|
const el = document.elementFromPoint(point.x, point.y);
|
||||||
if (el && el !== anchor && !anchor.contains(el)) return point;
|
if (el && el !== anchor && !anchor.contains(el)) {
|
||||||
|
return { where: 'outside', x: point.x, y: point.y, over: name(el) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dans le lecteur, à l'écart de ses commandes. Balayage par bandes du
|
||||||
|
// haut vers le bas : la barre de contrôles occupe le bas chez tous les
|
||||||
|
// lecteurs, et le sommet de l'image est ce qui a le moins de chances de
|
||||||
|
// porter un bouton.
|
||||||
|
const CONTROLS = 'button,[role="button"],a,input,select,textarea,'
|
||||||
|
+ '[class*="control"],[class*="tooltip"],[class*="menu"],[class*="btn"],[class*="bar"]';
|
||||||
|
const points = [];
|
||||||
|
for (const fy of [0.18, 0.3, 0.1, 0.42]) {
|
||||||
|
for (const fx of [0.5, 0.25, 0.75]) {
|
||||||
|
const point = { x: rect.left + rect.width * fx, y: rect.top + rect.height * fy };
|
||||||
|
if (inside(point)) points.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deux passes, et pas une seule avec exclusion : la surface vidéo nue est
|
||||||
|
// le seul endroit dont on sache avec certitude qu'il n'épingle rien. Un
|
||||||
|
// habillage sans nom parlant (un voile cliquable, un dégradé) fait un
|
||||||
|
// repli acceptable — les lecteurs masquent leurs contrôles après
|
||||||
|
// inactivité même curseur posé sur l'image — mais il ne doit pas être
|
||||||
|
// préféré à la vidéo elle-même quand celle-ci est atteignable.
|
||||||
|
for (const point of points) {
|
||||||
|
if (document.elementFromPoint(point.x, point.y) === anchor) {
|
||||||
|
return { where: 'inside', x: point.x, y: point.y, over: name(anchor) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const point of points) {
|
||||||
|
const el = document.elementFromPoint(point.x, point.y);
|
||||||
|
if (!el || el.closest(CONTROLS)) continue;
|
||||||
|
return { where: 'inside', x: point.x, y: point.y, over: name(el) };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})())`).catch(() => null);
|
})())`).catch(() => null);
|
||||||
if (typeof raw !== 'string') return null;
|
if (typeof raw !== 'string') return null;
|
||||||
return JSON.parse(raw) as { x: number; y: number } | null;
|
return JSON.parse(raw) as ParkPoint | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async evaluate(expression: string, userActivation = false): Promise<unknown> {
|
private async evaluate(expression: string, userActivation = false): Promise<unknown> {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { blankPage, closeWindow, delay as sleep, openUrl } from './browser.ts';
|
|||||||
import {
|
import {
|
||||||
FirefoxController,
|
FirefoxController,
|
||||||
describeMetrics,
|
describeMetrics,
|
||||||
|
describePark,
|
||||||
type PlaybackSample,
|
type PlaybackSample,
|
||||||
type StreamQualityOutcome,
|
type StreamQualityOutcome,
|
||||||
} from './firefox.ts';
|
} from './firefox.ts';
|
||||||
@@ -324,9 +325,35 @@ async function preparePlayer(waitMs: number): Promise<unknown> {
|
|||||||
// l'éloignement du curseur reste la dernière chose à toucher la page avant
|
// l'éloignement du curseur reste la dernière chose à toucher la page avant
|
||||||
// l'enregistrement.
|
// l'enregistrement.
|
||||||
await selectMaxStreamQuality();
|
await selectMaxStreamQuality();
|
||||||
|
|
||||||
|
// Les deux étapes ci-dessus se terminent chacune en écartant le curseur du
|
||||||
|
// lecteur. Toutes deux désactivées, plus rien ne le fait : la capture
|
||||||
|
// démarrerait avec les contrôles éventuellement figés à l'écran, hérités du
|
||||||
|
// dernier geste — la souris n'a aucune raison d'avoir bougé depuis.
|
||||||
|
if (!fullscreen.enabled && !watcher.snapshotSettings.streamQuality.enabled) {
|
||||||
|
await parkPointer();
|
||||||
|
}
|
||||||
|
|
||||||
return fullscreenResult;
|
return fullscreenResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Écarte le curseur de la barre de contrôles du lecteur.
|
||||||
|
*
|
||||||
|
* Sans effet hors mode BiDi : déplacer une souris dans une page suppose de
|
||||||
|
* parler au navigateur. Un échec n'est jamais bloquant — au pire, une
|
||||||
|
* incrustation reste visible sur l'enregistrement.
|
||||||
|
*/
|
||||||
|
async function parkPointer(): Promise<void> {
|
||||||
|
const bidi = driver();
|
||||||
|
if (!bidi) return;
|
||||||
|
const outcome = await bidi.parkPointer().catch((err: Error) => {
|
||||||
|
report('warn', `Curseur non écarté du lecteur : ${err.message}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (outcome) report('info', describePark(outcome));
|
||||||
|
}
|
||||||
|
|
||||||
/** Ouvre une page par la voie correspondant au mode de pilotage. */
|
/** Ouvre une page par la voie correspondant au mode de pilotage. */
|
||||||
async function openPage(url: string): Promise<unknown> {
|
async function openPage(url: string): Promise<unknown> {
|
||||||
const bidi = driver();
|
const bidi = driver();
|
||||||
|
|||||||
Reference in New Issue
Block a user