Skip to content

Instantly share code, notes, and snippets.

@stefb69
Last active April 30, 2025 21:38
Show Gist options
  • Save stefb69/d95d20d11961ab44271eeec3a22830da to your computer and use it in GitHub Desktop.
Save stefb69/d95d20d11961ab44271eeec3a22830da to your computer and use it in GitHub Desktop.
Plex Web 21:9 Auto‑Crop User Script

Plex Web 21:9 Auto‑Crop 🎬

Version 2.1 – Stable Ratio Edition (April 2025) 🎉

Un user‑script Tampermonkey/Greasemonkey qui détecte automatiquement les bandes noires (« hard letterbox ») dans Plex Web et applique un zoom adapté aux écrans ultra‑larges 21:9 sans jamais toucher aux vidéos déjà au bon format (16:9, 4:3…).


✨ Principales nouveautés depuis v2.0 🚀

Fonction v2.0 v2.1
Détection bande noire oui oui
Classification de ratio Association à un ratio standard (4:3, 16:9, 2.35:1…)
Seuil de tolérance approx. ± 5 % (RATIO_TOLERANCE)
Stabilité visuelle peut osciller Zoom figé tant que le format reste identique
Touches rapides w / [ / ] inchangées

Fonctionnalités 📺

  • Auto‑crop intelligent : mesure les bandes noires toutes les 1,5 s, calcule le ratio réel puis lisse les variations pour un affichage stable.
  • Respect des formats natifs : aucune modification si la vidéo est 16:9 ou 4:3.
  • Prise en charge des formats scope (2.35/2.39/2.40:1)… sans couper les sous‑titres !
  • Contrôles clavier
    • w : activer/désactiver le script
    • ] / [ : affiner manuellement le zoom par pas de 0,05 (désactive l’auto tant que la page n’est pas rechargée)
  • Paramètres ajustables dans l’en‑tête du script : seuils, fréquence d’analyse, listes de ratios.
  • 100 % JavaScript pur – aucune dépendance externe ; fonctionne sur Chrome, Edge, Firefox, Brave… partout où Plex Web tourne.

Installation 🛠️

  1. Installez Tampermonkey (ou Greasemonkey/Violentmonkey) dans votre navigateur.
  2. Téléchargez le fichier plex-zoom.user.js (version ≥ 2.1) ou copiez le code depuis ce dépôt.
  3. Créez un nouveau script dans Tampermonkey et collez le contenu, ou ouvrez directement le fichier : l’extension proposera l’installation.
  4. Rechargez Plex Web, lancez une vidéo scope ; appuyez sur w pour vérifier l’activation.

Astuce multi‑écrans : si vous passez souvent d’un moniteur 16:9 à un 21:9, laissez le script actif et utilisez w pour commuter à la volée. 🚦


Personnalisation express ⚙️

Constante Rôle Défaut
STANDARD_RATIOS Listes des ratios « canoniques » à reconnaître [1.33,1.60,1.78,2.00,2.35,2.40]
RATIO_TOLERANCE Ecart (%) max pour rester dans la même catégorie 0.05 (± 5 %)
BAR_THRESHOLD Barres ≤ N px considérées inexistantes 12
ANALYZE_EVERY Période d’échantillonnage (ms) 1500

Pour des bandes très bruitées, augmentez BAR_THRESHOLD ou DARK_LUMA. 🔧🔧🔧


Dépannage 🆘

Problème Piste
Le zoom reste à 1 sur un film scope Le flux vidéo vient d’un domaine différent sans CORS ; le navigateur bloque l’accès pixel. → Servez Plex via app.plex.tv ou activez le proxy sécurisé.
L’image oscille malgré tout Réduisez RATIO_TOLERANCE à 0.03 ou augmentez ANALYZE_EVERY pour lisser l’analyse.
Sous‑titres coupés Réglez manuellement [ / ], ou descendez RATIO_TOLERANCE pour un zoom plus doux.

Journal des versions 🗒️

  • 2.1 (2025‑04‑30) : classification de ratio + seuil ± 5 %, zoom figé, nouvelles constantes.
  • 2.0 (2025‑04‑29) : détection automatique des bandes noires, zoom dynamique, raccourcis clavier.
  • 1.0 (2025‑03‑22) : zoom manuel simple pour l’ultra‑large.

Licence 📄

MIT. Toute contribution est la bienvenue – ouvrez une issue ou une pull request ! 🎁

// ==UserScript==
// @name Plex Web 21-9 Auto-Crop + Detect + StableRatio
// @namespace https://github.com/stefb69
// @version 2.1
// @description Zoom 21:9 pour Plex Web – n’ajuste que lorsque le format vidéo change réellement (16:9, 4:3, 2.35, …).
// @author Stefb69
// @match https://app.plex.tv/*
// @match https://*.plex.direct/*
// @grant none
// @run-at document-end
// ==/UserScript==
(() => {
/* ───── Réglages clés ───────────────────────────────────────────── */
const STANDARD_RATIOS = [1.33, 1.60, 1.78, 2.00, 2.35, 2.40]; // 4:3, 16:10, 16:9, 18:9, Scope
const RATIO_TOLERANCE = 0.05; // 5 % d’écart max pour rester dans la même “case”
const BAR_THRESHOLD = 12; // barres ≤ 12 px = ignorées
const DARK_LUMA = 20; // 0-255 – seuil “noir” pour la détection
const ANALYZE_EVERY = 1500; // ms entre 2 analyses
/* Raccourcis */
const KEY_UP=']', KEY_DOWN='[', KEY_TOGGLE='w';
/* ───── Variables d’état ────────────────────────────────────────── */
let video = null, enabled = true;
let currentClass = null; // catégorie de ratio courante
let manualZoom = null, analyzeTimer = null;
/* ───── Utilitaires détection barres ────────────────────────────── */
const luma = (r,g,b)=>0.2126*r+0.7152*g+0.0722*b;
const sampleLine = (ctx,x,y,w,h)=>{
const d=ctx.getImageData(x,y,w,h).data;
let s=0; for(let i=0;i<d.length;i+=4) s+=luma(d[i],d[i+1],d[i+2]);
return s/(d.length/4);
};
function detectBars(v){
const W=v.videoWidth,H=v.videoHeight;
try{
const c=new OffscreenCanvas(W,H),ctx=c.getContext('2d',{willReadFrequently:true});
ctx.drawImage(v,0,0,W,H);
let top=0,bottom=0;
while(top<H/2 && sampleLine(ctx,0,top,W,1)<DARK_LUMA) top++;
while(bottom<H/2 && sampleLine(ctx,0,H-1-bottom,W,1)<DARK_LUMA) bottom++;
return {top,bottom};
}catch(e){return null;}
}
/* ───── Classification & zoom ───────────────────────────────────── */
function classifyRatio(width,height,bars){
if(!bars) return null;
const visibleH = height - (bars.top + bars.bottom);
if(visibleH<=0) return null;
const ratio = width / visibleH;
for(const r of STANDARD_RATIOS){
if(Math.abs(ratio-r)/r < RATIO_TOLERANCE) return r;
}
return null; // ratio exotique, on laisse tel quel
}
function computeZoom(ratio){
if(!ratio) return 1;
return ratio > 2 ? 1.33 : // Scope → remplir 21:9
ratio < 1.5 ? 1 : // 4:3 – on ne touche pas
1; // 16:9 ou proche – idem
}
function applyZoom(z){
if(!video) return;
video.style.objectFit='contain';
video.style.transform=`scale(${z})`;
video.style.transformOrigin='center center';
video.parentElement.style.overflow='hidden';
}
/* ───── Analyse périodique ──────────────────────────────────────── */
function analyze(){
if(!enabled||!video||manualZoom!==null) return;
const bars=detectBars(video);
if(!bars|| (bars.top+bars.bottom)<=BAR_THRESHOLD*2){ // pas de vraies barres
if(currentClass!==null){ currentClass=null; applyZoom(1); }
return;
}
const newClass=classifyRatio(video.videoWidth,video.videoHeight,bars);
if(!newClass) return;
if(currentClass===null || Math.abs(newClass-currentClass)/currentClass > RATIO_TOLERANCE){
currentClass=newClass;
applyZoom(computeZoom(newClass));
}
}
function startLoop(){
if(analyzeTimer) clearInterval(analyzeTimer);
analyze(); analyzeTimer=setInterval(analyze,ANALYZE_EVERY);
}
/* ───── Observateur DOM ─────────────────────────────────────────── */
new MutationObserver(()=>{ const v=document.querySelector('video'); if(v!==video){ video=v; manualZoom=null; currentClass=null; startLoop(); }})
.observe(document.body,{childList:true,subtree:true});
/* ───── Raccourcis clavier ─────────────────────────────────────── */
window.addEventListener('keydown',e=>{
if(!video) return;
switch(e.key){
case KEY_UP:
manualZoom=(manualZoom??computeZoom(currentClass||1))+0.05; applyZoom(manualZoom); break;
case KEY_DOWN:
manualZoom=Math.max(1,(manualZoom??computeZoom(currentClass||1))-0.05); applyZoom(manualZoom); break;
case KEY_TOGGLE:
enabled=!enabled;
if(!enabled){video.style.transform='';}else{manualZoom=null; analyze();}
break;
}
});
/* ───── Lancement initial ──────────────────────────────────────── */
const vid0=document.querySelector('video'); if(vid0){video=vid0; startLoop();}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment