fix(shorts): autoplay muet, langues FR/EN, proxy prod et Dailymotion embeds
CI / build-and-test (push) Successful in 14m9s
CI / build-and-test (push) Successful in 14m9s
- autoplay: les Shorts demarrent muets (comme Watch) pour garantir l'autoplay des iframes; le son se debloque a la premiere interaction (unlockSound) et reste controlable
- proxy prod: ajoute les alias /proxy/twitch-api, /proxy/twitch-auth, /proxy/dm, /proxy/odysee (evite le fallback SPA -> parse JSON 'status 200 ok:false'); forwardJson accepte les deux prefixes. Corrige aussi la validation des clips Twitch morts
- Dailymotion: allow_embed demande dans les flux shorts/trending -> les videos non-embeddables ('Forbidden') sont exclues
- langues: mapping language (Dailymotion/PeerTube/Odysee) + shortsLangOk applique a tous les fournisseurs; en+fr toujours acceptes
- PeerTube shorts: durationMin/Max cote serveur (plus de videos longues au hasard)
- Twitch: validation helix/clips de nouveau fonctionnelle + autoplay muet
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<div class="shorts-stage flex w-full flex-col items-center gap-3 rounded-2xl bg-black/60 p-2 sm:p-4" (click)="closeMenu()">
|
||||
<div class="shorts-stage flex w-full flex-col items-center gap-3 rounded-2xl bg-black/60 p-2 sm:p-4" (click)="unlockSound(); closeMenu()">
|
||||
<!-- Sélecteur : Pour toi + fournisseurs -->
|
||||
<div class="scroll-rail no-scrollbar flex max-w-full items-center justify-start gap-2 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
|
||||
<button (click)="toggleForYou()"
|
||||
<button (click)="unlockSound(); toggleForYou()"
|
||||
[attr.aria-pressed]="forYou()"
|
||||
class="chip shrink-0 px-3 py-2 rounded-full text-sm border transition-colors"
|
||||
[class]="forYou() ? 'bg-violet-600 border-violet-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'"
|
||||
@@ -9,7 +9,7 @@
|
||||
✨ Pour toi
|
||||
</button>
|
||||
<button *ngFor="let p of providersList()"
|
||||
(click)="selectProvider(p.id)"
|
||||
(click)="unlockSound(); selectProvider(p.id)"
|
||||
[attr.aria-pressed]="!forYou() && provider() === p.id"
|
||||
class="chip shrink-0 px-3 py-2 rounded-full text-sm border transition-colors"
|
||||
[class]="!forYou() && provider() === p.id ? 'bg-red-600 border-red-500 text-white' : 'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'">
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<!-- Scène immersive : flèches + cadre 9:16 + rail d'actions -->
|
||||
<div class="flex w-full min-h-0 items-center justify-center gap-1 sm:gap-3"
|
||||
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
|
||||
(wheel)="unlockSound(); onWheel($event)" (touchstart)="unlockSound(); onTouchStart($event)" (touchend)="onTouchEnd($event)">
|
||||
<!-- Flèche précédent (bureau, façon YouTube) -->
|
||||
<button type="button" (click)="prev(); $event.stopPropagation()" [disabled]="!canPrev()"
|
||||
class="chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-slate-800/80 text-white hover:bg-slate-700 disabled:opacity-30"
|
||||
|
||||
@@ -82,12 +82,16 @@ export class WatchShortComponent implements OnDestroy {
|
||||
/** Cadre 9:16 (plein écran + focus). */
|
||||
frameRef = viewChild<ElementRef<HTMLElement>>('shortsFrame');
|
||||
/**
|
||||
* Son coupé ou non. Par défaut le son est ACTIVÉ sur les Shorts : les
|
||||
* navigateurs le permettent dès la première interaction (molette/clic),
|
||||
* sinon le player démarre en pause et l'utilisateur appuie sur lecture.
|
||||
* Rebasculer rebascule l'URL d'embed (reprend au début).
|
||||
* Son coupé ou non. Démarrer MUET est la seule façon d'obtenir l'autoplay
|
||||
* garanti des iframes (politique navigateur) : c'est aussi ce que fait la
|
||||
* page Watch (mute=1). Le son est rétabli automatiquement à la première
|
||||
* interaction (clic/molette/tactile) via `unlockSound()`, et l'utilisateur
|
||||
* peut couper/réactiver via le bouton. Rebasculer recharge l'embed.
|
||||
*/
|
||||
muted = signal(false);
|
||||
muted = signal(true);
|
||||
/** L'utilisateur a-t-il touché au bouton son ? (sinon on débloque en auto). */
|
||||
private muteTouched = false;
|
||||
private soundUnlocked = false;
|
||||
/**
|
||||
* Embed Rumble autoritaire : id demandé -> vrai id scrapé via
|
||||
* /api/rumble/video/<id> (comme /watch). Les IDs de liste approximatifs
|
||||
@@ -517,9 +521,21 @@ export class WatchShortComponent implements OnDestroy {
|
||||
|
||||
// ---- Actions façon YouTube Shorts ----
|
||||
toggleMute(): void {
|
||||
this.muteTouched = true;
|
||||
this.muted.update(m => !m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Première interaction de l'utilisateur : le navigateur autorise alors un
|
||||
* embed avec son. On débloque le son une seule fois, sauf si l'utilisateur
|
||||
* a déjà choisi explicitement (mute activé manuellement).
|
||||
*/
|
||||
unlockSound(): void {
|
||||
if (this.soundUnlocked || this.muteTouched || !this.muted()) return;
|
||||
this.soundUnlocked = true;
|
||||
this.muted.set(false);
|
||||
}
|
||||
|
||||
toggleFullscreen(): void {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
@@ -1084,7 +1100,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
private accepts(v: Video): boolean {
|
||||
const p = String((v as any).provider || this.provider()).toLowerCase();
|
||||
const withProvider = { ...(v as object), provider: p } as Video;
|
||||
return shortsFeedAccepts(withProvider, p) && this.isPlayable(withProvider);
|
||||
return shortsFeedAccepts(withProvider, p) && this.isPlayable(withProvider) && this.shortsLangOk(withProvider);
|
||||
}
|
||||
|
||||
/** Jouabilité : l'embed répondra-t-il ? (indépendant du type de contenu) */
|
||||
@@ -1102,18 +1118,17 @@ export class WatchShortComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* La vidéo respecte-t-elle les langues de Shorts de l'utilisateur
|
||||
* (langue UI + préférences + transcripts) ? Seuls les items Twitch
|
||||
* portent la langue (champ Helix) ; sans métadonnée, le doute profite.
|
||||
* YouTube/PeerTube sont filtrés côté serveur (relevanceLanguage,
|
||||
* languageOneOf) ; Dailymotion/Odysee/Rumble n'exposent pas la langue.
|
||||
* La vidéo respecte-t-elle les langues acceptées (FR/EN + préférences) ?
|
||||
* On lit `language` (mappé pour DM/PeerTube/Odysee) ou `raw.language`
|
||||
* (Twitch). Sans métadonnée, le doute profite à la vidéo : YouTube et
|
||||
* PeerTube sont de toute façon filtrés côté serveur (relevanceLanguage,
|
||||
* languageOneOf) et Rumble n'expose pas la langue.
|
||||
*/
|
||||
private shortsLangOk(v: Video): boolean {
|
||||
const p = (v as any).provider || this.provider();
|
||||
if (String(p) !== 'twitch') return true;
|
||||
const lang = String((v as any)?.raw?.language || '').toLowerCase().split('-')[0];
|
||||
const lang = String((v as any)?.language || (v as any)?.raw?.language || '')
|
||||
.toLowerCase().split('-')[0];
|
||||
if (!lang || lang === 'other') return true;
|
||||
let accepted: string[] = ['en'];
|
||||
let accepted: string[] = ['en', 'fr'];
|
||||
try { accepted = this.api.getShortsLanguages(); } catch {}
|
||||
return accepted.includes(lang);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user