Offer, answer и одновременные предложения
Предыдущий урок подготовил дорожки и peer connection. Теперь браузеры должны договориться о параметрах соединения через offer и answer. Если обе стороны одновременно меняют набор медиа, они способны одновременно создать offer; такую коллизию нельзя надёжно разрешить случайным порядком кнопок. В этом уроке используем шаблон perfect negotiation с согласованными сервером polite и impolite ролями.
Состояние lesson-05 включает обработку negotiationneeded и входящих description. Сервер уже передаёт каждой стороне противоположную переговорную роль вместе с epoch. Роль polite не означает права администратора или обязанность уступать место в комнате. Она описывает поведение одной стороны при столкновении предложений, чтобы обе реплики переговорного состояния пришли к совместимому соглашению.
Что представляет SDP
Offer и answer содержат описания параметров сеанса в SDP. В них представлены направления и форматы медиа и сведения, нужные соединению. SDP не является видеозаписью и не несёт обычный поток кадров в JSON. Его нельзя редактировать произвольными заменами строк без понимания стандарта. В учебном проекте описание создаёт браузер и передаёт собственный signaling server.
setLocalDescription() без аргумента может создать подходящее локальное описание согласно состоянию соединения. В обработчике negotiationneeded ожидается offer; после принятого входящего offer — answer. Метод асинхронный, поэтому флаг makingOffer устанавливается до ожидания, а не после изменения signalingState. Иначе встречный пакет мог бы прийти, когда браузер ещё подготавливает предложение.
Ниже показана точная ветвь входящего description из src/app.js. makingOffer, ignoreOffer, isSettingRemoteAnswerPending, state.polite и pc уже определены в полном контроллере. Сервер передаёт только известную форму описания и подставляет подтверждённого отправителя. Клиент дополнительно сверяет epoch сообщения с текущей парой.
if(m.type==='description' && LESSON>=5){
const readyForOffer=!makingOffer && (pc.signalingState==='stable' || isSettingRemoteAnswerPending);
const collision=m.description.type==='offer' && !readyForOffer;
ignoreOffer=!state.polite && collision;if(ignoreOffer)return;
isSettingRemoteAnswerPending=m.description.type==='answer';
try{await pc.setRemoteDescription(m.description);}finally{isSettingRemoteAnswerPending=false;}
if(LESSON>=6){for(const candidate of state.candidates)await pc.addIceCandidate(candidate);state.candidates=[];}
if(m.description.type==='offer'){await pc.setLocalDescription();send({type:'description',description:pc.localDescription});}
}
Обнаружение столкновения
Условие readyForOffer учитывает отсутствие создаваемого локального предложения и подходящее состояние signaling. Особый флаг ожидания remote answer закрывает небольшой асинхронный промежуток перехода. Если приходит offer, когда сторона сама ещё делает offer или находится в неподходящем состоянии, возникает collision. Это событие относится к переговорной гонке, а не означает, что один пользователь отправил запрещённый пакет.
Impolite сторона игнорирует сталкивающееся предложение. Polite сторона принимает его, позволяя браузерной логике откатить своё несовместимое локальное предложение в поддерживаемом современном поведении. Одна сторона продолжает своё offer, другая отвечает. Если обе стороны независимо считали бы себя impolite, они могли бы обе проигнорировать чужое предложение и не завершить текущий обмен.
Поэтому распределение ролей — общий договор, а не локальная случайная настройка. В «Разговоре» сервер назначает её по фиксированным подтверждённым участникам. При новой паре он снова выдаёт роли и новый epoch. Нет смысла выводить право на сохранение или завершение комнаты из polite: эти действия относятся к серверной политике доступа, которую уроки сигнализации рассматривают отдельно.
Ответ и последовательность
После принятия remote description код сбрасывает флаг ожидания answer. Когда описание является offer, клиент создаёт local description и отправляет answer. Для входящего answer дополнительный ответ не создаётся. Такая ветвь предотвращает бессмысленный бесконечный обмен ответами. Важно отправлять pc.localDescription после завершения асинхронной операции, а не заранее созданный объект с предположениями о параметрах.
Входящие сообщения обрабатываются через последовательную promise-цепочку. Она не превращает WebRTC в синхронную программу, но не позволяет двум async-handler одного клиента одновременно применять несовместимые descriptions. Ошибки цепочки показываются интерфейсу, а следующий пакет должен иметь возможность обработаться дальше. Само по себе использование async в обработчике WebSocket не гарантирует очередность окончания обработчиков.
После самостоятельного входа обоих участников ожидается обмен offer/answer через собственный сервер. Однако без внешней ICE-конфигурации и полноценной передачи кандидатов пятый этап ещё не обещает достижимость всех сетей. Это этап согласования параметров, а не подтверждённая консультация в Интернете. Авторский запуск и анализ SDP реального браузера не проводились.
Старое поколение
Сообщение description должно принадлежать текущему epoch. Если Анна вернулась после обрыва, старый answer от предыдущей пары нельзя применять к новому объекту peer. Сервер и клиент оба проверяют поколение. Имя комнаты остаётся одинаковым, но это не делает старые переговорные пакеты подходящими для нового состояния. Аналогичная граница понадобится для очереди ICE candidates.
При закрытии старого peer обработчики снимаются, переговорные флаги сбрасываются, а новый объект получает собственный поток событий. Нельзя сохранить makingOffer=true от давно уничтоженного объекта и затем объяснять отсутствие новой связи сбоями сети. Такой отказ относится к контроллеру жизненного цикла. Поэтому reset состояния имеет значение даже при использовании рекомендованного переговорного шаблона.
Практический предел шаблона
Perfect negotiation уменьшает ручное ветвление «кто звонит первым», но не предоставляет всю бизнес-логику звонка. Приглашение, согласие на комнату, отмена, срок встречи и повторный вход остаются действиями приложения. Также шаблон не заменяет TURN и не исправляет блокировку устройства. Каждый слой должен возвращать понятный статус, чтобы человек не видел одно неопределённое «Подключение» при любой проблеме.
В реальном расширении следует учитывать поддержку выбранных браузеров и точный контракт обработки ошибок. Этот курс использует актуальные native API по первичным источникам, но не заявляет, что проверил все браузерные комбинации. Особенно важны поздние события при завершении, повторное согласование и одновременная смена источников. Такие сценарии требуют наблюдения после разрешённого запуска.
Для самостоятельного разбора рассмотрите одновременные offer Анны и Бориса и проследите обе ветви ролей. Затем сравните входящий answer и offer: ответ создаётся только во втором случае. Следующий урок дополнит эту модель сетевыми кандидатами и явно настроенным TURN, чтобы согласование параметров получило возможные маршруты медиатранспорта.
Сохраните различие между ролями алгоритма и кнопками интерфейса в документации проекта. Если позже появится консультант и клиент с разными полномочиями, это не повод назначать polite только клиенту как будто он обязан уступать бизнес-решения. Переговорные роли должны оставаться противоположными и устойчивыми для текущей пары. Разрешение демонстрации экрана, запись встречи или управление приглашением потребуется проверять отдельной политикой и отдельными действиями.
Оглавление серии · Исходные состояния проекта.
API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.