feat(twitch): rechargement de l'embed au premier geste (autoplay)
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
- /watch + /shorts : le player Twitch refuse de demarrer sans geste utilisateur (anti-bot) et l'iframe etant cross-origin on ne peut pas cliquer dedans. On ecoute document:pointerdown (+ le keydown existant) et on ajoute &r=1 a l'URL de l'embed de la video courante : l'iframe se recharge dans le contexte d'activation utilisateur. - Une seule fois par video (twitchNudgedFor) : pas de boucle de rechargement, les videos suivantes sont re-armees automatiquement. - Verifie sur l'instance de test : 6/6 (URL initiale sans r=1, r=1 apres le geste, aucune recharge au 2e geste) sur /watch et /shorts.
This commit is contained in:
@@ -364,6 +364,28 @@ export class WatchShortComponent implements OnDestroy {
|
||||
return this.exhaustedQueries.size < list.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge Twitch : le player Twitch refuse de démarrer sans geste utilisateur
|
||||
* (anti-bot) et on ne peut pas cliquer dans une iframe cross-origin. On
|
||||
* recharge donc l'embed au premier geste posé SUR LA PAGE (pointerdown /
|
||||
* touche — un clic dans l'iframe ne nous parvient pas), une seule fois par
|
||||
* short : `&r=1` n'est ajouté que si ce short est déjà « nudgé ».
|
||||
*/
|
||||
private twitchNudgedFor = signal<string | null>(null);
|
||||
|
||||
private nudgeTwitch(): void {
|
||||
const v = this.current();
|
||||
if (!v || this.providerOf(v) !== 'twitch' || !this.embedUrl()) return;
|
||||
const key = String(v.videoId || '');
|
||||
if (!key || this.twitchNudgedFor() === key) return;
|
||||
this.twitchNudgedFor.set(key);
|
||||
}
|
||||
|
||||
@HostListener('document:pointerdown')
|
||||
onPageGesture(): void {
|
||||
this.nudgeTwitch();
|
||||
}
|
||||
|
||||
readonly embedUrl = computed<SafeResourceUrl | null>(() => {
|
||||
const v = this.current();
|
||||
if (!v) return null;
|
||||
@@ -399,6 +421,8 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (p === 'twitch' && v.videoId) {
|
||||
const parentsSet = new Set<string>([host, 'localhost', '127.0.0.1']);
|
||||
const parentParams = Array.from(parentsSet).map(h => `parent=${encodeURIComponent(h)}`).join('&');
|
||||
// `&r=1` = ce short a déjà subi le nudge (voir twitchNudgedFor).
|
||||
const nudge = this.twitchNudgedFor() === String(v.videoId) ? '&r=1' : '';
|
||||
// Les Shorts Twitch = clips uniquement (jamais de live/channel).
|
||||
const clipId = (v as any).kind === 'clip' ? v.videoId : null;
|
||||
if (clipId) {
|
||||
@@ -406,7 +430,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
// n'expose aucune API de volume — muet est la seule combinaison
|
||||
// qui démarre sans geste. Le son reste disponible via « Ouvrir
|
||||
// chez le fournisseur » / Watch.
|
||||
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true`;
|
||||
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
||||
}
|
||||
// Repli : player channel/VOD (ne devrait pas arriver, les lives
|
||||
@@ -414,8 +438,8 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const base = 'https://player.twitch.tv/';
|
||||
const chan = !/^\d+$/.test(String(v.videoId)) ? v.videoId : null;
|
||||
const qs = chan
|
||||
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true`
|
||||
: `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=true`;
|
||||
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}`
|
||||
: `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(base + qs);
|
||||
}
|
||||
if (p === 'peertube' && v.videoId) {
|
||||
@@ -1915,6 +1939,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
onKey(ev: KeyboardEvent) {
|
||||
this.nudgeTwitch();
|
||||
// Menu FAB ouvert : ↑↓ doivent défiler PANNEAU, pas changer de short.
|
||||
if (this.items().length === 0 || this.menuOpen()) return;
|
||||
if (ev.key === 'ArrowDown') { ev.preventDefault(); this.next(); }
|
||||
|
||||
@@ -398,6 +398,33 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
||||
// Backend-provided embed URL for providers that need it (e.g., Rumble)
|
||||
private rumbleEmbed = signal<string | null>(null);
|
||||
|
||||
/**
|
||||
* Nudge Twitch : le player Twitch refuse de démarrer sans geste utilisateur
|
||||
* (anti-bot) et on ne peut pas cliquer dans une iframe cross-origin. On
|
||||
* recharge donc l'embed au premier geste posé SUR LA PAGE (pointerdown /
|
||||
* touche — un clic dans l'iframe ne nous parvient pas), ce qui se fait dans
|
||||
* le contexte d'activation utilisateur. Une seule fois par vidéo (`&r=1`
|
||||
* n'est ajouté que si la vidéo courante est déjà « nudgée »).
|
||||
*/
|
||||
private twitchNudgedFor = signal<string | null>(null);
|
||||
|
||||
/** Identité de l'embed Twitch affiché (change à chaque vidéo). */
|
||||
private twitchKey(): string {
|
||||
return [this.videoId(), this.twitchChannel(), this.twitchClip()].map((v) => v || '').join('|');
|
||||
}
|
||||
|
||||
private nudgeTwitch(): void {
|
||||
if (this.provider() !== 'twitch' || !this.embedUrl()) return;
|
||||
const key = this.twitchKey();
|
||||
if (!key || this.twitchNudgedFor() === key) return;
|
||||
this.twitchNudgedFor.set(key);
|
||||
}
|
||||
|
||||
@HostListener('document:pointerdown')
|
||||
onPageGesture(): void {
|
||||
this.nudgeTwitch();
|
||||
}
|
||||
|
||||
embedUrl = computed<SafeResourceUrl | null>(() => {
|
||||
const id = this.videoId();
|
||||
const p = this.provider();
|
||||
@@ -426,13 +453,15 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
||||
// Channel, clip ou VOD ; Twitch exige 'parent' = domaine d'embed.
|
||||
const parentsSet = new Set<string>([host, 'localhost', '127.0.0.1']);
|
||||
const parentParams = Array.from(parentsSet).map(h => `parent=${encodeURIComponent(h)}`).join('&');
|
||||
// `&r=1` = cette vidéo a déjà subi le nudge (voir twitchNudgedFor).
|
||||
const nudge = this.twitchNudgedFor() === this.twitchKey() ? '&r=1' : '';
|
||||
// Clips : player dédié clips.twitch.tv (le player VOD rejette les slugs).
|
||||
const clipId = clip || (/^\d+$/.test(String(id || '')) ? null : ((this.video() as any)?.kind === 'clip' ? id : null));
|
||||
if (clipId) {
|
||||
// autoplay+muet, comme les autres fournisseurs de la page (YouTube
|
||||
// mute=1, Dailymotion mute=1, Odysee muted=1) : le lecteur démarre
|
||||
// tout seul, le son se réactive via les commandes du player.
|
||||
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true`;
|
||||
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
||||
}
|
||||
const base = 'https://player.twitch.tv/';
|
||||
@@ -442,8 +471,8 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
||||
// Les VOD acceptent time=1h2m3s (seek au chargement, ex. transcript).
|
||||
const timeParam = !chan && start > 0 ? `&time=${encodeURIComponent(this.formatTwitchTime(start))}` : '';
|
||||
const qs = chan
|
||||
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true`
|
||||
: `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true&muted=true${timeParam}`;
|
||||
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}`
|
||||
: `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true&muted=true${timeParam}${nudge}`;
|
||||
const u = base + qs;
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
|
||||
}
|
||||
@@ -500,6 +529,7 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
onGlobalKey(ev: KeyboardEvent): void {
|
||||
this.nudgeTwitch();
|
||||
try {
|
||||
const target = ev.target as HTMLElement | null;
|
||||
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
||||
|
||||
Reference in New Issue
Block a user