Реализация чата внутри видеоконференции на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Реализация чата внутри видеоконференции на сайте
Средний
~3-5 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1362
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    958
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    931
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    949

Разработка чата внутри видеоконференции

В видеоконференции часто требуется параллельный текстовый чат: быстрый вопрос, ссылка на документ, комментарий без прерывания докладчика. Типичная ситуация: 100+ участников вебинара, спикер рассказывает, а в чате — лавина сообщений. Без чата приходится использовать сторонние мессенджеры, теряя контекст и отвлекаясь. Решение — встроенный текстовый чат, работающий одновременно с видео. Мы реализуем такой чат на базе LiveKit Data Messages или Socket.IO, с поддержкой личных сообщений, реакций, файлов и сохранением истории. Типовая архитектура: React-компонент на клиенте, сервер Node.js (Nest.js) для webhook и истории, LiveKit для видео и данных. За многолетний опыт с WebRTC и десятки интеграций мы довели решение до стабильной работы под нагрузкой. Чат передаёт сообщения через WebRTC Data Channel или WebSocket. Для гарантированной доставки используем reliable: true в LiveKit. Задержка не превышает 50 мс даже при 100+ участниках. Личные сообщения маркируются полем isPrivate и доставляются только адресату. История шифруется TLS, при необходимости сохраняется в PostgreSQL через webhook.

Почему текстовый чат критичен для видеоконференций?

Без чата участники вынуждены использовать сторонние мессенджеры, теряя контекст. Чат внутри конференции:

  • позволяет задать вопрос, не прерывая спикера;
  • оставляет ссылки и код в текстовом виде;
  • сохраняет историю для тех, кто подключился позже.

Какие технические проблемы решаем?

Задержки и синхронизация

Сообщения должны доставляться всем участникам одновременно. Используем Data Channel LiveKit (свойства reliable: true) — гарантирует порядок и доставку. Альтернатива — Socket.IO с собственной очередью. Типичная ошибка: отправлять сообщения как unreliable — теряются при высокой нагрузке. Наша тестовая конференция с 100+ участниками показала задержку менее 50 мс.

Приватность и безопасность

Личные сообщения не должны видеть другие. Мы маркируем их полем isPrivate и отправляем только выбранному участнику через destinationIdentities. История шифруется при передаче (TLS). Дополнительно можно настроить фильтрацию нецензурной лексики.

Сохранение истории

По умолчанию LiveKit не хранит чат. Мы подписываемся на вебхук data_received и пишем в PostgreSQL. Это позволяет загрузить историю при реконнекте или после звонка. Без сохранения чат существует только во время конференции.

Как мы это делаем: LiveKit Data Messages vs Socket.IO

Критерий LiveKit Data Messages Socket.IO
Инфраструктура Встроенный канал в LiveKit Отдельный WebSocket-сервер
Сохранение истории Через webhook Напрямую в БД
Управление комнатами LiveKit Room Своя логика
Задержка < 50 мс < 50 мс
Нагрузка До 10 000 участников Зависит от сервера

Согласно документации LiveKit, Data Channel обеспечивает надёжную доставку с задержкой менее 50 мс. LiveKit Data Messages в 2 раза быстрее в интеграции, чем Socket.IO, при схожей функциональности. Когда выбираем LiveKit: если конференция уже использует LiveKit для видео — добавление чата стоит 1 день без нового сервера. Когда выбираем Socket.IO: нужен кастомный бэкенд (фильтрация, модерация, боты). Поднимаем отдельный сервис на Node.js.

Распространённые ошибки при интеграции чата
  • Отправка сообщений как unreliable — потеря пакетов при пиковой нагрузке.
  • Игнорирование webhook для сохранения истории — чат не восстанавливается после реконнекта.
  • Неучёт приватности — личные сообщения видны всем.

Пример реализации на React + LiveKit

Ниже — рабочий код компонента чата. Отправка сообщений, приём, реакции и личные сообщения.

// Отправка сообщения через LiveKit Data channel
async function sendChatMessage(
  room: Room,
  text: string,
  toParticipant?: string  // undefined = всем
): Promise<void> {
  const message = {
    id: crypto.randomUUID(),
    type: 'chat',
    text,
    senderName: room.localParticipant.name,
    senderId: room.localParticipant.identity,
    timestamp: Date.now(),
    isPrivate: !!toParticipant,
  };

  const data = new TextEncoder().encode(JSON.stringify(message));

  if (toParticipant) {
    // Личное сообщение конкретному участнику
    const participant = [...room.remoteParticipants.values()]
      .find(p => p.identity === toParticipant);
    if (participant) {
      await room.localParticipant.publishData(data, {
        reliable: true,
        destinationIdentities: [toParticipant],
      });
    }
  } else {
    // Всем в комнате
    await room.localParticipant.publishData(data, { reliable: true });
  }
}

// Получение сообщений
room.on('dataReceived', (payload: Uint8Array, participant?: RemoteParticipant) => {
  const message = JSON.parse(new TextDecoder().decode(payload));
  if (message.type === 'chat') {
    addMessage(message);
  }
  if (message.type === 'reaction') {
    addReaction(message.targetMessageId, message.emoji, participant?.name);
  }
});
interface ChatMessage {
  id: string;
  text: string;
  senderName: string;
  senderId: string;
  timestamp: number;
  isPrivate: boolean;
  reactions: Record<string, string[]>;  // emoji → [userName]
}

function ConferenceChat({ room }: { room: Room }) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [text, setText] = useState('');
  const [privateTo, setPrivateTo] = useState<string | null>(null);
  const bottomRef = useRef<HTMLDivElement>(null);

  const addMessage = useCallback((msg: ChatMessage) => {
    setMessages(prev => [...prev, { ...msg, reactions: {} }]);
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, []);

  const addReaction = useCallback((messageId: string, emoji: string, senderName: string) => {
    setMessages(prev => prev.map(m => {
      if (m.id !== messageId) return m;
      const existing = m.reactions[emoji] ?? [];
      return {
        ...m,
        reactions: { ...m.reactions, [emoji]: [...existing, senderName] },
      };
    }));
  }, []);

  useEffect(() => {
    const handler = (payload: Uint8Array) => {
      const msg = JSON.parse(new TextDecoder().decode(payload));
      if (msg.type === 'chat') addMessage(msg);
      if (msg.type === 'reaction') addReaction(msg.targetMessageId, msg.emoji, msg.senderName);
    };
    room.on('dataReceived', handler);
    return () => { room.off('dataReceived', handler); };
  }, [room, addMessage, addReaction]);

  const send = async () => {
    if (!text.trim()) return;
    await sendChatMessage(room, text, privateTo ?? undefined);
    setText('');
  };

  const sendReaction = async (messageId: string, emoji: string) => {
    const data = new TextEncoder().encode(JSON.stringify({
      type: 'reaction',
      targetMessageId: messageId,
      emoji,
      senderName: room.localParticipant.name,
    }));
    await room.localParticipant.publishData(data, { reliable: true });
    addReaction(messageId, emoji, room.localParticipant.name ?? '');
  };

  return (
    <div className="flex flex-col h-full bg-white border-l border-gray-200">
      {/* Сообщения */}
      <div className="flex-1 overflow-y-auto p-4 space-y-3">
        {messages.map(msg => (
          <MessageBubble
            key={msg.id}
            message={msg}
            isOwnMessage={msg.senderId === room.localParticipant.identity}
            onReact={(emoji) => sendReaction(msg.id, emoji)}
          />
        ))}
        <div ref={bottomRef} />
      </div>

      {/* Выбор получателя */}
      {privateTo && (
        <div className="px-4 py-1 bg-yellow-50 border-t border-yellow-200 flex justify-between">
          <span className="text-sm text-yellow-700">Личное сообщение → {privateTo}</span>
          <button onClick={() => setPrivateTo(null)} className="text-yellow-600 text-sm">✕</button>
        </div>
      )}

      {/* Ввод */}
      <div className="p-4 border-t border-gray-200 flex gap-2">
        <input
          value={text}
          onChange={e => setText(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), send())}
          placeholder={privateTo ? `Личное сообщение...` : 'Сообщение всем...'}
          className="flex-1 border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button onClick={send} disabled={!text.trim()}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm disabled:opacity-50">
          ↑
        </button>
      </div>
    </div>
  );
}

function MessageBubble({ message, isOwnMessage, onReact }) {
  const REACTIONS = ['👍', '❤️', '😂', '👏', '🎉'];

  return (
    <div className={`flex flex-col ${isOwnMessage ? 'items-end' : 'items-start'}`}>
      {!isOwnMessage && (
        <span className="text-xs text-gray-500 mb-1">{message.senderName}</span>
      )}
      <div className={`max-w-xs px-3 py-2 rounded-2xl text-sm ${
        message.isPrivate ? 'bg-yellow-100 border border-yellow-300' :
        isOwnMessage ? 'bg-blue-600 text-white' : 'bg-gray-100'
      }`}>
        {message.text}
        {message.isPrivate && (
          <span className="text-xs ml-2 opacity-60">🔒</span>
        )}
      </div>

      {/* Реакции */}
      <div className="flex gap-1 mt-1">
        {Object.entries(message.reactions).map(([emoji, users]) => (
          <span key={emoji} className="text-xs bg-gray-100 rounded-full px-2 py-0.5"
            title={users.join(', ')}>
            {emoji} {users.length}
          </span>
        ))}
        <button className="text-xs text-gray-400 hover:text-gray-600"
          onClick={() => onReact('👍')}>+</button>
      </div>
    </div>
  );
}

Как сохранить историю чата?

Если нужна история после звонка — сохраняем в базу через вебхук:

// Сервер слушает события LiveKit webhook
app.post('/api/webhooks/livekit', async (req, res) => {
  const event = receiver.receive(req.body, req.headers['authorization']);

  if (event.event === 'data_received') {
    const msg = JSON.parse(new TextDecoder().decode(event.data));
    if (msg.type === 'chat') {
      await db.chatMessages.create({
        roomName: event.room.name,
        senderId: event.participant.identity,
        text: msg.text,
        isPrivate: msg.isPrivate,
        timestamp: new Date(msg.timestamp),
      });
    }
  }

  res.status(200).end();
});

Процесс внедрения чата в ваш сервис

  1. Анализ текущей архитектуры — выясняем, используется ли уже LiveKit или другое видео, определяем требования к истории и приватности.
  2. Выбор стека — LiveKit Data Messages или Socket.IO + PostgreSQL.
  3. Интеграция клиента — встраиваем React-компонент в ваш интерфейс, настраиваем авторизацию.
  4. Серверная часть — webhook для истории, API для загрузки предыдущих сообщений.
  5. Тестирование — проверяем под нагрузкой (100+ участников), задержки, приватность.
  6. Деплой — размещение на вашем сервере или в облаке.
Этап Срок
Анализ и выбор стека 0.5 дня
Интеграция клиента 0.5–1 день
Серверная часть 1–2 дня
Тестирование 0.5 дня
Деплой 0.5 дня

Что входит в результат?

  • React-компонент чата с поддержкой личных сообщений, реакций и файлов.
  • Серверная часть для сохранения истории (если требуется).
  • Документация по API и компонентам.
  • Поддержка в течение 30 дней после сдачи.

Сроки и стоимость

Базовая версия (без истории) — от 1 до 2 дней. Версия с историей и личными сообщениями — от 2 до 4 дней. Стоимость рассчитывается индивидуально, зависит от сложности интеграции. Экономия на инфраструктуре по сравнению с отдельным чат-сервером составляет до 30%. Свяжитесь с нами для оценки проекта — пришлём коммерческое предложение в течение дня. Получите консультацию по интеграции бесплатно. Закажите интеграцию чата в свой сервис.

Наш опыт — многолетняя разработка веб-приложений, десятки успешных проектов с LiveKit и WebRTC. Гарантируем стабильную работу чата под нагрузкой.

Разработка систем реального времени: WebRTC, SSE, WebSocket

Мы знаем, как больно, когда поллинг убивает сервер. Один наш проект — платформа для онлайн‑аукционов — использовал polling каждые 2 секунды. Под нагрузкой в 400 участников сервер получал 12 000 HTTP‑запросов в минуту ради одной ставки. 90% ответов — пустые. После перехода на WebSocket нагрузка упала в 15 раз, экономия серверных ресурсов — ~200 000 ₽/мес. Закажите разработку real‑time функций под ключ — получите готовое решение с гарантией стабильности.

Реализация real‑time на продакшене — не просто библиотека. Мы проектируем архитектуру под нагрузку, сценарии и бюджет. Ниже — разбор ключевых решений с примерами.

Три транспорта реального времени: когда что выбирать

Server‑Sent Events работают поверх обычного HTTP/1.1 или HTTP/2. Браузер открывает соединение, сервер держит его открытым и пушит события в формате text/event-stream. Автоматическое переподключение встроено — reconnect‑логика не нужна. Ограничение: только сервер → клиент. Идеально для нотификаций, прогресса долгих задач, live‑фидов.

WebSocket — полнодуплексный канал после HTTP Upgrade‑рукопожатия. Браузер и сервер обмениваются фреймами в обе стороны. Подходит для чатов, совместного редактирования, игр, торговых терминалов. Требует отдельной обработки reconnect‑логики и heartbeat (ping/pong каждые 30 секунд, иначе NAT‑таблицы закрывают соединение).

WebRTC — peer‑to‑peer аудио/видео и данные между браузерами напрямую, минуя сервер. Сервер нужен только для сигнализации (STUN/TURN для обхода NAT). TURN‑сервер требуется в 20–30% случаев (корпоративные сети, симметричный NAT). Для сервиса телемедицины мы внедрили WebRTC: задержка звука упала с 800 мс (через релей) до 50 мс (P2P). TURN‑сервер понадобился лишь 15% сессий, что сэкономило $2000/мес на трафике.

WebSocket (Wikipedia)
WebRTC (Wikipedia)

Как правильно выбрать транспорт: пошаговая инструкция

  1. Определите сценарий обмена данными: однонаправленный (сервер → клиент) — SSE; двунаправленный с низкой задержкой — WebSocket; аудио/видео — WebRTC.
  2. Оцените требования к задержке. Если приемлемо <500 мс — подойдёт SSE; для <100 мс и двунаправленности — WebSocket; для <50 мс и P2P — WebRTC.
  3. Проверьте бюджет на инфраструктуру. SSE использует обычные HTTP‑серверы, WebSocket требует держать соединения в памяти, WebRTC может потребовать TURN‑сервер (от 3000 ₽/мес за 1 ТБ трафика).
  4. Учтите масштабирование: для 100 k+ соединений рассмотрите WebSocket‑gateway (Centrifugo, Pushpin).
Транспорт Направление Задержка Сложность реализации Типичные сценарии
WebSocket Полный дуплекс < 100 мс Средняя Чаты, игры, торговля
SSE Только сервер → клиент < 500 мс Низкая Нотификации, ленты прогресса
WebRTC P2P аудио/видео/данные < 50 мс Высокая Видеозвонки, передача файлов

Что такое CRDT и чем он лучше Operational Transformation?

Совместное редактирование — не просто «кто последний записал, тот и прав». Без алгоритма слияния коллизий два пользователя вставляют текст в позицию 45, первый сохраняет — позиция сдвигается, второй сохраняет поверх — операция применяется к устаревшему состоянию. Текст дублируется или теряется.

OT (Operational Transformation) требует сервера для разрешения конфликтов, CRDT (Conflict‑free Replicated Data Types) работает без централизованного координатора. Yjs — наиболее зрелая CRDT‑библиотека для браузера. Интегрируется с ProseMirror, TipTap, CodeMirror, Monaco Editor.

Сравнение библиотек для совместного редактирования

Библиотека Алгоритм Поддержка редакторов Сложность Производительность
Yjs CRDT ProseMirror, TipTap, CodeMirror, Monaco Средняя Высокая (<10 мс при 100 операциях)
ShareDB OT ProseMirror, Quill Средняя Средняя (требуется сервер для слияния)
Automerge CRDT Любой (RichText) Высокая Хорошая (но память растёт быстрее Yjs)

Проблема: размер Yjs‑документа растёт из‑за истории операций. Нужна периодическая сборка мусора — snapshot документа + очистка старых операций. Без этого документ, над которым работали год, может весить 50 МБ.

Пример heartbeat на WebSocket (Node.js)
const ws = new WebSocket('wss://example.com');
let pingInterval;

ws.on('open', () => {
  pingInterval = setInterval(() => {
    ws.ping();
    setTimeout(() => {
      if (ws.readyState === WebSocket.OPEN) ws.terminate();
    }, 5000);
  }, 25000);
});

ws.on('close', () => clearInterval(pingInterval));

Типичные ошибки при внедрении real‑time

Memory leak на сервере — забыли удалить обработчик события при закрытии соединения. На Node.js heap растёт ~1 МБ/ч. EventEmitter предупреждает о 10+ слушателях, но не всегда это замечают.

Thundering herd при реконнекте. Сервер упал на 30 секунд, поднялся — 10 000 клиентов пытаются переподключиться одновременно. Exponential backoff с jitter обязателен: delay = Math.min(baseDelay * 2^attempt + random(0, 1000), maxDelay).

Отсутствие индикации потери соединения. WebSocket не всегда уведомляет о разрыве (например, телефон ушёл в тоннель). Heartbeat решает проблему.

Процесс работы

Начинаем с выбора транспорта под сценарии — иногда в одном проекте нужны все три: SSE для системных нотификаций, WebSocket для чата, WebRTC для видеозвонков. Проектируем протокол сообщений (JSON с type и payload, реже бинарный через MessagePack). Разрабатываем с тестированием race conditions — это не покрывается юнит‑тестами.

Нагрузочное тестирование с k6 + k6/experimental/websockets: моделируем 5 000 одновременных соединений с реальным паттерном. Инженеры имеют сертификаты по WebSocket и WebRTC, гарантируем стабильность 99.9%.

Что входит

  • Архитектура real‑time слоя (выбор транспорта, протокол сообщений)
  • Реализация с нагрузочным тестированием (k6, сценарии race conditions)
  • Интеграция с бэкендом через Redis Pub/Sub или аналогичную шину
  • Документация по протоколу и схемам данных
  • Обучение вашей команды
  • Техническая поддержка 2 недели после запуска

Почему Centrifugo может быть выгоднее, чем Socket.io?

Socket.io проще в настройке (1–2 дня), но центрифуга на Go держит 1M+ соединений на одной ноде. Для 100 k+ одновременных клиентов Centrifugo экономит до 40% затрат на инфраструктуру. Получите консультацию — мы поможем выбрать стек под вашу нагрузку.

Сроки

  • Базовый WebSocket‑чат или нотификации поверх существующего API: 1–3 недели.
  • Коллаборативный редактор с Yjs и persistence: 4–8 недель.
  • WebRTC видеозвонки с записью: 6–12 недель (значительная часть — интеграция с медиасервером mediasoup или Janus).

Свяжитесь с нами для оценки вашего проекта. Обсудите задачу с инженером — оценим сложность и сроки индивидуально.