# @mostajs/media — Changelog

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

## 2.4.0 — 2026-05-21

### Fix — composite-split : le flux distant ne se connectait jamais

`useRemoteSource` : le `useEffect` d'auto-connexion avait des deps vides
`[]` → il ne s'exécutait qu'au montage. En mode composite-split,
`autoConnect` ne passe à `true` qu'au step `active` (le `<MultiTakeRecorder>`
est monté dès le step `configure`) → `connect()` n'était jamais rappelé,
le flux distant restait « non connecté ». Deps corrigées
(`[opts.autoConnect, connect]`) → connexion réactive dès que `autoConnect`
bascule.

### Feature — composite-split : cycle de layout 4 vues

Le compositeur `useCompositeCapture` gère 4 layouts (au lieu du simple
swap 2 vues) : **split** (écran | flux SFU 50/50 + webcam en incrustation),
**screen** (écran en grand + 2 PiP), **sfu** (flux distant en grand +
2 PiP), **webcam** (webcam en grand + 2 PiP). Les sources non principales
passent en incrustation PiP empilée. Bouton dédié dans l'UI active qui
cycle split → screen → sfu → webcam. API : `compositeLayout` /
`cycleCompositeLayout` (exposées aussi par `useMultiTakeSession`).

## 2.3.1 — 2026-05-21

### Fix — Composite split : preview visuel du split

L'UI active du mode « Composite split » affiche désormais le split
**écran | flux distant SFU** + webcam en incrustation — miroir du
canvas-compositor. Avant, seul l'écran était prévisualisé : l'utilisateur
enregistrait le split « à l'aveugle ». `swapped` échange les deux moitiés
de la preview comme dans le compositeur. Un badge d'état (attente /
connexion / erreur) s'affiche sur la moitié distante tant qu'aucun
publisher n'est connecté.

## 2.3.0 — 2026-05-21

### Feature — Mode « Composite split » (écran + flux distant SFU + webcam)

Nouveau 4ᵉ mode du `<MultiTakeRecorder>` : enregistre en multi-take un
composite à **3 sources**.

- **`useCompositeCapture`** (copie modifiée de `useScreenCapture`)
  composite écran + flux distant SFU/P2P + webcam. Layout « split »
  écran | distant 50/50 (séparateur central) + webcam en incrustation
  dès qu'un flux distant a une piste vidéo ; audio distant mixé dans
  l'enregistrement. API : `setRemoteStream(stream)` / `hasRemote`.
- **`useMultiTakeSession`** accepte l'option `composite` : toute la
  machinerie multi-take (pause/reprise, IndexedDB, persistance L3
  cross-reload) fonctionne à l'identique sur le compositeur 3 sources.
- **`<MultiTakeRecorder>`** : 4ᵉ choix « Composite split » au configure
  step, abonnement au flux distant via `useRemoteSource` (salle
  d'attente incluse), badge d'état de connexion dans l'UI active.

`useScreenCapture` n'est pas touché — le mode « Screen + audio » reste
identique ; `useCompositeCapture` est une copie autonome.

## 2.2.1 — 2026-05-21

### Fix — QR codes du mode sfu-viewer (rendu local, COEP-safe)

- Les QR codes du configure step sont désormais **générés localement**
  (`<QrImage>` de `@mostajs/qrpanel/qr-image`) au lieu d'un service
  externe (`api.qrserver.com`). Sous COEP `require-corp` — cross-origin
  isolation requise par ffmpeg.wasm — l'image QR cross-origin était
  bloquée par la politique et s'affichait en carré blanc.
- Nouveau peer dependency : `@mostajs/qrpanel` (≥ 0.5.0) — la génération
  de QR relève du métier de qrpanel, pas de media (`<QrImage>` y a été
  ajouté en sous-export `./qr-image`).

### Change — Config sfu-viewer : un seul champ « Classroom »

La section room du configure step est restructurée en **un champ unique
« Classroom / Room ID »**, partagé par le QR professeur (publisher), le QR
élèves (`/classroom`) et l'abonnement du recorder. « API SFU base » passe
en « Réglages avancés ». Plus de risque de désynchroniser la room du
publisher et celle du viewer.

## 2.2.0 — 2026-05-21

### Feature — Viewer SFU réutilisable + page élève + `llms.txt`

Extraction du viewer SFU en composant pur réutilisable et ajout d'une
page-élève prête à monter :

- **`<SfuViewer>`** (nouveau) — viewer pur d'un flux SFU : preview vidéo +
  écran de salle d'attente (compte à rebours + carrousel de pubs). Présentation
  pure, reçoit l'état `remote` (`UseRemoteSourceReturn`) en prop. Factorisé
  depuis `<SfuViewerRecorder>`, qui le réutilise désormais.
- **`<SfuViewerPage>`** (nouveau, `@mostajs/media/pages/SfuViewerPage`) —
  page-composant « rejoindre une classe » pour un élève : monte `<SfuViewer>`
  + `useRemoteSource({ waitForPublisher, autoConnect })` + chrome. Room lue
  depuis la prop ou `?room=` de l'URL. Aucun enregistrement (viewer pur).
- **`<MultiTakeRecorder>`** : nouvelle prop `sfuViewerBaseUrl` — le QR « viewer »
  du configure step pointe vers la page élève (`<sfuViewerBaseUrl>?room=X`) au
  lieu du `viewer.html` de démo.
- **`llms.txt`** ajouté à la racine du module (fiche dense de toutes les
  fonctionnalités pour assistants LLM).

## 2.1.2 — 2026-05-20

### Feature — Publication WHIP du take (le take diffuse + enregistre)

Le mode screen-record peut désormais **diffuser son flux en direct** vers une
room SFU tout en l'enregistrant :
- `useScreenCapture` / `useMultiTakeSession` exposent `getRecordingStream()`
  — le stream composite (canvas screen+webcam + audio) effectivement enregistré.
- `<MultiTakeRecorder>` : bouton **"📡 Publier vers SFU"** pendant un take →
  `publishSfu()` (import dynamique `@mostajs/media-sfu/publisher`). La
  publication s'arrête automatiquement à la fin du take.

**Peer dep optionnelle** : nécessite `@mostajs/media-sfu` ≥ 0.2.2 installé
côté consumer (import dynamique).

## 2.1.1 — 2026-05-20

### Feature — Salle d'attente SFU + QR publisher/viewer

- **`useRemoteSource` option `waitForPublisher`** : au lieu d'échouer quand
  aucun publisher n'est actif, le hook reste en statut `'waiting'`, poll
  l'état de la room (`getSfuRoomStatus`) et s'abonne **automatiquement** dès
  qu'un publisher démarre. L'élève peut rejoindre la classe avant le prof.
- **`<SfuViewerRecorder>`** : écran de salle d'attente (waitForPublisher
  défaut true) — message "en attente du professeur", **compte à rebours**
  optionnel (`scheduledStartAt`), et **carrousel de pubs** (`waitingAds`)
  pour occuper l'attente.
- **`<MultiTakeRecorder>`** : nouvelles props `sfuWaitForPublisher`,
  `sfuScheduledStartAt`, `sfuWaitingAds` (passées à SfuViewerRecorder).
- **Configure step `sfu-viewer`** : affiche 2 QR codes — lien PROFESSEUR
  (publisher) + lien ÉLÈVES (viewer) — dérivés de la room, à transmettre.

Nouveau type exporté : `WaitingAd`.

## 2.1.0 — 2026-05-20

### Feature — P2.1 : mode `sfu-viewer` (enregistrer un flux distant)

Le `<MultiTakeRecorder>` gagne un 3ᵉ mode de capture **`sfu-viewer`** : au lieu
de capturer l'écran local, il **s'abonne à un flux SFU distant** (un publisher
diffuse dans une room = classroom) et l'**enregistre** comme take(s).

Architecture — séparation métier stricte :
- La logique WHEP vit dans **`@mostajs/media-sfu/subscriber`** (`subscribeSfu()`),
  pas dans `@mostajs/media`. p2p/mcu suivront le même pattern dans leurs modules.
- Nouveau hook **`useRemoteSource({ protocol, apiBase, roomId })`** — orchestrateur
  unifié, import dynamique du subscriber du protocole (peer deps optionnelles).
- Nouveau composant **`<SfuViewerRecorder>`** — preview WHEP + MediaRecorder
  multi-take sur le flux distant. Pas de canvas-compositing (flux déjà composé).

Nouvelles props `<MultiTakeRecorder>` : `sfuRoom`, `sfuApiBase`, `sfuIceServers`.
Le configure step expose le mode + saisie room/apiBase.

**Peer dep optionnelle** : le mode `sfu-viewer` nécessite `@mostajs/media-sfu`
installé côté consumer (import dynamique — pas requis si on n'utilise pas ce mode).

## 2.0.13 — 2026-05-20

### Feature — Load complet projet (initialAdSlots / initialAudioTracks / initialProjectName)

`<VideoEditor>` ne pouvait restaurer qu'une partie d'un ProjectFile sauvé :
`initialClips`, `initialSubtitles`, `initialLogo` existaient, mais à la
réouverture le **nom du projet**, les **ad slots** et les **pistes audio
chaînées** étaient perdus.

3 nouvelles props :
- `initialAdSlots?: AdSlot[]` — passée à `useVideoEditor` (nouveau champ
  `UseVideoEditorInput.initialAdSlots`, init du state `adSlots`).
- `initialAudioTracks?: Array<{ id, filename, inlineData?, mimeType?,
  durationSec?, startSec?, volume? }>` — désérialisées au mount (base64
  inlineData → Blob → File → objectURL), one-shot.
- `initialProjectName?: string` — pré-remplit l'input "Nom du projet".

Couplé à `ProjectFile` v2 (logo + audioTracks + name), un consumer peut
désormais faire un round-trip save → load **complet** : clips, subtitles,
adSlots, logo, audioTracks, name, settings.

## 2.0.12 — 2026-05-19

### Fix exports types

`ProjectFile`, `AdSlot`, `AdSlotKind`, `AudioTrackInput`, `AudioTrackMultiInput`,
`LogoInput`, `LogoPosition` étaient déclarés dans `types/editor.ts` mais pas
re-exportés depuis le barrel `@mostajs/media`. Consumers TypeScript ne pouvaient
pas les importer pour leur propre logique de load/save projet.

## 2.0.11 — 2026-05-19

### Feature — ProjectFile.name + zone de saisie

Le bouton 💾 Save project sauvait des projets anonymes (identifiés uniquement
par ULID). Ajout d'un champ `name?: string` au ProjectFile + un input texte
"Nom du projet" au-dessus du bouton Save (section Project sidebar).

Le nom est trimmé ; vide = `undefined` dans le JSON (consumer affiche un
fallback `Project <id>`).

## 2.0.10 — 2026-05-19

### Feature — ProjectFile v2 : logo + audioTracks persistés

`ProjectFile` v1 ne sauvait que `clips + subtitles + settings + adSlots` — les
états `logoFile/position/sizePct/opacity/marginPct` et `audioTracks[]` étaient
locaux au composant `<VideoEditor>` et perdus à chaque save.

`ProjectFile` v2 (`version: 2`) ajoute 2 champs optionnels :
- `logo?: { inlineData?: base64, filename?, position?, sizePct?, opacity?, marginPct? }`
- `audioTracks?: Array<{ id, filename, inlineData?, mimeType?, durationSec?, startSec?, volume?, fadeInSec?, fadeOutSec?, label? }>`

Le bouton "💾 Save project" lit ces états locaux, sérialise les `File` en
base64 inlineData, et étend le ProjectFile avant `onSaveRequested(data)`.

Backwards-compat : v1 reste lisible (champs `logo`/`audioTracks` absents =
zéro logo, zéro audio track). `setProjectData(project v2)` ne restore PAS
encore les états locaux du composant (TODO v2.0.11 — load flow complet).

## 2.0.9 — 2026-05-19

### Fix — Export WebM / GIF avec clips hétérogènes (concat filter + image loop)

Deux bugs liés au concat multi-clips :

1. **`Error: Option loop not found. Aborted()`** sur clip image — l'option
   input `-loop 1` n'est pas reconnue par certains builds ffmpeg.wasm.
   Remplacée par le filter `loop=loop=-1:size=1:start=0,…,trim=duration=N,
   setpts=PTS-STARTPTS` dans `baseFilter`. L'input devient juste `-i image.ext`.

2. **Concat demuxer `-c copy` cassait si segments hétérogènes** (image 4:3
   1280×960 + vidéo 16:9 1280×720). MP4 transcodait après donc passait, mais
   WebM/GIF lisaient directement `final.webm` corrompu. Remplacé par
   **concat filter** : `[0:v]scale+pad+setsar+fps[v0];[1:v]…;[v0][v1]…concat=n=N:v=1:a=0[vout]`
   avec scale+pad uniforme (16:9 letterbox) + re-encode libvpx. Plus lent
   mais accepte n'importe quelle hétérogénéité.

Bonus diagnostic : try/catch enrichi sur concat (logs ffmpeg + WASM message).

## 2.0.8 — 2026-05-19

### Feature — P0-J5 complétion : VideoEditorHandle imperative ref

Avant : `<VideoEditor onAddTakeRequested>` exposait le bouton "+ Ajouter un take"
mais le hook `useVideoEditor.insertVideoClip` était inaccessible depuis l'extérieur
(hook interne au composant). Le consumer devait passer par `initialClips`
(remount avec reset des cuts/overlays/subtitles en cours).

`<VideoEditor>` est désormais wrap-é dans `React.forwardRef` et expose un handle
typé `VideoEditorHandle` :

```ts
export interface VideoEditorHandle {
  insertVideoClip(blob: Blob, durationSec: number): Promise<void>
  insertImage(file: File, durationSec?: number): Promise<void>
  splitAtCurrent(): void
  getClips(): Clip[]
  exportClips(options: ExportOptions): Promise<ExportResult>
}
```

Pattern boucle record↔édit :

```tsx
const editorRef = useRef<VideoEditorHandle>(null)

<VideoEditor ref={editorRef} initialClips={initialClips}
  onAddTakeRequested={() => setMiniRecorderOpen(true)} />

{miniRecorderOpen && (
  <Modal>
    <MultiTakeRecorder onSessionComplete={(takes) => {
      const t = takes[0]
      if (t.videoResult.blob) editorRef.current?.insertVideoClip(t.videoResult.blob, t.durationMs / 1000)
      setMiniRecorderOpen(false)
    }} />
  </Modal>
)}
```

**Non-breaking** : `<VideoEditor>` sans `ref` continue de fonctionner exactement
comme avant.

### Consumer — camera-studio adopte le pattern

`Entreprise/demo/camera-studio/app/page.tsx` migre de `onAddTakeRequested={() =>
setPhase('record')}` (qui aurait reset les cuts existants) vers un modal mini-recorder
overlay qui appelle `editorRef.current.insertVideoClip(...)`. Le `<VideoEditor>`
reste monté en arrière-plan, l'édition en cours est préservée.

## 2.0.7 — 2026-05-19

### Fix — Refus des takes au blob vide (defense-in-depth)

`IndexedDBStore.finalize()` retournait silencieusement `new Blob([])` quand
aucun chunk n'avait été persisté (cas : MediaRecorder stop avant 1er
ondataavailable, ou track invalide). Le blob 0-byte était truthy donc passait
les checks downstream et crash plus loin dans `ffmpeg-compose.ensureSource`.

Trois gardes coordonnées :

1. **`IndexedDBStore.finalize()`** : throw explicite si `chunks.length === 0`.
2. **`useScreenCapture.stopRecording`** : catch finalize error → setError +
   reset videoResult=null + blob vide. Plus de propagation silencieuse.
3. **`useMultiTakeSession.stopTake`** : refuse le take si `result.video.blob.size === 0`
   ET `result.blob.size === 0`. Le take n'est pas ajouté à `state.takes` —
   l'utilisateur voit le bouton "Aller à l'édition" inactif au lieu d'un
   crash à l'export.

## 2.0.6 — 2026-05-19

### Fix critique — libvpx-vp9 crash WASM remplacé par libvpx (VP8)

Cause confirmée du `RuntimeError: index out of bounds` 2.0.5 (logs activés) :
`libvpx-vp9` dans ffmpeg.wasm crash après ~19 frames sur 1280x956 (et d'autres
combinaisons résolution × SIMD). Bug latent depuis longtemps, masqué quand
les segments tenaient en RAM avant le crash.

Pipeline intermédiaire (par-clip avant concat) bascule sur `libvpx` (VP8) :
- `-c:v libvpx -b:v 1M -crf 30 -cpu-used 4 -deadline realtime -auto-alt-ref 0`
- Plus stable WASM (pas de SIMD problématique).
- `auto-alt-ref 0` désactive les alt-ref frames (2-pass implicite, source d'OOB).
- `cpu-used 4 -deadline realtime` = single-pass rapide, qualité acceptable
  pour intermédiaire (les segments sont ensuite transcodés en libx264 MP4
  ou copiés tels quels en WebM → qualité finale préservée).

## 2.0.5 — 2026-05-19

### Fix — Diagnostic ffmpeg OOB (RuntimeError index out of bounds)

Bug : export 2 clips → `RuntimeError: index out of bounds` côté ffmpeg.wasm,
**zéro log visible** dans la console car `composeClips` n'auto-loggait pas
les messages ffmpeg (le consumer devait fournir `onLog`).

Quatre corrections coordonnées :

1. **Auto console.warn des messages ffmpeg matchant** `error|invalid|out of|failed|cannot|unsupported`
   dans `composeClips`, préfixé `[@mostajs/media ffmpeg]`. Plus jamais de
   diagnostic muet sans configurer le consumer.
2. **`ensureSource` accepte `blobsByUrl`** pour bypasser `fetch(blob:URL)` —
   évite l'OOB causé par une blob URL revoquée entre finalize et export
   (data.byteLength === 0 → ffmpeg lit un fichier vide → OOB WASM). Détecte
   aussi le MIME pour aligner l'extension du fichier dans le FS ffmpeg
   (`.mp4` au lieu de `.webm` hardcodé si le record est en H.264).
3. **Garde durée invalide pré-flight** : un clip avec `srcEndSec <= srcStartSec`
   ou `durationSec === 0` lève désormais une erreur explicite *avant* `ff.exec`,
   au lieu d'un OOB WASM cryptique.
4. **Try/catch autour de `ff.exec`** : les `RuntimeError` natifs WASM sont
   enrichis avec les 8 derniers logs ffmpeg + les 12 premiers args, pour
   donner du contexte exploitable au consumer.

Cible : le bug d'export reporté en 2.0.4 ne crashera plus silencieusement —
soit le fix passe (extension MIME corrigée + blob direct), soit l'erreur
diagnostique précisément la cause.

## 2.0.4 — 2026-05-19

### Fix — Audio-only path crash dans VideoEditor (defense-in-depth)

Bug : un take audio-only finalisé par `<MultiTakeRecorder mode='audio-only'>` était
synthétisé comme un MediaTake « vidéo » (`videoResult.kind: 'video'`) avec un
blob `audio/webm`. Si le consumer oubliait de tester `meta.recordingMode ===
'audio-only'` avant de router vers `<VideoEditor>`, `composeClips` lançait
ffmpeg sur le blob audio → **Error: ffmpeg clip 0 failed (code 1)**.

Trois fixes coordonnés (défense en profondeur) :

1. **`MultiTakeRecorder.tsx`** : le take audio-only est désormais marqué
   `videoResult.kind: 'audio'` (sémantiquement correct).
2. **`lib/take-to-clip.ts`** : `takesToEditorInput()` détecte les takes audio
   (via `kind` ou `mimeType.startsWith('audio/')`) et les **filtre** de
   `initialClips`. Retourne en plus `skippedAudioTakes` pour que le consumer
   puisse les router vers un playback dédié.
3. **`lib/ffmpeg-compose.ts`** : `composeClips()` accepte `blobsByUrl` et
   refuse explicitement un clip dont le blob source a un MIME `audio/*` —
   message d'erreur clair plutôt que crash ffmpeg cryptique.

### UX — Bouton « Changer le mode » prominent

L'écran `step='active'` affichait un bouton « ← Réglages » très discret
(11px, gris pâle). Remplacé par un header contextuel : affiche le mode
courant + format + un bouton **« ⚙ Changer le mode »** en cyan saturé,
avec confirmation si un enregistrement est en cours (évite la perte de
take silencieuse). Idem dans la branche audio-only active.

### Breaking — `TakesToEditorInputResult.skippedAudioTakes`

`takesToEditorInput()` retourne désormais 3 champs au lieu de 2. Non-breaking
pour les consumers qui destructurent uniquement `{ initialClips, initialBlobMap }`.

## 2.0.3 — 2026-05-19

### Fix exports types

Les types nouveaux de 2.0.2 *(`MultiTakeRecorderMeta`, `MultiTakeRecorderLogo`,
`MultiTakeRecordingMode`, `MultiTakeSurface`, `MultiTakeOutputFormat`)* étaient
déclarés mais pas re-exportés depuis le barrel `@mostajs/media`. Les consumers
en TypeScript ne pouvaient pas les importer → erreur build "has no exported
member named 'MultiTakeRecorderMeta'". Tous exposés via le barrel maintenant.

## 2.0.2 — 2026-05-19

### Feature — Parité complète CaptureEditorPage

`<MultiTakeRecorder>` reçoit désormais un **step "configure" pre-flight** qui
expose les 15 paramètres précédemment ouverts uniquement dans `CaptureEditorPage`
(legacy mono-record). Le composant gère 2 phases :
1. **Configure** *(défaut au mount sauf `initialSessionId` passé)* — cards
   réglables : mode, format, target, sources A/V, audio storage, video storage,
   audio recording optionnel.
2. **Active** *(après clic "Démarrer")* — UI multi-take avec preview, takes
   accumulés, controles, viewers protocoles inline. Bouton "← Réglages" pour
   revenir à configure *(session courante préservée)*.

### Nouvelles props *(toutes additives, non-breaking)*

- `initialRecordingMode` : `'screen-record' | 'audio-only'` — mode-only audio
  délègue à `<AudioOnlyRecorder>` *(single-take ; multi-take audio à venir)*.
- `initialOutputFormat` : `'mp4' | 'webm' | 'gif'` — défaut pour VideoEditor.
- `initialSurface` : `'window' | 'full-screen'` — hint picker getDisplayMedia.
- `initialMicEnabled` : bool — mic on/off *(défaut true)*.
- `initialWebcamEnabled` : bool — *(alias de l'ancien `webcamOverlay`)*.
- `initialSystemAudioEnabled` : bool — *(alias de l'ancien `systemAudio`)*.
- `initialVideoStorage` : `ChunkStorage` — 4 stratégies *(memory/indexeddb/filesystem/server)*.
- `initialVideoServerUrl` : string — endpoint POST si storage='server'.
- `initialAudioStorage` : `AudioRecordStorage` — 4 stratégies idem.
- `initialAudioServerUrl` : string — endpoint POST si storage='server'.
- `initialRecordAudioSeparately` : bool — *(alias de l'ancien `recordAudioSeparately`)*.
- `initialVideoOnly` : bool — vidéo pure sans audio embarqué *(force recordAudioSep)*.
- `cameraDeviceId` / `cameraLabelHint` : pre-select caméra *(passé à `startScreenShare`)*.
- `initialLogo` : `MultiTakeRecorderLogo` — watermark passé via `onSessionComplete.meta.logo`.

### `onSessionComplete` étendu

Signature : `(takes, sessionId, meta?: MultiTakeRecorderMeta) => void`.
Le 3ᵉ arg `meta` contient TOUTES les options résolues *(mode, format, sources,
storages, URLs, logo, etc.)* pour que le caller puisse les piper directement à
`<VideoEditor>` ou les persister dans son propre ProjectFile.

### UI Configure step

- Bandeau "↻ Session(s) interrompue(s)" affiché dans configure si IDB contient
  des sessions in-progress *(reprise directe → step='active')*.
- Cards stylées : Mode, Format de sortie, Capture target, Audio & video sources
  *(toggle on/off)*, Audio recording optionnel *(checkbox + storage select +
  server URL conditionnel)*, Video storage *(select + server URL conditionnel +
  note explicative selon la stratégie)*.
- CTA "Démarrer le recorder →" / "Démarrer audio" selon le mode choisi.

### Notes

- Audio-only multi-take *(plusieurs prises audio chaînées sans screen capture)*
  reste à faire — orchestrator dédié `useMultiTakeAudioSession` à concevoir.
- Le `filesystem` storage reste **incompatible avec L3 cross-reload** *(picker
  exige user gesture frais)* — explicitement noté dans le select + JSDoc.

## 2.0.1 — 2026-05-19

### Fix

- **`<MultiTakeRecorder>` preview swap-aware** — la preview affichait toujours
  l'écran en plein cadre + webcam en overlay fixe, ce qui ne reflétait pas le
  toggle swap. Désormais la preview bascule visuellement screen↔webcam en
  miroir EXACT du canvas-compositor *(comme `<RecorderPreview>` en v1.12)*,
  donc WYSIWYG : ce que l'utilisateur voit = ce qui est enregistré.

## 2.0.0 — 2026-05-19

Refonte majeure côté record + édition multi-take. Aucune suppression d'API
publique : les hooks et composants 1.x restent exportés comme avant. Bump major
pour signaler l'arrivée des primitives multi-take (`useMultiTakeSession`,
`MultiTakeRecorder`, persistence L3 IDB) et clarifier la sémantique
`stopRecording` vs `stopScreenShare`.

### Ajouts

- **Pause / Resume natifs** sur `useScreenCapture` et `useVideoRecorder` —
  `pauseRecording()` / `resumeRecording()` + state `paused: boolean`. Suspend
  également le 2ᵉ `MediaRecorder` audio quand `recordAudioSeparately=true`,
  pour éviter la désync au resume. Le chrono est gelé pendant la pause.
- **`useMultiTakeSession`** — orchestrator au-dessus de `useScreenCapture` qui
  accumule les takes (`state.takes: MediaTake[]`), expose
  `startTake / stopTake / startNewTake / deleteTake / reorderTakes /
  setTakeTitle`, et gère la persistence L3 (`restoreSession / finalizeSession /
  discardSession`).
- **`<MultiTakeRecorder>`** — composant UI prêt-à-l'emploi : preview live,
  chrono REC/PAUSED, bandeau des takes (thumbnails redimensionnables avec
  drag + delete + title editable), bouton "+ Nouveau take", "🔄 Changer
  l'écran", sélecteur multi-caméra, bandeau "↻ Reprendre une session
  interrompue" au mount.
- **`takesToEditorInput(takes)`** — adapter qui convertit
  `MediaTake[]` en `{ initialClips, initialBlobMap }` consommable par
  `<VideoEditor initialClips={...} initialBlobMap={...}>`.
- **`useVideoEditor.insertVideoClip(blob, durationSec)`** — symétrique à
  `insertImage`, permet d'append une vidéo dans la timeline d'édition.
  Wire-up via la prop `<VideoEditor onAddTakeRequested>` qui expose un bouton
  "📹 Ajouter un take" dans la barre d'actions.
- **Persistence L3 cross-reload** (`lib/session-storage.ts`) — DB IDB séparée
  `mostajs-media-sessions` (préserve `mostajs-media-chunks` v1 intacte) avec
  `appendTakeToSession`, `removeTakeFromSession`, `listOpenSessions`,
  `loadSession`, `finalizeSession`, `discardSession`, `gcExpiredSessions(7d)`.
- **`StartRecordingOptions.baseId?`** — override l'identifiant interne
  (`rec-<ts>-<rand>`) avec une valeur fournie par le caller. Utilisé par
  `useMultiTakeSession` pour grouper les takes d'une session sous un préfixe
  commun `<sessionId>-take<N>-{video,audio}` et permettre L3.
- **`getStorageEstimate()`** — helper qui wrap `navigator.storage.estimate()`
  pour bandeau warning sur quota IDB.
- **`pickVideoMime`** — fallback étendu vers H.264/MP4 (compat Safari iOS,
  prép P2.1 smartphone push).
- **Viewer URLs configurables** (composant `MultiTakeRecorder`) — 3 inputs
  `<input type="url">` SFU/MCU/P2P avec valeurs défaut `studio.amia.fr` +
  surcharge via prop `defaultViewerUrls` + persistance localStorage clé
  `mostajs-media-viewer-urls`.
- **Viewers inline split-screen** — toggle "📺 Inline" par protocole, ouvre
  un iframe à droite de la preview avec resize natif CSS (resize: both).
  ⚠ Ces viewers inline NE SONT PAS inclus dans le recording — l'iframe est
  cross-origin sandboxed. L'intégration native SFU (subscriber WHEP) +
  compose dans canvas-compositor est planifiée P2.4.
- **Auto-restore au mount** quand `initialSessionId` est fourni à
  `useMultiTakeSession` — évite collision `baseId` et garantit que les takes
  existants en IDB sont chargés avant d'ajouter de nouveaux takes.

### Clarifications de sémantique (non breaking, mais nouvellement documentées)

- **`stopRecording` ≠ `stopScreenShare` / `stopCamera`** : le premier finalise
  un take et **laisse les streams source vivants** (multi-take ready) ; le
  second termine toute la session. Cohérent avec le comportement existant en
  1.12.x mais explicitement codifié en JSDoc.
- **`onSessionComplete(takes, sessionId)`** — signature du callback de
  `<MultiTakeRecorder>` étendue avec sessionId comme 2ᵉ arg. Permet au caller
  d'utiliser sessionId comme unique clé de round-trip (réutilisable comme
  `initialSessionId` au prochain mount → auto-restore).
- **Filesystem storage incompatible avec L3** — JSDoc clarifie que
  `showSaveFilePicker` exige un user gesture frais, donc non rejoignable
  cross-reload. Pour L3, n'utiliser que `indexeddb`.

### Suppressions / cleanups internes

- `chunksRef` leftover dans `useScreenCapture` (déclaration et reset jamais
  lus depuis v1.11.9, les chunks passent par `videoStoreRef` / `ChunkStore`).
- `@ts-expect-error` → `@ts-ignore` dans `lib/ffmpeg-compose.ts` pour rester
  type-safe quand `@ffmpeg/ffmpeg` est optionnellement résolu.

### Notes de migration depuis 1.12.x

Aucune intervention requise pour les consumers qui utilisent
`useScreenCapture` / `useVideoRecorder` / `<VideoEditor>` / `<ScreenRecorder>`
de manière standard. Pour profiter du multi-take :

```tsx
import { MultiTakeRecorder, VideoEditor, takesToEditorInput } from '@mostajs/media'

function MyPage() {
  const [phase, setPhase] = useState<'record' | 'edit'>('record')
  const [takes, setTakes] = useState([])
  const [sessionId, setSessionId] = useState<string | undefined>()

  if (phase === 'edit') {
    const { initialClips, initialBlobMap } = takesToEditorInput(takes)
    return <VideoEditor
      source=""
      initialClips={initialClips}
      initialBlobMap={initialBlobMap}
      onAddTakeRequested={() => setPhase('record')}
    />
  }
  return <MultiTakeRecorder
    initialSessionId={sessionId}
    onSessionComplete={(t, sid) => { setTakes(t); setSessionId(sid); setPhase('edit') }}
  />
}
```

Le hook `useMultiTakeSession` est exposé pour les consumers qui veulent
construire leur propre UI au-dessus.

### Limites connues

- Les **edits éditeur** (split, trim, overlay) sont **perdus** quand
  l'utilisateur clique "+ Ajouter un take" et revient au recorder — seules
  les takes brutes (recordées) sont préservées via IDB. Le workflow
  iteratif édit↔take complet nécessite la couche serveur (P1) qui persiste
  le ProjectFile v2 dans `@mostajs/storage` (cf. design canonique §3.5).
- L'**ingestion de stream distant SFU/MCU/P2P comme source dans le recording**
  n'est pas implémentée — iframes inline visibles mais pas inclus dans le
  blob final. Planifié P2.4 (WHEP subscriber + compose multi-source).
- **MCU** : iframe inline pointable mais le backend n'a été démarré que
  récemment ; vérifier disponibilité avant smoke.

## 1.12.x — précédent

Voir git history pour le détail des versions 1.10.x à 1.12.0 (caméra multi-source,
chunks IndexedDB, audio séparé, video editor, ad slots, etc.).
