● Terminé. B7/B10/B11 corrigés → les 11 bugs B1→B11 sont tous traités
  (@mostajs/media-mcu 0.2.0, build ✅, 11/11 tests).
     
  Derniers correctifs

  - B7 (ports, TOCTOU) — registre en mémoire des paires de ports réservées
  (reservedPorts) + releasePort() au teardown/close → deux sessions du même
  process ne collisionnent plus. (Inter‑process reste atténué par la plage
  dédiée 40000‑49998.)
  - B10 (scalabilité) — pool de workers mediasoup (1 router/worker, round‑robin
  par session, défaut min(cpus,4), option workers) au lieu d'un worker unique.
  - B11 (API) — inputRemove → session.removeInput() (retire 1 source, recompose)
  au lieu de fermer la session ; outputUrl renvoie une URL servable si
  publicBaseUrl est configuré.
  - Bonus — mismatch de layout (grid-2x2/pip-bottom-right… des types ↔ grid/pip
  du moteur) corrigé via mapLayout().

  Documentation B1 → B11

  Tableau statut consolidé ajouté dans docs/02-AUDIT-BUGS-MEDIA-MCU.md §0
  (sévérité · zone · bug · statut · correctif · fichier) + détails dans
  CHANGELOG.md 0.2.0 :

  ┌───────────────────────┬────────────────┬────────────────┬──────────────┐
  │                       │                │                │              │
  ├───────────────────────┼────────────────┼────────────────┼──────────────┤
  │ B1 ✅                 │ B2 ✅          │ B3 ✅          │ B4 ✅        │
  │ transport/connexion   │ compositing    │ finalisation   │ encodings    │
  │                       │ N→1            │                │              │
  ├───────────────────────┼────────────────┼────────────────┼──────────────┤
  │ B5 ✅ DTLS role       │ B6 ✅ restart  │ B7 ✅ ports    │ B8 ✅        │
  │                       │ multi          │ réservés       │ timestamps   │
  ├───────────────────────┼────────────────┼────────────────┼──────────────┤
  │ B9 ✅ timers          │ B10 ✅ pool    │ B11 ✅ API     │ + mapLayout  │
  │                       │ workers        │                │              │
  └───────────────────────┴────────────────┴────────────────┴──────────────┘

  Reste (hors périmètre) : test d'intégration runtime réel WHIP→mediasoup→ffmpeg
  (lourd : worker natif + ffmpeg + vrai publisher) ; TOCTOU inter‑process
  (acceptée). Les correctifs runtime (B1/B3/B6) sont validés par revue + tests
  unitaires des fonctions pures.

  Mémoire à jour. @mostajs/media-mcu@0.2.0 est non publié (build local).

  Veux‑tu que je publie 0.2.0, que j'écrive le test d'intégration réel, ou qu'on
  revienne à RaceVision (brancher la régie sur ce MCU / le module @mostajs/ads)
  ?




testd'integration 
  \
   echo "registry media-mcu = $(npm view @mostajs/media-mcu version
   2>/dev/null)"; \
   echo "=== ffmpeg ==="; command -v ffmpeg && ffmpeg -version 2>/dev/null |
   head -1; \
   echo "=== ffmpeg encoders vp8/opus ==="; ffmpeg -hide_banner -encoders
   2>/dev/null | grep -iE "libvpx|libopus|libx264|aac " | head; \
   echo "=== mediasoup worker binary ==="; ls
   node_modules/mediasoup/worker/out/*/mediasoup-worker 2>/dev/null || find
   node_modules/mediasoup -name "mediasoup-worker" -type f 2>/dev/null |
   head; \
   echo "=== smoke createWorker ==="; timeout 40 node --input-type=module -e
   "
   import { createWorker } from 'mediasoup'
   const w = await createWorker({ logLevel:'warn' })
   const r = await w.createRouter({
   mediaCodecs:[{kind:'video',mimeType:'video/VP8',clockRate:90000}] })
   console.log('worker pid', w.pid, 'router', !!r.id)
   w.close(); process.exit(0)
   " 2>&1 | tail -5
   Check ffmpeg + mediasoup worker availability




● Tout est fait.

  ✅ Publication

  @mostajs/media-mcu@0.2.0 publié sur npm (registry confirme 0.2.0) — files
  enrichi (CHANGELOG.md + llms.txt), moduleInfo aligné.
  
  ✅ Test d'intégration réel (passe)

  test-scripts/mcu-integration.test.mjs — chaîne runtime complète sans 
  navigateur :

  ffmpeg (mire VP8 → RTP)  →  mediasoup PlainTransport (comedia)  →  Producer
     →  session.startRecording() (consume → ffmpeg)  →  recording.webm
     →  closeSession() → finalisation B3

  Résultat réel observé : keyframes producer-trace … in / consumer-trace … out,
  recording.webm = 256 668 octets, events session.created, output.ready, 
  session.closed. → valide le pipeline RTP→mediasoup→ffmpeg et la finalisation
  B3 (fichier valide non tronqué).

  Prérequis validés dans l'environnement : ffmpeg 6.0 (libvpx/libopus) + worker
  mediasoup natif.

  ✅ Scripts + commandes dans test-scripts/ (ton exigence)

  - test-scripts/mcu-unit.test.mjs (11) · test-scripts/mcu-integration.test.mjs
  (1)
  - test-scripts/RUN-TESTS.md — toutes les commandes de lancement (npm test,
  node --test test-scripts/…) + prérequis + tableau de couverture.

  Suite complète : 12/12 ✅.

  Docs/mémoire à jour

  CHANGELOG.md (section Tests), audit docs/02-…§0 (12/12), mémoire
  media-mcu-audit.
  
  ▎ Restent hors périmètre (documentés) : chemin navigateur WHIP/DTLS (B1/B5 — 
  ▎ non testable headless, couvert par les tests unitaires SDP) et TOCTOU 
  ▎ inter‑process (acceptée, plage de ports dédiée).

  Le module media-mcu est désormais corrigé (B1→B11), testé (unit + intégration 
  réel) et publié. Veux‑tu revenir à RaceVision (brancher la régie « A » sur ce
  MCU + @mostajs/ads) ou autre chose ?



● Corrigé. Le script de test d'intégration est bien dans test-scripts/ — le
  problème réel était qu'il écrivait ses fichiers de sortie dans /tmp (via
  tmpdir()), ce qui violait ta règle « tout dans test-scripts/, jamais /tmp ».

  Ce que j'ai fait

  - test-scripts/mcu-integration.test.mjs : remplacé tmpdir() par un dossier
  sous test-scripts/ (mkdtempSync(join(TEST_DIR, 'mcu-int-')), via
  import.meta.url), nettoyé en fin de test (rmSync).
  - Re-run : test passe (recording.webm généré sous test-scripts/ puis supprimé)
  ; test-scripts/ propre après (aucun résidu, aucun fichier hors du
  répertoire).
  - test-scripts/RUN-TESTS.md : note explicite « artefacts sous test-scripts/,
  jamais /tmp ».

  Contenu de test-scripts/ (tout y est)

  test-scripts/
  ├── mcu-unit.test.mjs          # 11 tests unitaires (pur)
  ├── mcu-integration.test.mjs   # test d'intégration réel (ffmpeg+mediasoup) —
  sortie sous test-scripts/
  └── RUN-TESTS.md               # commandes de lancement + prérequis

  Scripts et commandes de lancement sont bien tous enregistrés dans le
  répertoire des tests, et les artefacts y restent confinés.


● Module @mostajs/media-mcu 0.2.0 déployé sur https://studio.amia.fr/mcu/ ✅

  Confirmations :
  - Connexion : via ssh amia (comme demandé). Sudo non requis (proxy Apache
  /mcu/ + /api/mcu/ déjà en place). 
  - Version déployée : package.json 0.2.0 + dist/index.js version: '0.2.0'.
  - PM2 : mosta-mcu-studio → 0.2.0, online (restart effectué, uptime 27 s).
  - Endpoint public : GET https://studio.amia.fr/mcu/ → HTTP 200.

  Étapes exécutées par scripts/deploy.sh mcu (avec SSH_HOST=amia) :
  1. Build local tsc (0.2.0)
  2. rsync dist/ + examples/ + package.json/README/LICENSE →
  amia:/home/hmd/prod/mosta-media-mcu/
  3. ecosystem.config.cjs rendu (port 4588, SFU_ANNOUNCED_IP=212.132.109.40, RTC
  50500‑50999, MCU_OUTPUT_DIR, FFMPEG_PATH)
  4. npm install --omit=dev --legacy-peer-deps → worker mediasoup présent
  5. pm2 restart mosta-mcu-studio + pm2 save
  6. healthcheck 200
  
  Le module corrigé (B1→B11), testé (12/12 dont intégration réel) et publié npm
  est donc en production sur studio.amia.fr/mcu/.

  Souhaites‑tu que je vérifie un flux réel (créer une session via /api/mcu/ et
  tester un enregistrement), ou qu'on revienne à RaceVision (brancher la régie
  sur ce MCU) ?



● Tout est implémenté, déployé, testé, documenté et committé. Récapitulatif des
  trois points.
   
  #3 — Réponse : l'audio est géré par le MCU (pas un mute manuel)

  En mode program, le graphe ffmpeg du MCU ne mappe que l'audio de la source à 
  l'antenne (-map 0:a:<idx>, pas d'amix) — automatique au setProgram. Les autres
  sources restent ingérées (vidéo dispo) mais leur audio n'est pas dans le
  programme. Consigné dans mosta-media-mcu/docs/DESIGN-PROGRAM-BUS.md (#1).

  Nouveau besoin — PFL (entendre avant l'antenne) → modèle deux bus audio

  - ON-AIR (MCU) = audio solo de la source diffusée.
  - PFL régie (🎧) = côté navigateur du régisseur, écoute d'une source en
  preview avant le TAKE — n'affecte pas l'antenne. Implémenté comme contrôle
  console (toggle 🎧) ; le flux audio des tuiles arrivera avec le multiview SFU.
  Documenté (§5bis du DESIGN).

  #2 — Régie LIVE (program-bus média) — fait & validé en prod

  - @mostajs/media-mcu@0.3.1 publié + déployé studio.amia.fr/mcu/ :
  setProgram(inputId) (plein écran + audio solo) ; fix : 1 inputId partagé par
  source (vidéo+audio) ; API programSet + route /program ; page publisher WHIP
  studio.amia.fr/mcu/publish. Doc : README how-to-use + llms + CHANGELOG +
  DESIGN-PROGRAM-BUS.
  - Pont régie↔MCU (geo-enroll) : /regie/mcu/session, /regie/mcu/ingest (QR →
  publisher), /regie/program/take pilote programSet. ProgramState.mcuSessionId.
  CORS pour le callback caméra.
  - Console geo.amia.fr/regie/ : bouton connexion MCU, QR par source (caméra
  publie au scan), clic-TAKE (passe à l'antenne), 🎧 PFL.
  - Validé (test-scripts/test-regie-mcu.sh, vert) : session + QR ingest (bonne
  origine MCU) + TAKE → programSet ok. Publisher accessible.
  
  Commits poussés : mosta-media-stack (f1331ae) · mosta-geo (058cd3d). Mémoire à
  jour.

  ---
  Reste pour la régie pleinement « live » (roadmap v0.3/v0.4)
  
  - Multiview SFU : flux preview vidéo + audio par tuile (via
  @mostajs/media-sfu) → le PFL 🎧 devient réel (le régisseur entend vraiment).
  - Transitions (cut/fade) + HLS « C » (egress du programme vers le grand
  public).
  - Test device réel : scanner un QR avec un téléphone → publication caméra WHIP
  → TAKE à l'antenne.
  
  Veux-tu que j'enchaîne sur le multiview SFU (pour rendre le 🎧 PFL et l'aperçu
  des caméras réellement live) ? 

✻ Crunched for 13m 56s

❯ explique moi la cohabitation entre MCU et SFU                                 
  
● Bonne question — c'est le cœur de l'archi média. Voici comment SFU et MCU
  cohabitent, en général puis dans RaceVision.

  1. Deux topologies, deux métiers
  
  ┌─────────────┬───────────────────────────┬───────────────────────────────┐
  │             │ SFU (@mostajs/media-sfu)  │   MCU (@mostajs/media-mcu)    │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │             │ Distribuer : 1 source → N │ Composer : N sources → 1 flux │
  │ Rôle        │  spectateurs (router,     │  (mix vidéo + audio)          │
  │             │ sans recomposer)          │                               │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │ Traitement  │ Pas de transcode (forward │ ffmpeg transcode (scale,      │
  │             │  des paquets RTP)         │ xstack/overlay, amix/solo)    │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │             │ Très faible               │ Plus élevée                   │
  │ Latence     │ (~sub-seconde, WebRTC     │ (décodage→compo→réencodage)   │
  │             │ bout-à-bout)              │                               │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │ Coût        │ Léger (relai)             │ Lourd (CPU/GPU par sortie)    │
  │ serveur     │                           │                               │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │ Sortie      │ N flux WebRTC             │ 1 flux unique → fichier / HLS │
  │             │ indépendants              │  / RTMP                       │
  ├─────────────┼───────────────────────────┼───────────────────────────────┤
  │ Scalabilité │ Horizontale (relais en    │ Verticale (1 compo = 1        │
  │             │ cascade)                  │ process ffmpeg)               │
  └─────────────┴───────────────────────────┴───────────────────────────────┘

  Règle mnémotechnique : SFU = aiguillage (chacun reçoit les flux séparés et
  compose côté client), MCU = table de mixage (le serveur compose UNE image/son
  pour tous).

  2. Pourquoi les deux ? Ils ne font pas le même travail

  - Le SFU est parfait quand chaque client veut plusieurs flux séparés, en 
  direct, à faible latence → c'est exactement le multiview de régie (le
  régisseur voit toutes les caméras + écoute celle qu'il veut, PFL 🎧) et le
  suivi temps réel des publieurs.
  - Le MCU est nécessaire quand on veut UNE sortie composée pour le grand public
  : l'écran « Studio LIVE TALK » plein écran + un seul audio à l'antenne,
  encodée en HLS pour des milliers de spectateurs (un lecteur HLS ne sait pas
  mixer 6 WebRTC — il lui faut un flux).

  Autrement dit : SFU pour produire/contrôler (régie, faible latence), MCU pour
  diffuser (programme unique → HLS scalable).

  3. La cohabitation propre : une seule ingestion, partagée

  Le piège serait de faire publier chaque caméra deux fois (vers le SFU et vers
  le MCU). On évite ça : les deux sont basés sur mediasoup, qui sait relayer un
  flux d'un router à l'autre (pipeToRouter / PipeTransport). Donc :

                                ┌─────────────── SFU (router) ───────────────┐
    Caméra (moto/ambulance/  →  │  forward faible latence → MULTIVIEW RÉGIE   │
  (le régisseur
     podium/plateau) — WHIP     │      (toutes les tuiles + PFL 🎧 audio)     │
   regarde/écoute)
     getUserMedia, 1 ingest  ┐  └─────────────────────────────────────────────┘
                             │            │ pipe (PipeTransport, intra-serveur)
                             │            ▼
                             │  ┌─────────────── MCU (router) ───────────────┐
                             └─►│  ffmpeg : compose le PROGRAM (source choisie │
                                │  plein écran + audio SOLO) → 1 flux → HLS « C
  »│  (le public regarde)
                                └─────────────────────────────────────────────┘

  - 1 publication par caméra (WHIP, getUserMedia).
  - Le SFU la diffuse telle quelle aux tuiles de la régie (preview vidéo + audio
  par source → le 🎧 PFL).
  - Le même producer est pipé au MCU, qui n'en compose qu'une à la fois dans le
  PROGRAM.
  - Le régisseur monitore via le SFU, fait TAKE → le MCU commute (setProgram) →
  la sortie HLS bascule.

  C'est la séparation preview (SFU) / program (MCU) des vraies régies de
  télévision.
  
  4. Qui fait quoi dans le flux audio
  
  - PFL / écoute régie = côté SFU (le régisseur déµute la tuile qu'il veut
  entendre) — n'affecte pas l'antenne.
  - Audio on-air = côté MCU (-map 0:a:<source program>, solo) — c'est ce que le
  public entend.
  → Les deux bus sont physiquement séparés : c'est pour ça qu'on peut entendre
  une source en preview avant de la passer à l'antenne.

  5. Rôles & sécurité (cohérent avec DESIGN-ROLES-DIRECTION.md)

  Le scope de clé API (@mostajs/api-keys) distingue les acteurs sur le même 
  socle mediasoup :
  - caméra = scope ingest (publie son flux, rien d'autre) ;
  - régisseur = scope program/write (peut setProgram, voir le multiview SFU) ;
  - spectateur = lecture HLS (aucun WebRTC), via CDN.

  6. Où on en est dans RaceVision (concret)
  
  ┌─────────────────────────────────┬───────────────────────────────────────┐
  │             Brique              │                 État                  │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ MCU program-bus (compo + audio  │ ✅ déployé studio.amia.fr/mcu/        │
  │ solo + setProgram)              │ (0.3.1)                               │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ Ingest caméra (WHIP)            │ ✅ page studio.amia.fr/mcu/publish    │
  │                                 │ (vers le MCU)                         │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ Pont régie → MCU (clic-TAKE)    │ ✅                                    │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ SFU multiview régie (preview +  │ ⬜ à brancher                         │
  │ PFL audio)                      │                                       │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ Pipe SFU→MCU (1 seule           │ ⬜                                    │
  │ ingestion)                      │                                       │
  ├─────────────────────────────────┼───────────────────────────────────────┤
  │ HLS « C » (egress du programme) │ ⬜                                    │
  └─────────────────────────────────┴───────────────────────────────────────┘
  
  ▎ Aujourd'hui la caméra publie directement dans le MCU. Pour le multiview live
  ▎ + PFL réel, l'étape suivante est : faire publier dans le SFU, piper vers le

  7. Quand se passer de l'un ou l'autre

  - Petit plateau, peu de spectateurs, faible latence prioritaire → SFU seul
  (compo côté client).
  - Diffusion massive / archivage / un seul rendu → MCU (puis HLS/CDN).
  - Régie pro (notre cas) → les deux : SFU pour la régie/preview, MCU pour le
  programme public.

  Tu veux que je branche le SFU pour le multiview (publication via SFU + pipe
  vers le MCU + tuiles live dans la console, ce qui rend le 🎧 PFL réel) ?


 ● Tout sert (tile.html public 200, console 200). Je vérifie que l'ingest renvoie
  bien tileUrl + sfuRoom + params sfu dans le publisher :

● Bash(ssh amia 'B=http://127.0.0.1:4611
      SID=$(curl -s -X POST $B/regie/sources -H "content-type: application/json"
      -d "{\"course\":\"cyclisme-alger-2026\",\"kind\":\"…)
  ⎿  "sfuRoom":"rv-66eaa8ef-f277-40f1-b944-196301852635"
     "tileUrl":"https://studio.amia.fr/sfu/tile.html?sfu=https%3A%2F%2Fstud
     io.amia.fr%2Fsfu&room=rv-66eaa8ef-f277-40f1-b944-196301852635&audio=0"
     … +2 lines (ctrl+o to expand)

● ✅ SFU multiview branché de bout en bout : sfuRoom (1/source), tileUrl
  (tile.html WHEP), publisher dual-publish (MCU programme + SFU room). Je commit
  + mets à jour roadmap/mémoire.

● Bash(cd /home/hmd/dev/MostaGare-Install/mostajs/mosta-media-stack && \
      git add mosta-media-sfu/examples/tile.html
      mosta-media-sfu/examples/server.mjs mosta-media-mcu…)
  ⎿  Waiting…

