# Changelog — @mostajs/media-mcu

**Auteur** : Dr Hamid MADANI <drmdh@msn.com>

## 0.3.3 (2026-06-08) — HLS **live** (fenêtre glissante) + sorties **multi-format**

**Corrigé (vue publique « C »)** : le HLS sortait en `-hls_list_size 0 -hls_flags
append_list` = **playlist VOD sans fin** → le lecteur démarrait sur les segments les
**plus anciens** (impression « rediffusion enregistrée »), et les DTS non monotones
faisaient planter hls.js après accumulation. Désormais **fenêtre glissante live** :
`-hls_time 2 -hls_list_size 6 -hls_flags delete_segments+omit_endlist+independent_segments`
(+ `-g 48 -sc_threshold 0`) → le spectateur démarre **au direct** et les vieux segments
sont supprimés.

**Ajouté** : `outputExt` accepte une **liste CSV** (`'m3u8,mp4'`) → une **seule passe
ffmpeg** écrit plusieurs formats SIMULTANÉS (HLS live pour la diffusion + MP4 d'archive),
partageant le même `-filter_complex`/compositing. Le m3u8, s'il est présent, reste la
playlist primaire (`recording.m3u8`). Nouveau `SpawnFfmpegOptions.outputPaths`.

**Corrigé (B12 — nettoyage à la déconnexion)** : quand un publisher WHIP se déconnectait
(page caméra fermée, écran verrouillé, perte réseau), son input **restait** dans la session ;
en republiant, le SDP ffmpeg accumulait des flux **morts** (`vp8 … unspecified size`, `0x0`)
→ `Error configuring filter graph` → **HLS figé**. Désormais : écoute `dtlsstatechange`
(`closed`/`failed`) et `icestatechange` (`closed`) sur le transport d'ingest →
`removeInput()` automatique. De plus `removeInput()` retire **tous** les producers d'un
inputId (vidéo + audio le partagent) et `startRecording()` **purge les producers fermés**
avant de composer (double filet). La page caméra exemple gagne un **wake-lock** + un
avertissement « gardez cette page ouverte » pour éviter la mise en veille mobile.

## 0.3.2 (2026-06-07) — sortie HLS du programme (vue publique « C »)

**Ajouté** : `CreateMcuOptions.outputExt` (`'webm'`|`'m3u8'`|`'mp4'`) → le programme
peut sortir en **HLS live** (`recording.m3u8` + segments) pour la diffusion grand
public scalable. L'exemple `server.mjs` lit `MCU_OUTPUT_EXT` et sert la playlist +
segments via `GET /api/mcu/sessions/:id/hls/:file` (CORS). ffmpeg gère déjà m3u8.

## 0.3.1 (2026-06-07) — fix : un inputId PARTAGÉ par source (vidéo+audio)

**Corrigé (bus programme)** : `inputAdd` (WHIP) attribuait un `inputId` **différent**
à chaque producer (vidéo ≠ audio) → `setProgram(inputId)` ne sélectionnait qu'une
moitié de la source. Désormais **un seul `inputId` par publication WHIP**, partagé par
tous ses producers → le bus programme commute la **source entière** (vidéo plein écran
+ son audio). Le `Location` de la réponse WHIP renvoie cet `inputId` partagé
(≠ producerId), cohérent avec `setProgram`/`removeInput`.

## 0.3.0 (2026-06-07) — Bus PROGRAMME (régie : commuter la source diffusée)

**Ajouté** : capacité **bus programme** pour la régie (type Studio LIVE TALK) — l'écran
diffusé montre **une** source à la fois ; **seul le PROGRAM émet l'audio**.

- `McuSession.setProgram(inputId: string | null)` — commute la source diffusée :
  **vidéo plein écran** + **audio SOLO** de cette source (les autres restent en ingest,
  muettes). Reconstruit le pipeline ffmpeg si l'enregistrement tourne. `null` ⇒ retour au layout.
- `McuSession.programInputId` (lecture) ; `switchLayout(l)` remet `programInputId=null` (hors `program`).
- Layout `'program'` (public + moteur). `buildFilterComplex(layout, vCount, aCount, res, program?)`
  gagne un 5ᵉ paramètre `{ videoIndex?, audioIndex? }` : en mode `program`,
  vidéo = `[0:v:<videoIndex>]scale[vout]`, audio = `0:a:<audioIndex>` (**solo, jamais d'amix**).
  `spawnFfmpeg` accepte `program`. Les ordinaux sont calculés depuis `producer.appData.inputId`.
- API : handler **`programSet`** → `POST /mcu/sessions/{id}/program { inputId }` (rôle régie/write).
- Tests : 3 cas program-bus (plein écran + audio solo, défaut index 0, commutation). Suite 14/14.

**Rétro-compatible** : sans `setProgram`, comportement inchangé (layouts existants).

## 0.2.1 (2026-06-07) — fix ICE : ports RTC du worker dans la plage pare-feu

**Corrigé (bloquant connexion)** : `createMcuServer` ne passait **pas** `rtcMinPort`/`rtcMaxPort`
à `createWorker` → mediasoup utilisait la plage RTC **par défaut (10000-59999)**, hors de la plage
ouverte au pare-feu → **ICE `checking → failed`** côté navigateur (la signalisation WHIP/answer
réussissait pourtant). Désormais chaque worker du pool est créé avec
`rtcMinPort: opts.minPort, rtcMaxPort: opts.maxPort` (comme `@mostajs/media-sfu`).

> Déploiement : ouvrir la plage RTC MCU au pare-feu (ex. `ufw allow 50500:50999/udp`), distincte
> de celle du SFU. Diagnostic : `test-scripts/diag-ice-ports.sh`.

## 0.2.0 (2026-06-07) — correctifs d'audit + compositing N→1

Reprise du module parqué après audit (`docs/02-AUDIT-BUGS-MEDIA-MCU.md`). Build ✅, **11/11 tests** unitaires (`test-scripts/mcu-unit.test.mjs`).

### Corrigé
- **B1 (connexion)** — un WebRtcTransport **par source WHIP** (plus de transport mis en cache) → fini le double `connect()` (« connect() already called ») au 2ᵉ appel.
- **B3 (finalisation)** — `ffmpeg.stop()` **attend réellement** l'`exit` (SIGINT puis SIGKILL à 3 s) ; ne se fie plus à `child.killed` (vrai dès l'envoi du signal) → fichiers **non tronqués**.
- **B4 (track)** — `parseWhipOffer` : `encodings: [{}]` quand l'offre n'a pas de SSRC (au lieu de `[]`) → le RTP est routé par mid/BUNDLE au lieu de rester non rattaché.
- **B5 (connexion)** — rôle DTLS **dérivé de `a=setup`** (active→client, passive→server, actpass→client) ; `a=setup` de la réponse cohérent (opposé du rôle publisher).
- **B6 (track multi)** — `startRecording()` **reconstruit** le pipeline avec **tous** les producers (gère l'arrivée tardive d'une source) au lieu de l'ignorer.
- **B8 (A/V)** — `-use_wallclock_as_timestamps` n'est plus combiné à `-c copy`.
- **B9 (fuite)** — les `requestKeyFrame` rejoués (setTimeout) sont **annulés** au teardown/close.

### Ajouté
- **B2 — compositing N→1** : `buildFilterComplex(layout, …)` (fonction pure testée) →
  - `single`/`focus-speaker` : 1 source (scale) ou focus 1ʳᵉ,
  - `grid` : **xstack 2×2** (2 à 4 sources),
  - `pip` : **overlay** coin,
  - **amix** des N audios.
  - `switchLayout()` **recompose à chaud** (restart du graphe ffmpeg).
  - Sortie : `-c copy` si mono-source webm ; **ré-encodage** (libvpx/libx264 + opus/aac) si compositing/mp4/m3u8.

- **B7 (concurrence)** — registre en mémoire des **paires de ports réservées** + libération au teardown/close → deux sessions du même process ne choisissent plus le même port (TOCTOU intra-process fermée ; reste mitigée vis-à-vis d'autres process par la plage dédiée).
- **B10 (scalabilité)** — **pool de workers mediasoup** (1 router/worker, round-robin par session, défaut `min(cpus,4)`, option `workers`) au lieu d'un seul worker.
- **B11 (API)** — `inputRemove` retire **une seule** source (`session.removeInput`, recompose le pipeline) au lieu de fermer la session ; `outputUrl` renvoie une **URL servable** si `publicBaseUrl` est configuré.
- **Bonus** — correction du **mismatch de layout** (types `grid-2x2`/`pip-bottom-right`… ↔ moteur ffmpeg `grid`/`pip`) via `mapLayout()`.

### Tests
- **Unitaires** (`test-scripts/mcu-unit.test.mjs`, 11) : fonctions pures SDP/filtres/rôles DTLS.
- **Intégration réel** (`test-scripts/mcu-integration.test.mjs`) : RTP VP8 (ffmpeg) → mediasoup PlainTransport → `session.startRecording()` → `recording.webm` finalisé (valide ~250 Ko, event `output.ready`) — valide le pipeline runtime + finalisation B3. **Suite : 12/12.**
- Lancement documenté : `test-scripts/RUN-TESTS.md`.

### Reste (hors périmètre)
- Chemin **navigateur WHIP/DTLS** (B1/B5) non testable headless → couvert par les tests unitaires SDP. TOCTOU **inter-process** acceptée (plage 40000-49998 disjointe de mediasoup).

## 0.1.0 — version initiale (parquée)
MVP mono-source recording (mediasoup PlainTransport → ffmpeg → .webm). Compositing non implémenté ; bugs B1–B11 (cf. audit).
