JFIF    $         d d  7    * Z qn )ZP 0   !1 A "#a$2Qr   D8 a Ri [f  \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H  ~ , l s / 9  ć: [=/#f ; ) x { ٛE Q )~     = 𘙲r*2~  a _V = '  ku m F  D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq  1dđ #̩ ͑ 0  ; H ] u 搂 @:~ ו K L  N s h } O I R* 8 :2 ! lD JV o (  3 = M ( z Ȱ+i *N A  r6K n S l ) ! J J Ӂ* %  ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c  P A\IL Nj! J ۱+ O\ ͔ ] ޒ S߼ J ȧ c%A N ol  ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f  h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0  'G=i 2N +0 e 2 ]] 9 VT P  O׮ 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P  < 0 x <     !1AQ "Raq 02Br #S CTb  ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV׸ YT D I 6 2 '8Y2  7'\T P.6 d &˦ @V q i  |8 -O Ε  ] ʔ U=   T L 8= ;6c |  !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U# 6w +  +s &r+n ڐ   { @ 29 gL u " T Ù  M=6  (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 !  OS  r D  5 7 % | ?x> L |  / nD 6?/8 w # [ ) L 7 +6〼T A T g   !  %5  Mm Z /c  -{1_Je "| ^$ 'O  & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@  u6  BԒ  i YN * f 0 _ GhKW u  lT <nj- 0{  M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q  $ H8o Q˼EV ~ k 0 h' L  Pp M -M"  m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @񄞅~p   #   !1QAa q  ? AL0L  x LU 4 d ; x   " 16ad0 7.\ߋ 0  = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X  &Q  З non]я u ג./ ; gZ$ M QР 2M ]h ni 0  P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _  1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3  6 #Ik+ v  {ƒ v i ( ވ  ? a e)  X xy/*@Q Ǽ Ծ ) $ IWO l c G;  !i  k{ gOŀ L ('x  T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ)  X 5 c QɎMy W ?  Q *!R > 6=7)Xj 5`J ] e  8%t ! + ' ! 1Q 5   !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $  v , ,m   㵜5u bV  =sY+ @d { N ! dn O < .- B;  _w J t 6;Q  Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш  e黅X ۢ F :)[ F G X ƹ / w _c MeD   ,ʡc c .  W D  tA $ j@ : ) - # u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P  6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L !  ~ y5 g9T̅J kbM' +s :S +B) v @ M  j e C f j E 0  Y\Qn z G 1 д~ W o{T 9?`  Rm yhsy3 !HAD ] m  c1 ~ 2 L  S u7x T ;j$ `} 4-> L#  vzŏ I L S  ֭  T { r  jGK C ; b  pU= - ` Bs K. S F w4  M q] Z dH S 0 ) tLg Tiny File Manager | videoforge_wasm.html
  • File: videoforge_wasm.html
  • Full Path: /home/mata8712/public_html/on-me-trouve/compress/videoforge_wasm.html
  • File size: 33.58 KB
  • MIME-type: text/html
  • Charset: utf-8
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VideoForge — Encodeur Wasm</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,400;0,500;1,400&family=Unbounded:wght@400;700;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

:root{
  --bg:#08080a;
  --s1:#111114;
  --s2:#18181c;
  --s3:#222228;
  --s4:#2c2c34;
  --bd:rgba(255,255,255,0.06);
  --bd2:rgba(255,255,255,0.11);
  --bd3:rgba(255,255,255,0.18);
  --acc:#c8ff00;
  --acc2:rgba(200,255,0,0.10);
  --acc3:rgba(200,255,0,0.05);
  --txt:#ebebea;
  --muted:#7a7a88;
  --dim:#44444e;
  --danger:#ff4f4f;
  --ok:#3dffa0;
  --info:#38c4ff;
  --warn:#ffb830;
  --mono:'DM Mono',monospace;
  --head:'Unbounded',sans-serif;
}

body{background:var(--bg);color:var(--txt);font-family:var(--mono);min-height:100vh;overflow-x:hidden}

/* Scanline texture */
body::before{
  content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
  background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.07) 2px,rgba(0,0,0,0.07) 4px);
}

/* ── Header ── */
.hdr{
  position:relative;z-index:2;
  display:flex;align-items:center;gap:14px;
  padding:20px 28px;
  border-bottom:1px solid var(--bd);
}
.hdr-logo{
  font-family:var(--head);font-size:17px;font-weight:900;
  letter-spacing:-0.03em;
  background:linear-gradient(135deg,var(--acc),#80ff80);
  -webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
}
.hdr-sub{font-size:11px;color:var(--muted);margin-left:2px}
.hdr-right{margin-left:auto;display:flex;align-items:center;gap:10px}
.pill{
  display:inline-flex;align-items:center;gap:6px;
  padding:4px 11px;border-radius:20px;font-size:11px;
  background:var(--s3);border:1px solid var(--bd2);color:var(--muted);
}
.pill.ok{background:rgba(61,255,160,0.08);border-color:rgba(61,255,160,0.2);color:var(--ok)}
.pill.loading{background:rgba(56,196,255,0.08);border-color:rgba(56,196,255,0.2);color:var(--info)}
.pill.err{background:rgba(255,79,79,0.08);border-color:rgba(255,79,79,0.2);color:var(--danger)}
.dot{width:6px;height:6px;border-radius:50%;background:currentColor}
.dot.pulse{animation:pulse 1.4s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}

/* ── Layout ── */
.wrap{position:relative;z-index:1;max-width:860px;margin:0 auto;padding:28px 20px;display:grid;gap:16px}

/* ── Card ── */
.card{
  background:var(--s1);border:1px solid var(--bd);border-radius:14px;
  padding:22px 24px;
}
.card-hd{
  font-size:10px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted);margin-bottom:18px;
  display:flex;align-items:center;gap:8px;
}
.card-hd::before{content:'';width:2px;height:11px;background:var(--acc);border-radius:2px;flex-shrink:0}

/* ── Drop zone ── */
.drop{
  border:1.5px dashed var(--bd2);border-radius:10px;padding:40px 20px;
  text-align:center;cursor:pointer;position:relative;overflow:hidden;
  transition:border-color .2s,background .2s;
  background:var(--acc3);
}
.drop:hover,.drop.over{border-color:var(--acc);background:var(--acc2)}
.drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
.drop-ico{
  width:52px;height:52px;margin:0 auto 14px;
  border-radius:12px;border:1px solid var(--bd2);background:var(--s2);
  display:flex;align-items:center;justify-content:center;
}
.drop-ico svg{width:26px;height:26px;stroke:var(--acc);fill:none;stroke-width:1.5}
.drop-ttl{font-family:var(--head);font-size:14px;font-weight:700;margin-bottom:6px}
.drop-hint{font-size:11px;color:var(--muted)}

/* file info bar */
.fbar{display:none;align-items:center;gap:12px;margin-top:12px;
  background:var(--s2);border:1px solid var(--bd);border-radius:9px;padding:12px 16px}
.fbar-ico{width:36px;height:36px;border-radius:8px;background:var(--acc2);
  border:1px solid rgba(200,255,0,.18);flex-shrink:0;
  display:flex;align-items:center;justify-content:center}
.fbar-ico svg{width:18px;height:18px;stroke:var(--acc);fill:none;stroke-width:1.5}
.fbar-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;font-family:var(--head)}
.fbar-sz{font-size:12px;color:var(--muted);white-space:nowrap}
.fbar-sz span{color:var(--txt)}

/* ── Grid ── */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
@media(max-width:580px){.grid2,.grid3{grid-template-columns:1fr}}
.full{grid-column:1/-1}

/* ── Field ── */
.fld{display:flex;flex-direction:column;gap:7px}
.lbl{font-size:11px;color:var(--muted);display:flex;align-items:center;justify-content:space-between}
.lbl-hint{color:var(--dim);font-size:10px}

select,input[type=number],input[type=text]{
  background:var(--s2);border:1px solid var(--bd2);border-radius:8px;
  color:var(--txt);font-family:var(--mono);font-size:12px;
  padding:9px 12px;outline:none;width:100%;
  transition:border-color .15s;-webkit-appearance:none;
}
select{cursor:pointer}
select:focus,input:focus{border-color:rgba(200,255,0,.35)}

/* ── Codec tabs ── */
.ctabs{display:flex;gap:4px;padding:4px;background:var(--s2);border-radius:10px;border:1px solid var(--bd);margin-bottom:14px}
.ctab{
  flex:1;padding:8px 6px;text-align:center;border-radius:7px;cursor:pointer;
  font-size:11px;color:var(--muted);transition:all .15s;border:1px solid transparent;
  font-family:var(--mono);
}
.ctab:hover{color:var(--txt)}
.ctab.on{background:var(--s3);color:var(--acc);border-color:rgba(200,255,0,.14)}
.ctab-name{font-weight:500}
.ctab-sub{font-size:9px;color:inherit;opacity:.65;display:block;margin-top:2px}

/* ── Slider ── */
.srow{display:flex;align-items:center;gap:10px}
input[type=range]{
  flex:1;-webkit-appearance:none;height:3px;
  background:var(--s4);border-radius:2px;outline:none;cursor:pointer;
}
input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;width:15px;height:15px;border-radius:50%;
  background:var(--acc);cursor:pointer;transition:transform .15s;
  box-shadow:0 0 0 3px rgba(200,255,0,.18);
}
input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.25)}
.sval{font-size:13px;color:var(--acc);min-width:34px;text-align:right;font-weight:500}

/* ── Presets ── */
.presets{display:flex;flex-wrap:wrap;gap:7px}
.pset{
  padding:5px 13px;border-radius:20px;border:1px solid var(--bd2);
  background:var(--s2);color:var(--muted);font-family:var(--mono);
  font-size:11px;cursor:pointer;transition:all .15s;
}
.pset:hover{border-color:var(--acc);color:var(--acc);background:var(--acc2)}
.pset.on{border-color:var(--acc);color:var(--acc);background:var(--acc2)}

/* ── Encode mode ── */
.modes{display:flex;gap:6px;flex-wrap:wrap}
.mode-btn{
  padding:6px 14px;border-radius:20px;border:1px solid var(--bd2);
  background:var(--s2);color:var(--muted);font-family:var(--mono);
  font-size:11px;cursor:pointer;transition:all .15s;
}
.mode-btn:hover{border-color:var(--bd3);color:var(--txt)}
.mode-btn.on{border-color:rgba(200,255,0,.3);color:var(--acc);background:var(--acc2)}

/* ── Estimator ── */
.est-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
.est-item{display:flex;flex-direction:column;gap:3px}
.est-lbl{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.07em}
.est-val{font-family:var(--head);font-size:20px;font-weight:700;letter-spacing:-.03em}
.est-arrow{font-size:22px;color:var(--dim)}
.est-ratio .est-val{font-size:14px;font-family:var(--mono)}

/* ── Progress ── */
.prog-wrap{margin-top:12px;display:none}
.prog-labels{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:7px}
.prog-track{height:3px;background:var(--s4);border-radius:2px;overflow:hidden}
.prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),#60ff60);border-radius:2px;transition:width .4s ease;width:0%}
.prog-pct{color:var(--acc);font-weight:500}

/* ── Encode button ── */
.btn-enc{
  width:100%;padding:15px;
  background:var(--acc);color:#07080a;
  font-family:var(--head);font-size:14px;font-weight:900;
  border:none;border-radius:10px;cursor:pointer;
  letter-spacing:-.02em;transition:all .15s;
  display:flex;align-items:center;justify-content:center;gap:10px;
}
.btn-enc:hover:not(:disabled){background:#b8f000;transform:translateY(-1px)}
.btn-enc:active:not(:disabled){transform:none}
.btn-enc:disabled{opacity:.35;cursor:not-allowed;transform:none}
.btn-enc svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2.5}
@keyframes spin{to{transform:rotate(360deg)}}
.spin{animation:spin .7s linear infinite;display:inline-block}

/* ── Log ── */
.log-box{
  background:#060608;border:1px solid var(--bd);border-radius:9px;
  padding:12px 14px;font-size:11px;max-height:180px;overflow-y:auto;
  line-height:2;font-family:var(--mono);color:var(--dim);
}
.l-ok{color:var(--ok)}.l-err{color:var(--danger)}.l-inf{color:var(--info)}.l-warn{color:var(--warn)}
.log-line{display:block}

/* ── Result ── */
.res-card{
  display:none;flex-direction:column;gap:14px;
  margin-top:14px;padding:18px;
  background:var(--s2);border:1px solid rgba(61,255,160,.18);border-radius:10px;
}
.res-top{display:flex;align-items:center;gap:12px}
.res-ico{
  width:38px;height:38px;border-radius:9px;flex-shrink:0;
  background:rgba(61,255,160,.1);border:1px solid rgba(61,255,160,.2);
  display:flex;align-items:center;justify-content:center;
}
.res-ico svg{width:20px;height:20px;stroke:var(--ok);fill:none;stroke-width:2}
.res-title{font-family:var(--head);font-size:14px;font-weight:700}
.res-sub{font-size:11px;color:var(--muted);margin-top:2px}
.res-stats{display:flex;gap:24px;flex-wrap:wrap}
.rs{display:flex;flex-direction:column;gap:3px}
.rs-l{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em}
.rs-v{font-family:var(--head);font-size:16px;font-weight:700}
.btn-dl{
  display:inline-flex;align-items:center;gap:8px;
  padding:10px 18px;border-radius:8px;
  background:rgba(61,255,160,.08);border:1px solid rgba(61,255,160,.22);
  color:var(--ok);font-family:var(--mono);font-size:12px;font-weight:500;
  cursor:pointer;text-decoration:none;transition:all .15s;align-self:flex-start;
}
.btn-dl:hover{background:rgba(61,255,160,.16)}
.btn-dl svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2}

.err-msg{
  display:none;color:var(--danger);font-size:12px;
  padding:10px 14px;background:rgba(255,79,79,.07);
  border:1px solid rgba(255,79,79,.18);border-radius:8px;margin-top:10px;
}

/* ── Toggle ── */
.tog-row{display:flex;align-items:center;justify-content:space-between}
.tog-lbl{font-size:11px;color:var(--muted)}
.tog{position:relative;width:36px;height:19px;display:inline-block;flex-shrink:0}
.tog input{opacity:0;width:0;height:0}
.tog-sl{
  position:absolute;inset:0;cursor:pointer;
  background:var(--s3);border-radius:20px;border:1px solid var(--bd2);transition:.2s;
}
.tog-sl:before{
  content:'';position:absolute;width:13px;height:13px;
  left:2px;top:2px;background:var(--muted);border-radius:50%;transition:.2s;
}
input:checked+.tog-sl{background:rgba(200,255,0,.15);border-color:rgba(200,255,0,.3)}
input:checked+.tog-sl:before{transform:translateX(17px);background:var(--acc)}

hr.div{border:none;border-top:1px solid var(--bd);margin:4px 0}

/* ── Warning banner ── */
.warn-banner{
  background:rgba(255,184,48,.07);border:1px solid rgba(255,184,48,.2);
  border-radius:9px;padding:12px 16px;font-size:11px;color:var(--warn);
  display:flex;gap:10px;align-items:flex-start;line-height:1.6;
}
.warn-banner svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;flex-shrink:0;margin-top:1px}
</style>
</head>
<body>

<header class="hdr">
  <span class="hdr-logo">VideoForge</span>
  <span class="hdr-sub">/ wasm</span>
  <div class="hdr-right">
    <div class="pill loading" id="ffmpeg-status">
      <span class="dot pulse"></span>
      <span id="ffmpeg-status-txt">Chargement FFmpeg…</span>
    </div>
  </div>
</header>

<div class="wrap">

  <!-- Warning navigateur -->
  <div class="warn-banner">
    <svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
    <span>FFmpeg tourne <strong>dans votre navigateur</strong> via WebAssembly — aucun serveur requis. L'encodage peut être plus lent qu'un serveur dédié. Gardez l'onglet actif pendant le traitement.</span>
  </div>

  <!-- Upload -->
  <div class="card">
    <div class="card-hd">Source vidéo</div>
    <div class="drop" id="drop">
      <input type="file" id="file-in" accept="video/*">
      <div class="drop-ico">
        <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
      </div>
      <div class="drop-ttl">Déposer une vidéo ici</div>
      <div class="drop-hint">MP4 · MKV · AVI · MOV · WEBM · FLV — jusqu'à ~500 Mo</div>
    </div>
    <div class="fbar" id="fbar">
      <div class="fbar-ico"><svg viewBox="0 0 24 24"><path d="M15 10l-7-4v8l7-4zm-7-7h8a4 4 0 014 4v10a4 4 0 01-4 4H8a4 4 0 01-4-4V7a4 4 0 014-4z"/></svg></div>
      <span class="fbar-name" id="fname">—</span>
      <span class="fbar-sz"><span id="fsize">0</span> Mo</span>
    </div>
  </div>

  <!-- Codec -->
  <div class="card">
    <div class="card-hd">Codec</div>
    <div class="ctabs" id="ctabs">
      <div class="ctab on" data-codec="libx264" data-ext="mp4">
        <span class="ctab-name">H.264</span>
        <span class="ctab-sub">compatible universel</span>
      </div>
      <div class="ctab" data-codec="libvpx-vp9" data-ext="webm">
        <span class="ctab-name">VP9</span>
        <span class="ctab-sub">−40% vs H.264</span>
      </div>
      <div class="ctab" data-codec="mpeg4" data-ext="mp4">
        <span class="ctab-name">MPEG-4</span>
        <span class="ctab-sub">compatibilité max</span>
      </div>
    </div>
    <div class="grid2">
      <div class="fld">
        <span class="lbl">Format de sortie</span>
        <select id="fmt">
          <option value="mp4">MP4</option>
          <option value="webm">WebM</option>
          <option value="mkv">MKV</option>
          <option value="avi">AVI</option>
        </select>
      </div>
      <div class="fld">
        <span class="lbl">Mode d'encodage</span>
        <select id="enc-mode">
          <option value="crf">CRF — Qualité constante</option>
          <option value="vbr">VBR — Bitrate cible</option>
          <option value="size">Taille cible (Mo)</option>
        </select>
      </div>
    </div>
  </div>

  <!-- Compression -->
  <div class="card">
    <div class="card-hd">Compression</div>

    <div class="fld" style="margin-bottom:16px">
      <span class="lbl">Preset</span>
      <div class="presets">
        <button class="pset" data-p="ultrafast" data-crf="30">⚡ Rapide</button>
        <button class="pset on" data-p="medium" data-crf="23">⚖ Équilibré</button>
        <button class="pset" data-p="slow" data-crf="20">🎯 Qualité</button>
        <button class="pset" data-p="veryslow" data-crf="17">💎 Maximum</button>
      </div>
    </div>

    <!-- CRF -->
    <div id="sec-crf">
      <div class="fld">
        <span class="lbl">CRF <span class="lbl-hint">0 = sans perte · 51 = basse qualité · 18-28 recommandé</span></span>
        <div class="srow">
          <input type="range" id="crf" min="0" max="51" value="23" step="1">
          <span class="sval" id="crf-v">23</span>
        </div>
      </div>
    </div>

    <!-- VBR -->
    <div id="sec-vbr" style="display:none">
      <div class="grid2">
        <div class="fld">
          <span class="lbl">Bitrate vidéo <span class="lbl-hint">kb/s</span></span>
          <input type="number" id="vbr" value="1200" min="100" max="30000" step="100">
        </div>
        <div class="fld">
          <span class="lbl">Bitrate audio <span class="lbl-hint">kb/s</span></span>
          <input type="number" id="abr" value="96" min="32" max="320" step="32">
        </div>
      </div>
    </div>

    <!-- Taille cible -->
    <div id="sec-size" style="display:none">
      <div class="fld">
        <span class="lbl">Taille cible <span class="lbl-hint">Mo</span></span>
        <input type="number" id="tgt-size" value="90" min="5" max="4000" step="5">
      </div>
    </div>

    <hr class="div" style="margin:16px 0">

    <div class="grid3">
      <div class="fld">
        <span class="lbl">Résolution</span>
        <select id="res">
          <option value="">Original</option>
          <option value="1920x1080">1080p</option>
          <option value="1280x720">720p</option>
          <option value="854x480">480p</option>
          <option value="640x360">360p</option>
        </select>
      </div>
      <div class="fld">
        <span class="lbl">FPS</span>
        <select id="fps">
          <option value="">Original</option>
          <option value="30">30</option>
          <option value="25">25</option>
          <option value="24">24</option>
        </select>
      </div>
      <div class="fld">
        <span class="lbl">Audio</span>
        <select id="aud">
          <option value="aac">AAC (recommandé)</option>
          <option value="mp3">MP3</option>
          <option value="none">Supprimer</option>
        </select>
      </div>
    </div>

    <hr class="div" style="margin:16px 0">

    <div class="grid2">
      <div class="fld">
        <div class="tog-row">
          <span class="tog-lbl">Optimiser pour streaming web</span>
          <label class="tog"><input type="checkbox" id="faststart" checked><span class="tog-sl"></span></label>
        </div>
      </div>
      <div class="fld">
        <div class="tog-row">
          <span class="tog-lbl">Supprimer l'audio</span>
          <label class="tog"><input type="checkbox" id="mute"><span class="tog-sl"></span></label>
        </div>
      </div>
      <div class="fld">
        <span class="lbl">Début <span class="lbl-hint">secondes</span></span>
        <input type="number" id="trim-s" placeholder="0" min="0" step="1">
      </div>
      <div class="fld">
        <span class="lbl">Durée max <span class="lbl-hint">secondes</span></span>
        <input type="number" id="trim-d" placeholder="entier" min="1" step="1">
      </div>
    </div>
  </div>

  <!-- Estimateur -->
  <div class="card" id="est-card" style="display:none">
    <div class="card-hd">Estimation</div>
    <div class="est-row">
      <div class="est-item">
        <span class="est-lbl">Source</span>
        <span class="est-val" id="est-orig">—</span>
      </div>
      <span class="est-arrow">→</span>
      <div class="est-item">
        <span class="est-lbl">Estimé</span>
        <span class="est-val" id="est-out" style="color:var(--acc)">—</span>
      </div>
      <div class="est-item est-ratio" style="margin-left:auto">
        <span class="est-lbl">Réduction</span>
        <span class="est-val" id="est-ratio">—</span>
      </div>
    </div>
    <div class="prog-wrap" id="prog-wrap">
      <div class="prog-labels">
        <span id="prog-label">Encodage…</span>
        <span class="prog-pct" id="prog-pct">0%</span>
      </div>
      <div class="prog-track"><div class="prog-fill" id="prog-fill"></div></div>
    </div>
  </div>

  <!-- Bouton -->
  <button class="btn-enc" id="btn-enc" disabled>
    <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
    Encoder la vidéo
  </button>

  <!-- Output -->
  <div class="card" id="out-card" style="display:none">
    <div class="card-hd">Journal</div>
    <div class="log-box" id="log"></div>
    <div class="res-card" id="res-card">
      <div class="res-top">
        <div class="res-ico"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>
        <div>
          <div class="res-title">Encodage terminé !</div>
          <div class="res-sub" id="res-fname">—</div>
        </div>
      </div>
      <div class="res-stats">
        <div class="rs"><span class="rs-l">Source</span><span class="rs-v" id="rs-src">—</span></div>
        <div class="rs"><span class="rs-l">Résultat</span><span class="rs-v" id="rs-out" style="color:var(--acc)">—</span></div>
        <div class="rs"><span class="rs-l">Réduction</span><span class="rs-v" id="rs-rat" style="color:var(--ok)">—</span></div>
        <div class="rs"><span class="rs-l">Durée</span><span class="rs-v" id="rs-time">—</span></div>
      </div>
      <a class="btn-dl" id="dl-btn" href="#" download>
        <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
        Télécharger la vidéo encodée
      </a>
    </div>
    <div class="err-msg" id="err-msg"></div>
  </div>

</div>

<!-- FFmpeg.wasm via esm.sh (cross-origin isolé via SharedArrayBuffer si COOP/COEP dispo) -->
<script type="module">
// ─── Import FFmpeg.wasm ──────────────────────────────────────────────────────
import { FFmpeg } from 'https://esm.sh/@ffmpeg/ffmpeg@0.12.15';
import { fetchFile, toBlobURL } from 'https://esm.sh/@ffmpeg/util@0.12.2';

// ─── État global ─────────────────────────────────────────────────────────────
let ffmpeg = null;
let loaded  = false;
let selFile = null;
let curCodec = 'libx264';
let curPreset = 'medium';
let downloadURL = null;

const $ = id => document.getElementById(id);
const log = (msg, cls='') => {
  const s = document.createElement('span');
  s.className = 'log-line' + (cls ? ' '+cls : '');
  s.textContent = msg;
  $('log').appendChild(s);
  $('log').scrollTop = 9999;
};

// ─── Chargement FFmpeg.wasm ───────────────────────────────────────────────────
async function loadFFmpeg() {
  const st   = $('ffmpeg-status');
  const stxt = $('ffmpeg-status-txt');
  try {
    ffmpeg = new FFmpeg();

    ffmpeg.on('log', ({ message }) => {
      // Extraire le % de progression depuis la sortie FFmpeg
      const m = message.match(/time=(\d+):(\d+):([\d.]+)/);
      if (m && window._totalSec > 0) {
        const secs = parseInt(m[1])*3600 + parseInt(m[2])*60 + parseFloat(m[3]);
        const pct  = Math.min(99, Math.round(secs / window._totalSec * 100));
        setProgress(pct, 'Encodage en cours…');
      }
    });

    // Charger le core WASM depuis CDN esm.sh
    const baseURL = 'https://esm.sh/@ffmpeg/core@0.12.9/dist/umd';
    await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`,   'text/javascript'),
      wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    });

    loaded = true;
    st.className = 'pill ok';
    stxt.textContent = 'FFmpeg prêt';
    st.querySelector('.dot').classList.remove('pulse');
    if (selFile) $('btn-enc').disabled = false;
  } catch(e) {
    st.className = 'pill err';
    stxt.textContent = 'Échec chargement';
    st.querySelector('.dot').classList.remove('pulse');
    console.error('FFmpeg load error:', e);
  }
}
loadFFmpeg();

// ─── UI helpers ──────────────────────────────────────────────────────────────
function setProgress(pct, label='') {
  $('prog-fill').style.width = pct + '%';
  $('prog-pct').textContent  = pct + '%';
  if (label) $('prog-label').textContent = label;
}

function humanSize(bytes) {
  if (bytes >= 1073741824) return (bytes/1073741824).toFixed(2) + ' Go';
  if (bytes >= 1048576)    return (bytes/1048576).toFixed(1)    + ' Mo';
  return (bytes/1024).toFixed(0) + ' Ko';
}

function updateEstimate() {
  if (!selFile) return;
  const orig = selFile.size / 1048576;
  const mode = $('enc-mode').value;
  let est;
  if (mode === 'crf') {
    const crf = parseInt($('crf').value);
    const codec_factor = curCodec === 'libvpx-vp9' ? 0.55 : curCodec === 'mpeg4' ? 0.9 : 1;
    est = orig * Math.pow(2,(crf-23)/6) * codec_factor * 0.62;
  } else if (mode === 'vbr') {
    const vbr = (parseInt($('vbr').value)||1200) + (parseInt($('abr').value)||96);
    est = vbr * 1000 * 60 / 8 / 1048576; // estimation sur 60s
  } else {
    est = parseFloat($('tgt-size').value) || 90;
  }
  const ratio = orig > 0 ? ((1 - est/orig)*100).toFixed(0) : 0;
  $('est-orig').textContent = orig.toFixed(1) + ' Mo';
  $('est-out').textContent  = est.toFixed(1)  + ' Mo';
  $('est-ratio').textContent = (ratio > 0 ? '−' : '+') + Math.abs(ratio) + '%';
  $('est-ratio').style.color = ratio > 0 ? 'var(--ok)' : 'var(--danger)';
}

// ─── Fichier ─────────────────────────────────────────────────────────────────
function handleFile(f) {
  if (!f || !f.type.startsWith('video/')) return;
  selFile = f;
  const mb = (f.size/1048576).toFixed(1);
  $('fname').textContent = f.name;
  $('fsize').textContent = mb;
  $('fbar').style.display = 'flex';
  $('est-card').style.display = 'block';
  if (loaded) $('btn-enc').disabled = false;
  updateEstimate();
}

$('file-in').addEventListener('change', e => handleFile(e.target.files[0]));
const dz = $('drop');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('over'); });
dz.addEventListener('dragleave', () => dz.classList.remove('over'));
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('over'); handleFile(e.dataTransfer.files[0]); });

// ─── Codec tabs ───────────────────────────────────────────────────────────────
document.querySelectorAll('.ctab').forEach(t => {
  t.addEventListener('click', () => {
    document.querySelectorAll('.ctab').forEach(x => x.classList.remove('on'));
    t.classList.add('on');
    curCodec = t.dataset.codec;
    // Forcer le format compatible
    const ext = t.dataset.ext;
    $('fmt').value = ext;
    updateEstimate();
  });
});

// ─── Mode d'encodage ─────────────────────────────────────────────────────────
$('enc-mode').addEventListener('change', function() {
  $('sec-crf').style.display  = this.value === 'crf'  ? 'block' : 'none';
  $('sec-vbr').style.display  = this.value === 'vbr'  ? 'block' : 'none';
  $('sec-size').style.display = this.value === 'size' ? 'block' : 'none';
  updateEstimate();
});

// ─── CRF slider ──────────────────────────────────────────────────────────────
$('crf').addEventListener('input', function() {
  $('crf-v').textContent = this.value;
  updateEstimate();
});
$('vbr').addEventListener('input', updateEstimate);
$('abr').addEventListener('input', updateEstimate);
$('tgt-size').addEventListener('input', updateEstimate);

// ─── Presets ─────────────────────────────────────────────────────────────────
document.querySelectorAll('.pset').forEach(b => {
  b.addEventListener('click', () => {
    document.querySelectorAll('.pset').forEach(x => x.classList.remove('on'));
    b.classList.add('on');
    curPreset = b.dataset.p;
    $('crf').value = b.dataset.crf;
    $('crf-v').textContent = b.dataset.crf;
    updateEstimate();
  });
});

// ─── Encodage ────────────────────────────────────────────────────────────────
$('btn-enc').addEventListener('click', async () => {
  if (!selFile || !loaded) return;

  const btn = $('btn-enc');
  btn.disabled = true;
  btn.innerHTML = '<span class="spin">◌</span> Encodage en cours…';

  $('out-card').style.display = 'block';
  $('res-card').style.display = 'none';
  $('err-msg').style.display  = 'none';
  $('prog-wrap').style.display = 'block';
  setProgress(2, 'Lecture du fichier…');
  $('log').innerHTML = '';

  const fmt      = $('fmt').value;
  const mode     = $('enc-mode').value;
  const crf      = $('crf').value;
  const vbr      = $('vbr').value;
  const abr      = $('abr').value;
  const tgtSize  = parseFloat($('tgt-size').value) || 90;
  const res      = $('res').value;
  const fps      = $('fps').value;
  const aud      = $('aud').value;
  const mute     = $('mute').checked;
  const trimS    = parseFloat($('trim-s').value) || 0;
  const trimD    = parseFloat($('trim-d').value) || 0;

  // Nom de fichier de sortie
  const baseName  = selFile.name.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9_\-]/g,'_');
  const outName   = baseName + '_encoded.' + fmt;
  const inName    = 'input.' + (selFile.name.split('.').pop() || 'mp4');

  const t0 = Date.now();

  try {
    log('→ Lecture du fichier source…', 'l-inf');
    setProgress(5, 'Chargement en mémoire…');
    const fileData = await fetchFile(selFile);
    await ffmpeg.writeFile(inName, fileData);
    log('→ Fichier chargé en mémoire WASM (' + humanSize(selFile.size) + ')', 'l-inf');

    // Estimer la durée via méta si disponible
    window._totalSec = 0;
    try {
      await new Promise(resolve => {
        const vid = document.createElement('video');
        vid.preload = 'metadata';
        vid.src = URL.createObjectURL(selFile);
        vid.onloadedmetadata = () => { window._totalSec = vid.duration || 0; resolve(); };
        vid.onerror = resolve;
        setTimeout(resolve, 3000);
      });
    } catch(_){}

    // ─── Construction des arguments FFmpeg ──────────────────────────────────
    const args = ['-y'];

    // Trim entrée
    if (trimS > 0) { args.push('-ss', String(trimS)); }
    args.push('-i', inName);
    if (trimD > 0) { args.push('-t', String(trimD)); }

    // Codec vidéo
    args.push('-c:v', curCodec);

    // Qualité / bitrate
    if (mode === 'crf') {
      if (curCodec === 'libvpx-vp9') {
        args.push('-crf', crf, '-b:v', '0');
      } else {
        args.push('-crf', crf);
      }
    } else if (mode === 'vbr') {
      args.push('-b:v', vbr + 'k');
    } else if (mode === 'size') {
      // Calcul bitrate depuis taille cible
      const dur = window._totalSec > 0 ? window._totalSec : 60;
      const audioBps = (parseInt(abr)||96) * 1000;
      const totalBits = tgtSize * 1048576 * 8;
      const vidBitrate = Math.max(100, Math.round((totalBits - audioBps * dur) / dur / 1000));
      args.push('-b:v', vidBitrate + 'k');
      log(`→ Taille cible ${tgtSize} Mo → bitrate vidéo calculé : ${vidBitrate} kb/s`, 'l-inf');
    }

    // Preset (sauf VP9 wasm qui ne le supporte pas bien)
    if (curCodec === 'libx264' || curCodec === 'mpeg4') {
      args.push('-preset', curPreset);
    } else if (curCodec === 'libvpx-vp9') {
      const speedMap = {ultrafast:5, medium:3, slow:1, veryslow:0};
      args.push('-cpu-used', String(speedMap[curPreset] ?? 3));
      args.push('-deadline', 'good');
    }

    // Résolution
    if (res) {
      const [w,h] = res.split('x');
      args.push('-vf', `scale=${w}:${h}:force_original_aspect_ratio=decrease,pad=${w}:${h}:(ow-iw)/2:(oh-ih)/2`);
    }

    // FPS
    if (fps) args.push('-r', fps);

    // Audio
    if (mute || aud === 'none') {
      args.push('-an');
    } else {
      args.push('-c:a', aud === 'mp3' ? 'libmp3lame' : 'aac');
      args.push('-b:a', (parseInt(abr)||96) + 'k');
    }

    // Threads max
    args.push('-threads', '0');

    args.push(outName);

    log('→ Commande : ffmpeg ' + args.join(' '), 'l-inf');
    setProgress(8, 'Encodage en cours…');

    await ffmpeg.exec(args);

    setProgress(98, 'Lecture du résultat…');
    const outData = await ffmpeg.readFile(outName);
    const blob    = new Blob([outData.buffer], { type: 'video/' + fmt });
    const outSize = blob.size;

    if (outSize === 0) throw new Error('Le fichier de sortie est vide. Essayez un autre codec ou format.');

    // Nettoyage mémoire
    await ffmpeg.deleteFile(inName);
    await ffmpeg.deleteFile(outName);

    // Lien de téléchargement
    if (downloadURL) URL.revokeObjectURL(downloadURL);
    downloadURL = URL.createObjectURL(blob);
    const dlBtn = $('dl-btn');
    dlBtn.href     = downloadURL;
    dlBtn.download = outName;

    setProgress(100, 'Terminé !');
    const elapsed = ((Date.now()-t0)/1000).toFixed(1);
    const reduction = selFile.size > 0
      ? ((1 - outSize/selFile.size)*100).toFixed(1)
      : '0';

    log('✓ Encodage terminé en ' + elapsed + 's', 'l-ok');
    log('→ ' + humanSize(selFile.size) + ' → ' + humanSize(outSize) + ' (−' + reduction + '%)', 'l-ok');

    $('res-card').style.display = 'flex';
    $('res-fname').textContent  = outName;
    $('rs-src').textContent     = humanSize(selFile.size);
    $('rs-out').textContent     = humanSize(outSize);
    $('rs-rat').textContent     = '−' + reduction + '%';
    $('rs-time').textContent    = elapsed + 's';

  } catch(e) {
    log('✗ Erreur : ' + e.message, 'l-err');
    $('err-msg').textContent   = e.message;
    $('err-msg').style.display = 'block';
    setProgress(0, '');
  }

  btn.disabled = false;
  btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg> Encoder à nouveau';
});
</script>
</body>
</html>