diff --git a/src/components/shorts/watch-short.component.html b/src/components/shorts/watch-short.component.html
index d12aeff..5d1c163 100644
--- a/src/components/shorts/watch-short.component.html
+++ b/src/components/shorts/watch-short.component.html
@@ -33,6 +33,15 @@
+ } @else if (embedWaiting()) {
+
+
diff --git a/src/components/shorts/watch-short.component.ts b/src/components/shorts/watch-short.component.ts
index 97574cf..d010927 100644
--- a/src/components/shorts/watch-short.component.ts
+++ b/src/components/shorts/watch-short.component.ts
@@ -373,6 +373,14 @@ export class WatchShortComponent implements OnDestroy {
*/
private twitchNudgedFor = signal
(null);
+ /**
+ * L'embed Twitch ne se charge qu'APRÈS un geste (méthode « touch listener
+ * b4 loading the iframe ») : le premier chargement tombe alors dans le
+ * contexte d'activation utilisateur. `hasBeenActive` = l'utilisateur a déjà
+ * interagi avec la page (clic sur un lien, etc.) → on charge tout de suite.
+ */
+ private twitchCanLoad = signal(!!(typeof navigator !== 'undefined' && (navigator as any).userActivation?.hasBeenActive));
+
private nudgeTwitch(): void {
const v = this.current();
if (!v || this.providerOf(v) !== 'twitch' || !this.embedUrl()) return;
@@ -381,9 +389,26 @@ export class WatchShortComponent implements OnDestroy {
this.twitchNudgedFor.set(key);
}
+ /** Tout geste : autorise le chargement de l'embed Twitch puis le nudge. */
+ private onUserGesture(): void {
+ if (!this.twitchCanLoad()) this.twitchCanLoad.set(true);
+ this.nudgeTwitch();
+ }
+
+ /** Vrai tant que l'embed Twitch attend un premier geste (affichage). */
+ readonly embedWaiting = computed(() => {
+ const v = this.current();
+ return !!v && this.providerOf(v) === 'twitch' && !this.twitchCanLoad();
+ });
+
@HostListener('document:pointerdown')
onPageGesture(): void {
- this.nudgeTwitch();
+ this.onUserGesture();
+ }
+
+ @HostListener('document:wheel')
+ onPageWheel(): void {
+ this.onUserGesture();
}
readonly embedUrl = computed(() => {
@@ -419,6 +444,9 @@ export class WatchShortComponent implements OnDestroy {
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
if (p === 'twitch' && v.videoId) {
+ // Pas encore de geste : on ne charge PAS l'embed (le premier chargement
+ // doit tomber dans un contexte d'activation utilisateur).
+ if (!this.twitchCanLoad()) return null;
const parentsSet = new Set([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).
@@ -777,6 +805,9 @@ export class WatchShortComponent implements OnDestroy {
} catch {}
this.autoAdvanceKey = '';
if (!v?.videoId) return;
+ // Embed Twitch en attente d'un geste : il n'y a rien à l'écran, inutile de
+ // faire défiler le flux (sinon les videos « switchent » sans jamais jouer).
+ if (this.providerOf(v) === 'twitch' && !this.twitchCanLoad()) return;
const known = Number((v as Video).duration || 0);
const secs = (Number.isFinite(known) && known > 0) ? known : this.estimatedDuration(v);
if (!Number.isFinite(secs) || secs <= 0) return;
@@ -1939,7 +1970,7 @@ export class WatchShortComponent implements OnDestroy {
@HostListener('document:keydown', ['$event'])
onKey(ev: KeyboardEvent) {
- this.nudgeTwitch();
+ this.onUserGesture();
// 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(); }
diff --git a/src/components/watch/watch.component.html b/src/components/watch/watch.component.html
index 1ba1ef9..450b385 100644
--- a/src/components/watch/watch.component.html
+++ b/src/components/watch/watch.component.html
@@ -92,6 +92,15 @@
referrerpolicy="origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share"
allowfullscreen title="Lecteur vidéo">
+ } @else if (twitchWaiting()) {
+
+
+
+
Touchez la page pour charger la vidéo
+
} @else if (videoSource()) {
(null);
+ /**
+ * L'embed Twitch ne se charge qu'APRÈS un geste (méthode « touch listener
+ * b4 loading the iframe ») : le premier chargement tombe alors dans le
+ * contexte d'activation utilisateur. `hasBeenActive` = l'utilisateur a déjà
+ * interagi avec la page (ex. clic sur un lien pour y arriver) → on charge
+ * tout de suite (la fenêtre d'activation de 5 s est encore ouverte).
+ */
+ private twitchCanLoad = signal(!!(typeof navigator !== 'undefined' && (navigator as any).userActivation?.hasBeenActive));
+
/** Identité de l'embed Twitch affiché (change à chaque vidéo). */
private twitchKey(): string {
return [this.videoId(), this.twitchChannel(), this.twitchClip()].map((v) => v || '').join('|');
@@ -420,9 +429,23 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
this.twitchNudgedFor.set(key);
}
+ /** Tout geste : autorise le chargement de l'embed Twitch puis le nudge. */
+ private onUserGesture(): void {
+ if (!this.twitchCanLoad()) this.twitchCanLoad.set(true);
+ this.nudgeTwitch();
+ }
+
+ /** Vrai tant que l'embed Twitch attend un premier geste (affichage). */
+ readonly twitchWaiting = computed(() => this.provider() === 'twitch' && !this.twitchCanLoad());
+
@HostListener('document:pointerdown')
onPageGesture(): void {
- this.nudgeTwitch();
+ this.onUserGesture();
+ }
+
+ @HostListener('document:wheel')
+ onPageWheel(): void {
+ this.onUserGesture();
}
embedUrl = computed(() => {
@@ -450,6 +473,9 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
if (p === 'twitch') {
+ // Pas encore de geste : on ne charge PAS l'embed (le premier chargement
+ // doit tomber dans un contexte d'activation utilisateur).
+ if (!this.twitchCanLoad()) return null;
// Channel, clip ou VOD ; Twitch exige 'parent' = domaine d'embed.
const parentsSet = new Set([host, 'localhost', '127.0.0.1']);
const parentParams = Array.from(parentsSet).map(h => `parent=${encodeURIComponent(h)}`).join('&');
@@ -529,7 +555,7 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
@HostListener('document:keydown', ['$event'])
onGlobalKey(ev: KeyboardEvent): void {
- this.nudgeTwitch();
+ this.onUserGesture();
try {
const target = ev.target as HTMLElement | null;
if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;