@mostajs/media-mcu
v0.1.0
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.
Downloads
150
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.
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) |
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 |
