# @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 - 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` - `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 createMcuApiHandlers(opts: CreateMcuApiOptions): McuApiHandlers interface McuServer { createSession(opts: { layout: McuLayout; resolution?: McuResolution; sessionId?: string }): Promise getSession(sessionId: string): McuSession | undefined closeSession(sessionId: string): Promise listSessions(): McuSessionInfo[] close(): Promise } 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 removeInput(inputId: string): Promise switchLayout(layout: McuLayout): Promise setProgram(inputId: string | null): Promise // 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:]scale[vout], audio = 0:a: (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 } interface McuEvent { type: 'session.created'|'session.closed'|'input.added'|'input.removed' | 'output.ready'|'output.failed'|'layout.switched' sessionId?: string; inputId?: string details?: Record; 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.