# @mostajs/media-mcu — fiche LLM
> MCU (Multipoint Conferencing Unit) : reçoit N flux RTP via mediasoup PlainTransport, les mixe avec ffmpeg en 1 sortie HLS/RTMP/mp4.

- Version: 0.3.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
- Chemin: mostajs/mosta-media-stack/mosta-media-mcu · Statut audit: complet (dist/)
- v0.3 : **bus PROGRAMME** — `session.setProgram(inputId)` commute la source diffusée
  (vidéo plein écran + **audio SOLO** de cette source) ; layout `'program'` ; API `programSet`.

## RÔLE
Multipoint Conferencing Unit pour la topologie MCU : le serveur décode N flux
WebRTC entrants et les compose (mix, layout grille/PIP) avec ffmpeg en UN seul
flux/fichier de sortie (HLS, RTMP, mp4). CPU serveur très élevé (transcode H.264)
→ pas scalable horizontalement sans encodeur matériel. Cas d'usage : régie multi-cam,
replay/archivage d'une conférence en 1 vidéo composée, broadcast vers HLS/RTMP.
Ingest publisher type WHIP (`WebRtcTransport`), recording via `PlainTransport` →
ffmpeg. Handlers Web-standard `Request → Response`. Le module ne persiste rien :
le fichier final est retourné au caller pour upload (hook storage).

## INSTALLATION
```
npm i @mostajs/media-mcu
```
Dépendances runtime : `mediasoup` (compile un worker C++) + `sdp-transform`.
`@mostajs/media-sfu` est un peer optionnel. ffmpeg doit être installé sur l'hôte.

## EXPORTS
- `createMcuServer(opts: CreateMcuOptions): Promise<McuServer & { router; mcuOpts }>`
- `createMcuApiHandlers(opts: CreateMcuApiOptions): McuApiHandlers`
- `moduleInfo: { name; version; scope }`
- Types : `CreateMcuOptions`, `McuEvent`, `McuInput`, `McuSession`, `McuSessionInfo`,
  `McuServer`, `McuLayout`, `McuResolution`, `CreateMcuApiOptions`, `McuApiHandlers`,
  `ApiPrincipal`, `PermissionChecker`

## EXPORTS PAR SOUS-CHEMIN
- `@mostajs/media-mcu/server` → `createMcuServer`
- `@mostajs/media-mcu/api` → `createMcuApiHandlers`
- `@mostajs/media-mcu/types` → tous les types

## API — SIGNATURES
```ts
createMcuServer(opts: CreateMcuOptions): Promise<McuServer & { router; mcuOpts }>
createMcuApiHandlers(opts: CreateMcuApiOptions): McuApiHandlers

interface McuServer {
  createSession(opts: { layout: McuLayout; resolution?: McuResolution; sessionId?: string }): Promise<McuSession>
  getSession(sessionId: string): McuSession | undefined
  closeSession(sessionId: string): Promise<void>
  listSessions(): McuSessionInfo[]
  close(): Promise<void>
}
interface McuSession {
  readonly id; readonly layout: McuLayout; readonly resolution: McuResolution
  readonly inputs: McuInput[]; readonly outputPlaylistPath: string; readonly createdAt
  readonly programInputId: string | null   // source diffusée par le bus PROGRAMME (v0.3)
  addInput(producerId: string): Promise<McuInput>
  removeInput(inputId: string): Promise<void>
  switchLayout(layout: McuLayout): Promise<void>
  setProgram(inputId: string | null): Promise<void> // v0.3 : commute le PROGRAM (vidéo plein écran + audio solo)
  close(): Promise<{ path: string; size: number; mimeType: string } | null>  // null si aucun recording
}

// McuApiHandlers — routes
sessionCreate(req)                          // POST   /mcu/sessions — { layout, resolution? }
sessionList(req)                            // GET    /mcu/sessions
sessionClose(req, ctx:{sessionId})          // DELETE /mcu/sessions/[sessionId]
inputAdd(req, ctx:{sessionId})              // POST   /mcu/sessions/[sessionId]/inputs — body SDP offer (WHIP-like)
inputRemove(req, ctx:{sessionId,inputId})   // DELETE /mcu/sessions/[sessionId]/inputs/[inputId]
outputUrl(req, ctx:{sessionId})             // GET    /mcu/sessions/[sessionId]/output — signed HLS playlist URL
layoutSwitch(req, ctx:{sessionId})          // POST   /mcu/sessions/[sessionId]/layout — { layout }
programSet(req, ctx:{sessionId})            // POST   /mcu/sessions/[sessionId]/program — { inputId } (v0.3 bus programme)
```

## TYPES CLÉS
```ts
type McuLayout = 'grid-2x2'|'grid-3x3'|'focus-speaker'|'pip-bottom-right'|'vertical-strip'|'single'|'program'
// 'program' (v0.3) : 1 source diffusée plein écran + audio SOLO ; sélectionnée via setProgram(inputId).
// buildFilterComplex(layout, vCount, aCount, res, program?:{videoIndex?,audioIndex?}) — en mode program,
//   vidéo = [0:v:<videoIndex>]scale[vout], audio = 0:a:<audioIndex> (SOLO, jamais d'amix).
type McuResolution = '720p' | '1080p'
interface CreateMcuOptions {
  listenIps: Array<{ ip: string; announcedIp?: string }>
  minPort: number; maxPort: number
  outputDir: string                       // path FS des segments HLS générés
  ffmpegPath?: string                     // défaut '/usr/bin/ffmpeg'
  storageBucket?: string                  // défaut 'mcu-outputs'
  onEvent?: (event: McuEvent) => void | Promise<void>
}
interface McuEvent {
  type: 'session.created'|'session.closed'|'input.added'|'input.removed'
      | 'output.ready'|'output.failed'|'layout.switched'
  sessionId?: string; inputId?: string
  details?: Record<string, unknown>; timestamp: number
}
interface McuInput { id; sessionId; producerId; rtpPort; ssrc; addedAt }
interface CreateMcuApiOptions {
  mcu: McuServer
  permissionChecker: PermissionChecker    // (req) => ApiPrincipal | null  (null => 401)
  auditLog?: (event: { route: string; status: number; user?: string }) => void
}
```

## PATTERN
```ts
import { createMcuServer, createMcuApiHandlers } from '@mostajs/media-mcu'

const mcu = await createMcuServer({
  listenIps: [{ ip: '0.0.0.0', announcedIp: '203.0.113.10' }],
  minPort: 50000, maxPort: 59999,
  outputDir: '/var/mcu/hls',
  ffmpegPath: '/usr/bin/ffmpeg',
})
const h = createMcuApiHandlers({ mcu, permissionChecker: (req) => verifyJwt(req) })
// app/api/mcu/sessions/route.ts
export const POST = (req) => h.sessionCreate(req)

// Cycle session : createSession({layout}) -> inputAdd (WHIP) x N -> ffmpeg démarre
//   dès le 1er producer -> outputUrl (HLS) -> close() => { path, size, mimeType }
const session = await mcu.createSession({ layout: 'grid-2x2', resolution: '1080p' })
const file = await session.close()   // upload file.path vers le storage
```

## DÉPEND DE
- `@mostajs/media-sfu` (peer optionnel) — le MCU se place souvent en aval du SFU.
- Non-mostajs : `mediasoup` (worker C++), `sdp-transform`, ffmpeg (binaire hôte).

## PIÈGES
- ffmpeg DOIT être installé sur l'hôte ; `ffmpegPath` défaut `/usr/bin/ffmpeg`.
- CPU très élevé : transcode H.264 côté serveur — 1 instance ≠ scalable sans
  encodeur matériel. Pour juste enregistrer 1 caméra, préférer `@mostajs/media`.
- ffmpeg démarre dès qu'au moins 1 producer (audio OU vidéo) existe ; ordre critique
  géré en interne : PlainTransport+consumer en `paused`, allocation ports dgram,
  spawn ffmpeg, attente ~500 ms, puis `consumer.resume()`.
- `session.close()` retourne `null` si aucun recording n'a démarré, sinon
  `{ path, size, mimeType }` — c'est au caller d'uploader le fichier.
- `listenIps.announcedIp` = IP publique ; range `minPort`–`maxPort` UDP à ouvrir.
- MVP : `McuSessionImpl` gère 1 publisher recording par session.
- `removeInput` est présent mais le mix multi-input dynamique reste en évolution
  (v0.1.0) — le cas nominal est 1 publisher → 1 fichier composé.

## RÉFÉRENCES
- README.md (mosta-media-mcu) — guide d'usage.
- docs/ — documentation complémentaire.
- examples/ — exemples · test-scripts/ — scripts de test.
