feat(twitch): son par défaut + pas d'avance auto sur les clips
CI / build-and-test (push) Canceled after 0s

- /watch : muted=true supprime — son actif dès le démarrage (le chargement
  de l'embed a lieu dans un geste, donc l'autoplay sonore est autorisé).
- /shorts : muted piloté par le bouton Son du menu (défaut : son ON) — ce
  bouton était jusqu'ici sans effet sur Twitch.
- /shorts : plus d'avance auto pour Twitch. L'embed clips est cross-origin et
  n'expose AUCUNE API d'état : impossible de savoir si la vidéo est en pause,
  donc on ne change jamais de vidéo tout seul (swipe / flèches / molette).
- Verifs instance locale : 4/4 (URLs + aucune avance après 75 s d'un clip de
  49 s) + 6/6 (nudge) + 8/8 (chargement au premier geste).
This commit is contained in:
2026-10-01 12:31:21 -04:00
parent 059481b9ea
commit f3fd57014a
2 changed files with 17 additions and 18 deletions
+12 -12
View File
@@ -131,9 +131,10 @@ export class WatchShortComponent implements OnDestroy {
* auquel cas l'utilisateur tape une fois sur la vidéo (geste qui débloque * auquel cas l'utilisateur tape une fois sur la vidéo (geste qui débloque
* les suivantes). * les suivantes).
* *
* Exceptions (muet forcé, autoplay garanti) : * Cas particuliers :
* - Twitch clips : l'embed `clips.twitch.tv` n'expose AUCUNE API de son — * - Twitch clips : l'embed `clips.twitch.tv` n'expose AUCUNE API de son —
* `muted=true` est la seule combinaison qui démarre sans geste ; * l'état muet passe par le paramètre `muted` de l'URL (défaut : son ON,
* piloté par le bouton Son du menu) ;
* - Rumble : `autoplay=1` (son) par défaut, `autoplay=2` (muet) si * - Rumble : `autoplay=1` (son) par défaut, `autoplay=2` (muet) si
* l'utilisateur coupe le son. * l'utilisateur coupe le son.
*/ */
@@ -454,11 +455,9 @@ export class WatchShortComponent implements OnDestroy {
// Les Shorts Twitch = clips uniquement (jamais de live/channel). // Les Shorts Twitch = clips uniquement (jamais de live/channel).
const clipId = (v as any).kind === 'clip' ? v.videoId : null; const clipId = (v as any).kind === 'clip' ? v.videoId : null;
if (clipId) { if (clipId) {
// `muted=true` FORCÉ (indépendant du bouton Son) : l'embed clips // Son piloté par le bouton Son du menu (`muted`, défaut = son ON) —
// n'expose aucune API de volume — muet est la seule combinaison // le rechargement de l'embed applique le nouvel état.
// qui démarre sans geste. Le son reste disponible via « Ouvrir const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`;
// chez le fournisseur » / Watch.
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u); return this.sanitizer.bypassSecurityTrustResourceUrl(u);
} }
// Repli : player channel/VOD (ne devrait pas arriver, les lives // Repli : player channel/VOD (ne devrait pas arriver, les lives
@@ -466,8 +465,8 @@ export class WatchShortComponent implements OnDestroy {
const base = 'https://player.twitch.tv/'; const base = 'https://player.twitch.tv/';
const chan = !/^\d+$/.test(String(v.videoId)) ? v.videoId : null; const chan = !/^\d+$/.test(String(v.videoId)) ? v.videoId : null;
const qs = chan const qs = chan
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}` ? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`
: `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=true${nudge}`; : `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(base + qs); return this.sanitizer.bypassSecurityTrustResourceUrl(base + qs);
} }
if (p === 'peertube' && v.videoId) { if (p === 'peertube' && v.videoId) {
@@ -805,9 +804,10 @@ export class WatchShortComponent implements OnDestroy {
} catch {} } catch {}
this.autoAdvanceKey = ''; this.autoAdvanceKey = '';
if (!v?.videoId) return; if (!v?.videoId) return;
// Embed Twitch en attente d'un geste : il n'y a rien à l'écran, inutile de // Twitch : l'embed clips est cross-origin et n'expose AUCUNE API d'état —
// faire défiler le flux (sinon les videos « switchent » sans jamais jouer). // on ne peut pas savoir si la vidéo est en pause. Pas d'avance auto pour
if (this.providerOf(v) === 'twitch' && !this.twitchCanLoad()) return; // ne jamais changer de vidéo pendant une pause (navigation manuelle).
if (this.providerOf(v) === 'twitch') return;
const known = Number((v as Video).duration || 0); const known = Number((v as Video).duration || 0);
const secs = (Number.isFinite(known) && known > 0) ? known : this.estimatedDuration(v); const secs = (Number.isFinite(known) && known > 0) ? known : this.estimatedDuration(v);
if (!Number.isFinite(secs) || secs <= 0) return; if (!Number.isFinite(secs) || secs <= 0) return;
+5 -6
View File
@@ -484,10 +484,9 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
// Clips : player dédié clips.twitch.tv (le player VOD rejette les slugs). // 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)); const clipId = clip || (/^\d+$/.test(String(id || '')) ? null : ((this.video() as any)?.kind === 'clip' ? id : null));
if (clipId) { if (clipId) {
// autoplay+muet, comme les autres fournisseurs de la page (YouTube // Son PAR DÉFAUT (demande utilisateur) : le chargement a lieu dans un
// mute=1, Dailymotion mute=1, Odysee muted=1) : le lecteur démarre // geste (voir twitchCanLoad), donc l'autoplay sonore est autorisé.
// 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${nudge}`;
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u); return this.sanitizer.bypassSecurityTrustResourceUrl(u);
} }
const base = 'https://player.twitch.tv/'; const base = 'https://player.twitch.tv/';
@@ -497,8 +496,8 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
// Les VOD acceptent time=1h2m3s (seek au chargement, ex. transcript). // Les VOD acceptent time=1h2m3s (seek au chargement, ex. transcript).
const timeParam = !chan && start > 0 ? `&time=${encodeURIComponent(this.formatTwitchTime(start))}` : ''; const timeParam = !chan && start > 0 ? `&time=${encodeURIComponent(this.formatTwitchTime(start))}` : '';
const qs = chan const qs = chan
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}` ? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true${nudge}`
: `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true&muted=true${timeParam}${nudge}`; : `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true${timeParam}${nudge}`;
const u = base + qs; const u = base + qs;
return this.sanitizer.bypassSecurityTrustResourceUrl(u); return this.sanitizer.bypassSecurityTrustResourceUrl(u);
} }