⚡ WebRTC · руководство

сигнальный сервер + клиент · светлая и тёмная стороны
☀️ 🌙

📡 Создание и использование WebRTC: клиент + сервер

Peer-to-peer Node.js Socket.IO Темизация

WebRTC (Web Real-Time Communication) позволяет браузерам обмениваться аудио, видео и данными напрямую, минуя центральный сервер. Однако для установки соединения необходим сигнальный сервер, который обменивается метаданными (SDP, ICE-кандидаты). В этом руководстве мы разберём создание полноценного приложения: сервер на Node.js + Socket.IO, клиентский интерфейс с видеочатом и, конечно, реализуем переключение между светлой и тёмной темами.

🧑‍💻 Клиент А
📡 Сигнальный сервер (Node.js)
🧑‍💻 Клиент Б

Ниже — пошаговое описание, код и особенности темизации интерфейса.

🖥️ 1. Сигнальный сервер на 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

📄 server.js (основной код)

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}`));

Сервер прослушивает события, маршрутизирует сообщения между участниками одной комнаты. Для продакшена стоит добавить обработку повторных подключений и более надёжное управление комнатами.

📱 2. Клиентская сторона: HTML, JS и WebRTC API

Клиент получает доступ к камере/микрофону, создаёт 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;
}

📞 Создание RTCPeerConnection и обработка ICE

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];
  };
}

🔄 Обмен SDP (offer/answer)

// инициатор создаёт 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-соединение.

🎨 3. Светлая и тёмная тема: CSS-переменные + JavaScript

Переключение тем реализовано с помощью 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;
}

🕹️ Логика переключения (JS)

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);
});

Этот же подход используется на текущей странице — попробуйте переключить тему, и вы увидите, как плавно меняются цвета. Все компоненты (кнопки, карточки, блоки кода) автоматически адаптируются.

👆 кнопка в стиле темы ← цвет зависит от текущей темы

📌 4. Деплой и важные замечания

🚀 Готовый пример

Полный код клиента и сервера можно объединить в один проект. Для тестирования запустите сервер, откройте два браузера (или вкладки) и наблюдайте, как устанавливается соединение. А переключатель темы будет радовать глаз.