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

Три кита веб-звонка медиа сигналинг и транспорт

Технология зрела давно: первый публичный показ WebRTC в Chrome состоялся в 2011 году, а в январе 2021 года консорциум W3C выпустил редакцию WebRTC 1.0 в статусе официальной рекомендации. К тому моменту технология годами жила в продакшене, так что документ скорее закрепил сложившуюся практику. Смысл в том, что браузер умеет три вещи, которые раньше требовали отдельную программу.

Первый кит, захват медиа. Метод getUserMedia отдаёт странице звук с микрофона и картинку с камеры, если пользователь разрешит доступ. Второй кит, транспорт. Объект RTCPeerConnection упаковывает медиа в пакеты, шифрует их, сжимает подходящим кодеком и решает сетевые задачи: куда слать пакеты и как снижать битрейт при потерях. Третий кит, сигналинг, и тут кроется главный сюрприз для новичка. Спецификация сознательно не описывает, как собеседники находят друг друга. Два браузера должны обменяться описанием своих возможностей и списком сетевых адресов, но протокол этого обмена каждый проект придумывает сам. Обычно это JSON поверх WebSocket до собственного сервера, и такая свобода оборачивается тем, что готового сигналинга из коробки не существует.

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

Захват микрофона и камеры через getUserMedia на защищённой странице

Браузер соглашается отдать микрофон только странице, открытой по HTTPS; единственное исключение, localhost для локальной разработки. На обычном http метод getUserMedia просто отсутствует, и звонок не стартует. Отказ в доступе запоминается, и повторный запрос без вмешательства в настройки уже не появится. Код захвата выглядит скромно:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  },
  video: { width: 1280, height: 720, frameRate: 30 }
});

Три флага в секции audio не украшение. Подавление эха убирает звук динамиков, попадающий в микрофон, иначе собеседники услышат себя с задержкой. Шумоподавление чистит гул вентилятора и уличный фон, автоусиление выравнивает тихий микрофон ноутбука и громкую гарнитуру до сопоставимого уровня. Всё это браузер делает локально, до кодирования; флаги включены по умолчанию, но их полезно прописывать явно, ведь значения по умолчанию различаются между браузерами. Ошибки стоит ловить по имени: NotFoundError говорит об отсутствии устройства, NotAllowedError об отказе, NotReadableError о микрофоне, занятом другой программой, и каждой ошибке нужен свой вежливый текст на экране, ведь молчаливая кнопка звонка выглядит как поломка сайта.

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

Сигналинг обмен SDP описаниями и кандидатами ICE

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

const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.example.net:3478" },
    {
      urls: "turn:turn.example.net:3478",
      username: "operator",
      credential: "strong-secret"
    }
  ]
});

stream.getTracks().forEach(track => pc.addTrack(track, stream));

pc.ontrack = event => {
  remoteAudio.srcObject = event.streams[0];
};

pc.onicecandidate = event => {
  if (event.candidate) {
    socket.send(JSON.stringify({ kind: "ice", payload: event.candidate }));
  }
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.send(JSON.stringify({ kind: "offer", payload: pc.localDescription }));

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

Обход сетевых адресов со STUN и TURN сервером coturn

Домашний роутер раздаёт устройствам адреса вида 192.168.1.34, и такой адрес бесполезен для собеседника из другой сети. Работа STUN сервера предельно проста: браузер шлёт ему пакет, тот возвращает адрес отправителя, каким его видит внешняя сеть. Экономия очевидна: STUN отвечает одним пакетом и не участвует в разговоре. Но у медали есть оборот. Симметричный NAT, любимый режим операторов мобильного интернета, подменяет внешний порт для каждого получателя, и подсказка STUN превращается в ложную. Прямой путь закрыт, и тогда в дело вступает TURN: сервер принимает медиа у одной стороны и пересылает второй. Звонок продолжает работать, хотя трафик идёт через посредника.

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

listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=long-random-string
realm=turn.example.net
total-quota=200
external-ip=203.0.113.10

Параметр use-auth-secret включает выдачу временных паролей по схеме REST: сервер генерирует логин с меткой времени и подпись, которые живут несколько часов. Это спасает от сценария, когда статический пароль из браузера утащили и гоняют через ретранслятор чужой трафик. Планировать стоит и полосу: голосовой поток Opus занимает примерно 30-40 кбит/с, видеозвонок 1-2 Мбит/с, причём ретранслятор пропускает каждый поток дважды, на вход и на выход. Канал в 100 Мбит/с вытягивает лишь пару десятков видеосессий, поэтому coturn размещают в дата-центре рядом с атс, а не на слабом хостинге.

Шифрование и кодеки звук Opus и обязательный DTLS

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

Аудиокодеком по умолчанию служит Opus: частота дискретизации 48 кГц, битрейт от 6 до 510 кбит/с, причём скорость выбирается автоматически и сжимается при потерях. Для разборчивой речи хватает 24-32 кбит/с, и Opus переживает потери пакетов заметно лучше старых кодеков телефонии. Видеовозможности шире: VP8, VP9, H.264, а в свежих браузерах и AV1, но между сторонами браузер выбирает общий вариант из пересечения списков. Полезно контролировать выбор на этапе разработки: задержка в полсекунды при идеальной сети почти всегда означает не кодек, а неудачный маршрут через перегруженный TURN.

Стыковка звонка из браузера с SIP атс через WebSocket

Внутри компаний голосовая инфраструктура почти всегда уже есть: SIP атс, внутренние номера операторов, очереди и записи разговоров. Писать колл-центр заново никому не нужно, задача в том, чтобы браузер вошёл в эту систему как обычный абонент. У Asterisk для этого есть встроенный транспорт: SIP поверх WebSocket поднимается его же HTTP сервером, обычно на порту 8089 с TLS. Включение в файле http.conf выглядит так:

[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8089
tlsenable=yes
tlscertdir=/etc/asterisk/keys

В браузере же живёт библиотека JsSIP, говорящая по этому транспорту на чистом SIP:

const socket = new JsSIP.WebSocketInterface("wss://pbx.example.net:8089/ws");
const ua = new JsSIP.UA({
  uri: "sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.",
  password: "internal-pass",
  sockets: [socket]
});
ua.start();

const session = ua.call("sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.", {
  mediaConstraints: { audio: true, video: false }
});

session.on("confirmed", () => {
  remoteAudio.srcObject = session.connection.getRemoteStreams()[0];
});

Для голосовой поддержки видео обычно выключают: это экономит трафик мобильного посетителя и кодеки на атс. Если в разговоре участвуют трое и больше, на сцену выходит SFU, сервер выборочной пересылки, который принимает поток от каждого и рассылает остальным; открытые решения mediasoup и Janus закрывают этот класс задач, а исторический подход MCU с микшированием в общий поток почти ушёл из-за задержек и стоимости. Если же SIP атс не умеет WebSocket, между ней и WebRTC ставят шлюз, и два мира стыкуются без переделки атс.

Контроль качества через getStats и порядок внедрения по шагам

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

const report = await pc.getStats();
report.forEach(entry => {
  if (entry.type === "inbound-rtp" && entry.kind === "audio") {
    console.log("потеряно пакетов:", entry.packetsLost);
    console.log("дрожание, мс:", Math.round(entry.jitter * 1000));
  }
  if (entry.type === "candidate-pair" && entry.state === "succeeded") {
    console.log("время оборота, мс:", Math.round(entry.roundTripTime * 1000));
  }
});

Показатели стоит собирать во время разговора и отсылать на сервер: время оборота до 150 миллисекунд воспринимается как обычная телефония, доля потерь выше 2-3 процентов делает речь дёрганой, а растущее дрожание предвещает обрыв. По этим же цифрам строят алерты: если пятая часть звонков валится на TURN, значит, ретранслятор перегружен или стоит не в той сети. Отдельного внимания заслуживает мобильный интернет: переход между Wi-Fi и сотой разрывает соединение, и продуманный интерфейс предлагает перезвонить одним касанием вместо загадочной тишины.

Внедрение звонков на сайт редко проваливается из-за кода, чаще из-за пропущенного шага инфраструктуры:

  1. перевести страницу на HTTPS и убедиться, что сертификат валиден без предупреждений браузера;
  2. поднять сигнальный WebSocket сервис с комнатами и авторизацией посетителя;
  3. развернуть coturn со схемой временных паролей и проверить его с внешнего адреса;
  4. подключить браузер к SIP атс и провести первые тесты из мобильной сети, а не из офиса;
  5. включить сбор метрик getStats и настроить алерты на потери, дрожание и обрывы.

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