🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Примеры Коммуникации WebRTC в Реальном Времени
Комплексные примеры WebRTC для P2P аудио/видео коммуникации, каналов данных, обмена экраном и сервера сигнализации
💻 Реализация P2P Видеозвонка
🟡 intermediate
⭐⭐⭐⭐
Полная настройка видеозвонка WebRTC с доступом к камере и аудио
⏱️ 30 min
🏷️ 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();
💻 Коммуникация Каналов Данных WebRTC
🟡 intermediate
⭐⭐⭐
Реализация каналов данных для сообщений и передачи файлов в реальном времени
⏱️ 25 min
🏷️ 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,
type: 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);
💻 Реализация Обмена Экраном
🟡 intermediate
⭐⭐⭐
Полная настройка обмена экраном с поддержкой вкладок и аудио
⏱️ 20 min
🏷️ 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);
💻 Сервер Сигнализации WebRTC
🔴 complex
⭐⭐⭐⭐
WebSocket сервер для установления P2P соединений и медиа-неготивации
⏱️ 35 min
🏷️ 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
});
});
💻 Реализация SFU Медиа Сервера
🔴 complex
⭐⭐⭐⭐⭐
Архитектура SFU для масштабируемых видеоконференций с множеством участников
⏱️ 45 min
🏷️ 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);