Elysia Tools
Navigation mobile
Web Technologies
Exemples de Communication Temps Réel WebRTC
Exemples complets WebRTC pour communication audio/vidéo P2P, canaux de données, partage d'écran et serveur de signalisation
Exemples
Entrées de cette collection
Implémentation Appel Vidéo P2P
Configuration complète appel vidéo WebRTC avec accès caméra et audio
Difficulté
7/10
Temps estimé
30 min
Étiquettes
webrtc, p2p, video, audio, peer-connection, media-stream
// WebRTC Peer-to-Peer Video Call Implementation
// Complete setup for video and audio communication
class WebRTCPeerConnection {
constructor(config = {}) {
this.localVideo = document.getElementById('localVideo');
this.remoteVideo = document.getElementById('remoteVideo');
this.pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'password'
}
],
...config
});
this.setupPeerConnection();
this.setupMediaStreams();
}
async setupMediaStreams() {
try {
// Get local media stream with video and audio
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
this.localVideo.srcObject = stream;
// Add tracks to peer connection
stream.getTracks().forEach(track => {
this.pc.addTrack(track, stream);
});
} catch (error) {
console.error('Error accessing media devices:', error);
}
}
setupPeerConnection() {
// Handle incoming tracks
this.pc.ontrack = (event) => {
if (event.streams && event.streams[0]) {
this.remoteVideo.srcObject = event.streams[0];
}
};
// Handle ICE candidates
this.pc.onicecandidate = (event) => {
if (event.candidate) {
// Send candidate to remote peer via signaling server
this.sendSignalingMessage({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// Handle connection state changes
this.pc.onconnectionstatechange = () => {
console.log('Connection state:', this.pc.connectionState);
if (this.pc.connectionState === 'connected') {
console.log('WebRTC connection established!');
}
};
// Handle ICE connection state changes
this.pc.oniceconnectionstatechange = () => {
console.log('ICE connection state:', this.pc.iceConnectionState);
};
}
async createOffer() {
try {
const offer = await this.pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
await this.pc.setLocalDescription(offer);
// Send offer to remote peer
this.sendSignalingMessage({
type: 'offer',
offer: offer
});
} catch (error) {
console.error('Error creating offer:', error);
}
}
async handleOffer(offer) {
try {
await this.pc.setRemoteDescription(offer);
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
// Send answer back
this.sendSignalingMessage({
type: 'answer',
answer: answer
});
} catch (error) {
console.error('Error handling offer:', error);
}
}
async handleAnswer(answer) {
try {
await this.pc.setRemoteDescription(answer);
} catch (error) {
console.error('Error handling answer:', error);
}
}
async handleIceCandidate(candidate) {
try {
await this.pc.addIceCandidate(candidate);
} catch (error) {
console.error('Error adding ICE candidate:', error);
}
}
sendSignalingMessage(message) {
// Implement WebSocket or signaling server communication
console.log('Sending signaling message:', message);
}
// Media controls
toggleAudio() {
const audioTrack = this.localVideo.srcObject.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
}
}
toggleVideo() {
const videoTrack = this.localVideo.srcObject.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
}
}
// Hang up the call
hangup() {
if (this.pc) {
this.pc.close();
this.pc = null;
}
// Stop local media
if (this.localVideo.srcObject) {
this.localVideo.srcObject.getTracks().forEach(track => track.stop());
this.localVideo.srcObject = null;
}
this.remoteVideo.srcObject = null;
}
}
// Usage example
const webrtcConnection = new WebRTCPeerConnection();
// Start the call
webrtcConnection.createOffer();- format
- JavaScript
- apis
- ["RTCPeerConnection","MediaDevices","MediaStream"]
- features
- ["ice-handling","media-stream","connection-management"]
- browsers
- ["Chrome","Firefox","Safari","Edge"]
Communication Canal Données WebRTC
Implémentation de canaux de données pour messagerie et transfert de fichiers en temps réel
Difficulté
6/10
Temps estimé
25 min
Étiquettes
data-channel, real-time, messaging, file-transfer, p2p
// WebRTC Data Channel Implementation
// Real-time data communication between peers
class WebRTCDataChannel {
constructor(peerConnection) {
this.pc = peerConnection;
this.dataChannel = null;
this.messageCallbacks = new Map();
this.setupDataChannel();
}
setupDataChannel() {
// Create data channel for reliable messaging
this.dataChannel = this.pc.createDataChannel('chat', {
ordered: true,
maxRetransmits: 3,
protocol: 'json'
});
this.setupDataChannelHandlers();
}
setupDataChannelHandlers() {
this.dataChannel.onopen = () => {
console.log('Data channel opened');
this.onChannelOpen();
};
this.dataChannel.onmessage = (event) => {
this.handleMessage(event.data);
};
this.dataChannel.onclose = () => {
console.log('Data channel closed');
this.onChannelClose();
};
this.dataChannel.onerror = (error) => {
console.error('Data channel error:', error);
this.onChannelError(error);
};
}
handleMessage(data) {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'chat':
this.onChatMessage(message);
break;
case 'file':
this.onFileTransfer(message);
break;
case 'typing':
this.onTypingIndicator(message);
break;
case 'cursor':
this.onCursorUpdate(message);
break;
default:
console.log('Unknown message type:', message.type);
}
// Call custom callbacks
if (this.messageCallbacks.has(message.type)) {
this.messageCallbacks.get(message.type)(message);
}
} catch (error) {
console.error('Error parsing message:', error);
}
}
// Send different types of messages
sendChatMessage(text, sender = 'Anonymous') {
const message = {
type: 'chat',
text: text,
sender: sender,
timestamp: Date.now()
};
this.sendMessage(message);
}
sendFile(file, onProgress) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const message = {
type: 'file',
name: file.name,
size: file.size,
mimeType: file.type,
data: event.target.result
};
this.sendMessage(message);
resolve(message);
};
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
}
sendTypingIndicator(isTyping) {
const message = {
type: 'typing',
isTyping: isTyping,
timestamp: Date.now()
};
this.sendMessage(message);
}
sendCursorUpdate(x, y) {
const message = {
type: 'cursor',
x: x,
y: y,
timestamp: Date.now()
};
this.sendMessage(message);
}
sendCustomMessage(type, data) {
const message = {
type: type,
data: data,
timestamp: Date.now()
};
this.sendMessage(message);
}
sendMessage(message) {
if (this.dataChannel && this.dataChannel.readyState === 'open') {
this.dataChannel.send(JSON.stringify(message));
} else {
console.error('Data channel is not open');
}
}
// Event handlers (can be overridden)
onChannelOpen() {
console.log('Data channel ready for communication');
}
onChannelClose() {
console.log('Data channel communication ended');
}
onChannelError(error) {
console.error('Data channel communication error:', error);
}
onChatMessage(message) {
console.log('Chat message received:', message);
}
onFileTransfer(message) {
console.log('File transfer received:', message);
}
onTypingIndicator(message) {
console.log('Typing indicator:', message);
}
onCursorUpdate(message) {
console.log('Cursor update:', message);
}
// Register custom message handlers
onMessageType(type, callback) {
this.messageCallbacks.set(type, callback);
}
// Data channel status
isOpen() {
return this.dataChannel && this.dataChannel.readyState === 'open';
}
close() {
if (this.dataChannel) {
this.dataChannel.close();
this.dataChannel = null;
}
}
}
// File transfer helper class
class FileTransferHandler {
constructor(dataChannel) {
this.dataChannel = dataChannel;
this.receivingFiles = new Map();
}
async receiveFile(fileInfo, onProgress) {
const chunks = [];
let receivedBytes = 0;
return new Promise((resolve, reject) => {
const handleChunk = (chunk) => {
chunks.push(chunk);
receivedBytes += chunk.byteLength;
if (onProgress) {
onProgress(receivedBytes, fileInfo.size);
}
if (receivedBytes >= fileInfo.size) {
const blob = new Blob(chunks, { type: fileInfo.type });
const file = new File([blob], fileInfo.name, { type: fileInfo.type });
resolve(file);
}
};
this.dataChannel.onMessageType('file-chunk', (message) => {
if (message.fileId === fileInfo.id) {
handleChunk(message.data);
}
});
});
}
async sendFile(file, onProgress) {
const chunkSize = 16384; // 16KB chunks
const fileId = Date.now().toString();
// Send file metadata
this.dataChannel.sendCustomMessage('file-info', {
id: fileId,
name: file.name,
size: file.size,
type: file.type
});
// Send file in chunks
const reader = new FileReader();
let offset = 0;
const sendNextChunk = () => {
const chunk = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(chunk);
};
reader.onload = (event) => {
this.dataChannel.sendCustomMessage('file-chunk', {
fileId: fileId,
data: event.target.result
});
offset += chunkSize;
if (onProgress) {
onProgress(offset, file.size);
}
if (offset < file.size) {
setTimeout(sendNextChunk, 10); // Small delay to prevent blocking
}
};
sendNextChunk();
}
}
// Usage example
const webrtcDataChannel = new WebRTCDataChannel(peerConnection);
// Set up message handlers
webrtcDataChannel.onMessageType('game-state', (message) => {
updateGameDisplay(message.data);
});
// Send messages
webrtcDataChannel.sendChatMessage('Hello from peer!');
webrtcDataChannel.sendTypingIndicator(true);- format
- JavaScript
- apis
- ["RTCDataChannel","FileReader","Blob"]
- features
- ["reliable-messaging","file-transfer","custom-protocols"]
- protocols
- ["JSON","Binary"]
Implémentation Partage Écran
Configuration complète de partage d'écran avec support des onglets et audio
Difficulté
6/10
Temps estimé
20 min
Étiquettes
screen-share, desktop-capture, tab-sharing, picture-in-picture, recording
// WebRTC Screen Sharing Implementation
// Screen capture, tab sharing, and audio support
class ScreenShareManager {
constructor(peerConnection) {
this.pc = peerConnection;
this.screenStream = null;
this.localVideo = document.getElementById('localScreen');
this.isSharing = false;
}
async startScreenShare(options = {}) {
try {
// Get screen share stream
this.screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: options.cursor || 'always',
displaySurface: options.displaySurface || 'monitor'
},
audio: options.audio || false,
preferCurrentTab: options.preferCurrentTab || false
});
// Replace local video with screen share
this.localVideo.srcObject = this.screenStream;
// Add or replace video tracks in peer connection
const videoTracks = this.screenStream.getVideoTracks();
const sender = this.pc.getSenders().find(s =>
s.track && s.track.kind === 'video'
);
if (sender && videoTracks.length > 0) {
// Replace existing video track
await sender.replaceTrack(videoTracks[0]);
} else if (videoTracks.length > 0) {
// Add new video track
this.pc.addTrack(videoTracks[0], this.screenStream);
}
// Add audio tracks if available
const audioTracks = this.screenStream.getAudioTracks();
audioTracks.forEach(track => {
const audioSender = this.pc.getSenders().find(s =>
s.track && s.track.kind === 'audio'
);
if (audioSender) {
audioSender.replaceTrack(track);
} else {
this.pc.addTrack(track, this.screenStream);
}
});
this.isSharing = true;
// Listen for stream end
this.screenStream.getVideoTracks()[0].onended = () => {
this.stopScreenShare();
};
return this.screenStream;
} catch (error) {
console.error('Error starting screen share:', error);
throw error;
}
}
async startTabShare() {
try {
// Use Chrome extension API for tab sharing (if available)
if (chrome && chrome.desktopCapture) {
return new Promise((resolve, reject) => {
chrome.desktopCapture.chooseDesktopMedia(
['screen', 'window', 'tab'],
(streamId) => {
if (streamId) {
navigator.mediaDevices.getUserMedia({
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: streamId
}
}
}).then(resolve).catch(reject);
} else {
reject(new Error('No stream selected'));
}
}
);
});
} else {
// Fallback to standard screen capture with tab preference
return this.startScreenShare({
preferCurrentTab: true,
displaySurface: 'browser'
});
}
} catch (error) {
console.error('Error starting tab share:', error);
throw error;
}
}
async stopScreenShare() {
try {
// Stop all tracks in screen stream
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => track.stop());
this.screenStream = null;
}
// Clear local video
this.localVideo.srcObject = null;
// Try to restore camera stream if available
await this.restoreCameraStream();
this.isSharing = false;
} catch (error) {
console.error('Error stopping screen share:', error);
}
}
async restoreCameraStream() {
try {
const cameraStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
this.localVideo.srcObject = cameraStream;
// Replace tracks in peer connection
const videoTracks = cameraStream.getVideoTracks();
const sender = this.pc.getSenders().find(s =>
s.track && s.track.kind === 'video'
);
if (sender && videoTracks.length > 0) {
await sender.replaceTrack(videoTracks[0]);
}
// Add audio tracks
const audioTracks = cameraStream.getAudioTracks();
audioTracks.forEach(track => {
const audioSender = this.pc.getSenders().find(s =>
s.track && s.track.kind === 'audio'
);
if (audioSender) {
audioSender.replaceTrack(track);
}
});
} catch (error) {
console.error('Error restoring camera stream:', error);
}
}
async toggleScreenShare() {
if (this.isSharing) {
await this.stopScreenShare();
} else {
await this.startScreenShare();
}
}
// Get available screen sources
async getScreenSources() {
try {
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === 'videoinput');
}
return [];
} catch (error) {
console.error('Error getting screen sources:', error);
return [];
}
}
// Check screen sharing capabilities
getCapabilities() {
return {
screenCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
tabCapture: !!(chrome && chrome.desktopCapture),
audioCapture: !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia),
cursorSupport: true,
multipleSources: true
};
}
// Add picture-in-picture support
async enablePictureInPicture() {
try {
if (this.localVideo && 'requestPictureInPicture' in this.localVideo) {
await this.localVideo.requestPictureInPicture();
}
} catch (error) {
console.error('Error enabling picture-in-picture:', error);
}
}
disablePictureInPicture() {
if (this.localVideo && 'exitPictureInPicture' in this.localVideo) {
this.localVideo.exitPictureInPicture();
}
}
// Recording functionality
startRecording() {
if (!this.screenStream) return null;
const chunks = [];
const mediaRecorder = new MediaRecorder(this.screenStream, {
mimeType: MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
? 'video/webm;codecs=vp9'
: 'video/webm'
});
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
this.onRecordingComplete(blob);
};
mediaRecorder.start();
return mediaRecorder;
}
onRecordingComplete(blob) {
// Handle recording completion
console.log('Screen share recording completed:', blob);
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `screen-share-${Date.now()}.webm`;
a.click();
URL.revokeObjectURL(url);
}
// Get current status
getStatus() {
return {
isSharing: this.isSharing,
hasVideo: !!(this.screenStream && this.screenStream.getVideoTracks().length),
hasAudio: !!(this.screenStream && this.screenStream.getAudioTracks().length),
trackCount: this.screenStream ? this.screenStream.getTracks().length : 0
};
}
}
// UI Integration Example
class ScreenShareUI {
constructor(screenShareManager) {
this.screenShare = screenShareManager;
this.setupUI();
}
setupUI() {
// Create control buttons
this.createShareButton();
this.createTabShareButton();
this.createStopButton();
this.createRecordButton();
// Create status display
this.createStatusDisplay();
}
createShareButton() {
const button = document.createElement('button');
button.textContent = 'Share Screen';
button.onclick = async () => {
try {
await this.screenShare.startScreenShare();
this.updateStatus();
} catch (error) {
this.showError('Failed to start screen share: ' + error.message);
}
};
document.body.appendChild(button);
}
createTabShareButton() {
const button = document.createElement('button');
button.textContent = 'Share Tab';
button.onclick = async () => {
try {
await this.screenShare.startTabShare();
this.updateStatus();
} catch (error) {
this.showError('Failed to start tab share: ' + error.message);
}
};
document.body.appendChild(button);
}
createStopButton() {
const button = document.createElement('button');
button.textContent = 'Stop Sharing';
button.onclick = async () => {
await this.screenShare.stopScreenShare();
this.updateStatus();
};
document.body.appendChild(button);
}
createRecordButton() {
const button = document.createElement('button');
button.textContent = 'Start Recording';
button.onclick = () => {
if (!this.recording) {
this.recording = this.screenShare.startRecording();
button.textContent = 'Stop Recording';
} else {
this.recording.stop();
this.recording = null;
button.textContent = 'Start Recording';
}
};
document.body.appendChild(button);
}
createStatusDisplay() {
this.statusDiv = document.createElement('div');
this.statusDiv.style.marginTop = '10px';
this.updateStatus();
document.body.appendChild(this.statusDiv);
}
updateStatus() {
const status = this.screenShare.getStatus();
this.statusDiv.innerHTML = `
<div>Sharing: ${status.isSharing ? 'Yes' : 'No'}</div>
<div>Video: ${status.hasVideo ? 'Yes' : 'No'}</div>
<div>Audio: ${status.hasAudio ? 'Yes' : 'No'}</div>
<div>Tracks: ${status.trackCount}</div>
`;
}
showError(message) {
alert(message);
}
}
// Usage example
const screenShareManager = new ScreenShareManager(peerConnection);
const screenShareUI = new ScreenShareUI(screenShareManager);- format
- JavaScript
- apis
- ["getDisplayMedia","MediaRecorder","PictureInPicture"]
- features
- ["screen-capture","tab-capture","audio-capture","recording"]
- browsers
- ["Chrome","Firefox","Edge"]
Serveur Signalisation WebRTC
Serveur WebSocket pour établissement de connexions P2P et négociation médias
Difficulté
8/10
Temps estimé
35 min
Étiquettes
signaling, websocket, peer-connection, room-management, server
// WebRTC Signaling Server Implementation
// Node.js WebSocket server for peer connection coordination
const WebSocket = require('ws');
const http = require('http');
const express = require('express');
const cors = require('cors');
class WebRTCSignalingServer {
constructor(port = 8080) {
this.port = port;
this.app = express();
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
this.rooms = new Map(); // room_id -> Set of peers
this.peers = new Map(); // peer_id -> { ws, room_id, metadata }
this.setupExpress();
this.setupWebSocket();
}
setupExpress() {
this.app.use(cors());
this.app.use(express.json());
// Static files for client
this.app.use(express.static('public'));
// API endpoints
this.app.get('/api/rooms', (req, res) => {
const rooms = Array.from(this.rooms.keys()).map(roomId => ({
id: roomId,
peerCount: this.rooms.get(roomId).size
}));
res.json(rooms);
});
this.app.post('/api/rooms', (req, res) => {
const roomId = this.generateRoomId();
this.rooms.set(roomId, new Set());
res.json({ roomId });
});
this.app.get('/api/rooms/:roomId/peers', (req, res) => {
const roomId = req.params.roomId;
const roomPeers = this.rooms.get(roomId);
if (!roomPeers) {
return res.status(404).json({ error: 'Room not found' });
}
const peers = Array.from(roomPeers).map(peerId => ({
id: peerId,
metadata: this.peers.get(peerId)?.metadata
}));
res.json(peers);
});
}
setupWebSocket() {
this.wss.on('connection', (ws, req) => {
const peerId = this.generatePeerId();
console.log(`Peer connected: ${peerId}`);
// Store peer connection
this.peers.set(peerId, {
ws: ws,
room_id: null,
metadata: {
userAgent: req.headers['user-agent'],
ip: req.socket.remoteAddress,
connectedAt: Date.now()
}
});
// Send peer ID to client
this.sendToPeer(peerId, {
type: 'peer-id',
peerId: peerId
});
// Handle messages
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
this.handleMessage(peerId, data);
} catch (error) {
console.error(`Invalid message from peer ${peerId}:`, error);
this.sendError(peerId, 'Invalid message format');
}
});
// Handle disconnection
ws.on('close', () => {
console.log(`Peer disconnected: ${peerId}`);
this.handlePeerDisconnect(peerId);
});
// Handle errors
ws.on('error', (error) => {
console.error(`WebSocket error for peer ${peerId}:`, error);
});
});
}
handleMessage(peerId, message) {
const peer = this.peers.get(peerId);
if (!peer) return;
switch (message.type) {
case 'join-room':
this.handleJoinRoom(peerId, message.roomId);
break;
case 'leave-room':
this.handleLeaveRoom(peerId);
break;
case 'offer':
this.handleRelayMessage(peerId, message, 'offer');
break;
case 'answer':
this.handleRelayMessage(peerId, message, 'answer');
break;
case 'ice-candidate':
this.handleRelayMessage(peerId, message, 'ice-candidate');
break;
case 'room-info':
this.sendRoomInfo(peerId);
break;
case 'peer-list':
this.sendPeerList(peerId);
break;
case 'direct-message':
this.handleDirectMessage(peerId, message);
break;
default:
console.log(`Unknown message type: ${message.type}`);
}
}
handleJoinRoom(peerId, roomId) {
const peer = this.peers.get(peerId);
if (!peer) return;
// Leave current room if in one
if (peer.room_id) {
this.handleLeaveRoom(peerId);
}
// Join new room
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(peerId);
peer.room_id = roomId;
console.log(`Peer ${peerId} joined room ${roomId}`);
// Notify room about new peer
this.broadcastToRoom(roomId, {
type: 'peer-joined',
peerId: peerId,
metadata: peer.metadata
}, peerId);
// Send room info to new peer
this.sendRoomInfo(peerId);
}
handleLeaveRoom(peerId) {
const peer = this.peers.get(peerId);
if (!peer || !peer.room_id) return;
const roomId = peer.room_id;
this.rooms.get(roomId).delete(peerId);
// Clean up empty rooms
if (this.rooms.get(roomId).size === 0) {
this.rooms.delete(roomId);
}
console.log(`Peer ${peerId} left room ${roomId}`);
// Notify room about peer leaving
this.broadcastToRoom(roomId, {
type: 'peer-left',
peerId: peerId
});
peer.room_id = null;
}
handleRelayMessage(senderId, message, messageType) {
const peer = this.peers.get(senderId);
if (!peer || !peer.room_id) return;
// Add sender info to message
message.senderId = senderId;
// Relay to all peers in room except sender
this.broadcastToRoom(peer.room_id, message, senderId);
}
handleDirectMessage(senderId, message) {
const recipientId = message.recipientId;
const recipient = this.peers.get(recipientId);
if (recipient) {
message.senderId = senderId;
this.sendToPeer(recipientId, message);
} else {
this.sendError(senderId, `Recipient ${recipientId} not found`);
}
}
handlePeerDisconnect(peerId) {
const peer = this.peers.get(peerId);
if (!peer) return;
// Leave room if in one
if (peer.room_id) {
this.handleLeaveRoom(peerId);
}
// Clean up peer data
this.peers.delete(peerId);
}
sendToPeer(peerId, message) {
const peer = this.peers.get(peerId);
if (peer && peer.ws.readyState === WebSocket.OPEN) {
peer.ws.send(JSON.stringify(message));
}
}
sendError(peerId, error) {
this.sendToPeer(peerId, {
type: 'error',
error: error
});
}
broadcastToRoom(roomId, message, excludePeerId = null) {
const room = this.rooms.get(roomId);
if (!room) return;
room.forEach(peerId => {
if (peerId !== excludePeerId) {
this.sendToPeer(peerId, message);
}
});
}
sendRoomInfo(peerId) {
const peer = this.peers.get(peerId);
if (!peer || !peer.room_id) return;
const room = this.rooms.get(peer.room_id);
const peers = Array.from(room || []).filter(id => id !== peerId);
this.sendToPeer(peerId, {
type: 'room-info',
roomId: peer.room_id,
peers: peers,
peerCount: peers.length
});
}
sendPeerList(peerId) {
const peer = this.peers.get(peerId);
if (!peer) return;
const allPeers = Array.from(this.peers.keys())
.filter(id => id !== peerId)
.map(id => ({
id: id,
metadata: this.peers.get(id)?.metadata
}));
this.sendToPeer(peerId, {
type: 'peer-list',
peers: allPeers
});
}
generatePeerId() {
return `peer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
generateRoomId() {
return Math.random().toString(36).substr(2, 8).toUpperCase();
}
start() {
this.server.listen(this.port, () => {
console.log(`Signaling server running on port ${this.port}`);
});
}
stop() {
this.wss.close();
this.server.close();
}
// Server statistics
getStats() {
return {
totalPeers: this.peers.size,
totalRooms: this.rooms.size,
rooms: Array.from(this.rooms.entries()).map(([id, peers]) => ({
id,
peerCount: peers.size
}))
};
}
}
// Room management extension
class RoomManager {
constructor(signalingServer) {
this.server = signalingServer;
// Add room management endpoints
this.server.app.delete('/api/rooms/:roomId', (req, res) => {
const roomId = req.params.roomId;
const room = this.server.rooms.get(roomId);
if (!room) {
return res.status(404).json({ error: 'Room not found' });
}
// Disconnect all peers in room
room.forEach(peerId => {
this.server.sendToPeer(peerId, {
type: 'room-closed',
roomId: roomId,
reason: 'Room deleted by admin'
});
const peer = this.server.peers.get(peerId);
if (peer) {
peer.room_id = null;
}
});
this.server.rooms.delete(roomId);
res.json({ message: 'Room deleted' });
});
// Add room settings
this.server.app.patch('/api/rooms/:roomId/settings', (req, res) => {
const roomId = req.params.roomId;
const settings = req.body;
// Store room settings (implement room settings storage)
console.log(`Room ${roomId} settings updated:`, settings);
// Broadcast settings update to room
this.server.broadcastToRoom(roomId, {
type: 'room-settings-updated',
roomId: roomId,
settings: settings
});
res.json({ message: 'Settings updated' });
});
}
}
// Usage example
const signalingServer = new WebRTCSignalingServer(8080);
const roomManager = new RoomManager(signalingServer);
// Start server
signalingServer.start();
// Graceful shutdown
process.on('SIGINT', () => {
console.log('Shutting down signaling server...');
signalingServer.stop();
process.exit(0);
});
// Health check endpoint
signalingServer.app.get('/health', (req, res) => {
const stats = signalingServer.getStats();
res.json({
status: 'healthy',
...stats
});
});- format
- JavaScript
- frameworks
- ["Node.js","WebSocket","Express"]
- features
- ["room-management","peer-discovery","message-relay"]
- protocols
- ["WebSocket","HTTP"]
Implémentation Serveur Médias SFU
Architecture SFU pour visioconférences évolutives avec plusieurs participants
Difficulté
9/10
Temps estimé
45 min
Étiquettes
sfu, media-server, scalable, video-conference, simulcast, quality-control
// WebRTC SFU (Selective Forwarding Unit) Implementation
// Scalable media server for multi-participant conferences
const WebSocket = require('ws');
const { RTCPeerConnection, RTCSessionDescription } = require('wrtc');
const uuid = require('uuid');
class SFUMediaServer {
constructor() {
this.rooms = new Map(); // roomId -> Room instance
this.peers = new Map(); // peerId -> Peer instance
this.wss = new WebSocket.Server({ port: 3000 });
this.setupWebSocket();
}
setupWebSocket() {
this.wss.on('connection', (ws, req) => {
console.log('New SFU client connected');
ws.on('message', async (data) => {
try {
const message = JSON.parse(data);
await this.handleMessage(ws, message);
} catch (error) {
console.error('Error handling message:', error);
}
});
ws.on('close', () => {
console.log('SFU client disconnected');
this.handleDisconnect(ws);
});
});
}
async handleMessage(ws, message) {
const { type, roomId, peerId, data } = message;
switch (type) {
case 'join-room':
await this.handleJoinRoom(ws, roomId, peerId, data);
break;
case 'leave-room':
this.handleLeaveRoom(peerId);
break;
case 'offer':
await this.handleOffer(peerId, data);
break;
case 'ice-candidate':
await this.handleIceCandidate(peerId, data);
break;
case 'request-tracks':
await this.handleTrackRequest(peerId, data);
break;
case 'update-layout':
this.handleLayoutUpdate(roomId, data);
break;
default:
console.log(`Unknown message type: ${type}`);
}
}
async handleJoinRoom(ws, roomId, peerId, metadata = {}) {
try {
// Create room if it doesn't exist
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new SFURoom(roomId));
}
const room = this.rooms.get(roomId);
// Create peer instance
const peer = new SFUPeer(peerId, ws, room, metadata);
this.peers.set(peerId, peer);
// Add peer to room
await room.addPeer(peer);
console.log(`Peer ${peerId} joined room ${roomId}`);
} catch (error) {
console.error('Error joining room:', error);
ws.send(JSON.stringify({
type: 'error',
error: error.message
}));
}
}
handleLeaveRoom(peerId) {
const peer = this.peers.get(peerId);
if (!peer) return;
peer.leave();
this.peers.delete(peerId);
}
async handleOffer(peerId, offerData) {
const peer = this.peers.get(peerId);
if (!peer) return;
await peer.handleOffer(offerData);
}
async handleIceCandidate(peerId, candidateData) {
const peer = this.peers.get(peerId);
if (!peer) return;
await peer.handleIceCandidate(candidateData);
}
async handleTrackRequest(peerId, request) {
const peer = this.peers.get(peerId);
if (!peer) return;
await peer.handleTrackRequest(request);
}
handleLayoutUpdate(roomId, layout) {
const room = this.rooms.get(roomId);
if (!room) return;
room.updateLayout(layout);
}
handleDisconnect(ws) {
// Find and disconnect peer
for (const [peerId, peer] of this.peers.entries()) {
if (peer.ws === ws) {
this.handleLeaveRoom(peerId);
break;
}
}
}
}
class SFURoom {
constructor(roomId) {
this.roomId = roomId;
this.peers = new Map(); // peerId -> Peer instance
this.tracks = new Map(); // trackId -> Track instance
this.layout = 'grid'; // grid, speaker, custom
this.maxVideoQuality = '1080p';
}
async addPeer(peer) {
this.peers.set(peer.id, peer);
// Send existing tracks to new peer
for (const [trackId, track] of this.tracks.entries()) {
await this.sendTrackToPeer(peer, track);
}
// Notify other peers about new peer
this.broadcastToPeers({
type: 'peer-joined',
peerId: peer.id,
metadata: peer.metadata
}, peer.id);
}
removePeer(peerId) {
const peer = this.peers.get(peerId);
if (!peer) return;
// Clean up peer's tracks
for (const trackId of peer.trackIds) {
this.tracks.delete(trackId);
}
// Remove peer from room
this.peers.delete(peerId);
// Notify other peers
this.broadcastToPeers({
type: 'peer-left',
peerId: peerId
});
console.log(`Peer ${peerId} left room ${this.roomId}`);
}
async sendTrackToPeer(peer, track) {
try {
// Create transceiver for the track
const transceiver = peer.pc.addTransceiver(track.track, {
direction: 'sendonly'
});
// Store track mapping
peer.trackMappings.set(track.id, {
transceiver: transceiver,
trackId: track.id
});
console.log(`Sent track ${track.id} to peer ${peer.id}`);
} catch (error) {
console.error(`Error sending track to peer ${peer.id}:`, error);
}
}
addTrack(peerId, track) {
const trackId = uuid.v4();
const sfuTrack = new SFUTrack(trackId, peerId, track);
this.tracks.set(trackId, sfuTrack);
// Send track to all other peers
this.broadcastToPeers({
type: 'track-added',
trackId: trackId,
peerId: peerId,
metadata: sfuTrack.metadata
}, peerId);
return sfuTrack;
}
removeTrack(trackId) {
const track = this.tracks.get(trackId);
if (!track) return;
this.tracks.delete(trackId);
// Notify all peers
this.broadcastToPeers({
type: 'track-removed',
trackId: trackId
});
}
updateLayout(layout) {
this.layout = layout;
// Recalculate track quality and simulcast layers
this.optimizeTracks();
// Notify peers about layout change
this.broadcastToPeers({
type: 'layout-updated',
layout: layout
});
}
optimizeTracks() {
// Implement track optimization based on layout
// Adjust quality levels, enable/disable simulcast layers
console.log(`Optimizing tracks for layout: ${this.layout}`);
}
broadcastToPeers(message, excludePeerId = null) {
for (const [peerId, peer] of this.peers.entries()) {
if (peerId !== excludePeerId) {
peer.send(message);
}
}
}
}
class SFUPeer {
constructor(id, ws, room, metadata = {}) {
this.id = id;
this.ws = ws;
this.room = room;
this.metadata = metadata;
this.pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
});
this.trackIds = new Set();
this.trackMappings = new Map();
this.requestedTracks = new Set();
this.setupPeerConnection();
this.setupWebSocket();
}
setupPeerConnection() {
// Handle incoming tracks
this.pc.ontrack = (event) => {
console.log(`Received track from peer ${this.id}:`, event.track);
const trackId = uuid.v4();
this.trackIds.add(trackId);
// Add track to room
this.room.addTrack(this.id, event.track);
};
// Handle ICE candidates
this.pc.onicecandidate = (event) => {
if (event.candidate) {
this.send({
type: 'ice-candidate',
candidate: event.candidate
});
}
};
// Handle connection state
this.pc.onconnectionstatechange = () => {
console.log(`Peer ${this.id} connection state: ${this.pc.connectionState}`);
};
}
setupWebSocket() {
this.ws.on('close', () => {
this.leave();
});
this.ws.on('error', (error) => {
console.error(`WebSocket error for peer ${this.id}:`, error);
});
}
async handleOffer(offerData) {
try {
const offer = new RTCSessionDescription(offerData);
await this.pc.setRemoteDescription(offer);
// Create answer
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
// Send answer back
this.send({
type: 'answer',
answer: answer
});
} catch (error) {
console.error(`Error handling offer for peer ${this.id}:`, error);
}
}
async handleIceCandidate(candidateData) {
try {
const candidate = new RTCIceCandidate(candidateData);
await this.pc.addIceCandidate(candidate);
} catch (error) {
console.error(`Error adding ICE candidate for peer ${this.id}:`, error);
}
}
async handleTrackRequest(request) {
const { trackId, quality } = request;
if (!this.room.tracks.has(trackId)) {
console.warn(`Track ${trackId} not found`);
return;
}
// Adjust track quality based on request
await this.adjustTrackQuality(trackId, quality);
console.log(`Peer ${this.id} requested track ${trackId} with quality ${quality}`);
}
async adjustTrackQuality(trackId, quality) {
const track = this.room.tracks.get(trackId);
if (!track) return;
// Implement quality adjustment logic
// This could involve simulcast layer selection or bitrate adjustment
console.log(`Adjusting track ${trackId} quality to ${quality}`);
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
leave() {
console.log(`Peer ${this.id} leaving`);
// Clean up peer connection
if (this.pc) {
this.pc.close();
}
// Remove from room
this.room.removePeer(this.id);
}
}
class SFUTrack {
constructor(id, peerId, track) {
this.id = id;
this.peerId = peerId;
this.track = track;
this.metadata = {
kind: track.kind,
enabled: track.enabled,
readyState: track.readyState
};
this.quality = 'auto'; // auto, high, medium, low
this.simulcastLayers = [];
}
setQuality(quality) {
this.quality = quality;
// Implement actual quality adjustment
}
enable() {
this.track.enabled = true;
this.metadata.enabled = true;
}
disable() {
this.track.enabled = false;
this.metadata.enabled = false;
}
}
// Usage example
const sfuServer = new SFUMediaServer();
console.log('SFU Media Server running on ws://localhost:3000');
// Add graceful shutdown
process.on('SIGINT', () => {
console.log('Shutting down SFU server...');
process.exit(0);
});
// Monitoring and statistics
class SFUMonitor {
constructor(sfuServer) {
this.sfu = sfuServer;
// Add monitoring endpoint
setInterval(() => {
this.logStats();
}, 30000); // Log stats every 30 seconds
}
logStats() {
const stats = this.getStats();
console.log('SFU Statistics:', stats);
}
getStats() {
let totalPeers = 0;
let totalTracks = 0;
for (const room of this.sfu.rooms.values()) {
totalPeers += room.peers.size;
totalTracks += room.tracks.size;
}
return {
rooms: this.sfu.rooms.size,
peers: totalPeers,
tracks: totalTracks,
timestamp: new Date().toISOString()
};
}
}
const monitor = new SFUMonitor(sfuServer);- format
- JavaScript
- frameworks
- ["Node.js","WebSocket","WebRTC"]
- features
- ["track-forwarding","quality-control","simulcast","room-management"]
- protocols
- ["WebRTC","WebSocket"]
Outils
Outils souvent utilisés avec cet exemple
Associé