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
* les suivantes).
*
* Exceptions (muet forcé, autoplay garanti) :
* Cas particuliers :
* - 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
* l'utilisateur coupe le son.
*/
@@ -454,11 +455,9 @@ export class WatchShortComponent implements OnDestroy {
// Les Shorts Twitch = clips uniquement (jamais de live/channel).
const clipId = (v as any).kind === 'clip' ? v.videoId : null;
if (clipId) {
// `muted=true` FORCÉ (indépendant du bouton Son) : l'embed clips
// 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${nudge}`;
// Son piloté par le bouton Son du menu (`muted`, défaut = son ON) —
// le rechargement de l'embed applique le nouvel état.
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
// 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 chan = !/^\d+$/.test(String(v.videoId)) ? v.videoId : null;
const qs = chan
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}`
: `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=true${nudge}`;
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`
: `?video=${encodeURIComponent(v.videoId)}&${parentParams}&autoplay=true&muted=${this.muted()}${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(base + qs);
}
if (p === 'peertube' && v.videoId) {
@@ -805,9 +804,10 @@ 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;
// Twitch : l'embed clips est cross-origin et n'expose AUCUNE API d'état —
// on ne peut pas savoir si la vidéo est en pause. Pas d'avance auto pour
// 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 secs = (Number.isFinite(known) && known > 0) ? known : this.estimatedDuration(v);
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).
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${nudge}`;
// Son PAR DÉFAUT (demande utilisateur) : le chargement a lieu dans un
// geste (voir twitchCanLoad), donc l'autoplay sonore est autorisé.
const u = `https://clips.twitch.tv/embed?clip=${encodeURIComponent(clipId)}&${parentParams}&autoplay=true${nudge}`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
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).
const timeParam = !chan && start > 0 ? `&time=${encodeURIComponent(this.formatTwitchTime(start))}` : '';
const qs = chan
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true&muted=true${nudge}`
: `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true&muted=true${timeParam}${nudge}`;
? `?channel=${encodeURIComponent(chan)}&${parentParams}&autoplay=true${nudge}`
: `?video=${encodeURIComponent(id)}&${parentParams}&autoplay=true${timeParam}${nudge}`;
const u = base + qs;
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}