Media Server Setup - nself-org/nchat GitHub Wiki
Complete guide for setting up the media server infrastructure for nself-chat v0.4.0.
The media server provides scalable audio/video communication using:
- MediaSoup SFU - Selective Forwarding Unit for efficient media routing
- coturn - TURN/STUN server for NAT traversal
- FFmpeg - Recording and transcoding
- Redis - Coordination and state management
- Socket.IO - Real-time signaling
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Client ββββββΆβ Media Server ββββββΆβ MediaSoup β
β (WebRTC) βββββββ (Socket.IO) βββββββ Workers β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β β β
β β β
βΌ βΌ βΌ
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β TURN/STUN β β Redis β β FFmpeg β
β (coturn) β β (State/Sync) β β (Recording) β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
- Docker and Docker Compose installed
- Node.js 20+ (for local development)
- pnpm 9.15+ (optional, for dependency management)
- Open ports: 3100, 3478, 40000-49999
cd .backend
chmod +x scripts/setup-media-server.sh
./scripts/setup-media-server.shThe script will:
- Detect your public IP address
- Generate secure secrets
- Create configuration files
- Setup directories
- Build Docker images
- Start services
chmod +x scripts/test-media-server.sh
./scripts/test-media-server.sh- Media Server API: http://localhost:3100
- Health Check: http://localhost:3100/api/health
- TURN Server: turn:localhost:3478
- Redis: localhost:6379
cd .backend
cp .env.media.example .env.mediaEdit .env.media:
# Public IP (replace with your server's public IP)
MEDIA_SERVER_PUBLIC_IP=your.public.ip.address
TURN_EXTERNAL_IP=your.public.ip.address
# Security (generate with: openssl rand -base64 32)
JWT_SECRET=your-secure-jwt-secret-here
TURN_CREDENTIAL=your-turn-credential-here
# MediaSoup Workers (match CPU cores)
MEDIASOUP_NUM_WORKERS=4
# Recording
RECORDING_ENABLED=true
# CORS (your frontend URL)
CORS_ORIGIN=https://your-frontend.com# Build images
docker-compose -f docker-compose.media.yml build
# Start services
docker-compose -f docker-compose.media.yml up -d
# Check logs
docker-compose -f docker-compose.media.yml logs -fEdit .backend/coturn/turnserver.conf if needed:
# Update realm
realm=your-domain.com
# Add static users
user=username:password
# Set external IP
external-ip=YOUR_PUBLIC_IP
Restart coturn:
docker-compose -f docker-compose.media.yml restart coturncd /path/to/frontend
pnpm add mediasoup-client socket.io-client# .env.local
NEXT_PUBLIC_MEDIA_SERVER_URL=http://localhost:3100
NEXT_PUBLIC_MEDIA_SERVER_WS=ws://localhost:3100// src/lib/media/media-client.ts
import { io, Socket } from 'socket.io-client'
import { Device } from 'mediasoup-client'
export class MediaClient {
private socket: Socket
private device: Device
constructor(token: string) {
this.socket = io(process.env.NEXT_PUBLIC_MEDIA_SERVER_WS!, {
auth: { token },
transports: ['websocket', 'polling'],
})
this.device = new Device()
}
async joinRoom(roomId: string, userId: string, displayName: string) {
return new Promise((resolve, reject) => {
this.socket.emit('join-room', { roomId, userId, displayName }, (response: any) => {
if (response.error) {
reject(new Error(response.error))
} else {
resolve(response)
}
})
})
}
// ... more methods
}// src/components/call/VideoCall.tsx
import { useEffect, useState } from 'react';
import { MediaClient } from '@/lib/media/media-client';
export function VideoCall({ roomId, token }: Props) {
const [client, setClient] = useState<MediaClient | null>(null);
useEffect(() => {
const mediaClient = new MediaClient(token);
mediaClient.joinRoom(roomId, userId, displayName)
.then(() => {
setClient(mediaClient);
})
.catch(console.error);
return () => {
mediaClient.disconnect();
};
}, [roomId]);
return (
<div>
{/* Video elements */}
</div>
);
}// Adjust in .backend/custom-services/media-server/src/config.ts
mediasoup: {
numWorkers: 4, // Match CPU cores
rtcMinPort: 40000,
rtcMaxPort: 49999,
// Audio codecs
audioCodecs: ['opus'],
// Video codecs
videoCodecs: ['VP8', 'VP9', 'H264'],
}# .env.media
RECORDING_ENABLED=true
RECORDING_DIR=/recordings
RECORDING_MAX_SIZE_MB=1000
# Video settings
RECORDING_VIDEO_CODEC=libx264
RECORDING_AUDIO_CODEC=aac
RECORDING_RESOLUTION=1280x720
RECORDING_FRAME_RATE=30# .env.media
INSTANCE_ID=media-server-1
MAX_ROOMS_PER_INSTANCE=100
MAX_PARTICIPANTS_PER_ROOM=50Generate SSL certificates:
openssl req -x509 -newkey rsa:4096 \
-keyout key.pem -out cert.pem \
-days 365 -nodesUpdate turnserver.conf:
# Enable TLS
cert=/etc/coturn/cert.pem
pkey=/etc/coturn/key.pem
# Remove these lines
# no-tls
# no-dtls
# Allow media server
sudo ufw allow 3100/tcp
# Allow TURN/STUN
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 5349/tcp
# Allow RTC ports
sudo ufw allow 40000:49999/tcp
sudo ufw allow 40000:49999/udp
# Allow relay ports
sudo ufw allow 49152:65535/udp# Production
MEDIA_SERVER_PUBLIC_IP=production.ip
MEDIASOUP_NUM_WORKERS=8
LOG_LEVEL=warn
CORS_ORIGIN=https://your-domain.com
# Staging
MEDIA_SERVER_PUBLIC_IP=staging.ip
MEDIASOUP_NUM_WORKERS=4
LOG_LEVEL=infoEnable Prometheus and Grafana:
docker-compose -f docker-compose.media.yml --profile monitoring up -dAccess dashboards:
- Prometheus: http://localhost:9091
- Grafana: http://localhost:3001 (admin/admin)
# Start multiple media servers
docker-compose -f docker-compose.media.yml up -d --scale media-server=3
# Use load balancer (nginx, haproxy, etc.)
upstream media_servers {
least_conn;
server media-server-1:3100;
server media-server-2:3100;
server media-server-3:3100;
}# Health endpoint
curl http://localhost:3100/api/health
# Server stats
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3100/api/stats
# Docker logs
docker-compose -f docker-compose.media.yml logs -f media-serverThe media server exposes metrics at /metrics:
curl http://localhost:3100/metricsKey metrics:
-
media_rooms_total- Total active rooms -
media_participants_total- Total active participants -
media_recordings_active- Active recordings -
media_workers_busy- Busy worker count -
media_memory_usage- Memory usage
docker stats nself-media-server
docker stats nself-coturn
docker stats nself-redis-mediaSolution:
-
Check if service is running:
docker ps | grep media-server -
Check logs:
docker logs nself-media-server
-
Verify port is open:
netstat -tuln | grep 3100
Solution:
-
Test TURN connectivity:
# Install coturn-utils sudo apt-get install coturn-utils # Test TURN turnutils_uclient -v \ -u username -w password \ localhost
-
Check external IP is set correctly:
docker logs nself-coturn | grep "External IP"
-
Verify firewall allows TURN ports
Solution:
-
Adjust bandwidth settings in
config.ts:maxIncomingBitrate: 2000000, // 2 Mbps initialAvailableOutgoingBitrate: 1500000, // 1.5 Mbps
-
Enable simulcast for adaptive quality:
// Client-side const producer = await transport.produce({ track: videoTrack, encodings: [ { maxBitrate: 100000 }, // Low { maxBitrate: 500000 }, // Medium { maxBitrate: 1500000 }, // High ], codecOptions: { videoGoogleStartBitrate: 1000, }, })
Solution:
-
Check FFmpeg is installed:
docker exec nself-media-server which ffmpeg -
Check recording directory permissions:
docker exec nself-media-server ls -la /recordings -
Check disk space:
df -h
Solution:
-
Ensure Redis is running:
docker ps | grep redis -
Test connection:
docker exec nself-redis-media redis-cli ping -
Check network:
docker network inspect nself-network
Health check endpoint.
Response:
{
"status": "healthy",
"timestamp": "2024-01-30T12:00:00Z",
"instanceId": "media-server-1"
}Get server statistics (requires authentication).
Headers:
Authorization: Bearer <token>
Response:
{
"workers": { "total": 4 },
"rooms": { "total": 5 },
"participants": { "total": 12 },
"recordings": { "active": 2 },
"uptime": 3600,
"memory": { "heapUsed": 128000000 }
}Get ICE server configuration (requires authentication).
Response:
{
"iceServers": [
{ "urls": "stun:stun.l.google.com:19302" },
{
"urls": "turn:localhost:3478",
"username": "nself",
"credential": "secret"
}
]
}Create or get room.
Response:
{
"roomId": "room-123",
"routerId": "router-456",
"participantCount": 3
}Start recording.
Response:
{
"id": "recording-123",
"roomId": "room-123",
"status": "active",
"startedAt": "2024-01-30T12:00:00Z"
}-
join-room- Join a room -
create-transport- Create WebRTC transport -
connect-transport- Connect transport -
produce- Start producing media -
consume- Start consuming media -
pause-producer- Pause producer -
resume-producer- Resume producer -
leave-room- Leave room
-
participant-joined- New participant joined -
participant-left- Participant left -
new-producer- New producer available -
producer-paused- Producer paused -
producer-resumed- Producer resumed -
producer-closed- Producer closed
Increase file descriptor limits for high-concurrency:
# /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536Allocate more resources to media server:
# docker-compose.media.yml
media-server:
deploy:
resources:
limits:
cpus: '4.0'
memory: 4G
reservations:
cpus: '2.0'
memory: 2GUse host network mode for better performance:
media-server:
network_mode: hostNote: This removes Docker network isolation.
-
Use Strong Secrets
openssl rand -base64 32
-
Enable TLS/DTLS
- Configure SSL certificates for TURN
- Use HTTPS for media server API
-
Restrict CORS
CORS_ORIGIN=https://your-domain.com
-
Implement Rate Limiting
RATE_LIMIT_MAX_REQUESTS=100 RATE_LIMIT_WINDOW_MS=60000
-
Use Firewall Rules
- Only allow necessary ports
- Restrict access to monitoring endpoints
-
Regular Updates
docker-compose -f docker-compose.media.yml pull docker-compose -f docker-compose.media.yml up -d
- β Media server infrastructure setup
- π Implement frontend WebRTC client
- π Add call management UI
- π Implement screen sharing
- π Add recording playback
- π Setup monitoring dashboards
For issues or questions:
- Check logs:
docker-compose -f docker-compose.media.yml logs -f - Run tests:
./scripts/test-media-server.sh - Review configuration:
.env.mediaandturnserver.conf