fix(shorts): orientation autoritaire, langues YouTube et autoplay fiable
CI / build-and-test (push) Successful in 14m18s

- format: des que largeur/hauteur sont connues, l'orientation fait autorite (horizontale/carree => jamais short, meme <75 s). Repli duree reserve aux providers muets
- dimensions mappees: Dailymotion (width,height), PeerTube (aspectRatio), Odysee (value.video.width/height) => validation verticale reelle
- YouTube: langue via snippet.defaultAudioLanguage (filtre FR/EN)
- autoplay: demarrage muet, suppression du deblocage automatique au scroll/selection (reload d'embed hors geste => ecran noir); son via le bouton dedie
- PeerTube embed: muted=1 pour l'autoplay
- server/search-filters.mjs: miroir de la regle d'orientation + tests mis a jour
This commit is contained in:
2026-09-29 14:51:56 -04:00
parent d4895bae92
commit 37681c4f53
7 changed files with 73 additions and 50 deletions
@@ -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)="unlockSound(); 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)="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)="unlockSound(); toggleForYou()"
<button (click)="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)="unlockSound(); selectProvider(p.id)"
(click)="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)="unlockSound(); onWheel($event)" (touchstart)="unlockSound(); onTouchStart($event)" (touchend)="onTouchEnd($event)">
(wheel)="onWheel($event)" (touchstart)="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"
+8 -20
View File
@@ -83,15 +83,14 @@ export class WatchShortComponent implements OnDestroy {
frameRef = viewChild<ElementRef<HTMLElement>>('shortsFrame');
/**
* 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.
* garanti des iframes (politique navigateur) — c'est aussi ce que fait la
* page Watch (mute=1). L'utilisateur active le son via le bouton dédié :
* un clic sur ce bouton recharge l'embed AVEC son tout en restant dans un
* contexte de geste utilisateur (autoplay autorisé). On ne débloque jamais
* le son automatiquement : recharger l'embed pendant un scroll relançait un
* embed non lisible (écran noir).
*/
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
@@ -243,7 +242,8 @@ export class WatchShortComponent implements OnDestroy {
// : on la dérive de l'URL vidéo, avec repli sur l'instance active.
// "Sorry this video does not exist" venait d'un mauvais host.
const inst = this.peerTubeHostOf(v) || this.instances.activePeerTubeInstance();
const u = `https://${inst}/videos/embed/${encodeURIComponent(v.videoId)}?autoplay=1&api=1`;
// `muted=1` est requis pour que l'autoplay ne soit pas bloqué.
const u = `https://${inst}/videos/embed/${encodeURIComponent(v.videoId)}?autoplay=1&muted=${this.muted() ? 1 : 0}&api=1`;
return this.sanitizer.bypassSecurityTrustResourceUrl(u);
}
if (p === 'odysee') {
@@ -521,21 +521,9 @@ 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) {