fix(shorts): hover-chrome, swipe mobile garanti, avance auto fiabilisee
CI / build-and-test (push) Successful in 15m2s
CI / build-and-test (push) Successful in 15m2s
Chrome auto-hide au hover desktop (2,5 s). Capteur tactile mobile : swipe garanti (l'iframe mangeait les touchers), tap = play/pause YT ou 6 s d'acces direct au lecteur sinon. Fleche retour accueil mobile, cadre quasi plein ecran. Avance auto : handshake YT listening (les evenements player n'arrivaient jamais), onStateChange canonique, retry borne anti-prefetch. Docs 9.9.
This commit is contained in:
@@ -518,6 +518,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const i = this.index();
|
||||
const list = this.items();
|
||||
this.ytProgress.set(null);
|
||||
this.ytPlaying.set(true);
|
||||
this.menuOpen.set(false);
|
||||
if (!v) return;
|
||||
const nx = (i >= 0 && i + 1 < list.length) ? list[i + 1] : null;
|
||||
@@ -559,6 +560,10 @@ export class WatchShortComponent implements OnDestroy {
|
||||
try { document.removeEventListener('fullscreenchange', this.handleFullscreenChange); } catch {}
|
||||
try { if (this.autoAdvanceTimer) clearTimeout(this.autoAdvanceTimer); } catch {}
|
||||
this.autoAdvanceTimer = null;
|
||||
try { if (this.chromeIdleTimer) clearTimeout(this.chromeIdleTimer); } catch {}
|
||||
this.chromeIdleTimer = null;
|
||||
try { if (this.passthroughTimer) clearTimeout(this.passthroughTimer); } catch {}
|
||||
this.passthroughTimer = null;
|
||||
if (this.shareCopiedTimer) { try { clearTimeout(this.shareCopiedTimer); } catch {} }
|
||||
}
|
||||
|
||||
@@ -600,6 +605,15 @@ export class WatchShortComponent implements OnDestroy {
|
||||
/** Timer d'avance auto en fin de vidéo (repli générique). */
|
||||
private autoAdvanceTimer: any = null;
|
||||
private autoAdvanceKey = '';
|
||||
/** État de lecture YouTube (via `playerState`, pour le toggle tap). */
|
||||
private ytPlaying = signal(true);
|
||||
/** Chrome (boutons/méta) visible — auto-hide sur desktop au repos. */
|
||||
readonly chromeVisible = signal(true);
|
||||
private chromeIdleTimer: any = null;
|
||||
/** Accès direct au lecteur (overlay effacé 6 s, tap non-YouTube). */
|
||||
readonly passthrough = signal(false);
|
||||
readonly passthroughHint = signal(false);
|
||||
private passthroughTimer: any = null;
|
||||
|
||||
/**
|
||||
* Durée effective pour l'avance auto quand la durée réelle est inconnue.
|
||||
@@ -649,11 +663,43 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (typeof document !== 'undefined' && document.hidden) return;
|
||||
if (this.autoAdvanceKey !== key) return;
|
||||
const cur = this.current();
|
||||
if (this.index() === atIndex && cur && String(cur.videoId) === id) this.next();
|
||||
if (this.index() === atIndex && cur && String(cur.videoId) === id) this.advanceWithRetry();
|
||||
} catch {}
|
||||
}, Math.min(600000, (secs + 2.5) * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Avance avec réessais bornés : `next()` peut être absorbé sans effet quand
|
||||
* un pré-chargement est en cours (`busyMore`) ou que la page suivante n'est
|
||||
* pas encore arrivée — sans réessai, la vidéo reste bloquée sur son écran
|
||||
* de fin. 2 réessais à 2,5 s puis abandon (jamais de boucle).
|
||||
*/
|
||||
private advanceWithRetry(attempt = 0): void {
|
||||
try {
|
||||
const before = this.index();
|
||||
if (!this.items().length || this.loading()) return;
|
||||
this.next();
|
||||
if (this.index() !== before) return;
|
||||
if (attempt < 2) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (this.index() === before && this.items().length > 0 && !this.loading()) {
|
||||
this.advanceWithRetry(attempt + 1);
|
||||
}
|
||||
} catch {}
|
||||
}, 2500);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** Fin de lecture vue par un player (garde anti-double). */
|
||||
private advanceFromPlayer(id: string): void {
|
||||
const key = `end:${id}`;
|
||||
if (this.autoAdvanceKey === key) return;
|
||||
this.autoAdvanceKey = key;
|
||||
this.advanceWithRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relance la lecture YouTube si l'autoplay a calé : commande `playVideo`
|
||||
* via l'API iframe (`enablejsapi=1` déjà présent dans l'URL d'embed).
|
||||
@@ -664,18 +710,30 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (!v?.videoId || this.providerOf(v) !== 'youtube') return;
|
||||
const id = String(v.videoId);
|
||||
const atIndex = this.index();
|
||||
// Handshake SANS LEQUEL le player YouTube n'émet RIEN (ni infoDelivery
|
||||
// ni onStateChange) : c'est ce qui rendait l'avance auto et la barre de
|
||||
// progression inopérantes. Envoyé tout de suite + après chargement.
|
||||
this.postToPlayer({ event: 'listening' });
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const cur = this.current();
|
||||
if (this.index() !== atIndex || !cur || String(cur.videoId) !== id) return;
|
||||
const frame = this.frameRef()?.nativeElement;
|
||||
const iframe = frame?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
iframe?.contentWindow?.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: '' }), '*');
|
||||
this.postToPlayer({ event: 'listening' });
|
||||
if (this.ytPlaying()) this.postToPlayer({ event: 'command', func: 'playVideo', args: '' });
|
||||
} catch {}
|
||||
}, 1500);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** Post un message à l'iframe du short courant (API YouTube, sans effet sinon). */
|
||||
private postToPlayer(msg: object): void {
|
||||
try {
|
||||
const frame = this.frameRef()?.nativeElement;
|
||||
const iframe = frame?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
iframe?.contentWindow?.postMessage(JSON.stringify(msg), '*');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* File de résolution Rumble (concurrence bornée) : chaque id est vérifié
|
||||
* via le backend (comme /watch). Succès => l'embed bascule sur le vrai
|
||||
@@ -743,6 +801,55 @@ export class WatchShortComponent implements OnDestroy {
|
||||
this.muted.update(m => !m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome auto-hide (desktop) : visible au survol / à l'interaction, masqué
|
||||
* après 2,5 s d'immobilité — façon YouTube. Sur tactile, `mousemove` ne
|
||||
* tire presque jamais : le chrome reste visible en permanence.
|
||||
*/
|
||||
pokeChrome(): void {
|
||||
try {
|
||||
this.chromeVisible.set(true);
|
||||
if (this.chromeIdleTimer) clearTimeout(this.chromeIdleTimer);
|
||||
this.chromeIdleTimer = setTimeout(() => {
|
||||
try { this.chromeVisible.set(false); } catch {}
|
||||
}, 2500);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tap sur la vidéo (mobile : l'overlay capte le toucher, l'iframe ne le
|
||||
* voit pas — voir `.shorts-gesture`) :
|
||||
* - YouTube : play/pause via l'API iframe (état suivi par `playerState`) ;
|
||||
* - autres (pas d'API) : 6 s d'accès DIRECT au lecteur — l'overlay
|
||||
* s'efface et les boutons du fournisseur deviennent touchables, puis
|
||||
* l'overlay revient seul. Discret et réversible.
|
||||
*/
|
||||
private onVideoTap(): void {
|
||||
try {
|
||||
const v = this.current();
|
||||
if (!v) return;
|
||||
this.pokeChrome();
|
||||
if (this.providerOf(v) === 'youtube') {
|
||||
const play = !this.ytPlaying();
|
||||
this.postToPlayer({ event: 'command', func: play ? 'playVideo' : 'pauseVideo', args: '' });
|
||||
this.ytPlaying.set(play);
|
||||
return;
|
||||
}
|
||||
this.enterPassthrough();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private enterPassthrough(): void {
|
||||
try {
|
||||
this.passthrough.set(true);
|
||||
this.passthroughHint.set(true);
|
||||
if (this.passthroughTimer) clearTimeout(this.passthroughTimer);
|
||||
this.passthroughTimer = setTimeout(() => {
|
||||
try { this.passthrough.set(false); this.passthroughHint.set(false); } catch {}
|
||||
}, 6000);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
toggleFullscreen(): void {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
@@ -857,6 +964,26 @@ export class WatchShortComponent implements OnDestroy {
|
||||
try { this.skipUnplayable(); } catch {}
|
||||
return;
|
||||
}
|
||||
// Fin/début/pause canoniques (`onStateChange`, plus fiable que le temps :
|
||||
// pas de marge). Requiert le handshake `listening` (voir nudgeYouTubePlay).
|
||||
if (data.event === 'onStateChange') {
|
||||
const st = Number(data.info);
|
||||
const cur = this.current();
|
||||
if (!cur || String(cur.videoId) !== String(v.videoId) || !Number.isFinite(st)) return;
|
||||
const id = String(v.videoId);
|
||||
if (st === 0) {
|
||||
this.advanceFromPlayer(id);
|
||||
} else if (st === 2) {
|
||||
// Pause utilisateur : état + minuteur désarmé (on n'avance pas une
|
||||
// vidéo mise en pause).
|
||||
this.ytPlaying.set(false);
|
||||
this.autoAdvanceKey = `paused:${id}`;
|
||||
} else if (st === 1) {
|
||||
this.ytPlaying.set(true);
|
||||
if (this.autoAdvanceKey.startsWith('paused:')) this.scheduleAutoAdvance(v);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.event === 'infoDelivery' && data.info && typeof data.info === 'object') {
|
||||
const ct = Number(data.info.currentTime);
|
||||
let dur = Number(data.info.duration);
|
||||
@@ -869,23 +996,21 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const id = String(v.videoId);
|
||||
if (state === 0) {
|
||||
// Fin certaine (plus fiable que le temps : pas de marge) → suivant.
|
||||
const key = `end:${id}`;
|
||||
if (this.autoAdvanceKey !== key) {
|
||||
this.autoAdvanceKey = key;
|
||||
this.next();
|
||||
}
|
||||
this.advanceFromPlayer(id);
|
||||
return;
|
||||
}
|
||||
if (state === 2) {
|
||||
// Pause utilisateur : on désarme le minuteur générique pour ne
|
||||
// pas faire avancer une vidéo mise en pause.
|
||||
this.ytPlaying.set(false);
|
||||
this.autoAdvanceKey = `paused:${id}`;
|
||||
return;
|
||||
}
|
||||
if (state === 1 && this.autoAdvanceKey.startsWith('paused:')) {
|
||||
if (state === 1) {
|
||||
// Reprise après pause : on réarme le minuteur (repli, la fin
|
||||
// temps réel prime de toute façon).
|
||||
this.scheduleAutoAdvance(v);
|
||||
this.ytPlaying.set(true);
|
||||
if (this.autoAdvanceKey.startsWith('paused:')) this.scheduleAutoAdvance(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -898,11 +1023,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (ct >= dur - 0.75) {
|
||||
const cur = this.current();
|
||||
if (cur && String(cur.videoId) === String(v.videoId)) {
|
||||
const key = `end:${String(v.videoId)}`;
|
||||
if (this.autoAdvanceKey !== key) {
|
||||
this.autoAdvanceKey = key;
|
||||
this.next();
|
||||
}
|
||||
this.advanceFromPlayer(String(v.videoId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1730,6 +1851,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const threshold = 50;
|
||||
if (dy < -threshold) this.next();
|
||||
else if (dy > threshold) this.prev();
|
||||
else if (Math.abs(dy) < 12) this.onVideoTap();
|
||||
this.lastSwipeY = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user