Видеозвонки

Групповой P2P · Восстановление связи · Проверка устройств

Назначение и границы

Создать звонок в браузере, скопировать его URL и пригласить нескольких участников — в том числе больше двух. Участники общаются голосом, включают камеры и демонстрируют экран. Создатель комнаты может защитить вход кодом. Базовая реализация доступна в разделе Видеозвонки. Допустимое количество участников в реальных сетях пока не установлено.

Создание звонка и приглашение по ссылке

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

  1. Администратор открывает раздел видеозвонков и создаёт комнату. При необходимости назначает код доступа.
  2. Сервер создаёт непредсказуемый идентификатор комнаты. Комната существует независимо от подключения браузера создателя.
  3. Интерфейс показывает URL комнаты и кнопку Скопировать приглашение. Код включается в ссылку, если он известен текущей странице: введён при создании или входе, получен из приглашения либо задан в настройках. После перезагрузки страницы ссылка может потребовать ручного ввода кода; сервер исходный код не возвращает.
  4. Гости открывают приглашение, указывают отображаемое имя, проверяют устройства и явно нажимают кнопку входа. Само открытие ссылки не включает камеру и микрофон.
  5. Если кода в ссылке нет, его можно ввести вручную. Неверный код не позволяет получить список участников и подключиться к обмену служебными сообщениями.

Примеры адресов комнат (идентификатор в таблице условный):

ПриглашениеПример
Комната без кода/calls/r_7Kp9xQ2m
Защищённая комната, код вводится вручную/calls/r_7Kp9xQ2m
Защищённая комната, код включён в ссылку/calls/r_7Kp9xQ2m#code=team-demo

При копировании формируется полный URL с доменом текущего сайта; код экранируется для передачи в URL. Часть после # не попадает в адрес первоначального HTTP-запроса. Браузер читает код и отправляет его серверу по HTTPS для проверки. Само наличие кода в ссылке не означает, что вход разрешён. После чтения интерфейс убирает код из адресной строки. Код хранится в состоянии страницы для входа и повторного копирования приглашения; при обновлении настроек комнаты состояние сбрасывается.

Любой получатель полной ссылки получает и код доступа. Это общий секрет комнаты, а не удостоверение личности и не право управлять звонком. Код всё равно может оказаться в скопированном тексте, снимках экрана, расширениях браузера или клиентской аналитике. Новая телеметрия не должна содержать секреты приглашений, токены сессий комнаты.

Вход в защищённую комнату

sequenceDiagram
    actor Creator as Создатель
    participant UI as Интерфейс звонка
    participant API as API комнат
    actor Guest as Гость
    participant Signal as Сервис сигналинга
    Creator->>UI: Создать комнату, при необходимости задать код
    UI->>API: Создать комнату
    API-->>UI: ID комнаты и права создателя
    UI-->>Creator: Скопировать URL с кодом после решётки
    Creator->>Guest: Отправить приглашение
    Guest->>UI: Открыть ссылку и нажать «Войти»
    UI->>API: Передать ID комнаты и код по HTTPS
    alt Код и режим доступа разрешают вход
        API-->>UI: Сессия участника
        UI->>Signal: Подключиться с сессией участника
        Signal-->>UI: Участники и события подключения
    else Вход запрещён или комната закрыта
        API-->>UI: Отказать во входе
    end

Код проверяется на сервере. Вместо исходного кода хранится хеш с солью. После проверки сервер выдаёт подписанную сессию участника без срока действия. При пересылке сообщений сигналинга проверяется принадлежность отправителя и адресата к комнате. Права создателя отделены от общего приглашения: гости не должны получать возможность менять код или завершать комнату.

Комнату создаёт и управляет ею авторизованный пользователь. В настройках каждой комнаты выбирается один из двух режимов: всем по ссылке или только авторизованным. Код доступа — независимая настройка и работает в обоих режимах. При переключении на обязательную авторизацию уже подключённые гости отключаются. Сервер проверяет доступ при входе и повторно при подключении к сигналингу.

Комната хранится до явного закрытия создателем; автоматическое истечение срока действия пока не добавлено. Код ограничен 64 символами и 72 байтами; пустой код означает отсутствие защиты кодом. Ограничений частоты входа, создания комнат и изменения настроек нет.

Групповая P2P-топология

Каждый участник устанавливает WebRTC-соединение с каждым другим участником. Центрального участника, который пересылает чужие медиапотоки, нет. Для четырёх участников получается шесть соединений, по три у каждого.

flowchart LR
    A[Участник А] <-->|Звук / камера / экран| B[Участник Б]
    A <--> C[Участник В]
    A <--> D[Участник Г]
    B <--> C
    B <--> D
    C <--> D
    S[Сервис сигналинга] -. SDP и ICE .-> A
    S -. SDP и ICE .-> B
    S -. SDP и ICE .-> C
    S -. SDP и ICE .-> D

Сервер координирует состав комнаты и пересылает служебные сообщения для установления соединений: предложения и ответы SDP, кандидаты ICE. Он не смешивает видео и не пересылает медиапотоки как SFU. STUN помогает определить сетевые адреса; TURN ретранслирует соединение между двумя участниками, если прямой сетевой путь недоступен. Использование TURN не превращает комнату в групповую медиасистему с топологией «звезда».

В полносвязной сети количество соединений равно n × (n − 1) / 2. Каждый участник отправляет медиаданные остальным n − 1 участникам, поэтому с ростом группы увеличиваются исходящий трафик и нагрузка на устройство. Такой подход предназначен для небольших групп. Допустимый размер комнаты нужно определить проверками в реальных браузерах и сетях, а не только расчётом числа соединений.

Камера, микрофон и демонстрация экрана

В интерфейсе есть предварительная проверка устройств, переключатели микрофона и камеры, основная сцена и правая колонка участников, кнопки демонстрации экрана, копирования приглашения и выхода. Активный звонок вместе с уменьшенным хедером занимает 100dvh; футер скрывается. Панель управления закреплена снизу внутри звонка. Миниатюры участников переключаются страницами, выбор миниатюры выводит участника на основную сцену. Настройки и приглашение открываются в диалогах. Можно войти с выключенной камерой или микрофоном.

Демонстрация начинается только после нажатия Поделиться экраном и выбора экрана, окна или вкладки в системном диалоге браузера. Экран передаётся отдельной видеодорожкой, поэтому камера может оставаться включённой. Каждая входящая дорожка связана с участником и назначением: микрофон, камера или экран.

Остановка демонстрации через интерфейс сайта или средствами браузера убирает дорожку экрана у всех собеседников, но не завершает звонок. Новые участники получают текущие активные дорожки. Переключение камеры и экрана требует замены дорожек или повторного согласования соединения с обработкой одновременных предложений и кандидатов ICE, пришедших до готовности соединения. Звук вкладки или системы не захватывается: демонстрация передаёт только изображение. Голос передаётся отдельной дорожкой микрофона.

Дорожки экрана могут быть у нескольких участников; их демонстрации отображаются в карточках участников. Захват камеры, микрофона и экрана требует поддерживаемого браузера и защищённого контекста: HTTPS, а при разработке допустим localhost.

Самостоятельная проверка устройств

Страница Проверка устройств и демонстрации экрана доступна без регистрации и без создания комнаты. На ней можно выбрать камеру и микрофон, увидеть изображение, проверить уровень входящего звука и воспроизвести короткий тестовый сигнал в динамиках. Названия устройств появляются после выдачи разрешения браузером.

В локальном предпросмотре камеры доступны размытие фона и замена фона выбранной картинкой. Модель выделения силуэта загружается при включении эффекта и работает на устройстве; видео и выбранная картинка не отправляются на сервер. Возможны неточности на границах силуэта и снижение плавности на слабых устройствах. При ошибке показывается исходное изображение камеры. Эффекты действуют только на странице проверки и не передаются в звонок.

Проверка демонстрации открывает нативный диалог выбора источника и показывает локальный предпросмотр без звука. Можно выбрать другой источник или остановить захват на странице либо средствами браузера. Вкладки, окна приложений и экраны предлагаются в зависимости от браузера и ОС; произвольная область доступна только если её выбор предоставляет системный диалог. Универсального выбора области рабочего стола через браузерный API нет.

Звук и видео при самостоятельной проверке обрабатываются локально и никуда не отправляются. Вход в комнату не включает устройства автоматически. При выходе со страницы или из звонка дорожки захвата останавливаются. Звук микрофона не направляется в динамики, чтобы не создавать эхо; устройство воспроизведения выбирается средствами операционной системы.

Жизненный цикл комнаты и соединений

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

Каждому участию в звонке назначается собственный идентификатор сессии, в том числе при открытии нескольких вкладок одним пользователем. Явный выход сразу удаляет участника. При обрыве WebSocket сервер сохраняет участника без таймера удаления, сообщает остальным о восстановлении связи и пока не объявляет его вышедшим.

Состояние связи и автоматическое восстановление

В интерфейсе отдельно показана связь с сервером сигналинга и состояние медиасоединений с каждым участником. При потере сигналинга появляются сообщение о переподключении, номер попытки и отсчёт до следующей попытки. У других участников отображается, что собеседник восстанавливает связь с сервером.

Клиент сохраняет камеру, микрофон, демонстрацию экрана и работающие WebRTC-соединения. Если медиапуть доступен, разговор продолжается. Само восстановление сигналинга не гарантирует непрерывность медиа при потере сети.

Повторные попытки выполняются с интервалами 1, 2, 4, 8 и далее 10 секунд без ограничения их количества и общей продолжительности восстановления. Клиент также проверяет отклик сигналинга: отсутствие сообщений в течение 15 секунд запускает восстановление даже без события закрытия WebSocket.

После подключения клиент сверяет состав комнаты, сохраняет исправные медиасоединения и восстанавливает потерянные. После перезапуска серверного процесса состав участников собирается заново, а медиасоединения пересоздаются. Наличие нового поколения соединения позволяет отбрасывать сигналы от заменённого соединения.

Длительный обрыв сигналинга не останавливает захват устройств и не завершает попытки восстановления. Число попыток восстановления WebRTC также не ограничено. При явном отказе в доступе попытки прекращаются сразу, появляется кнопка Вернуться ко входу. Закрытие комнаты также завершает звонок и останавливает захват.

Сессия участника

Подписанная сессия выдаётся после проверки входа через GraphQL. Она используется при первом подключении и при восстановлении с тем же ID участника, в том числе после перезапуска сигналинга. Одноразовых допусков, срока действия и привязки к версии настроек нет.

Токен хранится только в памяти страницы и не добавляется в приглашение или адрес WebSocket. Для подписи используется JWT_SECRET; смена секрета делает старые токены недействительными. Перезагрузка страницы требует обычного входа. Сервер по-прежнему проверяет существование пользователя, блокировку, закрытие комнаты и режим входа только для авторизованных.

При каждом восстановлении сервер проверяет существование комнаты, её закрытие, требование авторизации и состояние учётной записи участника. Смена кода не выкидывает уже допущенных участников; новый код потребуется для нового входа. Включение обязательной авторизации отключает гостей и запрещает восстановление их гостевых сессий. Одна сессия имеет одно активное подключение сигналинга: новое подключение заменяет старое.

Нельзя гарантировать мгновенную остановку клиента по команде сервера, если клиент потерял связь с сервером. Закрытие комнаты или запрет доступа применяются при получении серверного сообщения либо при восстановлении.

Что реализовано в системе

Часть системыОтветственность
Сущность CallRoomID комнаты, создатель, необязательный хеш кода доступа, статус, время создания; автоматическое истечение срока действия пока не реализовано. Сохраняются данные комнаты, а не медиапотоки.
Операции комнатыСоздание, вход участника, смена кода и закрытие. Авторизация и валидация относятся к CallRoom.
Сессии участниковВременное участие в комнате, стабильный идентификатор сессии, подписанная сессия без срока действия, присутствие и сведения о дорожках. Сессия не равна учётной записи пользователя.
Сервис сигналингаАвторизованное подключение, актуальные списки участников, уведомления о входе и выходе, пересылка SDP/ICE только внутри комнаты. Один сервер хранит присутствие в памяти.
Клиент WebRTCЖизненный цикл соединений, очередь ICE, согласование параметров, локальный захват, удалённые потоки, освобождение ресурсов и переподключение.
Настройки STUN/TURNУстановление связи в разных сетях и выдача допущенным участникам постоянных данных доступа к TURN. Развёртывание и пропускную способность нужно согласовать и проверить.
ФронтендСтраницы создания звонка и комнаты, проверка устройств, отображение участников и экранов, копирование приглашения, понятные сообщения об ошибках доступа и подключения.
ДокументацияЭти страницы с обновлениями по мере реализации и проверки ограничений.

Запросы и мутации комнат используют существующие GraphQL-подходы проекта, сгенерированные клиентские документы и типы. Место серверного кода CallRoom — server/schema/types/CallRoom, клиентских операций — app/gql/src/CallRoom.graphql. Операции: callRoom, createCallRoom, updateCallRoom, joinCallRoom и closeCallRoom. Сигналинг подключается через WebSocket по адресу /api/calls/socket. Используется существующая модель пользователя; комната звонка отделена от диалогов с ИИ.

Ориентир в проекте agent — src/components/world3d/hooks/useVoiceChat: голосовая полносвязная P2P-сеть с сигналингом через WebSocket и настройкой TURN. Видеозвонки сайта имеют собственную реализацию видео, демонстрации экрана, комнат, сессий, повторного согласования и восстановления связи. Трёхмерный мир и пространственный звук не являются зависимостями этих звонков.

Развёртывание и ограничения

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

Для STUN можно задать переменную CALL_STUN_URLS: адреса через запятую. По умолчанию используется stun:stun.l.google.com:19302. Для TURN задаются CALL_TURN_URLS, CALL_TURN_USERNAME и CALL_TURN_CREDENTIAL — постоянные реквизиты пользователя, настроенного на TURN-сервере. Приложение не задаёт им срок действия. Генерация временных реквизитов через CALL_TURN_SECRET удалена; существующий TURN с такой настройкой нужно перевести на постоянные реквизиты. Сам TURN-сервер необходимо разворачивать отдельно. Без TURN связь в некоторых сетях может не установиться; интерфейс показывает это ограничение. Адреса контейнеров в конфигурации должны использовать полные имена контейнеров.

Сайт должен работать по HTTPS (localhost допустим для разработки), а обратный прокси — пропускать WebSocket по адресу /api/calls/socket. Видеопотоки не проходят через API сайта. Проверку восстановления после реального сетевого обрыва и перезапуска через используемый прокси нужно проводить отдельно.

Для согласования соединений использован рекомендуемый подход WebRTC. Выбор экрана выполняется по действию пользователя средствами getDisplayMedia.

Проверки

Основной браузерный сценарий находится в tests/calls.browser.ts. Он запускается командой npx tsx tests/calls.browser.ts против отдельного локального сервера на порту 3100; адрес можно переопределить через CALL_TEST_URL. Перед запуском передайте CALL_TEST_ADMIN_TOKEN — токен активного администратора отдельной тестовой базы. Сценарий также проверяет запрет создания комнаты обычным пользователем. Для тестового сервера нужна отдельная SQLite-база, разрешённая тестовая регистрация NEXT_PUBLIC_SITE_SIGNUP_STRATEGY=ANY и собственный JWT_SECRET. Не используйте рабочую базу: сценарий создаёт тестовые учётные записи и комнаты.

В Chromium с виртуальными устройствами проверены: три участника с двусторонней передачей аудио и видео, запуск и остановка демонстрации, вход во время демонстрации, продолжение звонка после выхода создателя, неверный и изменённый код, запрет чужих настроек, обязательная авторизация, завершение комнаты, индикатор микрофона, мобильная вёрстка и остановка камеры при уходе со страницы. В этом же браузерном сценарии проверен принудительный обрыв WebSocket во время демонстрации: отображение попытки, автоматическое восстановление и сохранение существующих WebRTC-соединений и захвата экрана, а также восстановление отдельно оборванного медиасоединения.

Тесты транспорта и серверных сессий запускаются командой npx vitest run app/Custom/components/Calls/helpers/createCallSignaling.test.ts server/schema/types/CallRoom/helpers/setupCallSignaling.test.ts. Они проверяют повторные попытки, продолжение восстановления после 60 секунд без остановки устройств, обнаружение молчащего соединения, отказ в доступе, восстановление ID участника после обрыва и перезапуска сигналинга, сохранение сессии после смены кода, запрет восстановления закрытой или ограниченной комнаты, подпись и отсутствие срока действия токена.

Браузерный сценарий не проверяет эффекты фона и отдельную панель проверки демонстрации на странице устройств. Нативный диалог выбора источника вручную не проверен.

Проверка использует виртуальные устройства Chromium. Она не подтверждает качество физической камеры, слышимость динамиков, работу TURN, совместимость всех браузеров или устойчивость в реальных внешних сетях. Запись звонков, SFU и топология «звезда», а также масштабирование на большие комнаты в текущий объём не входят.

Написать мне