范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

做一个视频通话给自己用吧

  讲前须知
  WebRTC ,名称源自网页即时通信 (英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的 API。它于 2011 年 6 月 1 日开源并在 Google、Mozilla、Opera 支持下被纳入万维网联盟的 W3C 推荐标准。
  首先,他即是 API 也是协议。
  其次,他是浏览器进行音频与视频通话的 API,其实还有屏幕共享的功能。
  最后,它现在已经处于 W3C 标准,各大浏览器厂商已经对他进行兼容了。
  但是如果我们想使用好 webrtc,就得先了解 websocket。而对于 websocket,大家应该都比较熟悉了,比如社交聊天、多人游戏、协同编辑、视频会议、基于位置的应用(地图)、等等需要高实时的场景。我们比较常用的微信、QQ、某些直播软件等等也都是基于 websocket 实现消息与信令的转发。大家看到这里可能在信令这里迟疑了,接着看。
  webrtc 是 P2P 的一种技术,什么是 P2P?其实就是 端对端,就说是你的音频、视频流不经过服务器中转,直接由一端发送到另一端。
  不经过服务器中转,也就说时候,如果通过过程中服务器突然崩溃,是不是通话还能继续?
  是的!但是发送音频视频流前,一定是需要建立 P2P 连接的,建立连接前一定需要服务器进行信令转发,这个信令就是通话两端的标识。
  而如果想用 webrtc 实现通话,就得先中转信令、建立连接。而建立连接的话最好是要用 websocket 进行信令转发的。大家都知道,websocket 是个通道,在这个通道的所有端,都可以收到任意一端的消息流,包括发消息的本人。
  为什么不经过服务器就可以直接获取到对方的视频音频流呢?是因为建立了 P2P 通道,这个 P2P 在中转信令的时候就已经通了,传输视频音频流的时候还要啥服务器啊。这个时候,肯定有小伙伴表示怀疑,音频视频流可以不通过服务器?是的,我骗了大家,确实要经过服务器,但是只是线上需要服务器转发,如果我们是本地两台或者多台同一局域网的端 进行 webrtc 音频视频流的转发,确实不需要中转服务器,但是线上有可能需要,也有可能不需要,这里又涉及到了一个打洞 的概念。
  我们平常可能会听到比较牛 x 的词汇,什么打洞、内网穿透、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 的本质是什么?SDP 又是什么?offer 就是个信令名字,创建 offer 本质上就是创建 sdp。
  给大家看看 sdp 的本质,就是自身的媒体信息和编解码信息sdp: {     type: "offer",     sdp: "v=0r " +       "o=- 890410854023526853 2 IN IP4 127.0.0.1r " +       "s=-r " +       "t=0 0r " +       "a=group:BUNDLE audio videor " +       "a=extmap-allow-mixedr " +       "a=msid-semantic: WMS EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Ycr " +       "m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126r " +       "c=IN IP4 0.0.0.0r " +       "a=rtcp:9 IN IP4 0.0.0.0r " +       "a=ice-ufrag:+b1Tr " +       "a=ice-pwd:2MMQo86tKV27zgrrsMhvhGqKr " +       "a=ice-options:trickler " +       "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:9Ar " +       "a=setup:actpassr " +       "a=mid:audior " +       "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levelr " +       "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timer " +       "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01r " +       "a=sendrecvr " +       "a=rtcp-muxr " +       "a=rtpmap:111 opus/48000/2r " +       "a=rtcp-fb:111 transport-ccr " +       "a=fmtp:111 minptime=10;useinbandfec=1r " +       "a=rtpmap:63 red/48000/2r " +       "a=fmtp:63 111/111r " +       "a=rtpmap:103 ISAC/16000r " +       "a=rtpmap:104 ISAC/32000r " +       "a=rtpmap:9 G722/8000r " +       "a=rtpmap:0 PCMU/8000r " +       "a=rtpmap:8 PCMA/8000r " +       "a=rtpmap:106 CN/32000r " +       "a=rtpmap:105 CN/16000r " +       "a=rtpmap:13 CN/8000r " +       "a=rtpmap:110 telephone-event/48000r " +       "a=rtpmap:112 telephone-event/32000r " +       "a=rtpmap:113 telephone-event/16000r " +       "a=rtpmap:126 telephone-event/8000r " +       "a=ssrc:1511813723 cname:P0KGpA3OHyfIh1hwr " +       "a=ssrc:1511813723 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc a3daa1c2-1f35-426f-a242-2a0286202c04r " +       "a=ssrc:1511813723 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Ycr " +       "a=ssrc:1511813723 label:a3daa1c2-1f35-426f-a242-2a0286202c04r " +       "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 116r " +       "c=IN IP4 0.0.0.0r " +       "a=rtcp:9 IN IP4 0.0.0.0r " +       "a=ice-ufrag:+b1Tr " +       "a=ice-pwd:2MMQo86tKV27zgrrsMhvhGqKr " +       "a=ice-options:trickler " +       "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:9Ar " +       "a=setup:actpassr " +       "a=mid:videor " +       "a=extmap:14 urn:ietf:params:rtp-hdrext:toffsetr " +       "a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timer " +       "a=extmap:13 urn:3gpp:video-orientationr " +       "a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01r " +       "a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delayr " +       "a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typer " +       "a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timingr " +       "a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacer " +       "a=sendrecvr " +       "a=rtcp-muxr " +       "a=rtcp-rsizer " +       "a=rtpmap:96 VP8/90000r " +       "a=rtcp-fb:96 goog-rembr " +       "a=rtcp-fb:96 transport-ccr " +       "a=rtcp-fb:96 ccm firr " +       "a=rtcp-fb:96 nackr " +       "a=rtcp-fb:96 nack plir " +       "a=rtpmap:97 rtx/90000r " +       "a=fmtp:97 apt=96r " +       "a=rtpmap:98 VP9/90000r " +       "a=rtcp-fb:98 goog-rembr " +       "a=rtcp-fb:98 transport-ccr " +       "a=rtcp-fb:98 ccm firr " +       "a=rtcp-fb:98 nackr " +       "a=rtcp-fb:98 nack plir " +       "a=fmtp:98 profile-id=0r " +       "a=rtpmap:99 rtx/90000r " +       "a=fmtp:99 apt=98r " +       "a=rtpmap:100 VP9/90000r " +       "a=rtcp-fb:100 goog-rembr " +       "a=rtcp-fb:100 transport-ccr " +       "a=rtcp-fb:100 ccm firr " +       "a=rtcp-fb:100 nackr " +       "a=rtcp-fb:100 nack plir " +       "a=fmtp:100 profile-id=2r " +       "a=rtpmap:101 rtx/90000r " +       "a=fmtp:101 apt=100r " +       "a=rtpmap:102 H264/90000r " +       "a=rtcp-fb:102 goog-rembr " +       "a=rtcp-fb:102 transport-ccr " +       "a=rtcp-fb:102 ccm firr " +       "a=rtcp-fb:102 nackr " +       "a=rtcp-fb:102 nack plir " +       "a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001fr " +       "a=rtpmap:121 rtx/90000r " +       "a=fmtp:121 apt=102r " +       "a=rtpmap:127 H264/90000r " +       "a=rtcp-fb:127 goog-rembr " +       "a=rtcp-fb:127 transport-ccr " +       "a=rtcp-fb:127 ccm firr " +       "a=rtcp-fb:127 nackr " +       "a=rtcp-fb:127 nack plir " +       "a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001fr " +       "a=rtpmap:120 rtx/90000r " +       "a=fmtp:120 apt=127r " +       "a=rtpmap:125 H264/90000r " +       "a=rtcp-fb:125 goog-rembr " +       "a=rtcp-fb:125 transport-ccr " +       "a=rtcp-fb:125 ccm firr " +       "a=rtcp-fb:125 nackr " +       "a=rtcp-fb:125 nack plir " +       "a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01fr " +       "a=rtpmap:107 rtx/90000r " +       "a=fmtp:107 apt=125r " +       "a=rtpmap:108 H264/90000r " +       "a=rtcp-fb:108 goog-rembr " +       "a=rtcp-fb:108 transport-ccr " +       "a=rtcp-fb:108 ccm firr " +       "a=rtcp-fb:108 nackr " +       "a=rtcp-fb:108 nack plir " +       "a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01fr " +       "a=rtpmap:109 rtx/90000r " +       "a=fmtp:109 apt=108r " +       "a=rtpmap:35 AV1/90000r " +       "a=rtcp-fb:35 goog-rembr " +       "a=rtcp-fb:35 transport-ccr " +       "a=rtcp-fb:35 ccm firr " +       "a=rtcp-fb:35 nackr " +       "a=rtcp-fb:35 nack plir " +       "a=rtpmap:36 rtx/90000r " +       "a=fmtp:36 apt=35r " +       "a=rtpmap:124 H264/90000r " +       "a=rtcp-fb:124 goog-rembr " +       "a=rtcp-fb:124 transport-ccr " +       "a=rtcp-fb:124 ccm firr " +       "a=rtcp-fb:124 nackr " +       "a=rtcp-fb:124 nack plir " +       "a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001fr " +       "a=rtpmap:119 rtx/90000r " +       "a=fmtp:119 apt=124r " +       "a=rtpmap:123 H264/90000r " +       "a=rtcp-fb:123 goog-rembr " +       "a=rtcp-fb:123 transport-ccr " +       "a=rtcp-fb:123 ccm firr " +       "a=rtcp-fb:123 nackr " +       "a=rtcp-fb:123 nack plir " +       "a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001fr " +       "a=rtpmap:118 rtx/90000r " +       "a=fmtp:118 apt=123r " +       "a=rtpmap:114 red/90000r " +       "a=rtpmap:115 rtx/90000r " +       "a=fmtp:115 apt=114r " +       "a=rtpmap:116 ulpfec/90000r " +       "a=ssrc-group:FID 1741155232 1898443615r " +       "a=ssrc:1741155232 cname:P0KGpA3OHyfIh1hwr " +       "a=ssrc:1741155232 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc fb34f344-fbe3-45e9-969d-af4d9fb5bdc4r " +       "a=ssrc:1741155232 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Ycr " +       "a=ssrc:1741155232 label:fb34f344-fbe3-45e9-969d-af4d9fb5bdc4r " +       "a=ssrc:1898443615 cname:P0KGpA3OHyfIh1hwr " +       "a=ssrc:1898443615 msid:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Yc fb34f344-fbe3-45e9-969d-af4d9fb5bdc4r " +       "a=ssrc:1898443615 mslabel:EHsXxPKkpBfFwyGLbTIFHt4eXe6smVEHN9Ycr " +       "a=ssrc:1898443615 label:fb34f344-fbe3-45e9-969d-af4d9fb5bdc4r "   } setLocalDescription 方法是干什么的?setRemoteDescription 又是干什么的?setLocalDescription 是让自己的 webrtc 实例清楚,自己的媒体信息和编解码信息。 setRemoteDescription 是让对方的 webrtc 实例清楚,我的媒体信息和编解码信息。
  一个 offer,一个 answer,我们彼此都知道对方的媒体信息与编解码信息,这样我们才能好好协商,我这边该用什么方式对你的视频音频流进行解码、渲染。向 sturn/turn 中继服务器发送请求收集 candidate 是什么鬼?
  过程有些繁杂,具体流程小伙伴们可以看这篇文章 WebRTC TURN 协议初识及 turnserver 实践。目的
  了解 webrtc 的音视频采集、桌面采集;
  了解 websocket 和 webrtc 的整个链路建立过程;
  实现 1V1 文字传输、视频通话、语音通话、屏幕共享;
  实现视频通话、语音通话、屏幕共享过程中的截图、录音、录屏及 截图、录音、录屏的在线播放与下载;
  将以上功能部署上线;流程构建
  在这里,我们要对音视频建立过程画一个基本的流程图。
  基本流程图
  对于这些信令,我们使用 websocket 进行转发,这里大家会问,为什么不使用 http?
  首先,我们的要实现的 demo 本来就含有发送普通文本消息的功能,就是使用 websocket。(长短轮询太老了,性能太差)
  其次,第一点可以忽略,http 的请求会打回原路,A 向服务器请求,绝对不会传向 B。
  但是如果我们要使用 websocket 转发信令,就要清楚的了解,在同一管道的所有端都会收到这条消息。所以,对于上面流程图来说,其实所有的小箭头都是双向的。
  这时候,我们可以在 node 服务中来控制推送消息的方向,也可以在客户端来控制,这里我选择在 AB 端来控制。
  其次,我们在本地开发,如果使用一台电脑,两个浏览器的形式,websocket 文字消息是可以的。但是音视频通话不行,因为不管是传输通道还是音视频设备(麦克、扬声器等)都是冲突的。所以,我们可以通过同一局域网,使用两台电脑解决这个问题。并且,因为 webrtc 的安全限制,必须使用 https(不管是线上还是本地)与域名,我们可以通过线上配置 https 与域名,本地设置浏览器忽略 https 与配置 host 文件映射来解决这个问题。
  接下来,我们使用 vue 和 nodejs,可以最快最简单的实现 demo。
  废话少说,我们开撕!开撕
  展示部分代码socket-io
  这里我使用 socket.io 这个第三方包,快速的首发消息,转发信令。(建议大家使用 vue-socket.io)可以在组件中收发消息与信令。
  将 socket-io 的 websocket 建立连接与接收消息,接收信令放到 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);       }     });   },
  同样,我们在 node 服务中,也是使用 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,我们可以采集到音视频双轨的媒体流,我们传入一个参数 constraints,这个参数可以配置(控制采集音频还是视频)
  将采集到的动态媒体流赋值给 video 标签,我们自己的画面就显示在网页上了。
  同样,如果是音频采集,只需要配置参数 constraints 中的 audio 为 false 即可。
  电脑屏幕采集,只需要将 getUserMedia 这个 API 替换为 getDisplayMedia 即可。传输过程
  此时视频端发起端,采集到了媒体流后,需要发送 apply 信令给接收端。这个信令是询问接收端是否接起视频通话。
  如果接起,接收端会采集自己的音视频双轨的媒体流,并且初始化 peerconnection,将媒体流放入此管道,监听 ICE 候选信息 如果收集到,就发送给对方,并将自己的同意信令 reply,回复给视频发起端。
  如果拒绝接起,接收端会回复一个拒绝的信令给视频发起端。
  接收端此时收到拒绝,会关闭视频音频流的采集。
  接收端此时收到接起,会初始化 peerconnection,并将自己的媒体流放入此管道,监听 ICE 候选信息 如果收集到,就发送给对方。并且创建一个 offer(此 offer 包含 sdp),将 offer 放到本地后,发送 offer 给视频接收端。
  视频接收端接收到 offer,放到自己的远端,并且创建一个 answer,将 answer 放到本地后,发送给视频发起方。
  视频发起方接收到 answer,将 answer 放到远端。 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,      });  } 截图与录音录像录屏
  截图:我们可以使用 canvas 利用相关方法getContext("2d").drawImage  , 实现 web 层面的图片截取。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); 播放录音录像录屏
  只需要将保存的静态媒体流赋值给 video 标签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,我们需要配置这两块。
  我们的 node 服务不仅是 https+域名,websocket 也需要更为安全的 wss 协议,我们需要给我们的 websocket 配置 wss。
  在前面我们也提过,本地开发之所以能够成功,并且有效果,是因为内网是直接通信的,并没有走公网,也就没有实现内网穿透。
  如果我们想要在线上实现这个功能,我们必须配置 coturn 中转服务器。centos 内核的配置文章可以参考 这篇,ubuntu 内核的配置参考 这篇。缺陷
  在开发和上线后能够发现以下几个问题。
  环境、设备、信号溢出、算法不兼容产生的噪音、声学与线路产生的回音、网络拥塞及数据包传输速率不稳定产生的延迟。
  我们可以通过接入一些算法及提高硬件设备质量,来减少噪音回音,降低延迟。
  对于噪音,采集音频时可以设置 noiseSuppression: true  ,可以降低 一些环境及设备的噪音。
  对于回声,采集音频时可以设置 echoCancellation: true  ,可以去除回声。
  剩下的交给算法、设备和网络来处理了。
  在这方面的探索,我就到此为止了,大家可以接着研究 WebRTC 传输是如何保证音视频服务质量 ,研究一下成熟应用是如何解决这三大难点的。特效
  大家在视频通话过程中,可以使用多种特效。美颜、贴纸等等。
  然而在 webrtc 的 web 端领域,视频特效领域是非常潜的。造成这种情况的原因是 js 的性能问题。
  比较简单的方法就是使用 canvas 画布,对我们的视频图象加一层滤镜,但是在本质上并没有改变媒体流。传输到远端仍然是没有特效的。当然,我们可以通过 websocket 控制远端的视频特效,但是由于视频流没有改变,对方如果下载视频流的话,播放出来仍然是没有特效的。
  另一种方案如下,这里我就不做赘述,大家可以思考一下是如何实现的(以下为简单特效与贴纸)。
  多人视频
  需要创建 n-1 个 PeerConnection 连接,因为我们要与 n-1 个人进行视频共享,每个人都是这样。但是这里会涉及谁主动发 offer 的问题。我们可以让新加入的成员向其他 n-1 个成员发送 offer,也可以使 n-1 个成员向新加入的成员发送 offer。这里我们可以用遍历的方式生成 PeerConnection 和 offer。当然多人通话就看你服务器顶不顶的住了。
  这里我们就不知情的使用了多端通信的知名通信方案——Mesh,Mesh 就是两两通信从而形成网状结构。除了 Mesh 这种通信方案,还有 MCU,MCU 方案主要是将同一房间的所有终端的音视频流进行混合然后发向各个终端,这样服务器的压力其实是非常巨大的。另外还有 SFU 通信方案,中转服务器收到某终端音视频流后,单一转发到其他终端。总结
  经过上面一系列的理解、思考、构建、开发、部署等等,我们对 webrtc 有了一些初步的了解及认识。对于这方面的研究与探索我们都要继续继续深入下去。因为满足我们的好奇心与求知欲,提升我们的这一领域的技术,丰富我们整体的知识体系,何乐而不为呢。
  最后,以上所有的内容都来源于资料、个人实验及个人总结,文中有错的地方希望大家及时指正。

魔兽世界10。0版欧恩哈拉获取方法魔兽世界10。0版本有多个宠物坐骑能够获取,其中欧恩哈拉该怎么获得呢,想要了解的玩家请看下面魔兽世界10。0版欧恩哈拉获取方法,希望能够帮助大家。这是一个特殊的坐骑,它的使用方式不乌镇入风口来乌镇,看未来。始于2014年的世界互联网大会,已经走到第九个年头。互联网风起云涌,就如桥下千年流淌的京杭大运河,让千年古镇联通万物。深度覆盖的5G网络招手即停的无人驾驶出租构建有扎克伯格的元宇宙空荡荡除了钱什么都没有自从去年10月份扎克伯格将Facebook重新命名为Meta专注于元宇宙以来,一年多时间已经过去了。尽管扎克伯格全力以赴,但元宇宙情况似乎变得更糟糕。一些用户发现,当进入元宇宙平台初冬的新疆开都河畔白天鹅翩翩起舞日前已迎来70多羽白天鹅,它们是到此越冬的先遣军。白克斌摄预计12月初将有200到300羽天鹅陆续到此越冬。白克斌摄随着生态环境不断好转,越来越多的野生鸟类选择在这里越冬。白克斌摄一本书读懂中国园林背后的历史园林,是每个中国人心中的梦,它不仅仅只是一处景观,它更承载着中国文人寄托于生活之外的精神向往。如同中国山水画一样,中国园林虽是人工造景,但更讲究一种意境的营造。西方园林讲究的是规整科美诊断第二三大股东及其一致行动人拟合计减持不超过9公司股份科美诊断11月11日公告,第二大股东横琴君联致康投资企业(以下简称横琴君联致康)及其一致行动人第六大股东LOYALCLASSLIMITED(以下简称LOYALCLASS)拟合计减持北交所将于11月12日配合中国结算开展新增北交所证券类别通关测试北京商报讯(记者丁宁)11月11日晚间,北交所官网显示,北交所拟于近期配合中国证券登记结算有限责任公司(以下简称中国结算)开展新增北交所证券类别通关测试,具体有以下四方面内容。其一云旭桂活到老经到老头条创作挑战赛活到老经到老如果你提前知道,你要面临的人生,你是否还有勇气,重来一次呢?大抵这段话说的意思是,如果你提前知道你有这样一番人生,你是否还有勇气重过一次呢?最近关注到老罗天外来物造访地球有多容易?近日,天文学家又在地球与金星轨道上发现了三颗近地小行星,其中还隐藏着一颗直径1。5公里对地球具有潜在威胁的天体。让人不禁想起十月初,一则新闻上了热搜人类首次干预小行星轨道成功。据报美发射宇宙神5火箭,成功进行充气式返回舱试验任务美充气式返回舱模拟图。美宇宙神5火箭成功发射气象卫星和充气式返回舱,返回舱验证了充气式再入减速技术,成功着陆地面。据美国太空新闻网11月10日报道,美国当地时间10日,宇宙神5号火研究格陵兰岛部分冰盖变薄比预想严重丹麦美国等国研究人员9日公布的一项研究结果显示,格陵兰岛东北部冰盖变薄范围向内陆延伸200至300公里,范围比预想大,可能导致到本世纪末海平面上升至多15。5毫米。科学家此前关注格售价贵了8倍!爱马仕被斯凯奇起诉,指其鞋底设计侵犯多项专利权美国运动品牌斯凯奇起诉法国奢侈品牌爱马仕侵犯其鞋底技术专利。当地时间10月18日,美国运动品牌SkechersUSA,Inc。(斯凯奇,NYSESKX)在官网发布英文新闻稿宣布,斯曼联官方宣布C罗内部停赛无缘对阵切尔西C罗发文疑似回应北京时间10月21日凌晨,曼联俱乐部在其官网发布了官方声明,表示C罗不会随队参加当地时间周六对阵切尔西的英超联赛。随后,曼彻斯特晚报报道称,C罗还将会被曼联停训三天。曼联在昨天凌晨国乒今天八场外战!王楚钦遭遇劲敌,伊藤美诚再度登场,赛程出炉2022年10月21日,乒乓球澳门冠军赛继续进行,来到第三个比赛日,国际乒联安排16场比赛,直接决出男单女单八强席位,国乒今天八场外战,伊藤美诚再度登场,看点很多令人期待,最难打的CBA最新积分榜,冷门迭出辽宁不敌山西,同曦胜深圳,广东又垫底20222023赛季CBA联赛第5轮开打,首日共进行了6场比赛,冷门迭出,广厦大比分输给北京,辽宁加时107比110不敌山西,南京同曦111比107险胜深圳。CBA最新积分榜上,浙世界杯32强B组巡礼三狮军团英格兰在世界杯的历史里,英格兰可谓是名将频出。博比穆尔博比查尔顿足球绅士加里莱因克尔以及曾经风靡全球的足坛第一帅贝克汉姆。早在1863年,英格兰就成立了英格兰足球联合会,该联合会的成立标一个月后的世界杯,这些都已经准备好了卡塔尔世界杯的脚步近了整整一个月之后的11月20日足球世界杯将首次登陆西亚未来一个月里全球俱乐部联赛将逐渐偃旗息鼓32强的球员们将陆续集结赛场内外紧张调试准备迎接大赛这是2022年国羽8胜5负!陈雨菲石宇奇晋级8强,裁判抢戏郑思维黄雅琼险胜北京时间10月21日凌晨,2022年丹麦羽毛球公开赛18决赛全面结束!国羽13场比赛取得8胜5负的战绩。国羽女单陈雨菲何冰娇和韩悦三人晋级,王祉怡和张艺曼出局石宇奇晋级,成为国羽男惹不起!马龙轰112打崩日乒冠军,陈梦大胜,伊藤美诚化险为夷10月19日,WTT中国澳门赛继续进行,大满贯选手被惹毛了,狂轰113112,把日乒冠军快给打哭了,陈梦30轻取,伊藤美诚逆转晋级。惹不起!日乒冠军被马龙打到快哭了在下半区的11620!曼联完胜热刺,全场狂轰近30脚,C罗枯坐板凳提前回更衣室今天凌晨,英超联赛第12轮一场强强对话上演,曼联主场20击败热刺,守住联赛第5的位置。本场比赛,曼联在进攻端完全占优,上半场19次射门未能破门,其中安东尼的兜射击中立柱,下半场,弗让人害怕的思考宇宙中这么多恒星,为什么夜晚天空还是黑色的?人类历史上最绝妙的思考是什么?枫叶认为黑暗逻辑法则一定榜上有名200年前科学家奥博斯仰望星空提出一个疑问夜空为什么是黑色的?就是这个难题科学家讨论了近一个世纪的时间第一次听到这个说2023秋冬纽约婚纱周SarehNouri,白色的婚纱,公主?SarehNouri于2013年创立于纽约,她的婚纱以甜美和性感著称,但也有一些更时髦和优雅的元素。在本季发布的服装中,SarehNouri采用了新发布的面料,以鲜艳的色彩和丰富的
高端玩家章泽天离婚分5块到如今1350亿身家,多次救刘强东如果您喜欢这篇文章,请点击一下右上方的关注哦,感谢您的支持和鼓励,希望能给您带来舒适的阅读体验。同样是嫁入豪门,奚梦瑶变成了生育机器,郭碧婷被婆婆管控,而章泽天却从一个普通大学生变富大龙的消失,是娱乐圈的悲哀娱乐圈中明星的高片酬一直都是一个热门的话题虽然多次出台了相关规定,多次进行整改,但是依旧是一个天文数字!然而,并不是所有的明星都有那么高的收入,还是有一些明星,因为不红,所以没有资他是战狼中唯一的真正特种兵,身高1。5米,连吴京都敬他三分他是战狼中唯一一位真正的特种兵,身高只有1米5,却能被特种部队破例招收,连吴京都怕他三分,庄小龙到底有什么本事?2014年,吴京正在筹拍自己的电影战狼,电影中他饰演的主角就是一位特盘点如假包换真名媛,出生继承400亿财产,吊打拼单姐妹团有人为了立名媛人设丝袜都要拼单买但有人出身在罗马生来就是货真价实白富美一个包包抵一套房出生就继承400亿财产拼单团望而却步的奢侈品她们都信手拈来这些如假包换的真名媛到底有多壕?本期梁朝伟的入戏谢霆锋的巴掌,男明星的假戏真做,真令人胆颤在演戏中,导演编剧为了让剧情更真实,会安排一些吻戏床戏打戏。而演员为了给观众真实感,尽量用自己的表演方式把观众带入戏中。然而,有些演员过于敬业力求真实,竟然假戏真做。有人床戏借机占陈丽华对赵雅芝说唉!谁能想到我才比你大13岁啊!唐僧迟重瑞,没有被女儿国国王拿下,也没有被白骨精和蜘蛛精拿下,却为陈丽华这个名字停住了前进的步伐。对于她们的御弟哥哥的选择,这些美若天仙的女子恐怕直到现在也不知道自己究竟输在了哪里古力娜扎恋情曝光!徐开骋深夜多次去她家,与张天爱3年不官宣古力娜扎的新恋情!徐开聘深夜多次去她家,爱上张天爱3年,没有正式宣布前段时间,女演员古丽娜扎和徐开聘共同主演的新剧恋爱红尘被杀,作为男女主角的两人在杀青之后,似乎有了一些联系,于是胎盘,被很多人当成养生佳品,可胎盘真的这么神奇吗?胎盘,在民间深受欢迎,更被当成是餐桌上的养生滋补品,据传能够治疗疾病,包括心血管疾病神经系统疾病等等,绝对不能浪费了,一定要吃掉。胎盘是什么?胎盘,是胎儿用来摄取营养的地方,属于附小狗钱钱中出现的有关理财和储蓄的知识(一)财富法则最近看完了小狗钱钱2册书,我真的觉得这两本书写得很好,第一本教会你理财和储蓄,第二本教会你做人的原则,真的是为一个人的成长和成功费劲了心思。小狗钱钱属于儿童启蒙类书籍,但是我觉得成陌生的两个人却长得很像,可能是因为拥有相似的DNA?科幻网8月24日讯(王秀霞)不是双胞胎,却长得很像?这种情况在生活中并不少见。最近,细胞报告杂志上的一项新研究解释了其原因。具有相似面孔的人,可能是因为具有相似的DNA。(图片来源他又开始吹了?华为余承东问界M5EV是全世界颜值最高的SUV8月24日,问界M7正式开启了交付仪式,与此同时,华为常务董事余承东在演讲中表示,问界的M5EV将是全球颜值最高的SUV汽车。首先是问界M7,据了解,这款汽车是华为与赛力斯品牌合作