Peer-to-peer Node.js Socket.IO Темизация
WebRTC (Web Real-Time Communication) позволяет браузерам обмениваться аудио, видео и данными напрямую, минуя центральный сервер. Однако для установки соединения необходим сигнальный сервер, который обменивается метаданными (SDP, ICE-кандидаты). В этом руководстве мы разберём создание полноценного приложения: сервер на Node.js + Socket.IO, клиентский интерфейс с видеочатом и, конечно, реализуем переключение между светлой и тёмной темами.
Ниже — пошаговое описание, код и особенности темизации интерфейса.
Сигнальный сервер не передаёт медиапотоки — он только помогает пользователям «найти» друг друга.
Используем Express и Socket.IO для обработки событий: join-room, offer, answer, ice-candidate.
mkdir webrtc-server
cd webrtc-server
npm init -y
npm install express socket.io cors
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: "*", methods: ["GET", "POST"] }
});
io.on('connection', (socket) => {
console.log('✅ пользователь подключился:', socket.id);
// Присоединение к комнате (например, "room1")
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-connected', socket.id);
console.log(`👤 ${socket.id} вошёл в комнату ${roomId}`);
});
// Пересылка offer от звонящего
socket.on('offer', ({ target, offer }) => {
io.to(target).emit('offer', { from: socket.id, offer });
});
// Пересылка answer
socket.on('answer', ({ target, answer }) => {
io.to(target).emit('answer', { from: socket.id, answer });
});
// ICE-кандидаты
socket.on('ice-candidate', ({ target, candidate }) => {
io.to(target).emit('ice-candidate', { from: socket.id, candidate });
});
socket.on('disconnect', () => {
console.log('❌ пользователь отключился:', socket.id);
// здесь можно уведомить остальных в комнате
});
});
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => console.log(`🚀 Сигнальный сервер на порту ${PORT}`));
Сервер прослушивает события, маршрутизирует сообщения между участниками одной комнаты. Для продакшена стоит добавить обработку повторных подключений и более надёжное управление комнатами.
Клиент получает доступ к камере/микрофону, создаёт RTCPeerConnection, обменивается SDP и кандидатами через сервер.
В этом примере покажем ключевые фрагменты.
// подключение к серверу (замените URL на ваш)
const socket = io('http://localhost:3001');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
let localStream;
let peerConnection;
const configuration = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
// Запрос доступа к камере и микрофону
async function startMedia() {
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = localStream;
}
function createPeerConnection() {
peerConnection = new RTCPeerConnection(configuration);
// Добавляем локальные треки
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
// Обработка ICE-кандидатов
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice-candidate', {
target: remoteUserId,
candidate: event.candidate
});
}
};
// Получение удалённого потока
peerConnection.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
}
// инициатор создаёт offer
async function callUser(targetId) {
createPeerConnection();
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.emit('offer', { target: targetId, offer });
}
// принимающий обрабатывает offer
socket.on('offer', async ({ from, offer }) => {
createPeerConnection();
await peerConnection.setRemoteDescription(offer);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
socket.emit('answer', { target: from, answer });
});
// инициатор получает answer
socket.on('answer', async ({ answer }) => {
await peerConnection.setRemoteDescription(answer);
});
Вся магия WebRTC происходит после обмена этими сообщениями — браузеры устанавливают прямое P2P-соединение.
Переключение тем реализовано с помощью CSS custom properties и одного класса .dark на <body>.
Это позволяет менять цветовую схему без перезагрузки, а сохранение предпочтений в localStorage делает опыт бесшовным.
/* светлая тема (по умолчанию) */
:root {
--bg-page: #f8fafc;
--bg-surface: #ffffff;
--text-primary: #0f172a;
--accent: #2563eb;
/* ... */
}
/* тёмная тема */
body.dark {
--bg-page: #0b1120;
--bg-surface: #1e293b;
--text-primary: #f1f5f9;
--accent: #3b82f6;
}
const toggleBtn = document.getElementById('themeToggle');
const body = document.body;
// проверяем сохранённую тему
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
body.classList.add('dark');
}
toggleBtn.addEventListener('click', () => {
body.classList.toggle('dark');
const currentTheme = body.classList.contains('dark') ? 'dark' : 'light';
localStorage.setItem('theme', currentTheme);
});
Этот же подход используется на текущей странице — попробуйте переключить тему, и вы увидите, как плавно меняются цвета. Все компоненты (кнопки, карточки, блоки кода) автоматически адаптируются.
prefers-color-scheme, чтобы подхватывать системную тему.Полный код клиента и сервера можно объединить в один проект. Для тестирования запустите сервер, откройте два браузера (или вкладки) и наблюдайте, как устанавливается соединение. А переключатель темы будет радовать глаз.