Покупатель сложной техники редко оформляет заказ молча. Он открывает карточку товара, сверяет характеристики, и в какой-то момент хочет поговорить с живым человеком до оплаты. Номер в шапке сайта работает, но теряет часть заинтересованных: одни откладывают вопрос на потом, другие уходят туда, где до консультанта добираться проще. Кнопка "позвонить с сайта" сокращает путь до одного клика, и почти всегда под ней стоит 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 и сотой разрывает соединение, и продуманный интерфейс предлагает перезвонить одним касанием вместо загадочной тишины.
Внедрение звонков на сайт редко проваливается из-за кода, чаще из-за пропущенного шага инфраструктуры:
- перевести страницу на HTTPS и убедиться, что сертификат валиден без предупреждений браузера;
- поднять сигнальный WebSocket сервис с комнатами и авторизацией посетителя;
- развернуть coturn со схемой временных паролей и проверить его с внешнего адреса;
- подключить браузер к SIP атс и провести первые тесты из мобильной сети, а не из офиса;
- включить сбор метрик getStats и настроить алерты на потери, дрожание и обрывы.
После этого остаются только будни эксплуатации: контроль полосы на TURN, обновление сертификатов и живой пробный звонок каждые несколько недель с мобильного оператора. Кнопка, собранная по этой схеме, обходится без длинных номеров и очередей классической горячей линии, а выглядит для посетителя как обычный разговор по душам.