Фу Цзиньтин, фронтенд-группа четвертого центра прибыли WeDoctor.
Прежде чем говорить
WebRTC, названный в честьОбмен мгновенными сообщениями через ИнтернетАббревиатура (англ. Web Real-Time Communication) — это API, который поддерживает веб-браузеры для голосовых или видеоразговоров в реальном времени. Он был открыт 1 июня 2011 года и был включен в рекомендацию W3C консорциума World Wide Web при поддержке Google, Mozilla, Opera.
Прежде всего, это и API, и протокол.
Во-вторых, это API для браузера для совершения аудио- и видеозвонков, и в нем фактически есть функция совместного использования экрана.
Наконец, теперь он включен в стандарт W3C, и основные производители браузеров сделали его совместимым.
Но если мы хотим хорошо использовать webrtc, мы должны сначала понять websocket. Для веб-сокетов все должны быть знакомы с ними, такими как социальный чат, многопользовательские игры, совместное редактирование, видеоконференции, приложения на основе местоположения (карты) и другие сценарии, требующие высокой скорости в реальном времени. Наши наиболее часто используемые WeChat, QQ, некоторые программы для прямых трансляций и т. д. также основаны на веб-сокетах для реализации пересылки сообщений и сигнализации. Увидев это, вы, возможно, колебались, сигнализируя здесь, а затем читайте дальше.
webrtc — это технология P2P, что такое P2P? По сути он сквозной, то есть ваши аудио- и видеопотоки передаются напрямую из одного конца в другой, минуя сервер.
Без прохождения через сервер, то есть, если сервер вдруг выйдет из строя в процессе прохождения, может ли вызов продолжиться?
Да! Однако перед отправкой аудио- и видеопотоков необходимо установить P2P-соединение.Перед установкой соединения сервер должен переслать сигнализацию.Эта сигнализация является идентификацией обоих концов вызова.
Если вы хотите использовать webrtc для реализации вызова, вы должны сначала передать сигнализацию и установить соединение. При установлении соединения лучше всего использовать веб-сокет для переадресации сигналов. Как мы все знаем, веб-сокет — это канал, на всех концах канала вы можете получать поток сообщений с любого конца, включая человека, отправившего сообщение.
Почему вы можете напрямую получать видео- и аудиопоток другой стороны, минуя сервер? Именно потому, что установлен P2P-канал, и этот P2P уже прошел сигнальную передачу.Какой сервер нужен при передаче видео- и аудиопотоков. В это время должны быть какие-то друзья, которые сомневаются, что аудио- и видеопотоки не могут пройти через сервер? Да, я соврал вам, он должен проходить через сервер, но для этого требуется только переадресация сервера онлайн.Если мы пересылаем аудио- и видеопотоки webrtc с двух или более локальных концов одной локальной сети, на самом деле нет нужен сервер переноса, но линия может быть нужна, а может и не нужна, а вот ещесделать отверстиеКонцепция чего-либо.
Обычно мы можем услышать относительно хорошие слова, такие как пробивка отверстий, проникновение во внутреннюю сеть, обход NAT и различные высокоуровневые вещи, которые на самом деле вполне понятны. Как мы все знаем, два хоста в двух разных сетях не могут общаться напрямую, а должны проходить через общедоступную сеть или свои собственные шлюзы. Пробивание отверстий, проникновение во внутреннюю сеть и обход NAT на самом деле означают одно и то же, то есть использование udp, позволяющее нам соединить два хоста, которые не находятся в одной сети, не проходя через общедоступную сеть, а подключаясь напрямую. Учащиеся, которые играли со скорлупой арахиса, должны понимать концепцию проникновения в интранет.
Для локальной разработки два хоста подключаются к одной и той же локальной сети и могут взаимодействовать напрямую без проникновения в интрасеть.
Для онлайн-разработки, если вы можете успешно пробить дыры в STUN, вам не нужен сервер передачи. Однако есть вероятность неудачного бурения скважины, потому что общедоступной сети нет, это не приносит выгоды оператору, а приносит затраты на связь, которые необходимо ограничивать. Вероятность успешного бурения за границей составляет 70%, а в Китае менее 50%.
Поэтому, чтобы предотвратить неудачное пробивание отверстий, мы используем сервер ретрансляции TURN для пересылки данных потокового мультимедиа для окончательной гарантии. Кроме того, существует способобратное соединение, также может помочь нам установить P2P, его принцип заключается в том, что одна сторона должна выйти в сеть общего пользования, и существуют ограничения.
Сервер ретрансляции coturn состоит из двух частей: STUN и TURN, STUN помогает нам делать дыры, а TURN помогает нам пересылать потоковые медиаданные.
##Процесс подключения
Все приведенные ниже API обновлены по состоянию на 2021.12.06.
Переговоры со СМИ начинаются
A 与 B 通过后端服务进行了 websocket 连接,进入了相同的管道,A 与 B 都可以收到自己与对方的消息与信令。
A Create PeerConnect 创建了 RTCPeerConnection 实例(webrtc 连接实例)。
A AddStream (getUserMedia 方法)获取本地的音频流, 在本地展示。(打开通话先显示自己的视频画面)。
A 调用 webrtc 连接实例的 CreateOffer 方法,创建 offer(SDP 格式描述), 这个 offer 包含 A 自己的媒体信息和编解码信息。
A 调用 webrtc 连接实例的 setLocalDescription 方法将 offer 设置为本地描述,并且向 sturn/turn 中继服务器发送 bind request 请求收集 candidate(候选者)。
A 发送了 offer,信令服务器(后端服务)将其中转到 B。
B 收到 offer, Create PeerConnect 创建了 RTCPeerConnection 实例(webrtc 连接实例)。
B AddStream (getUserMedia 方法)获取本地的音频流, 在本地展示。(显示自己的视频画面)。
B 调用 webrtc 连接实例的 setRemoteDescription 方法,将 offer 设置为自己的远端描述。
B 调用 webrtc 连接实例的 CreateAnswer 方法,创建 answer(SDP 格式描述),这个 answer 包含 A 自己的媒体信息和编解码信息。
B 调用 webrtc 连接实例的 setLocalDescription 方法将 answer 设置为本地描述,并且向 sturn/turn 中继服务器发送 bind request 请求收集 candidate(候选者)。
B 发送了 answer,信令服务器(后端服务)将其中转到 A。
A 收到 answer,调用 webrtc 连接实例的 setRemoteDescription 方法,将 answer 设置为自己的远端描述。
Переговоры со СМИ завершены
sturn/turn 服务器这时候不在接收到 bind request 请求了, 回应 A onIceCandidate(候选者), 这里面包含 A 的公网 IP 与端口。
A 发送候选者给信令服务器(后端服务), 信令服务器中转到 B。
B 调用 webrtc 连接实例的 addIceCandidate 方法将候选者 A(A 的公网 IP 与端口)添加到 B 的候选者列表。
sturn/turn 服务器这时候不在接收到 bind request 请求了, 回应 B onIceCandidate(候选者), 这里面包含 B 的公网 IP 与端口。
B 发送候选者给信令服务器(后端服务), 信令服务器中转到 A。
A 调用 webrtc 连接实例的 addIceCandidate 方法将候选者 B(B 的公网 IP 与端口)添加到 A 的候选者列表。
这时候 A/B 都拿到了对方的通信候选者(公网 IP 与端口)。
进行 P2P 选出最优线路。
B 调用 webrtc 连接实例 onAddStream 将 A 的视频音频流播放出来。
A 调用 webrtc 连接实例 onAddStream 将 B 的视频音频流播放出来(通话开始,对方的视频有了)。
这时候如果内网穿透失败了。
sturn/turn 服务器(其实是 turn 起作用)就会帮忙转发音频流,因为他已经有所有的候选者列表(所有的 IP 与端口)。
углубить понимание
本地开发,同一局域网,P2P 连接建立不需要 coturn 中转服务器,因为不需要打洞,websocket 通道使主机能够在同一管道内,相互发送 offer 和 answer,可以建立 p2p。
线上环境,不同局域网,websocket 通道使主机在同一管道内,相互发送 offer 和 answer,想要建立 p2p(一定要使用 sturn 进行 nat 穿越),但是运营商截断(建立 p2p 失败),使用 turn 中转音频流。
##У меня есть вопросы
Что такое P2P-соединение?
P2P 流媒体技术,这个技术主要是解决服务器负载过大(传统的都是服务器转发音频流),多端不经过服务器转发音频流,而是在网状的 P2P 通道内进行。下载资源、直播、音视频通话、共享桌面等很大一部分是基于此技术实现。
В чем суть предложения? Что такое СДП?
offer 就是个信令名字,创建 offer 本质上就是创建 sdp。
Позвольте мне показать вам суть sdp, которая представляет собой собственную информацию о носителе и информацию о кодеке.
sdp: {
type: 'offer',
sdp: 'v=0\r\n' +
'o=- 890410854023526853 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n' +
'a=group:BUNDLE audio video\r\n' +
'a=extmap-allow-mixed\r\n' +
'a=msid-semantic: WMS EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc\r\n' +
'm=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
'a=ice-ufrag:+b1T\r\n' +
'a=ice-pwd:2MMQo86tKV27zgrrsMhvhGqK\r\n' +
'a=ice-options:trickle\r\n' +
'a=fingerprint:sha-256 A0:F2:F7:C0:BE:1B:8C:EF:6C:42:03:D7:6E:6B:B2:DC:AE:57:F1:F3:DD:67:86:F6:11:F5:5B:44:49:D5:44:9A\r\n' +
'a=setup:actpass\r\n' +
'a=mid:audio\r\n' +
'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n' +
'a=sendrecv\r\n' +
'a=rtcp-mux\r\n' +
'a=rtpmap:111 opus/48000/2\r\n' +
'a=rtcp-fb:111 transport-cc\r\n' +
'a=fmtp:111 minptime=10;useinbandfec=1\r\n' +
'a=rtpmap:63 red/48000/2\r\n' +
'a=fmtp:63 111/111\r\n' +
'a=rtpmap:103 ISAC/16000\r\n' +
'a=rtpmap:104 ISAC/32000\r\n' +
'a=rtpmap:9 G722/8000\r\n' +
'a=rtpmap:0 PCMU/8000\r\n' +
'a=rtpmap:8 PCMA/8000\r\n' +
'a=rtpmap:106 CN/32000\r\n' +
'a=rtpmap:105 CN/16000\r\n' +
'a=rtpmap:13 CN/8000\r\n' +
'a=rtpmap:110 telephone-event/48000\r\n' +
'a=rtpmap:112 telephone-event/32000\r\n' +
'a=rtpmap:113 telephone-event/16000\r\n' +
'a=rtpmap:126 telephone-event/8000\r\n' +
'a=ssrc:1511813723 cname:P0KGpA3OHyfIh1hw\r\n' +
'a=ssrc:1511813723 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc a3daa1c2-1f35-426f-a242-2a0286202c04\r\n' +
'a=ssrc:1511813723 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc\r\n' +
'a=ssrc:1511813723 label:a3daa1c2-1f35-426f-a242-2a0286202c04\r\n' +
'm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=rtcp:9 IN IP4 0.0.0.0\r\n' +
'a=ice-ufrag:+b1T\r\n' +
'a=ice-pwd:2MMQo86tKV27zgrrsMhvhGqK\r\n' +
'a=ice-options:trickle\r\n' +
'a=fingerprint:sha-256 A0:F2:F7:C0:BE:1B:8C:EF:6C:42:03:D7:6E:6B:B2:DC:AE:57:F1:F3:DD:67:86:F6:11:F5:5B:44:49:D5:44:9A\r\n' +
'a=setup:actpass\r\n' +
'a=mid:video\r\n' +
'a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\n' +
'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
'a=extmap:13 urn:3gpp:video-orientation\r\n' +
'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n' +
'a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n' +
'a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n' +
'a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n' +
'a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n' +
'a=sendrecv\r\n' +
'a=rtcp-mux\r\n' +
'a=rtcp-rsize\r\n' +
'a=rtpmap:96 VP8/90000\r\n' +
'a=rtcp-fb:96 goog-remb\r\n' +
'a=rtcp-fb:96 transport-cc\r\n' +
'a=rtcp-fb:96 ccm fir\r\n' +
'a=rtcp-fb:96 nack\r\n' +
'a=rtcp-fb:96 nack pli\r\n' +
'a=rtpmap:97 rtx/90000\r\n' +
'a=fmtp:97 apt=96\r\n' +
'a=rtpmap:98 VP9/90000\r\n' +
'a=rtcp-fb:98 goog-remb\r\n' +
'a=rtcp-fb:98 transport-cc\r\n' +
'a=rtcp-fb:98 ccm fir\r\n' +
'a=rtcp-fb:98 nack\r\n' +
'a=rtcp-fb:98 nack pli\r\n' +
'a=fmtp:98 profile-id=0\r\n' +
'a=rtpmap:99 rtx/90000\r\n' +
'a=fmtp:99 apt=98\r\n' +
'a=rtpmap:100 VP9/90000\r\n' +
'a=rtcp-fb:100 goog-remb\r\n' +
'a=rtcp-fb:100 transport-cc\r\n' +
'a=rtcp-fb:100 ccm fir\r\n' +
'a=rtcp-fb:100 nack\r\n' +
'a=rtcp-fb:100 nack pli\r\n' +
'a=fmtp:100 profile-id=2\r\n' +
'a=rtpmap:101 rtx/90000\r\n' +
'a=fmtp:101 apt=100\r\n' +
'a=rtpmap:102 H264/90000\r\n' +
'a=rtcp-fb:102 goog-remb\r\n' +
'a=rtcp-fb:102 transport-cc\r\n' +
'a=rtcp-fb:102 ccm fir\r\n' +
'a=rtcp-fb:102 nack\r\n' +
'a=rtcp-fb:102 nack pli\r\n' +
'a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\n' +
'a=rtpmap:121 rtx/90000\r\n' +
'a=fmtp:121 apt=102\r\n' +
'a=rtpmap:127 H264/90000\r\n' +
'a=rtcp-fb:127 goog-remb\r\n' +
'a=rtcp-fb:127 transport-cc\r\n' +
'a=rtcp-fb:127 ccm fir\r\n' +
'a=rtcp-fb:127 nack\r\n' +
'a=rtcp-fb:127 nack pli\r\n' +
'a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\r\n' +
'a=rtpmap:120 rtx/90000\r\n' +
'a=fmtp:120 apt=127\r\n' +
'a=rtpmap:125 H264/90000\r\n' +
'a=rtcp-fb:125 goog-remb\r\n' +
'a=rtcp-fb:125 transport-cc\r\n' +
'a=rtcp-fb:125 ccm fir\r\n' +
'a=rtcp-fb:125 nack\r\n' +
'a=rtcp-fb:125 nack pli\r\n' +
'a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n' +
'a=rtpmap:107 rtx/90000\r\n' +
'a=fmtp:107 apt=125\r\n' +
'a=rtpmap:108 H264/90000\r\n' +
'a=rtcp-fb:108 goog-remb\r\n' +
'a=rtcp-fb:108 transport-cc\r\n' +
'a=rtcp-fb:108 ccm fir\r\n' +
'a=rtcp-fb:108 nack\r\n' +
'a=rtcp-fb:108 nack pli\r\n' +
'a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\r\n' +
'a=rtpmap:109 rtx/90000\r\n' +
'a=fmtp:109 apt=108\r\n' +
'a=rtpmap:35 AV1/90000\r\n' +
'a=rtcp-fb:35 goog-remb\r\n' +
'a=rtcp-fb:35 transport-cc\r\n' +
'a=rtcp-fb:35 ccm fir\r\n' +
'a=rtcp-fb:35 nack\r\n' +
'a=rtcp-fb:35 nack pli\r\n' +
'a=rtpmap:36 rtx/90000\r\n' +
'a=fmtp:36 apt=35\r\n' +
'a=rtpmap:124 H264/90000\r\n' +
'a=rtcp-fb:124 goog-remb\r\n' +
'a=rtcp-fb:124 transport-cc\r\n' +
'a=rtcp-fb:124 ccm fir\r\n' +
'a=rtcp-fb:124 nack\r\n' +
'a=rtcp-fb:124 nack pli\r\n' +
'a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\r\n' +
'a=rtpmap:119 rtx/90000\r\n' +
'a=fmtp:119 apt=124\r\n' +
'a=rtpmap:123 H264/90000\r\n' +
'a=rtcp-fb:123 goog-remb\r\n' +
'a=rtcp-fb:123 transport-cc\r\n' +
'a=rtcp-fb:123 ccm fir\r\n' +
'a=rtcp-fb:123 nack\r\n' +
'a=rtcp-fb:123 nack pli\r\n' +
'a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\r\n' +
'a=rtpmap:118 rtx/90000\r\n' +
'a=fmtp:118 apt=123\r\n' +
'a=rtpmap:114 red/90000\r\n' +
'a=rtpmap:115 rtx/90000\r\n' +
'a=fmtp:115 apt=114\r\n' +
'a=rtpmap:116 ulpfec/90000\r\n' +
'a=ssrc-group:FID 1741155232 1898443615\r\n' +
'a=ssrc:1741155232 cname:P0KGpA3OHyfIh1hw\r\n' +
'a=ssrc:1741155232 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc fb34f344-fbe3-45e9-969d-af4d9fb5bdc4\r\n' +
'a=ssrc:1741155232 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc\r\n' +
'a=ssrc:1741155232 label:fb34f344-fbe3-45e9-969d-af4d9fb5bdc4\r\n' +
'a=ssrc:1898443615 cname:P0KGpA3OHyfIh1hw\r\n' +
'a=ssrc:1898443615 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc fb34f344-fbe3-45e9-969d-af4d9fb5bdc4\r\n' +
'a=ssrc:1898443615 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc\r\n' +
'a=ssrc:1898443615 label:fb34f344-fbe3-45e9-969d-af4d9fb5bdc4\r\n'
}
Что делает метод setLocalDescription? Что делает setRemoteDescription?
setLocalDescription 是让自己的 webrtc 实例清楚,自己的媒体信息和编解码信息。
setRemoteDescription 是让对方的 webrtc 实例清楚,我的媒体信息和编解码信息。
Предложение, ответ, мы оба знаем информацию о медиа и кодеках друг друга, так что мы можем хорошо договориться, какой метод я должен использовать для декодирования и рендеринга ваших видео- и аудиопотоков.
Какого черта отправить запрос на сервер ретрансляции поворота/поворота для сбора кандидатов?
Процесс немного сложный, для конкретного процесса вы можете прочитать эту статьюНачальное понимание протокола WebRTC TURN и практика работы с TurnServer.
Цель
Понимание захвата аудио и видео webrtc, захвата рабочего стола;
Понимать весь процесс установления ссылок websocket и webrtc;
Реализовать передачу текста 1V1, видеозвонок, голосовой вызов, совместное использование экрана;
Реализовать онлайн-воспроизведение и загрузку скриншотов, записей, записей экрана и скриншотов, записей и записей экрана во время видеозвонков, голосовых вызовов и демонстрации экрана;
Разверните вышеуказанные функции онлайн;
Построение процесса
Здесь мы хотим нарисовать базовую блок-схему процесса создания аудио и видео.
Основная блок-схема
Для этих сигнализаций мы используем websocket для переадресации, тут вы спросите, а почему бы не использовать http?
Во-первых, демонстрация, которую мы хотим реализовать, изначально содержит функцию отправки обычных текстовых сообщений, которая заключается в использовании веб-сокета. (Длинный и короткий опрос устарел и имеет низкую производительность)
Во-вторых, первый пункт можно проигнорировать, http-запрос вернется к исходному пути, A запрашивает сервер, и никогда не будет передан B.
Но если мы хотим использовать websocket для пересылки сигналов, нам нужно четко понимать, что это сообщение будет получено на всех концах одного и того же конвейера. Следовательно, для приведенной выше блок-схемы все маленькие стрелки на самом деле являются двунаправленными.
В это время мы можем контролировать направление push-сообщения в службе узла или мы можем управлять им на стороне клиента.Здесь я выбираю управление им на стороне AB.
Во-вторых, мы разрабатываем локально, если мы используем один компьютер и два браузера, возможны текстовые сообщения через веб-сокет. Однако аудио- и видеовызовы использовать нельзя, так как конфликтуют как канал передачи, так и аудио- и видеоустройства (микрофоны, динамики и т. д.). Следовательно, мы можем решить эту проблему, используя два компьютера через одну и ту же локальную сеть. И из-за ограничений безопасности webrtc необходимо использовать https (будь то онлайн или локально) и доменное имя Мы можем решить эту проблему, настроив https и доменное имя онлайн, настроив браузер локально на игнорирование https и настроив сопоставление файлов хоста.
Затем мы используем vue и nodejs для реализации демонстрации самым быстрым и простым способом.
Хватит нести чушь, порвем!
рвать
покажи код
socket-io
Здесь я использую socket.io, сторонний пакет, для быстрой отправки сообщений и передачи сигналов. (Рекомендуется использовать vue-socket.io) Вы можете отправлять и получать сообщения и сигналы в компонентах.
Веб-сокет socket-io устанавливает соединение, получает сообщения и помещает полученные сигналы в vuex.
async connectSocket({ commit, state, dispatch }) {
// 局域网
let socket = io.connect("http://172.28.74.16:3004");
// 线上
// let socket = io.connect("https://www.codeting.top:3004");
socket.on("connect", () => {
commit(types.SET_SOCKET, socket);
dispatch("handleChatData", state.friends);
});
// 监听好友发过来的消息
socket.on("friendMessage", (res) => {
if (res) {
commit(types.SET_MESSAGE, res);
} else {
console.log("有问题");
}
});
socket.on("apply", (res) => {
if (!state.isCalling) {
let text = "";
if (res.webRtcType === "video") {
text = "视频通话";
}
MessageBox(
`您的好友${res.userName}请求与你进行${text}, 是否同意?`,
"提示",
{
confirmButtonText: "同意",
cancelButtonText: "拒绝",
type: "warning",
}
)
.then(() => {
const friendInfo = {
account: res.userName,
_id: res.Id,
};
commit(types.SET_FRIENDINFO, friendInfo);
commit(types.SET_ISCALLING, true);
commit(types.SET_WEBRTCSTATE, "reply");
})
.catch(() => {
console.log("9. 拒绝接听");
});
} else {
}
});
socket.on("reply", (res) => {
if (res) {
localStorage.setItem("nowRoomId", res.roomId);
commit(types.SET_REPLYISAGREE, true);
}
});
socket.on("1v1ICEA", (res) => {
if (res && state.role === "receiver") {
commit(types.SET_ECE, res);
}
});
socket.on("1v1ICEB", (res) => {
if (res && state.role === "caller") {
commit(types.SET_ECE, res);
}
});
socket.on("1v1OFFER", (res) => {
if (res) {
commit(types.SET_ISOFFER, true);
commit(types.SET_OFFER, res);
}
});
socket.on("1v1ANSWER", (res) => {
if (res) {
commit(types.SET_ISANSWER, true);
commit(types.SET_ANSWER, res);
}
});
},
Точно так же в службе узла мы также используем пакет socket-io.
io.on("connect", function (socket) {
socket.on('friendMessage', async function (res) {
const roomId = res.userId > res.friendId ? res.userId + res.friendId : res.friendId + res.userId
io.to(roomId).emit('friendMessage', res)
});
socket.on('apply', async function (res) {
io.to(res.roomId).emit('apply', res)
});
socket.on('reply', data => {
io.to(data.roomId).emit('reply', data)
})
socket.on('1v1ICEA', data => {
console.log('1v1ICEA', data)
io.to(data.roomId).emit('1v1ICEA', data)
})
socket.on('1v1ICEB', data => {
io.to(data.roomId).emit('1v1ICEB', data)
})
socket.on('1v1OFFER', data => { // 转发 Offer
io.to(data.roomId).emit('1v1offer', data)
})
socket.on('1v1ANSWER', data => { // 转发 answer
io.to(data.roomId).emit('1v1answer', data)
})
});
Аудио и видео коллекция
Код аналогичен для видео, аудио и демонстрации экрана. Так, например, захват видео.
const constraints = {
audio: {
noiseSuppression: true,
echoCancellation: true,
},
video: true,
};
this.localstream = navigator.mediaDevices.getUserMedia(constraints);
let video = document.querySelector("#rtcA");
video.srcObject = this.localstream;
Используя getUserMedia, мы можем захватить двухканальный аудио- и видеопоток мультимедиа. Мы передаем ограничения параметра, которые можно настроить (управление захватом аудио или видео).
Назначьте захваченному динамическому медиапотоку тег видео, и наше собственное изображение будет отображаться на веб-странице.
Точно так же, если это захват звука, вам нужно только настроить видео в параметрах ограничений на false .
Для захвата экрана компьютера просто замените API getUserMedia на getDisplayMedia.
процесс передачи
В это время инициатор окончания видео должен отправить сигнализацию применения получателю после сбора медиапотока. Эта сигнализация предназначена для запроса получателю ответа на видеовызов.
При подключении получатель будет собирать свой собственный двухдорожечный медиапоток аудио и видео, инициализировать одноранговое соединение, помещать медиапоток в этот конвейер, отслеживать информацию кандидата ICE, если она будет собрана, отправлять ее другой стороне и отвечать с помощью собственный сигнал согласия и ответ автору видео.
Если он отказывается принимать вызов, приемник ответит инициатору видеосигналом об отказе.
В это время принимающая сторона получает отказ и закрывает набор видео- и аудиопотоков.
Когда принимающая сторона получает прием в это время, она инициализирует одноранговое соединение, помещает свой собственный медиапоток в этот конвейер и отслеживает информацию кандидата ICE.Если она будет собрана, она будет отправлена другой стороне. И создайте предложение (это предложение включает в себя sdp), после размещения предложения локально отправьте предложение на видеоприемник.
Видеоприемник получает предложение, размещает его на своем удаленном конце, формирует ответ, размещает ответ локально и отправляет его инициатору видео.
Создатель видео получает ответ и отправляет ответ на удаленный конец.
props: {
visible: Boolean,
friendInfo: Object,
webRtcType: String,
},
data() {
return {
showDialog: false,
localstream: {},
peer: null,
isToPeer: false,
iceServers: {
iceServers: [
{
url: "turn:www.codeting.top:3478", // xxxxx 为域名
credential: "xxxxx", // 密码
username: "xx", // 账号
},
],
sdpSemantics: "plan-b",
},
bufferFriend: [],
bufferMy: [],
mediaRecorder: {},
startRecordVideo: true,
};
},
watch: {
visible(val) {
this.handleVisible(val);
if (!val) {
this.$store.commit("chat/SET_ISCALLING", false);
}
},
"$store.state.chat.replyIsAgree"(v) {
if (v && this.$store.state.chat.role === "caller") {
let roomId = getLocalStorage("nowRoomId").nowRoomId;
this.initPeer({ roomId, webRtcType: "video" });
this.createOffer({ roomId, webRtcType: "video" });
}
},
"$store.state.chat.isEce"(v) {
if (v && this.$store.state.chat.role === "receiver") {
if (this.$store.state.chat.ece) {
this.onIce(this.$store.state.chat.ece);
}
}
if (v && this.$store.state.chat.role === "caller") {
if (this.$store.state.chat.ece) {
this.onIce(this.$store.state.chat.ece);
}
}
},
"$store.state.chat.isOffer"(v) {
if (v && this.$store.state.chat.role === "receiver") {
if (this.$store.state.chat.offer) {
this.onOffer(this.$store.state.chat.offer);
}
}
},
"$store.state.chat.isAnswer"(v) {
if (v && this.$store.state.chat.role === "caller") {
if (this.$store.state.chat.answer) {
this.onAnswer(this.$store.state.chat.answer);
}
}
},
},
methods: {
handleVisible(val) {
this.showDialog = val;
this.$emit("update:visible", val);
},
async apply() {
let constraints = null;
if (this.webRtcType === "video") {
constraints = {
audio: {
noiseSuppression: true,
echoCancellation: true,
},
video: true,
};
} else {
constraints = {
audio: true,
video: false,
};
}
this.localstream = await navigator.mediaDevices.getUserMedia(constraints);
let video = document.querySelector("#rtcA");
video.srcObject = this.localstream;
const userId = getCookie();
const friendId = this.friendInfo._id;
let roomId = userId > friendId ? userId + friendId : friendId + userId;
this.$store.state.chat.socket.emit("apply", {
webRtcType: this.webRtcType,
roomId: roomId,
userName: getLocalStorage("account").account,
id: userId,
});
},
reply(roomId) {
this.$store.state.chat.socket.emit("reply", {
roomId,
webRtcType: this.webRtcType,
});
},
async createP2P(data) {
await this.createMedia(data);
},
async createMedia(data) {
try {
let constraints = null;
if (this.webRtcType === "video") {
constraints = {
audio: {
noiseSuppression: true,
echoCancellation: true,
},
video: true,
};
} else {
constraints = {
audio: true,
video: false,
};
}
this.localstream = await navigator.mediaDevices.getUserMedia(
constraints
);
let video = document.querySelector("#rtcA");
video.srcObject = this.localstream;
} catch (e) {
console.log("getUserMedia: ", e);
}
await this.initPeer(data);
},
initPeer(data) {
let PeerConnection =
window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
this.peer = new PeerConnection(this.iceServers);
this.peer.addStream(this.localstream);
window.streamMy = this.localstream;
this.peer.onicecandidate = (event) => {
if (event.candidate && this.$store.state.chat.role === "caller") {
this.$store.state.chat.socket.emit("1v1ICEA", {
...data,
sdp: event.candidate,
});
}
if (event.candidate && this.$store.state.chat.role === "receiver") {
this.$store.state.chat.socket.emit("1v1ICEB", {
...data,
sdp: event.candidate,
});
}
};
this.peer.onaddstream = (event) => {
window.streamFriend = event.stream;
this.isToPeer = true;
let video = document.querySelector("#rtcB");
video.srcObject = event.stream;
};
},
async createOffer(data) {
try {
let offer = await this.peer.createOffer({
offerToReceiveAudio: 1,
offerToReceiveVideo: 1,
});
await this.peer.setLocalDescription(offer);
this.$store.state.chat.socket.emit("1v1offer", {
...data,
sdp: offer,
});
} catch (e) {
console.log("createOffer: ", e);
}
},
async onIce(data) {
try {
await this.peer.addIceCandidate(data.sdp);
} catch (e) {
console.log("onAnswer: ", e);
}
},
async onOffer(data) {
try {
await this.peer.setRemoteDescription(data.sdp);
let answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
this.$store.state.chat.socket.emit("1v1answer", {
...data,
sdp: answer,
});
} catch (e) {
console.log("onOffer: ", e);
}
},
async onAnswer(data) {
try {
await this.peer.setRemoteDescription(data.sdp);
} catch (e) {
console.log("onAnswer: ", e);
}
},
}
Воспроизвести экран другой стороны
В этот момент и получатель, и инициатор отслеживают информацию о кандидатах ICE, и если они будут собраны, они будут отправлены другой стороне. После того, как он отслеживается, он назначит динамический медиапоток другой стороны B и воспроизведет его.
if (event.candidate && this.$store.state.chat.role === "receiver") {
this.$store.state.chat.socket.emit("1v1ICEB", {
...data,
sdp: event.candidate,
});
}
Скриншоты и аудио- и видеозапись
Скриншот: мы можем воспользоваться преимуществами связанных методов, используя холстgetContext("2d").drawImage, чтобы добиться захвата изображения на веб-уровне.
let picture = document.querySelector("#picture");
let rtcA = document.querySelector("#rtcA");
picture.getContext("2d").drawImage(rtcA, 0, 0, 200, 120);
Аудио/видео/запись экрана: используйтеMediaRecorderСохраните наш медиапоток или медиапоток другой стороны в массив.
let that = this;
let options = {
mineType: "video/webm;code=vp8",
};
if (!MediaRecorder.isTypeSupported(options.mineType)) {
console.error(`${options.mineType}is not supported`);
}
try {
this.mediaRecorder = new MediaRecorder(window.streamFriend, options);
} catch (error) {
console.error(error, "失败");
return;
}
// 当数据有效时触发的事件
this.mediaRecorder.ondataavailable = function(e) {
if (e && e.data && e.data.size > 0) {
that.bufferFriend.push(e.data);
}
};
this.mediaRecorder.start(10);
Воспроизведение аудио и видео записи
Просто назначьте сохраненный статический медиапоток тегу видео.
let recplayer = document.querySelector("#recplayer");
let blob = new Blob(this.bufferFriend, { type: "video/webm" });
recplayer.src = window.URL.createObjectURL(blob);
recplayer.srcObject = null;
recplayer.controls = true;
recplayer.play();
Скачать аудио и видео запись
Точно так же мы можем загружать аудио- и видеопотоки.
download(videoName) {
var blob = new Blob(this.bufferFriend, { type: "video/webm" });
var url = window.URL.createObjectURL(blob);
var downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.style.display = "none";
downloadLink.download = `${videoName}.webm`; //可以命名任意格式例如 mp4/webm 等等,其中 webm 格式可以使用浏览器播放视频
downloadLink.click();
document.body.removeChild(downloadLink)
},
местный эффект
общий доступ к рабочему столу
видеозвонок
онлайн
Два важных условия для развертывания webrtc: имя домена и https, нам нужно настроить эти два.
Наша служба узла — это не только https + доменное имя, веб-сокету также нужен более безопасный протокол wss, нам нужно настроить wss для нашего веб-сокета.
Как мы упоминали ранее, причина, по которой локальная разработка может быть успешной и эффективной, заключается в том, что интрасеть взаимодействует напрямую, а не через общедоступную сеть, поэтому проникновение в интрасеть отсутствует.
Если мы хотим реализовать эту функцию онлайн, мы должны настроить сервер ретрансляции coturn. Статьи по настройке ядра Centos могут ссылаться наэто, справочник по конфигурации для ядра ubuntuэто.
дефект
Следующие проблемы могут быть обнаружены после разработки и запуска.
Среда, оборудование, переполнение сигнала, шум из-за несовместимости алгоритмов, эхо из-за акустики и линий, перегрузка сети и задержка из-за нестабильной скорости передачи пакетов.
Мы можем уменьшить эхо-шум и задержки, добавив некоторые алгоритмы и улучшив качество аппаратных устройств.
Для шума можно установить при сборе аудиоnoiseSuppression: true, что может уменьшить шум некоторых сред и оборудования.
Для эха его можно установить при захвате звука.echoCancellation: true, эхо можно убрать.
Алгоритмы, устройства и сети справляются с остальными.
На этом аспекте исследования я остановлюсь здесь, вы можете продолжить изучениеКак передача WebRTC обеспечивает качество аудио- и видеосервисов, изучите, как зрелые приложения решают эти три проблемы.
спецэффекты
Вы можете использовать различные спецэффекты во время видеозвонка. Красота, наклейки и многое другое.
Однако в веб-сфере webrtc область спецэффектов для видео очень скрыта. Причиной этого является проблема с производительностью js.
Более простой способ — использовать холст холста и добавить слой фильтра к нашему видеоизображению, но это не меняет сути медиапотока. Передача на дальний конец по-прежнему не дает никакого эффекта. Конечно, мы можем управлять спецэффектами удаленного видео через веб-сокет, но поскольку видеопоток не изменился, если другая сторона загрузит видеопоток, спецэффектов все равно не будет.
Другое решение заключается в следующем, я не буду здесь вдаваться в подробности, вы можете подумать над тем, как оно реализовано (далее простые спецэффекты и стикеры).
многопользовательское видео
Нам нужно создать n-1 соединений PeerConnection, потому что мы хотим обмениваться видео с n-1 людьми, со всеми. Но здесь будет затронут вопрос о том, кто активно выдает предложение. У нас могут быть новые участники, отправляющие предложения n-1 другим участникам, или n-1 участники могут отправлять предложения новым участникам. Здесь мы можем сгенерировать PeerConnection и предложить обходным способом. Конечно, это зависит от того, насколько хорошо ваш сервер может обрабатывать несколько вызовов.
Здесь мы неосознанно используем известную коммуникационную схему многотерминальной связи — Mesh, Mesh заключается в том, чтобы обмениваться данными попарно, чтобы образовать ячеистую структуру. В дополнение к коммуникационному решению Mesh существует также MCU.Решение MCU в основном смешивает аудио- и видеопотоки всех терминалов в одной комнате и отправляет их на каждый терминал, поэтому нагрузка на сервер на самом деле очень велика. Кроме того, существует схема связи SFU: после получения аудио- и видеопотока терминала сервер ретрансляции пересылает его на другие терминалы.
Суммировать
После описанной выше серии осмысления, размышлений, построения, разработки, развертывания и т. д. у нас есть некоторое предварительное понимание и понимание webrtc. Мы должны продолжать углубленные исследования и исследования в этой области. Потому что удовлетворить наше любопытство и жажду знаний, улучшить наши технологии в этой области и обогатить нашу общую систему знаний, почему бы и не сделать это.
Наконец, все вышеперечисленное содержание получено из данных, личных экспериментов и личных резюме.Если в тексте есть ошибки, я надеюсь, что вы исправите их вовремя.
Ссылаться на
Вуууу. HTML5rocks.com/ru/tutorial…