@mostajs/media-mcu
v0.3.3
Published
MCU (Multipoint Conferencing Unit) pour l'écosystème @mostajs/* — compose N flux WebRTC en 1 flux HLS/RTMP via ffmpeg. Réception RTP via mediasoup PlainTransport. Cas d'usage : régie, replay archivage, broadcast unique.
Maintainers
Readme
@mostajs/media-mcu
Auteur : Dr Hamid MADANI [email protected] 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 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
npm install @mostajs/media-mcu
# Pré-requis système (sur le serveur)
sudo apt install ffmpegRequires Node ≥ 18 + ffmpeg dans le PATH.
Bootstrap
// 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
// 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// 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)// 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.
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 = nullCô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.
Output HLS pour viewers
L'output MCU est un flux HLS standard, lisible par n'importe quel player (hls.js, native iOS Safari, VLC) :
<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 :
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
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
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 |
| @mostajs/media-sfu | 5-100 pairs, broadcast 1→N | mosta-media-sfu/README.md |
| @mostajs/media-mcu | Composite 1 flux output | ici |
