# @mostajs/media-mcu

**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
**License** : AGPL-3.0-or-later

MCU (Multipoint Conferencing Unit) pour l'écosystème `@mostajs/*` — compose **N flux WebRTC en 1 flux composite** (HLS / RTMP / file) via ffmpeg. Reçoit le RTP via mediasoup PlainTransport. Cas d'usage : régie broadcast unique, archivage post-event, mosaïque admin.

> **Scope v0.1.0 (MVP)** : N inputs WebRTC → 1 output HLS local. Layouts grid 2×2 / focus-speaker / picture-in-picture configurables. Recording HLS sur disque (storage backend via `@mostajs/storage`).

---

## Quand utiliser MCU vs SFU vs P2P ?

| Architecture | Pairs max | Output | CPU serveur | Cas d'usage |
|---|---|---|---|---|
| `@mostajs/media-p2p` | 2-4 | N streams individuels | quasi nul | Visio 1-1, petit groupe |
| `@mostajs/media-sfu` | 5-100 | N streams forwardés | faible | Cours, broadcast 1→N |
| **`@mostajs/media-mcu`** ← ici | N inputs → **1 output** | 1 flux HLS/RTMP | **élevé** (transcoding) | Régie unique, archivage, mosaïque |

**Règle simple** : MCU est le bon choix uniquement si tu as besoin d'un **seul flux composite output**, par exemple :
- Archive replay d'un cours mosaïque (prof + slides + apprenants vignettes)
- Broadcast vers une plateforme externe (YouTube Live, Twitch) qui n'accepte qu'1 flux RTMP
- Régie TV style avec switcher entre sources

Sinon SFU est presque toujours meilleur.

### ⚠️ Attention : MCU ≠ recording simple côté client

Si ton besoin est **un seul utilisateur qui enregistre son propre flux** (webcam → fichier `.webm`/`.mp4`), **n'utilise PAS MCU** — utilise [`@mostajs/media`](../mosta-media) côté browser. Comparatif :

| | `@mostajs/media` (browser) | `@mostajs/media-mcu` (serveur) |
|---|---|---|
| Où tourne le record | **Browser** (`MediaRecorder` API native) | **Serveur** (mediasoup → ffmpeg) |
| Install lourde ? | Zéro (lib JS pure) | **Compile C++ mediasoup-worker** (~3-5 min sur VPS 2 cœurs) |
| Postinstall | Aucun | Tente prebuild download, fallback compile-from-source si CDN/arch ne matchent pas |
| Cas d'usage | 1 user enregistre son propre flux | **Mixer N publishers en 1 fichier** |
| Output | Blob `.webm` côté client (upload via XHR) | `.webm` / `.mp4` / `.m3u8` côté serveur |
| Latence record | Temps réel browser-side | RTP → ffmpeg server-side (~quelques secondes lag) |

**Pourquoi MCU est si lourd à installer ?** mediasoup-worker est un binaire C++ natif incluant libwebrtc, libsrtp, OpenSSL, abseil, FBS schemas. npm tente d'abord un prebuild download du binaire précompilé ; en cas d'échec (DNS, arch CPU exotique), fallback compile-from-source ≈ 3-5 min sur 2 cœurs. C'est le **prix à payer pour décoder du SRTP/WebRTC serveur-side** — `@mostajs/media` n'a pas besoin de ça car le browser fait tout en natif via `MediaRecorder`.

---

## Quick start

### Installation

```bash
npm install @mostajs/media-mcu

# Pré-requis système (sur le serveur)
sudo apt install ffmpeg
```

Requires Node ≥ 18 + ffmpeg dans le PATH.

### Bootstrap

```ts
// lib/mcu-bootstrap.ts
import { createMcuServer } from '@mostajs/media-mcu/server'

export const mcu = await createMcuServer({
  listenIps: [{ ip: '0.0.0.0', announcedIp: process.env.SFU_ANNOUNCED_IP }],
  minPort: 41000,
  maxPort: 41100,
  outputDir: './data/mcu-outputs',
  ffmpegPath: '/usr/bin/ffmpeg',
})
```

### API handlers

```ts
// app/api/mcu/sessions/route.ts
import { createMcuApiHandlers } from '@mostajs/media-mcu/api'
import { mcu } from '@/lib/mcu-bootstrap'

const handlers = createMcuApiHandlers({ mcu, permissionChecker: yourAuth })

export const POST = handlers.sessionCreate    // créer une session MCU avec layout
export const GET = handlers.sessionList
```

```ts
// app/api/mcu/sessions/[id]/inputs/route.ts
// Add an input (peer publishe via WHIP standard)
export const POST = (req: Request, ctx: any) => handlers.inputAdd(req, ctx.params)
```

```ts
// app/api/mcu/sessions/[id]/output/route.ts
// Get HLS playlist URL pour l'output composite
export const GET = (req: Request, ctx: any) => handlers.outputUrl(req, ctx.params)
```

### Layouts disponibles

| Layout | Description |
|---|---|
| `grid-2x2` | 4 sources max, grille équivalente |
| `grid-3x3` | 9 sources max |
| `focus-speaker` | 1 grande + N petites (active speaker auto detect) |
| `pip-bottom-right` | 1 fond plein + 1 vignette coin |
| `vertical-strip` | 1 grande à gauche + colonne vignettes à droite |
| `single` | 1 seule source (effectivement = passthrough, debug) |
| `program` | **bus PROGRAMME (v0.3)** : 1 source diffusée plein écran + **audio SOLO** de cette source |

### Bus programme — commuter la source diffusée (v0.3)

Pour une **régie** (type Studio LIVE TALK) où l'écran diffusé montre **une** source à
la fois et où seul le PROGRAM émet l'audio : `setProgram(inputId)` met la source
choisie en plein écran **et active son audio en solo** (les autres sources restent en
ingest, muettes). Re-cliquer une autre source la commute instantanément.

```ts
const session = mcu.getSession(sessionId)

// le régisseur met la « Moto 1 » à l'antenne (vidéo plein écran + audio de la moto)
await session.setProgram('moto-1')      // inputId = appData.inputId de la source

// commuter vers le plateau (Studio LIVE TALK) — audio bascule sur le plateau
await session.setProgram('studio')

// revenir à un layout composé (grille…) : sortir du bus programme
await session.switchLayout('grid-2x2')  // remet programInputId = null
```

Côté API HTTP : `POST /mcu/sessions/{id}/program` avec `{ "inputId": "moto-1" }`
(handler `programSet`, réservé au rôle régie/write). `inputId: null` ⇒ retour au layout.
La sortie programme (1 vidéo + 1 audio) alimente directement HLS pour la vue « C ».

### Côté browser (publisher)

Identique à `@mostajs/media-sfu` (WHIP standard) — voir [media-sfu/README.md](../mosta-media-sfu/README.md).

### Output HLS pour viewers

L'output MCU est un flux HLS standard, lisible par n'importe quel player (hls.js, native iOS Safari, VLC) :

```html
<video controls>
  <source src="https://yourapp.com/mcu/outputs/{sessionId}/playlist.m3u8" type="application/vnd.apple.mpegurl">
</video>
```

Ou avec hls.js pour compat Chrome/Firefox :
```js
import Hls from 'hls.js'
const hls = new Hls()
hls.loadSource('/mcu/outputs/abc123/playlist.m3u8')
hls.attachMedia(videoElement)
```

---

## Architecture interne

```
   N Publishers (WHIP)
         │
         ▼
┌────────────────────────────────┐
│  mediasoup workers              │
│  PlainTransport per producer    │
│  ↓ RTP plain (out)              │
└────────────────────────────────┘
         │
         ▼
┌────────────────────────────────┐
│  ffmpeg pipeline                │
│  - N inputs UDP RTP             │
│  - filter_complex (layout)      │
│  - output HLS segments + .m3u8  │
└────────────────────────────────┘
         │
         ▼
   HLS files → @mostajs/storage bucket
         │
         ▼
   Player viewer (hls.js / video.js)
```

L'astuce : on utilise mediasoup en **mode receiver** uniquement (les peers WHIP créent des Producers, mais au lieu de redistribuer vers des Consumers, on `pipeProducer` vers un `PlainTransport` qui envoie en RTP brut vers ffmpeg local sur ports UDP fixes).

ffmpeg consomme N inputs RTP (`udp://127.0.0.1:PORT?ssrc=...`) et compose via `filter_complex` selon le layout choisi.

---

## API publique

### Server bootstrap

```ts
interface CreateMcuOptions {
  listenIps: Array<{ ip: string; announcedIp?: string }>
  minPort: number
  maxPort: number
  outputDir: string          // path FS pour les segments HLS
  ffmpegPath?: string        // default '/usr/bin/ffmpeg'
  storageBucket?: string     // si @mostajs/storage en backend, default 'mcu-outputs'
}

interface McuServer {
  createSession(opts: {
    layout: 'grid-2x2' | 'grid-3x3' | 'focus-speaker' | 'pip-bottom-right' | 'vertical-strip' | 'single'
    resolution?: '720p' | '1080p'  // default 720p
    audioMix?: 'all' | 'speaker-only'  // default all
  }): Promise<McuSession>
  getSession(sessionId: string): McuSession | undefined
  closeSession(sessionId: string): Promise<void>
  listSessions(): McuSessionInfo[]
}

interface McuSession {
  id: string
  layout: string
  inputs: McuInput[]
  outputPlaylistPath: string  // chemin HLS .m3u8
  addInput(producerId: string): Promise<McuInput>
  removeInput(producerId: string): Promise<void>
  switchLayout(newLayout: string): Promise<void>
  close(): Promise<void>
}
```

### API handlers

```ts
interface McuApiHandlers {
  sessionCreate: (req: Request) => Promise<Response>
  sessionList: (req: Request) => Promise<Response>
  sessionClose: (req: Request, ctx: { sessionId: string }) => Promise<Response>

  inputAdd: (req: Request, ctx: { sessionId: string }) => Promise<Response>    // WHIP-like
  inputRemove: (req: Request, ctx: { sessionId: string; inputId: string }) => Promise<Response>

  outputUrl: (req: Request, ctx: { sessionId: string }) => Promise<Response>   // signed URL HLS

  layoutSwitch: (req: Request, ctx: { sessionId: string }) => Promise<Response>
}
```

---

## Coût CPU / performance

| Layout | Resolution | Sources | CPU estimé |
|---|---|---|---|
| grid-2x2 | 720p | 4 | ~50-80% 1 core |
| grid-3x3 | 720p | 9 | ~100-150% (1.5 core) |
| focus-speaker | 720p | 4 | ~70% 1 core |
| pip-bottom-right | 720p | 2 | ~30% 1 core |

Pour scale : 1 process ffmpeg par session MCU. Si plusieurs sessions concurrent, prévoir multi-core ou GPU encoding (NVENC/QSV — v0.3).

Recommandation : **MCU = use sparingly**. Pour la majorité des cas, SFU + composite côté client (Canvas/WebGL) est plus économique.

---

## Roadmap

- **v0.1.0** : MVP layouts fixes + HLS output ← *current*
- **v0.2.0** : Custom layouts (JSON spec), active-speaker detection (voice activity)
- **v0.3.0** : GPU encoding (NVENC/QSV/VAAPI), RTMP output (vers YouTube Live, Twitch)
- **v0.4.0** : Time-shifted recording (start/stop dynamique), chapter markers

---

## Comparaison @mostajs/media-* (résumé)

| Module | Quand l'utiliser | Doc |
|---|---|---|
| `@mostajs/media-p2p` | ≤ 4 pairs, latence critique | [mosta-media-p2p/README.md](../mosta-media-p2p/README.md) |
| `@mostajs/media-sfu` | 5-100 pairs, broadcast 1→N | [mosta-media-sfu/README.md](../mosta-media-sfu/README.md) |
| `@mostajs/media-mcu` | Composite 1 flux output | ici |
