Использование DTMF с WebRTC
Для более полной поддержки аудио/видеоконференций, WebRTC поддерживает отправку DTMF удалённому участнику на RTCPeerConnection. Эта статья предоставляет краткий обзор принципов работы DTMF в WebRTC и руководство для разработчиков по отправке DTMF через RTCPeerConnection. Система DTMF часто называется «тональным набором», по старому торговому названию системы.
WebRTC не отправляет коды DTMF в виде аудиоданных. Вместо этого они отправляются вне полосы пропускания, как RTP-платежи. Однако, хотя возможно отправить DTMF с помощью WebRTC, в настоящее время нет способа обнаружить или получить входящий DTMF. WebRTC в настоящее время игнорирует эти платежи; это потому, что поддержка DTMF в WebRTC в первую очередь предназначена для использования с устаревшими телефонными службами, которые полагаются на тона DTMF для выполнения таких задач, как:
- Системы видеоконференций
- Системы меню
- Системы голосовой почты
- Ввод информации о кредитной карте или другой платежной информации
- Ввод кода доступа
Примечание: Хотя DTMF не отправляется удалённому участнику в качестве аудио, браузеры могут воспроизводить соответствующий тон локальному пользователю как часть пользовательского интерфейса, так как пользователи обычно привыкли слышать воспроизведение тонов на телефоне.
Отправка DTMF на RTCPeerConnection
У данного RTCPeerConnection может быть несколько медиапотоков, отправленных или полученных. При желании передать сигналы DTMF, необходимо сначала выбрать, на каком потоке их отправлять, так как DTMF отправляется как ряд платежей вне полосы пропускания на RTCRtpSender, ответственном за передачу данных потока другому участнику.
После выбора потока вы можете получить из его RTCRtpSender объект RTCDTMFSender, который вы будете использовать для отправки DTMF. После этого вы можете вызвать RTCDTMFSender.insertDTMF() для очереди сигналов DTMF, которые будут отправлены по потоку удалённому участнику. RTCRtpSender затем отправит тона другому участнику в виде пакетов вместе с аудиоданными потока.
Каждый раз, когда отправляется тон, RTCPeerConnection получает событие tonechange с свойством tone, указывающим, какой тон закончился воспроизведением, что позволяет обновить элементы интерфейса, например. Когда буфер тонов пуст, что указывает на то, что все тона были отправлены, в объект соединения доставляется событие tonechange со свойством tone, установленным в "" (пустую строку).
Если вы хотите узнать больше об этом, прочитайте RFC 3550: RTP: Транспортный протокол для приложений в реальном времени и RFC 4733: RTP-платежи для цифр DTMF, телефонных тонов и телефонных сигналов. Детали обработки RTP-платежей DTMF выходят за рамки этой статьи. Вместо этого мы сосредоточимся на использовании DTMF в контексте RTCPeerConnection, изучив пример работы.
Простой пример
В этом простом примере создаются два RTCPeerConnection, устанавливается соединение между ними, затем ожидает нажатия пользователем кнопки «Набрать». При нажатии кнопки строка DTMF отправляется через соединение с помощью RTCDTMFSender.insertDTMF(). После завершения передачи тонов соединение закрывается.
Примечание: Этот пример, очевидно, несколько искусственный, так как обычно два объекта RTCPeerConnection существуют на разных устройствах, а установление соединения происходит по сети вместо того, чтобы быть связанными непосредственно, как в этом примере.
HTML
HTML для этого примера очень простой; важны только три элемента:
- Элемент
<audio>для воспроизведения полученного аудиоRTCPeerConnection"вызываемого" абонента. - Элемент
<button>для запуска создания и подключения двух объектовRTCPeerConnectionи отправки тонов DTMF. - Элемент
<div>для получения и отображения текста журнала, показывающего статус.
<p> This example demonstrates the use of DTMF in WebRTC. Note that this example is "cheating" by generating both peers in one code stream, rather than having each be a truly separate entity. </p> <audio id="audio" autoplay controls></audio><br /> <button name="dial" id="dial">Dial</button> <div class="log"></div>
JavaScript
Давайте рассмотрим следующий код JavaScript. Имейте в виду, что процесс установления соединения здесь несколько искусственный; обычно вы не создаете оба конца соединения в одном документе.
Глобальные переменные
Сначала мы устанавливаем глобальные переменные.
let dialString = "12024561111";
let callerPC = null;
let receiverPC = null;
let dtmfSender = null;
let hasAddTrack = false;
let mediaConstraints = {
audio: true,
video: false,
};
let offerOptions = {
offerToReceiveAudio: 1,
offerToReceiveVideo: 0,
};
let dialButton = null;
let logElement = null;
Они расположены в следующем порядке:
dialString-
Строка DTMF, которую отправит звонящий, когда будет нажата кнопка «Набрать».
callerPCиreceiverPC-
Объекты
RTCPeerConnection, представляющие звонящего и получателя соответственно. Они будут инициализированы при запуске вызова в нашей функцииconnectAndDial(), как показано ниже в разделе Начало процесса подключения. dtmfSender-
Объект
RTCDTMFSenderдля соединения. Он будет получен во время настройки соединения в функцииgotStream(), показанной в разделе Добавление аудио в соединение. hasAddTrack-
Поскольку некоторые браузеры еще не реализовали
RTCPeerConnection.addTrack(), поэтому требуют использования устаревшего методаaddStream(), мы используем этот булеву переменную для определения того, поддерживает ли пользовательский агентaddTrack(); если нет, мы вернемся кaddStream(). Это определяется вconnectAndDial(), как показано в разделе Начало процесса подключения. mediaConstraints-
Объект, определяющий ограничения, которые необходимо использовать при запуске соединения. Мы хотим аудио-только соединение, поэтому
videoимеет значениеfalse, аaudioимеет значениеtrue. offerOptions-
Объект, предоставляющий параметры для указания при вызове
RTCPeerConnection.createOffer(). В этом случае мы указываем, что хотим получать аудио, но не видео. -
Эти переменные будут использоваться для хранения ссылок на кнопку «Набрать» и
<div>, в который будет записываться информация об отладке. Они будут установлены при первой загрузке страницы. См. раздел Инициализация ниже.
Инициализация
При загрузке страницы мы выполняем базовую настройку: получаем ссылки на кнопку «Набрать» и элемент поля вывода журнала, а затем используем addEventListener() для добавления обработчика события на кнопку «Набрать», чтобы при нажатии на нее вызывалась функция connectAndDial() для начала процесса подключения.
window.addEventListener("load", () => {
logElement = document.querySelector(".log");
dialButton = document.querySelector("#dial");
dialButton.addEventListener("click", connectAndDial, false);
});
Начало процесса подключения
При нажатии кнопки «Набрать» вызывается функция connectAndDial(). Это запускает построение соединения WebRTC для подготовки к отправке кодов DTMF.
function connectAndDial() {
callerPC = new RTCPeerConnection();
hasAddTrack = callerPC.addTrack !== undefined;
callerPC.onicecandidate = handleCallerIceEvent;
callerPC.onnegotiationneeded = handleCallerNegotiationNeeded;
callerPC.oniceconnectionstatechange = handleCallerIceConnectionStateChange;
callerPC.onsignalingstatechange = handleCallerSignalingStateChangeEvent;
callerPC.onicegatheringstatechange = handleCallerGatheringStateChangeEvent;
receiverPC = new RTCPeerConnection();
receiverPC.onicecandidate = handleReceiverIceEvent;
if (hasAddTrack) {
receiverPC.ontrack = handleReceiverTrackEvent;
} else {
receiverPC.onaddstream = handleReceiverAddStreamEvent;
}
navigator.mediaDevices
.getUserMedia(mediaConstraints)
.then(gotStream)
.catch((err) => log(err.message));
}
После создания RTCPeerConnection для звонящего (callerPC), мы проверяем, имеет ли он метод addTrack(). Если да, мы устанавливаем hasAddTrack в значение true; в противном случае мы устанавливаем его в значение false. Эта переменная позволит примеру работать даже в браузерах, еще не реализовавших новый метод addTrack(); мы сделаем это, вернувшись к устаревшему методу addStream().
Далее устанавливаются обработчики событий для звонящего. Мы рассмотрим их подробнее позже.
Затем создается второй RTCPeerConnection, представляющий принимающую сторону вызова, и он хранится в receiverPC; также устанавливается обработчик события onicecandidate для него.
Если поддерживается addTrack(), мы устанавливаем обработчик события ontrack для получателя; в противном случае мы устанавливаем обработчик onaddstream. События track и addstream отправляются при добавлении медиа в соединение.
Наконец, мы вызываем getUserMedia() для получения доступа к микрофону звонящего. Если это удается, вызывается функция gotStream(), в противном случае мы записываем ошибку, так как вызов не удался.
Добавление аудио в соединение
Как упоминалось выше, когда аудиовход с микрофона получен, вызывается функция gotStream(). Ее задача — сформировать поток, отправляемый получателю, чтобы процесс фактического начала передачи мог начаться. Она также получает доступ к RTCDTMFSender, который мы будем использовать для отправки DTMF в соединении.
function gotStream(stream) {
log("Got access to the microphone.");
let audioTracks = stream.getAudioTracks();
if (hasAddTrack) {
if (audioTracks.length > 0) {
audioTracks.forEach((track) => callerPC.addTrack(track, stream));
}
} else {
log(
"Your browser doesn't support RTCPeerConnection.addTrack(). Falling " +
"back to the <strong>deprecated</strong> addStream() method…",
);
callerPC.addStream(stream);
}
if (callerPC.getSenders) {
dtmfSender = callerPC.getSenders()[0].dtmf;
} else {
log(
"Your browser doesn't support RTCPeerConnection.getSenders(), so " +
"falling back to use <strong>deprecated</strong> createDTMFSender() " +
"instead.",
);
dtmfSender = callerPC.createDTMFSender(audioTracks[0]);
}
dtmfSender.ontonechange = handleToneChangeEvent;
}
После установки audioTracks в список аудиодорожек из потока микрофона пользователя, пришло время добавить медиа в RTCPeerConnection звонящего. Если addTrack() доступен в RTCPeerConnection, мы добавляем каждую аудиодорожку потока по одной, используя RTCPeerConnection.addTrack(). В противном случае мы вызываем RTCPeerConnection.addStream() для добавления потока в вызов как единого целого.
Далее мы проверяем, реализован ли метод RTCPeerConnection.getSenders(). Если он есть, мы вызываем его для callerPC и получаем первый элемент в возвращаемом списке отправителей; это RTCRtpSender , ответственный за передачу данных первой аудиодорожки вызова (над которой мы и будем отправлять DTMF). Затем мы получаем свойство RTCRtpSender dtmf, которое является объектом RTCDTMFSender, способным отправлять DTMF в соединении от звонящего к получателю.
Если getSenders() недоступен, мы вместо этого вызываем RTCPeerConnection.createDTMFSender() для получения объекта RTCDTMFSender. Хотя этот метод устарел, этот пример поддерживает его в качестве резервного варианта, чтобы позволить более старым браузерам (и тем, которые еще не обновились, чтобы поддерживать текущий API WebRTC DTMF) запустить пример.
Наконец, мы устанавливаем обработчик события ontonechange отправителя DTMF, чтобы получать уведомления каждый раз, когда завершается воспроизведение тона DTMF.
Функцию регистрации можно найти в конце документации.
Когда завершается воспроизведение тона
Каждый раз, когда завершается воспроизведение тона DTMF, для callerPC посылается событие tonechange. Обработчик этого события реализован как функция handleToneChangeEvent().
function handleToneChangeEvent(event) {
if (event.tone !== "") {
log(`Tone played: ${event.tone}`);
} else {
log("All tones have played. Disconnecting.");
callerPC.getLocalStreams().forEach((stream) => {
stream.getTracks().forEach((track) => {
track.stop();
});
});
receiverPC.getLocalStreams().forEach((stream) => {
stream.getTracks().forEach((track) => {
track.stop();
});
});
audio.pause();
audio.srcObject = null;
receiverPC.close();
callerPC.close();
}
}
Событие tonechange используется как для указания завершения воспроизведения отдельного тона, так и для указания завершения воспроизведения всех тонов. Свойство tone события представляет собой строку, указывающую, какой тон только что завершил воспроизведение. Если все тона завершили воспроизведение, tone является пустой строкой; в этом случае, RTCDTMFSender.toneBuffer пусто.
В этом примере мы выводим на экран, какой тон только что завершил воспроизведение. В более сложном приложении вы, возможно, обновите пользовательский интерфейс, например, чтобы указать, какой тон сейчас воспроизводится.
С другой стороны, если буфер тонов пуст, наш пример разработан для завершения вызова. Это выполняется путем остановки каждого потока как у звонящего, так и у получателя, итерацией по списку дорожек каждого RTCPeerConnection (как возвращается методом getTracks()) и вызовом метода stop() каждой дорожки.
После того, как все медиадорожки как звонящего, так и получателя будут остановлены, мы приостанавливаем элемент <audio> и устанавливаем его свойство srcObject в значение null. Это отсоединяет аудиопоток от элемента <audio>.
Затем, наконец, каждый RTCPeerConnection закрывается путем вызова его метода close().
Добавление кандидатов для звонящего
Когда ICE-слой звонящего генерирует нового кандидата, он посылает событие icecandidate для callerPC. Задача обработчика события icecandidate — передать кандидата получателю. В нашем примере мы напрямую контролируем как звонящего, так и получателя, поэтому мы можем просто напрямую добавить кандидата к получателю, вызвав его метод addIceCandidate(). Это обрабатывается функцией handleCallerIceEvent():
function handleCallerIceEvent(event) {
if (event.candidate) {
log(`Adding candidate to receiver: ${event.candidate.candidate}`);
receiverPC
.addIceCandidate(new RTCIceCandidate(event.candidate))
.catch((err) => log(`Error adding candidate to receiver: ${err}`));
} else {
log("Caller is out of candidates.");
}
}
Если событие icecandidate имеет свойство candidate, отличное от null, мы создаем новый объект RTCIceCandidate из строки event.candidate и "передаем" его получателю, вызвав receiverPC.addIceCandidate(), предоставив новый RTCIceCandidate в качестве входных данных. Если addIceCandidate() завершится неудачно, блок catch() выведет ошибку в наше поле журнала.
Если event.candidate равно null, это означает, что больше кандидатов недоступно, и мы регистрируем эту информацию.
Набор номера после открытия соединения
Наша конструкция требует, чтобы при установлении соединения мы немедленно отправили строку DTMF. Для этого мы следим за тем, чтобы звонящий получил событие iceconnectionstatechange. Это событие отправляется при наступлении одного из множества изменений в состоянии процесса ICE-соединения, включая успешное подключение.
function handleCallerIceConnectionStateChange() {
log(`Caller's connection state changed to ${callerPC.iceConnectionState}`);
if (callerPC.iceConnectionState === "connected") {
log(`Sending DTMF: "${dialString}"`);
dtmfSender.insertDTMF(dialString, 400, 50);
}
}
Событие iceconnectionstatechange на самом деле не содержит нового состояния, поэтому мы получаем текущее состояние процесса подключения из свойства RTCPeerConnection.iceConnectionState объекта callerPC. После записи нового состояния мы проверяем, является ли состояние "connected". Если да, мы записываем факт, что собираемся отправить DTMF, а затем вызываем dtmf.insertDTMF() для отправки DTMF на той же дорожке, что и данные аудио, на объекте RTCDTMFSender, который мы ранее сохранили в dtmfSender.
Наш вызов insertDTMF() задаёт не только отправляемый DTMF (dialString), но и длительность каждого тона в миллисекундах (400 мс) и интервал между тонами (50 мс).
Переговоры о подключении
Когда вызывающий RTCPeerConnection начинает получать медиаданные (после добавления потока микрофона), вызывающему передаётся событие negotiationneeded, сигнализирующее о необходимости начать переговоры о подключении с получателем. Как уже упоминалось, наш пример упрощён, потому что мы контролируем как вызывающего, так и получателя, поэтому handleCallerNegotiationNeeded() может быстро построить соединение, объединив необходимые вызовы для вызывающего и получателя, как показано ниже.
function handleCallerNegotiationNeeded() {
log("Negotiating…");
callerPC
.createOffer(offerOptions)
.then((offer) => {
log(`Setting caller's local description: ${offer.sdp}`);
return callerPC.setLocalDescription(offer);
})
.then(() => {
log(
"Setting receiver's remote description to the same as caller's local",
);
return receiverPC.setRemoteDescription(callerPC.localDescription);
})
.then(() => {
log("Creating answer");
return receiverPC.createAnswer();
})
.then((answer) => {
log(`Setting receiver's local description to ${answer.sdp}`);
return receiverPC.setLocalDescription(answer);
})
.then(() => {
log("Setting caller's remote description to match");
return callerPC.setRemoteDescription(receiverPC.localDescription);
})
.catch((err) => log(`Error during negotiation: ${err.message}`));
}
Поскольку различные методы, участвующие в установлении соединения, возвращают promise, мы можем объединить их следующим образом:
- Вызов
callerPC.createOffer()для получения предложения. - Затем использование этого предложения для установки локального описания вызывающего, вызвав
callerPC.setLocalDescription(). - Затем "передача" предложения получателю путём вызова
receiverPC.setRemoteDescription(). Это настраивает получателя, чтобы он знал конфигурацию вызывающего. - Затем получатель создаёт ответ, вызвав
receiverPC.createAnswer(). - Затем получатель устанавливает своё локальное описание, соответствующее только что созданному ответу, вызвав
receiverPC.setLocalDescription(). - Затем ответ "передаётся" вызывающему путём вызова
callerPC.setRemoteDescription(). Это позволяет вызывающему узнать конфигурацию получателя. - Если в любой момент произошла ошибка, в блоке
catch()выводится сообщение об ошибке в журнал.
Отслеживание других изменений состояния
Мы также можем следить за изменениями состояния сигнализации (путем принятия событий signalingstatechange) и состояния сбора ICE (путем принятия событий icegatheringstatechange). Мы не используем их для чего-либо, поэтому просто записываем их. Мы могли бы вообще не настраивать эти обработчики событий.
function handleCallerSignalingStateChangeEvent() {
log(`Caller's signaling state changed to ${callerPC.signalingState}`);
}
function handleCallerGatheringStateChangeEvent() {
log(`Caller's ICE gathering state changed to ${callerPC.iceGatheringState}`);
}
Добавление кандидатов получателю
Когда слой ICE получателя предлагает нового кандидата, он отправляет событие icecandidate объекту receiverPC. Задача обработчика события icecandidate — передать кандидата вызывающему. В нашем примере мы напрямую контролируем как вызывающего, так и получателя, поэтому мы можем просто добавить кандидата в вызывающего, вызвав его метод addIceCandidate(). Это обрабатывается handleReceiverIceEvent().
Этот код аналогичен обработчику событий icecandidate для вызывающего, представленному в разделе Добавление кандидатов вызывающему выше.
function handleReceiverIceEvent(event) {
if (event.candidate) {
log(`Adding candidate to caller: ${event.candidate.candidate}`);
callerPC
.addIceCandidate(new RTCIceCandidate(event.candidate))
.catch((err) => log(`Error adding candidate to caller: ${err}`));
} else {
log("Receiver is out of candidates.");
}
}
Если событие icecandidate имеет свойство candidate, отличное от null, мы создаём новый объект RTCIceCandidate из строки event.candidate и передаём его вызывающему, передавая его в callerPC.addIceCandidate(). Если addIceCandidate() терпит неудачу, блок catch() выводит ошибку в наш лог.
Если event.candidate равно null, это означает, что больше кандидатов недоступно, и мы записываем эту информацию.
Добавление медиаданных получателю
Когда получатель начинает получать медиаданные, получателю передаётся событие объекту RTCPeerConnection, receiverPC. Как объяснялось в разделе Начало процесса подключения, текущий стандарт WebRTC использует событие track для этого. Поскольку некоторые браузеры ещё не обновлены для поддержки этого, нам также необходимо обработать событие addstream. Это демонстрируется в методах handleReceiverTrackEvent() и handleReceiverAddStreamEvent() ниже.
function handleReceiverTrackEvent(event) {
audio.srcObject = event.streams[0];
}
function handleReceiverAddStreamEvent(event) {
audio.srcObject = event.stream;
}
Событие track содержит свойство streams с массивом потоков, частью которых является дорожка (одна дорожка может принадлежать многим потокам). Мы берём первый поток и прикрепляем его к элементу <audio>.
Событие addstream содержит свойство stream, указывающее на один поток, добавленный к дорожке. Мы прикрепляем его к элементу <audio>.
Ведение журнала
Функция log() используется в коде для добавления текста в поле <div> для отображения статуса и ошибок пользователю.
function log(msg) {
logElement.innerText += `${msg}\n`;
}
Результат
Вы можете попробовать этот пример здесь. После нажатия кнопки "Набрать", вы увидите ряд сообщений в журнале; затем начнётся набор. Если ваш браузер воспроизводит тона как часть своего пользовательского интерфейса, вы должны услышать их при передаче.
После завершения передачи тонов соединение закрывается. Вы можете снова нажать "Набрать", чтобы повторно подключиться и отправить тона.
См. также
- API WebRTC
- Продолжительность сеанса WebRTC
- Сигнализация и видеозвонки (учебник и пример, который подробнее объясняет процесс сигнализации)
- Введение в протоколы WebRTC
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Using_DTMF