предисловие
【с головы до ног] будет опубликован в виде серии статей, включая многопользовательское видео WebRTC, но не ограничиваясь этим. Ожидается, что в нем будут:
- Бой WebRTC (1): То есть этот вопрос, в основном про базовые пояснения и локальные одноранговые соединения один на один и сетевые одноранговые соединения.
- Битва WebRTC (2): в основном объясняется передача данных, многотерминальное локальное одноранговое соединение и сетевое одноранговое соединение.
- Битва WebRTC (3): реализация проекта видеочата один на один, включая, помимо прочего, скриншоты, записи и т. д.
- WebRTC + Canvas реализует общий проект артборда.
- Автор работ с открытым исходным кодом💘🍦🙈Vchat — система социального чата (vue + node + mongodb)серия статей
Поскольку вывод статей требует много энергии, указанный выше план не обязательно является порядком публикации, а статьи по другим направлениям будут публиковаться посередине, например по Vue, JavaScript или другим учебным темам. В статье я расскажу о некоторых ловушках, с которыми я столкнулся, чтобы напомнить всем быть внимательными и постараться сделать так, чтобы все меньше отвлекались в процессе обучения. Конечно, мой ответ не стандартный, просто мои личные размышления. Если у вас есть лучший способ, вы можете общаться друг с другом, и я надеюсь, что каждый может что-то получить.
Я также надеюсь, что вы можетеСфокусируйся наВолна, ваше внимание и поддержка также могут вдохновить меня на выпуск более качественных статей.
- Пример этой статьиИсходная библиотека webrtc-stream
- Репозиторий статей 🍹🍰fe-код
- эта статьядемонстрационный адрес(Рекомендовать Google для просмотра)
Начнем с рендеринга, цель этой задачи — добиться видеозвонка 1 V 1 (камеру ноутбука использовать нельзя, используется виртуальная камера). Статья относительно длинная, читать ее можно медленно после отметки.
В конце статьи естьгруппа обменаа такжеНет публики, надеюсь все поддержат, спасибо 🍻.
Что такое WebRTC?
WebRTC был разработан шведской компанией Gobal IP Solutions или сокращенно GIPS. Google приобрела GIPS в 2011 году и сделала ее исходный код открытым. Затем он работал с соответствующими органами по стандартизации IETF и W3C, чтобы обеспечить консенсус в отрасли. в:
- Веб-коммуникации в реальном времени (WEBRTC) Организация W3C: определяет API браузера.
- Связь в реальном времени в веб-браузерах (RTCWEB) Организация стандартов IETF: определяет протоколы, данные, безопасность и другие средства, необходимые для этого.
Проще говоря, WebRTC — это проект с открытым исходным кодом, который обеспечивает передачу аудио, видео и данных в реальном времени в веб-приложениях. При общении в реальном времени получение и обработка аудио и видео — очень сложный процесс. Например, кодирование и декодирование аудио- и видеопотоков, шумоподавление и эхоподавление и т. д., но в WebRTC все это делает базовая инкапсуляция браузера. Мы можем напрямую взять оптимизированный медиапоток и вывести его на локальный экран и динамики или перенаправить его на одноранговые узлы.
Механизм обработки аудио и видео WebRTC:
Поэтому мы можем реализовать одноранговое (P2P) соединение между браузерами без каких-либо сторонних плагинов для аудио- и видеосвязи в реальном времени. Конечно, WebRTC предоставляет нам некоторые API для использования.В процессе аудио- и видеосвязи в реальном времени мы в основном используем следующие три:
- getUserMedia: получение аудио- и видеопотоков (MediaStream)
- RTCPeerConnection: двухточечная связь
- RTCDataChannel: передача данных
Однако, хотя браузер решает за нас большую часть проблем с обработкой аудио и видео, нам все равно нужно уделять особое внимание размеру и качеству потока при запросе аудио и видео из браузера. Потому что, даже если аппаратное обеспечение может захватывать потоки с качеством HD, ЦП и пропускная способность могут не справляться, что мы должны учитывать при установке нескольких одноранговых соединений.
выполнить
Далее мы постепенно будем понимать процесс реализации WebRTC связи в реальном времени, анализируя упомянутый выше API.
getUserMedia
MediaStream
Вы можете быть знакомы с API getUserMedia, потому что он необходим для общих функций, таких как запись H5, в основном для получения медиапотока (т.е. MediaStream) устройства. Он может принимать ограничения объекта ограничения в качестве параметра, чтобы указать, какой тип медиапотока необходимо получить.
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
// 参数表示需要同时获取到音频和视频
.then(stream => {
// 获取到优化后的媒体流
let video = document.querySelector('#rtc');
video.srcObject = stream;
})
.catch(err => {
// 捕获错误
});
Кратко рассмотрим полученный MediaStream.
Вы можете видеть, что он имеет много свойств, нам просто нужно знать о нем, более подробную информацию можно просмотретьMDN.
* id [String]: 对当前的 MS 进行唯一标识。所以每次刷新浏览器或是重新获取 MS,id 都会变动。
* active [boolean]: 表示当前 MS 是否是活跃状态(就是是否可以播放)。
* onactive: 当 active 为 true 时,触发该事件。
В сочетании с приведенным выше рисунком давайте рассмотрим прототип и цепочку прототипов, упомянутые в предыдущем выпуске. Медиапоток__proto__
Он указывает на объект-прототип, соответствующий его конструктору, и в объекте-прототипе есть свойство конструктора, указывающее на соответствующий конструктор. То есть конструктор MediaStream — это функция с именем MediaStream. Это может быть немного запутанно.Для студентов, которые не знакомы с прототипом, вы можете прочитать предыдущую статью.Прототип JavaScript и цепочка прототипов и практика кода проверки холста.
Здесь же можно просмотреть некоторую информацию о полученном потоке через getAudioTracks(), getVideoTracks(), посмотреть дополнительную информациюMDN.
* kind: 是当前获取的媒体流类型(Audio/Video)。
* label: 是媒体设备,我这里用的是虚拟摄像头。
* muted: 表示媒体轨道是否静音。
совместимость
Продолжайте смотреть на getUserMedia,navigator.mediaDevices.getUserMedia
это новая версия API, старая версияnavigator.getUserMedia
. Во избежании проблем с совместимостью, можно немного с этим разобраться (в конечном счете скорость поддержки WebRTC сейчас невысокая, при необходимости можно выбрать какие-нибудь адаптеры, напримерadapter.js
).
// 判断是否有 navigator.mediaDevices,没有赋成空对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// 继续判断是否有 navigator.mediaDevices.getUserMedia,没有就采用 navigator.getUserMedia
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia = function(prams) {
let getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// 兼容获取
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
return new Promise(function(resolve, reject) {
getUserMedia.call(navigator, prams, resolve, reject);
});
};
}
navigator.mediaDevices.getUserMedia(constraints)
.then(stream => {
let video = document.querySelector('#Rtc');
if ('srcObject' in video) { // 判断是否支持 srcObject 属性
video.srcObject = stream;
} else {
video.src = window.URL.createObjectURL(stream);
}
video.onloadedmetadata = function(e) {
video.play();
};
})
.catch((err) => { // 捕获错误
console.error(err.name + ': ' + err.message);
});
constraints
Для объекта ограничения ограничений мы можем использовать его для указания некоторых свойств, связанных с медиапотоком. Например, укажите, следует ли получать определенный поток:
navigator.mediaDevices.getUserMedia({ audio: false, video: true });
// 只需要视频流,不要音频
Указанная ширина и высота видеопотока, а также желаемое значение частоты кадра:
// 获取指定宽高,这里需要注意:在改变视频流的宽高时,
// 如果宽高比和采集到的不一样,会直接截掉某部分
{ audio: false,
video: { width: 1280, height: 720 }
}
// 设定理想值、最大值、最小值
{
audio: true,
video: {
width: { min: 1024, ideal: 1280, max: 1920 },
height: { min: 776, ideal: 720, max: 1080 }
}
}
Для мобильных устройств вы также можете указать, чтобы получить переднюю камеру или заднюю камеру:
{ audio: true, video: { facingMode: "user" } } // 前置
{ audio: true, video: { facingMode: { exact: "environment" } } } // 后置
// 也可以指定设备 id,
// 通过 navigator.mediaDevices.enumerateDevices() 可以获取到支持的设备
{ video: { deviceId: myCameraDeviceId } }
Еще одна интересная вещь — установить источник видео на экран, но в настоящее время только Firefox поддерживает это свойство.
{ audio: true, video: {mediaSource: 'screen'} }
Я не буду продолжать работать здесь носильщиком, впереди еще много интересногоMDN, ^_^!
RTCPeerConnection
Интерфейс RTCPeerConnection представляет собой WebRTC-соединение локального компьютера с удаленным. Этот интерфейс предоставляет реализации методов для создания, обслуживания, мониторинга и закрытия соединений. —— МДН
Обзор
RTCPeerConnection 作为创建点对点连接的 API,是我们实现音视频实时通信的关键。在点对点通信的过程中,需要交换一系列信息,通常这一过程叫做 — 信令(signaling)。在信令阶段需要完成的任务:
* 为每个连接端创建一个 RTCPeerConnection,并添加本地媒体流。
* 获取并交换本地和远程描述:SDP 格式的本地媒体元数据。
* 获取并交换网络信息:潜在的连接端点称为 ICE 候选者。
Хотя WEBRTC называется соединением точка до точки, мы не представляем участие сервера в процессе реализации. Вместо этого нет способа общаться между двумяпноженным каналом. Это означает, что на этапе сигнализации нам нужна услуга связи, чтобы помочь нам построить это соединение. Сам Webrtc не указывает службу сигнализации, поэтому мы можем, но не ограничиваться, используя XMPP, XHR, сокет и т. Д. Для получения необходимыми биржи сигналов. Схема, которую я использую в работе, основан на протоколе XMPPStrophe.js
сделать двустороннюю связь, но в этом случае мы будем использоватьSocket.io
И Коа, чтобы сделать демонстрацию проекта.
Технология обхода NAT
Давайте сначала рассмотрим первую задачу подключения: создайте RTCPeerConnection для каждого конца подключения и добавьте локальные медиапотоки. По сути, если это общий режим прямой трансляции, то для вывода в конец воспроизведения нужно добавить только локальный поток, а другим участникам нужно только принять поток для просмотра.
Из-за различий между браузерами RTCPeerConnection также должен иметь префикс.
let PeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
let peer = new PeerConnection(iceServers);
Мы видим, что RTCPeerConnection также принимает параметр — iceServers, давайте посмотрим, как он выглядит:
{
iceServers: [
{ url: "stun:stun.l.google.com:19302"}, // 谷歌的公共服务
{
url: "turn:***",
username: ***, // 用户名
credential: *** // 密码
}
]
}
Параметры настроены с двумя URL-адресами, а именно STUN и TURN, Это ключ к WebRTC для достижения связи точка-точка, а также проблема, которую необходимо решить в обычных соединениях P2P: обход NAT.
Короче говоря, NAT (преобразование сетевых адресов) — это технология, которая, по-видимому, решает проблему отсутствия IP-адресов в IPV4, то есть общедоступный IP-адрес обычно соответствует n IP-адресам интрасети. Это также приведет к тому, что браузеры, которые не находятся в той же локальной сети, попытаются подключиться к WebRTC, но не смогут напрямую получить общедоступный IP-адрес другой стороны и не смогут взаимодействовать, поэтому требуется обход NAT (также называемый пробивкой отверстий). Ниже приведен основной процесс обхода NAT:
В обычных условиях для обхода NAT используется структура протокола ICE, полное название ICE — Interactive Connectivity Establishment, то есть интерактивное установление соединения. Он использует протокол STUN и протокол TURN для обхода. Дополнительную информацию о обходе NAT можно найти вРеализация обхода NAT по протоколу ICE (STUN&TURN),Стандартный протокол связи P2P (3) ICE.
На данный момент мы можем обнаружить, что для связи WebRTC требуются как минимум две службы:
- На этапе сигнализации требуется служба двусторонней связи для облегчения обмена информацией.
- STUN и TURN помогают обойти NAT.
Установить соединение «точка-точка»
Что за процесс представляет собой двухточечное соединение WebRTC Анализируем соединение, объединяя легенду.
Очевидно, в процессе вышеуказанного подключения:
абонент(здесь все относится к браузерам) нужно датьПриемный конецОтправьте сообщение под названием «предложение».
Приемный конецПосле получения запроса он возвращает ответное сообщение наабонент.
Это одна из вышеперечисленных задач, обмен метаданными нативных медиа в формате SDP. Информация sdp обычно выглядит так:
v=0
o=- 1837933589686018726 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS yvKeJMUSZzvJlAJHn4unfj6q9DMqmb6CrCOT
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
...
Но задача заключается не только в обмене, но и в необходимости сохранения информации о себе и другой стороне, поэтому добавляем еще немного материалов:
* **呼叫端** 创建 offer 信息后,先调用 setLocalDescription 存储本地 offer 描述,再将其发送给 **接收端**。
* **接收端** 收到 offer 后,先调用 setRemoteDescription 存储远端 offer 描述;然后又创建 answer 信息,同样需要调用 setLocalDescription 存储本地 answer 描述,再返回给 **呼叫端**
* **呼叫端** 拿到 answer 后,再次调用 setRemoteDescription 设置远端 answer 描述。
Здесь еще не хватает одного шага для соединения «точка-точка», то есть обмена сетевой информацией ICE-кандидатами. Однако на этом шаге и в обмене информацией о предложении и ответе нет никакой последовательности, и процесс тот же. То есть: вабонента такжеПриемный конецПосле того, как информация кандидата ICE подготовлена, обменяйтесь ею и сохраните информацию друг друга, тем самым завершив соединение.
Эта картина, как мне кажется, является относительно полной и подробно описывает весь процесс подключения. Что ж, подытожим:
* 基础设施:必要的信令服务和 NAT 穿越服务
* clientA 和 clientB 分别创建 RTCPeerConnection 并为输出端添加本地媒体流。如果是视频通话类型,则意味着,两端都需要添加媒体流进行输出。
* 本地 ICE 候选信息采集完成后,通过信令服务进行交换。
* 呼叫端(好比 A 给 B 打视频电话,A 为呼叫端)发起 offer 信息,接收端接收并返回一个 answer 信息,呼叫端保存,完成连接。
Локальное пиринговое соединение 1 на 1
После того, как основной процесс будет завершен, к вам выйдет мул или лошадь. Давайте сначала реализуем локальное одноранговое соединение, чтобы ознакомиться с процессом и некоторыми API. Локальное соединение означает соединение между двумя видео на локальной странице без использования службы. Забудьте об этом, давайте сделаем снимок, вы можете понять это с первого взгляда.
Чтобы прояснить цель, A, поскольку выходной конец должен получить локальный поток и добавить его к своему собственному RTCPeerConnection; B, как конец вызова, нет требований к выходу, поэтому ему нужно только получить поток.
Создать медиапоток
Макет страницы очень прост, всего два тега видео, представляющие A и B соответственно. Итак, давайте посмотрим на код напрямую.Хотя исходный код построен с помощью Vue, он не использует никакого специального API.Он мало чем отличается от синтаксиса класса es6 в целом, и есть подробные комментарии.Поэтому он рекомендуется, чтобы студенты без основы Vue могли напрямую читать его как es6. ПримерИсходная библиотека webrtc-stream
async createMedia() {
// 保存本地流到全局
this.localstream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
let video = document.querySelector('#rtcA');
video.srcObject = this.localstream;
this.initPeer(); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
}
Инициализировать RTCPeerConnection
initPeer() {
...
this.peerA.addStream(this.localstream); // 添加本地流
this.peerA.onicecandidate = (event) => {
// 监听 A 的ICE候选信息 如果收集到,就添加给 B 连接状态
if (event.candidate) {
this.peerB.addIceCandidate(event.candidate);
}
};
...
// 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src
this.peerB.onaddstream = (event) => {
let video = document.querySelector('#rtcB');
video.srcObject = event.stream;
};
this.peerB.onicecandidate = (event) => { 连接状态
// 监听 B 的ICE候选信息 如果收集到,就添加给 A
if (event.candidate) {
this.peerA.addIceCandidate(event.candidate);
}
};
}
Эта часть в основном предназначена для создания одноранговых экземпляров и обмена информацией ICE друг с другом. Однако здесь необходимо упомянуть одно свойство — iceConnectionState.
peer.oniceconnectionstatechange = (evt) => {
console.log('ICE connection state change: ' + evt.target.iceConnectionState);
};
мы можем пройтиoniceconnectionstatechange
метод для мониторинга состояния соединения ICE, которое имеет семь состояний:
new ICE代理正在收集候选人或等待提供远程候选人。
checking ICE代理已经在至少一个组件上接收了远程候选者,并且正在检查候选但尚未找到连接。除了检查,它可能还在收集。
connected ICE代理已找到所有组件的可用连接,但仍在检查其他候选对以查看是否存在更好的连接。它可能还在收集。
completed ICE代理已完成收集和检查,并找到所有组件的连接。
failed ICE代理已完成检查所有候选对,但未能找到至少一个组件的连接。可能已找到某些组件的连接。
disconnected ICE 连接断开
closed ICE代理已关闭,不再响应STUN请求。
То, на что нам нужно обратить внимание, завершено и отключено, одно срабатывает при завершении соединения, а другое срабатывает при разрыве соединения.
Создать соединение
async call() {
if (!this.peerA || !this.peerB) { // 判断是否有对应实例,没有就重新创建
this.initPeer();
}
try {
let offer = await this.peerA.createOffer(this.offerOption); // 创建 offer
await this.onCreateOffer(offer);
} catch (e) {
console.log('createOffer: ', e);
}
}
Необходимо определить, есть ли здесь соответствующий экземпляр, предназначенный для обработки повторного звонка после отбоя.
async onCreateOffer(desc) {
try {
await this.peerB.setLocalDescription(desc); // 呼叫端设置本地 offer 描述
} catch (e) {
console.log('Offer-setLocalDescription: ', e);
}
try {
await this.peerA.setRemoteDescription(desc); // 接收端设置远程 offer 描述
} catch (e) {
console.log('Offer-setRemoteDescription: ', e);
}
try {
let answer = await this.peerA.createAnswer(); // 接收端创建 answer
await this.onCreateAnswer(answer);
} catch (e) {
console.log('createAnswer: ', e);
}
},
async onCreateAnswer(desc) {
try {
await this.peerA.setLocalDescription(desc); // 接收端设置本地 answer 描述
} catch (e) {
console.log('answer-setLocalDescription: ', e);
}
try {
await this.peerB.setRemoteDescription(desc); // 呼叫端端设置远程 answer 描述
} catch (e) {
console.log('answer-setRemoteDescription: ', e);
}
}
Это в основном процесс, который повторялся несколько раз до этого и был записан в коде, видя это, идея должна быть более ясной. Однако есть одна вещь, которую необходимо пояснить: в данном случае A является вызывающей стороной, и B также может получить медиапоток от A. Поскольку после установления соединения оно становится двунаправленным, за исключением того, что B не добавил локальный поток при инициализации однорангового узла, поэтому у A не будет медиапотока B.
Сеть 1 на 1 пиринговое соединение
Предположительно все знакомы с основным процессом, и я несколько раз говорил туда-сюда с помощью диаграмм и примеров. Так что куй железо, пока горячо, на этот раз мы добавим сервис, чтобы сделать настоящее одноранговое соединение. Прежде чем читать следующую статью, я надеюсь, что у вас есть базовые знания о Koa и Scoket.io, и вы понимаете некоторые основные API. Незнакомым одноклассникам все равно, уже поздно видеть,Koa,Socke.io, или вы можете обратиться к моей предыдущей статьеVchat — система социального чата (vue + node + mongodb).
нужно
Или старые правила, сначала понять потребности. Картинки загружаются медленно, вы можете смотреть их напрямуюдемонстрационный адрес
Процесс подключения включает в себя несколько ссылок, поэтому я не буду делать здесь скриншоты по одному, вы можете сразу перейти на демо-адрес для просмотра. Кратко проанализируем, что мы собираемся делать: * После присоединения к комнате, получить всех онлайн участников комнаты. * Выберите любого участника, чтобы сделать звонок, то есть действие вызова. В это время нужно разобраться с некоторыми деталями: вы не можете звонить сами, одновременно может звонить только один человек, и вам нужно определить, находится ли другая сторона на вызове, и вам нужно сделать соответствующее суждение (одобрить, отклонить и позвонить) после звонка. * Никаких последующих действий после отказа или во время звонка не происходит, и вы можете позвонить другому человеку. После согласования пора приступать к установлению однорангового соединения.
присоединиться к комнате
Просто посмотрите на процесс присоединения к комнате:
// 前端
join() {
if (!this.account) return;
this.isJoin = true; // 输入框弹层逻辑
window.sessionStorage.account = this.account; // 刷新判断是否登录过
socket.emit('join', {roomid: this.roomid, account: this.account}); // 发送加入房间请求
}
// 后端
const sockS = {}; // 不同客户端对应的 sock 实例
const users = {}; // 成员列表
sock.on('join', data=>{
sock.join(data.roomid, () => {
if (!users[data.roomid]) {
users[data.roomid] = [];
}
let obj = {
account: data.account,
id: sock.id
};
let arr = users[data.roomid].filter(v => v.account === data.account);
if (!arr.length) {
users[data.roomid].push(obj);
}
sockS[data.account] = sock; // 保存不同客户端对应的 sock 实例
// 将房间内成员列表发给房间内所有人
app._io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id);
});
});
Список участников серверной части обрабатывается из-за логики нескольких комнат, которая возвращается в соответствии со списком участников каждой комнаты. Если у вас нет нескольких комнат, когда вы делаете это, вам не нужно так думать. Обработка sockS заключается в отправке приватных сообщений чата.
вызов
Меры предосторожности при звонках уже упоминались ранее, поэтому я расскажу об этом здесь. Следует отметить, что сообщение должно нести учетную запись себя и другой стороны, потому что это идентификация носка участника, который предварительно сохраняется в носке для отправки сообщений частного чата. Затем идут три упомянутых выше состояния, которые здесь различаются значениями типа 1, 2, 3, а затем дают разные ответы.
// 前端
apply(account) { // 发送请求
// account 对方account self 是自己的account
this.loading = true;
this.loadingText = '呼叫中'; // 呼叫中 loading
socket.emit('apply', {account: account, self: this.account});
},
reply(account, type) { // 处理回复
socket.emit('reply', {account: account, self: this.account, type: type});
}
// 收到请求
socket.on('apply', data => {
if (this.isCall) { // 判断是否在通话中
this.reply(data.self, '3');
return;
}
this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', {
confirmButtonText: '同意',
cancelButtonText: '拒绝',
type: 'warning'
}).then(async () => {
this.isCall = data.self;
this.reply(data.self, '1');
}).catch(() => {
this.reply(data.self, '2');
});
});
// 后端
sock.on('apply', data=>{ // 转发申请
sockS[data.account].emit('apply', data);
});
Серверная часть относительно проста, просто перенаправляет запрос соответствующему клиенту. На самом деле, серверная часть нашего примера — это в основном эта операция, поэтому внутренний код не будет опубликован, вы можете перейти к исходному коду, чтобы увидеть его напрямую.
Отвечать
Ответ и вызов — это одна и та же логика, просто работайте с разными ответами отдельно.
// 前端
socket.on('reply', async data =>{ // 收到回复
this.loading = false;
switch (data.type) {
case '1': // 同意
this.isCall = data.self; // 存储通话对象
break;
case '2': //拒绝
this.$message({
message: '对方拒绝了你的请求!',
type: 'warning'
});
break;
case '3': // 正在通话中
this.$message({
message: '对方正在通话中!',
type: 'warning'
});
break;
}
});
Создать соединение
Логика звонка и ответа в принципе понятна, так что продолжим думать, когда нужно создавать P2P-соединение? Как мы уже говорили ранее, ни отказ, ни вызов не должны обрабатываться, требуется только согласие, поэтому оно должно быть создано в месте запроса согласия. Следует отметить, что в запросе согласия есть два места: одно — когда вы щелкнули согласие, а другое — после того, как другая сторона узнает, что вы нажали согласие.
В этом примере вызывающий абонент отправляет предложение, на которое следует обратить внимание, пока одна сторона создает предложение, потому что после установления соединения оно становится двунаправленным.
socket.on('apply', data => { // 你点同意的地方
...
this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', {
confirmButtonText: '同意',
cancelButtonText: '拒绝',
type: 'warning'
}).then(async () => {
await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer
... // 这里不发 offer
})
...
});
socket.on('reply', async data =>{ // 对方知道你点了同意的地方
switch (data.type) {
case '1': // 只有这里发 offer
await this.createP2P(data); // 对方同意之后创建自己的 peer
this.createOffer(data); // 并给对方发送 offer
break;
...
}
});
Как и в видеозвонках, таких как WeChat, обеим сторонам необходимо передавать мультимедиа, потому что вам обоим нужно видеть друг друга. Таким образом, разница между этим и предыдущим локальным одноранговым соединением заключается в том, что каждому необходимо добавить медиапоток в свой собственный экземпляр RTCPeerConnection, а затем каждый может получить видеопоток другой стороны после подключения. При инициализации RTCPeerConnection не забудьте добавить функцию onicecandidate для отправки кандидатов ICE другой стороне.
async createP2P(data) {
this.loading = true; // loading动画
this.loadingText = '正在建立通话连接';
await this.createMedia(data);
},
async createMedia(data) {
... // 获取并将本地流赋值给 video 同之前
this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
},
initPeer(data) {
// 创建输出端 PeerConnection
...
this.peer.addStream(this.localstream); // 都需要添加本地流
this.peer.onicecandidate = (event) => {
// 监听ICE候选信息 如果收集到,就发送给对方
if (event.candidate) { // 发送 ICE 候选
socket.emit('1v1ICE',
{account: data.self, self: this.account, sdp: event.candidate});
}
};
this.peer.onaddstream = (event) => {
// 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src,改变相应loading状态,赋值省略
this.isToPeer = true;
this.loading = false;
...
};
}
Обмен информацией, такой как createOffer, такой же, как и раньше, но его необходимо перенаправить соответствующему клиенту через Socket. Затем каждый из них принимает соответствующие меры после получения сообщения.
socket.on('1v1answer', (data) =>{ // 接收到 answer
this.onAnswer(data);
});
socket.on('1v1ICE', (data) =>{ // 接收到 ICE
this.onIce(data);
});
socket.on('1v1offer', (data) =>{ // 接收到 offer
this.onOffer(data);
});
// 这里只贴一个 createOffer 的代码,因为和之前的思路都一样,只是写法有些区别
// 建议大家都自己敲一遍,有问题可以交流,也可以去源码查看。
async createOffer(data) { // 创建并发送 offer
try {
// 创建offer
let offer = await this.peer.createOffer(this.offerOption);
// 呼叫端设置本地 offer 描述
await this.peer.setLocalDescription(offer);
// 给对方发送 offer
socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer});
} catch (e) {
console.log('createOffer: ', e);
}
}
повесить трубку
Идея повесить трубку по-прежнему заключается в том, чтобы закрыть соответствующих пиров, но здесь сторона, которая кладет трубку, также должна использовать Socket, чтобы сообщить другой стороне, что вы повесили трубку, иначе другая сторона все еще ждет.
hangup() { // 挂断通话 并做相应处理 对方收到消息后一样需要关闭连接
socket.emit('1v1hangup', {account: this.isCall, self: this.account});
this.peer.close();
this.peer = null;
this.isToPeer = false;
this.isCall = false;
}
Справочная статья
группа обмена
Группа внешнего обмена QQ: 960807765, приветствуем все виды технического обмена, с нетерпением ждем вашего присоединения
постскриптум
Если вы это видите, и эта статья вам полезна, надеюсь, вы сможете поддержать автора своими ручонками, спасибо 🍻. Если в тексте что-то не так, укажите на это и поделитесь.
- Пример этой статьиИсходная библиотека webrtc-stream
- Репозиторий статей 🍹🍰fe-код
Прошлые статьи:
- Прототип JavaScript и цепочка прототипов и практика кода проверки холста
- Стой, Обещание!
- 💘🍦🙈Vchat — система социального чата с ног до головы (vue + node + mongodb)
Добро пожаловать в публичный аккаунтпередний двигатель, как можно скорее получите от автора статью, а также различные высококачественные статьи о внешнем интерфейсе, я надеюсь расти вместе с вами на пути к переднему концу в будущем.