Сотрудник открывает страницу личного кабинета и нажимает кнопку вызова прямо в интерфейсе. Через секунду раздаётся гудок, рядом с карточкой клиента появляется таймер разговора и поле для заметок, никакого отдельного приложения не нужно. Так выглядит телефония, встроенная в веб-интерфейс, и за этим сценарием стоит связка из двух технологий с очень разной историей.

SIP остаётся главным протоколом сигнализации для интернет-телефонии

Протокол установления сеанса, известный под сокращением SIP, появился в 1996 году как решение для мультимедийных сеансов в исследовательской сети. Стандартом он стал с документом RFC 2543, а в июне 2002 года был пересмотрен в RFC 3261, версию, которая считается основной.

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

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

Текстовая природа объясняет живучесть протокола: отладка ведётся обычным чтением логов, расширения добавляются без ломки базовой логики. За двадцать с лишним лет SIP оброс десятками расширений и стал основой не только интернет-телефонии, но и мобильной голосовой связи в сетях четвёртого поколения.

WebRTC отвечает за передачу голоса и не содержит сигнализации

Вторая половина связки выросла из покупки, которую крупная интернет-компания совершила в мае 2010 года: вместе с разработчиком софта для видеосвязи достались кодеки и алгоритмы эхоподавления. Технологию открыли, передали в организации по стандартизации и в мае 2011 года представили публичный проект связи в реальном времени из браузера.

Проект получил название WebRTC. Спецификация шла к статусу рекомендации десятилетие: черновик консорциума всемирной паутины вышел в октябре 2011 года, статус кандидата присвоен в ноябре 2017 года, окончательная рекомендация появилась в январе 2021 года.

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

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

SIP over WebSockets соединяет телефонную сигнализацию с веб-страницей

Чтобы SIP заработал в браузере, нужен транспорт, доступный из сценария. Обычный сокет веб-странице недоступен, зато протокол WebSocket умеет держать постоянное двустороннее соединение. Расширение SIP для работы через WebSocket описано в RFC 7118.

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

Типовая последовательность установления вызова выглядит так:

  1. браузер регистрируется на сервере, отправляя запрос с учётными данными;
  2. при исходящем вызове отправляется приглашение с описанием предлагаемых параметров медиа;
  3. сервер отвечает промежуточными и окончательными откликами о ходе вызова;
  4. после ответа абонента стороны подтверждают установление сеанса;
  5. по завершении разговора любая из сторон отправляет запрос на разрыв;
  6. регистрация периодически обновляется, чтобы сервер знал о доступности абонента.

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

Адрес абонента выглядит как электронная почта и разрешается через службу имён

Адрес записывается в форме пользователь и домен, разделённые символом @. Пример записи выглядит так:

sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

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

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

Подключение библиотеки в браузере требует защищённого контекста и аккуратной обработки событий

Практическая работа начинается с клиентской библиотеки, реализующей SIP поверх WebSocket: она берёт на себя разбор сообщений, управление транзакциями, поддержку диалогов и работу с медиа.

Скелет инициализации выглядит примерно так:

import { UserAgent, Registerer, Inviter, SessionState } from "sip-library";

const socket = new WebSocket("wss://sip.example.net/ws");

const userAgent = new UserAgent({
  uri: UserAgent.makeURI("sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript."),
  transportOptions: { sockets: [socket] },
  authorizationUsername: "operator",
  authorizationPassword: "секрет",
});

await userAgent.start();

const registerer = new Registerer(userAgent);
await registerer.register();

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

Исходящий вызов оформляется созданием объекта приглашения и последующим запуском:

async function call(target) {
  const uri = UserAgent.makeURI("sip:" + target + "@example.net");
  const inviter = new Inviter(userAgent, uri, {
    sessionDescriptionHandlerOptions: {
      constraints: { audio: true, video: false },
    },
  });

  inviter.stateChange.addListener((state) => {
    if (state === SessionState.Established) console.log("Разговор начался");
    if (state === SessionState.Terminated) console.log("Разговор завершён");
  });

  await inviter.invite();
  return inviter;
}

Обработчик изменения состояния это необходимость: интерфейс должен различать состояния набора, дозвона, разговора и завершения, иначе пользователь начнёт нажимать кнопку повторно.

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

Медиапоток и сигнализация идут разными путями и требуют разной диагностики

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

В первом случае проверяют соединение с сервером, учётные данные, состояние регистрации и доступность порта сигнализации. Типичные симптомы: страница не переходит в готовность, входящие не приходят, исходящие обрываются сразу после набора.

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

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

async function report(connection) {
  const stats = await connection.getStats();
  stats.forEach((item) => {
    if (item.type === "inbound-rtp" && item.kind === "audio") {
      console.log("Потеряно пакетов:", item.packetsLost, "Дрожание:", item.jitter);
    }
  });
}

Такой отчёт стоит писать в журнал вместе с идентификатором вызова: при разборе инцидента через сутки сохранённые цифры дадут ответ сразу.

Отдельная тонкость связана с аудиоустройствами: список гарнитур открывается только после разрешения на доступ к микрофону, поэтому порядок в интерфейсе должен быть сначала запрос доступа, потом выбор.

Безопасность встроенной телефонии строится вокруг учётных данных и защищённых схем

Интеграция телефонии в веб-интерфейс создаёт соблазн хранить учётные данные SIP в сценарии страницы. Так делать нельзя. Любой, кто откроет исходники, получит доступ к телефонии организации, а счёт за междугородние звонки придёт владельцу.

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

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

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

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

Кодеки и качество связи определяют впечатление сильнее интерфейса

Пользователь судит о телефонии по звуку, а не по красоте кнопок. Качество определяется кодеками и состоянием сети.

Современный стандарт для голоса это кодек Opus, обеспечивающий хорошее качество при умеренной полосе и умеющий подстраиваться под условия. Для совместимости со старым оборудованием применяются более простые кодеки, работающие на низких скоростях. Обязательный минимум для совместимости включает пару узкополосных кодеков и передачу сигналов нажатия клавиш в виде отдельных сообщений, описанных в RFC 4733. Без последнего не будут работать голосовые меню, где требуется ввод добавочного номера.

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

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

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

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