webrtc,其名称来源于Web instant Communication(英文:Web Real-Time Communication)的缩写,是一种支持在Web浏览器中进行实时语音对话或视频对话的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 relay服务器转发流媒体数据作为最后的保障。另外还有一种方式,反向连接,也可以帮助我们建立P2P。它的原则是一方必须上公网,这也是有限制的。
coturn中继服务器由STUN和turn两部分组成,STUN帮我们打洞,TURN帮我们转发流媒体数据。
# #连接过程
以下所有API都是截至2021年12月6日的最新版本
a和B由websocket通过后端服务连接,进入同一个管道。A和B都可以接收来自它们自己和彼此的消息和信令。 A Create PeerConnect创建了RTCPeerConnection实例(webrtc连接实例)。 addstream(getuser media方法)获取本地音频流并在本地显示它。(打开通话前先展示自己的视频图片)。 A调用webrtc连接实例的CreateOffer方法创建offer(SDP格式描述),包含A自身的媒体信息和编解码信息。 A调用webrtc连接实例的setLocalDescription方法将offer设置为本地描述,并向sturn/turn中继服务器发送bind request请求收集候选。 A发送offer,信令服务器(后端服务)转发给B, B收到offer,CreatePeerConnect创建RTCPeerConnection实例(webrtc连接实例)。 badaddstream(getuser media方法)获取本地音频流并在本地显示。(展示自己的视频图片)。 B调用webrtc连接实例的setRemoteDescription方法,并将offer设置为自己的远程描述。 B调用webrtc连接实例的CreateAnswer方法创建一个答案(SDP格式描述),其中包含A自己的媒体信息和编解码信息。 B调用webrtc连接实例的setLocalDescription方法设置本地描述的答案,向sturn/turn中继服务器发送bind request请求收集候选。 B发送应答,信令服务器(后端服务)转发给A. A接收应答,调用webrtc连接实例的setRemoteDescription方法,将应答设置为自己的远程描述。 媒体协商完成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调用AddStream上的webrtc连接实例播放A的视音频流, A调用AddStream上的webrtc连接实例播放出B的视音频流(通话开始,对方视频可用)。 此时,如果内网渗透失败。 sturn/turn server(其实turn works)会帮助转发音频流,因为它已经有了所有的候选列表(所有的IP和端口)。 更好的理解本地开发,同一个局域网,p2p连接的建立不需要coturn中转服务器,因为不需要打洞,websocket通道使主机能够在同一管道内互相发送offer和answer,P2P就可以建立了。 在线环境下,不同的局域网和websocket通道使主机能够在同一管道内互相发送offer和answer,想要建立p2p (nat穿越必须由sturn完成),但运营商切断(未能建立p2p)并使用turn传输音频流。 ##我有疑问。
P2P 连接是什么?P2P流媒体技术,这种技术主要是解决服务器过载的问题(传统上是服务器转发音频流),多终端不通过服务器转发音频流,而是在网状P2P通道中。下载资源、直播、音视频通话、共享桌面等。都是基于这项技术实现的。 要约的本质是什么?什么是SDP?offer是信令名,创建offer本质上就是创建sdp。 我给大家展示一下sdp的精髓,就是它自己的媒体信息和编解码信息。
sdp: { 类型:& # 39;报价& # 39;, SDP:& # 39;v = 0rn & # 39+ & # 39;o =-890410854023526853 2 IN IP4 127 . 0 . 0 . 1 rn & # 39;+ & # 39;s =-rn & # 39;+ & # 39;t = 0 0rn & # 39+ & # 39;a = group:BUNDLE audio videorn & # 39;+ & # 39;a = ext map-allow-mixed rn & # 39;+ & # 39;a = msid-semantic:WMS ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 ycrn & # 39;+ & # 39;m =音频9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126 rn & # 39;+ & # 39;c =在IP4 0 . 0 . 0 . 0 rn & # 39;+ & # 39;a = RTCP:9 IN IP4 0 . 0 . 0 . 0 rn & # 39;+ & # 39;a = ice-u frag:+B1 trn & # 39;+ & # 39;a = ice-pwd:2 mmqo 86 tkv 27 zgrrsmhvhgqkrn & # 39;+ & # 39;a = ice-options:tricklern & # 39;+ & # 39;a =指纹: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:9 arn & # 39;+ & # 39;a = setup:actpassrn & # 39;+ & # 39;a = mid:audiorn & # 39;+ & # 39;a = ext map:1 urn:IETF:params:RTP-HD rext:ssrc-audio-level rn & # 39;+ & # 39;a = ext map:2 http://www . webrtc . org/experiments/RTP-HD rext/ABS-send-timern & # 39;+ & # 39;a = ext map:3 http://www . IETF . org/id/draft-holmer-RM cat-transport-wide-cc-extensions-01rn & # 39;+ & # 39;a = sendrecvrn & # 39+ & # 39;a = RTCP-muxrn & # 39;+ & # 39;a = RTP map:111 opus/48000/2rn & # 39;+ & # 39;a = RTCP-FB:111 transport-ccrn & # 39;+ & # 39;a = fmtp:111 minptime = 10;useinbandfec = 1rn & # 39+ & # 39;a = RTP map:63 red/48000/2rn & # 39;+ & # 39;a = fmtp:63 111/111 rn & # 39;+ & # 39;a = RTP map:103 ISAC/16000 rn & # 39;+ & # 39;a = RTP map:104 ISAC/32000 rn & # 39;+ & # 39;a = RTP map:9 G722/8000 rn & # 39;+ & # 39;a = RTP map:0 PCMU/8000 rn & # 39;+ & # 39;a = RTP map:8 PCMA/8000 rn & # 39;+ & # 39;a = RTP map:106 CN/32000 rn & # 39;+ & # 39;a = RTP map:105 CN/16000 rn & # 39;+ & # 39;a = RTP map:13 CN/8000 rn & # 39;+ & # 39;a=rtpmap:110电话事件/48000 rn & # 39;+ & # 39;a=rtpmap:112电话事件/32000 rn & # 39;+ & # 39;a=rtpmap:113电话事件/16000 rn & # 39;+ & # 39;a=rtpmap:126电话事件/8000 rn & # 39;+ & # 39;a = ssrc:1511813723 cname:p 0 kgpa 3 ohyfih 1 hwrn & # 39;+ & # 39;a = ssrc:1511813723 msid:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 YC a3da a1c 2-1f 35-426 f-a242-2a 0286202 c 04 rn & # 39;+ & # 39;a = ssrc:1511813723 ms label:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 ycrn & # 39;+ & # 39;a=ssrc:1511813723标签:a3da a1c 2-1f 35-426 f-a242-2a 0286202 c 04 rn & # 39;+ & # 39;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 rn & # 39;+ & # 39;c =在IP4 0 . 0 . 0 . 0 rn & # 39;+ & # 39;a = RTCP:9 IN IP4 0 . 0 . 0 . 0 rn & # 39;+ & # 39;a = ice-u frag:+B1 trn & # 39;+ & # 39;a = ice-pwd:2 mmqo 86 tkv 27 zgrrsmhvhgqkrn & # 39;+ & # 39;a = ice-options:tricklern & # 39;+ & # 39;a =指纹: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:9 arn & # 39;+ & # 39;a = setup:actpassrn & # 39;+ & # 39;a = mid:videorn & # 39;+ & # 39;a = ext map:14 urn:IETF:params:RTP-HD rext:toff setrn & # 39;+ & # 39;a = ext map:2 http://www . webrtc . org/experiments/RTP-HD rext/ABS-send-timern & # 39;+ & # 39;a = ext map:13 urn:3gpp:video-orientation rn & # 39;+ & # 39;a = ext map:3 http://www . IETF . org/id/draft-holmer-RM cat-transport-wide-cc-extensions-01rn & # 39;+ & # 39;a = ext map:5 http://www . webrtc . org/experiments/RTP-HD rext/playout-delay rn & # 39;+ & # 39;a = ext map:6 http://www . webrtc . org/experiments/RTP-HD rext/video-content-typern & # 39;+ & # 39;a = ext map:7 http://www . webrtc . org/experiments/RTP-HD rext/video-timing rn & # 39;+ & # 39;a = ext map:8 http://www . webrtc . org/experiments/RTP-HD rext/color-spacern & # 39;+ & # 39;a = sendrecvrn & # 39+ & # 39;a = RTCP-muxrn & # 39;+ & # 39;a = RTCP-RSI zern & # 39;+ & # 39;a = RTP map:96 VP8/90000 rn & # 39;+ & # 39;a = RTCP-FB:96 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:96 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:96 CCM firn & # 39;+ & # 39;a = RTCP-FB:96 nack rn & # 39;+ & # 39;a = RTCP-FB:96 nack plirn & # 39;+ & # 39;a = RTP map:97 RTX/90000 rn & # 39;+ & # 39;a = fmtp:97 apt = 96rn & # 39;+ & # 39;a = RTP map:98 VP9/90000 rn & # 39;+ & # 39;a = RTCP-FB:98 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:98 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:98 CCM firn & # 39;+ & # 39;a = RTCP-FB:98 nack rn & # 39;+ & # 39;a = RTCP-FB:98 nack plirn & # 39;+ & # 39;a = fmtp:98 profile-id = 0rn & # 39;+ & # 39;a = RTP map:99 RTX/90000 rn & # 39;+ & # 39;a = fmtp:99 apt = 98rn & # 39;+ & # 39;a = RTP map:100 VP9/90000 rn & # 39;+ & # 39;a = RTCP-FB:100 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:100 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:100 CCM firn & # 39;+ & # 39;a=rtcp-fb:100纳克法& # 39;+ & # 39;a=rtcp-fb:100纳克每升。+ & # 39;a = fmtp:100 profile-id = 2rn & # 39;+ & # 39;a = RTP map:101 RTX/90000 rn & # 39;+ & # 39;a = fmtp:101 apt = 100 rn & # 39;+ & # 39;a = RTP map:102 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:102 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:102 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:102 CCM firn & # 39;+ & # 39;a = RTCP-FB:102 nack rn & # 39;+ & # 39;a = RTCP-FB:102 nack plirn & # 39;+ & # 39;a=fmtp:102水平-不对称-允许= 1;分组化模式= 1;profile-level-id = 42001 FRN & # 39;+ & # 39;a = RTP map:121 RTX/90000 rn & # 39;+ & # 39;a = fmtp:121 apt = 102 rn & # 39;+ & # 39;a = RTP map:127 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:127 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:127 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:127 CCM firn & # 39;+ & # 39;a = RTCP-FB:127 nack rn & # 39;+ & # 39;a = RTCP-FB:127 nack plirn & # 39;+ & # 39;a=fmtp:127水平-不对称-允许= 1;分组化模式= 0;profile-level-id = 42001 FRN & # 39;+ & # 39;a = RTP map:120 RTX/90000 rn & # 39;+ & # 39;a = fmtp:120 apt = 127 rn & # 39;+ & # 39;a = RTP map:125 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:125 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:125 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:125 CCM firn & # 39;+ & # 39;a = RTCP-FB:125 nack rn & # 39;+ & # 39;a = RTCP-FB:125 nack plirn & # 39;+ & # 39;a=fmtp:125水平-不对称-允许= 1;分组化模式= 1;profile-level-id = 42e 01 FRN & # 39;+ & # 39;a = RTP map:107 RTX/90000 rn & # 39;+ & # 39;a = fmtp:107 apt = 125 rn & # 39;+ & # 39;a = RTP map:108 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:108 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:108 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:108 CCM firn & # 39;+ & # 39;a=rtcp-fb:108纳克法& # 39;+ & # 39;a = RTCP-FB:108 nack plirn & # 39;+ & # 39;a=fmtp:108水平-不对称-允许= 1;分组化模式= 0;profile-level-id = 42e 01 FRN & # 39;+ & # 39;a = RTP map:109 RTX/90000 rn & # 39;+ & # 39;a = fmtp:109 apt = 108 rn & # 39;+ & # 39;a = RTP map:35 AV1/90000 rn & # 39;+ & # 39;a = RTCP-FB:35 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:35 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:35 CCM firn & # 39;+ & # 39;a = RTCP-FB:35 nack rn & # 39;+ & # 39;a = RTCP-FB:35 nack plirn & # 39;+ & # 39;a = RTP map:36 RTX/90000 rn & # 39;+ & # 39;a = fmtp:36 apt = 35rn & # 39;+ & # 39;a = RTP map:124 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:124 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:124 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:124 CCM firn & # 39;+ & # 39;a = RTCP-FB:124 nack rn & # 39;+ & # 39;a = RTCP-FB:124 nack plirn & # 39;+ & # 39;a=fmtp:124级-不对称-允许= 1;分组化模式= 1;profile-level-id = 4d 001 FRN & # 39;+ & # 39;a = RTP map:119 RTX/90000 rn & # 39;+ & # 39;a = fmtp:119 apt = 124 rn & # 39;+ & # 39;a = RTP map:123 H264/90000 rn & # 39;+ & # 39;a = RTCP-FB:123 goog-rem brn & # 39;+ & # 39;a = RTCP-FB:123 transport-ccrn & # 39;+ & # 39;a = RTCP-FB:123 CCM firn & # 39;+ & # 39;a = RTCP-FB:123 nack rn & # 39;+ & # 39;a = RTCP-FB:123 nack plirn & # 39;+ & # 39;a=fmtp:123水平-不对称-允许= 1;分组化模式= 1;profile-level-id = 64001 FRN & # 39;+ & # 39;a = RTP map:118 RTX/90000 rn & # 39;+ & # 39;a = fmtp:118 apt = 123 rn & # 39;+ & # 39;a=rtpmap:114红色/90000 rn & # 39;+ & # 39;a = RTP map:115 RTX/90000 rn & # 39;+ & # 39;a = fmtp:115 apt = 114 rn & # 39;+ & # 39;a = RTP map:116 ulp FEC/90000 rn & # 39;+ & # 39;a = ssrc-group:FID 1741155232 1898443615 rn & # 39;+ & # 39;a = ssrc:1741155232 cname:p 0 kgpa 3 ohyfih 1 hwrn & # 39;+ & # 39;a = ssrc:1741155232 msid:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 YC FB 34 f 344-FBE 3-45e 9-969d-af 4d 9 FB 5 BDC 4 rn & # 39;+ & # 39;a = ssrc:1741155232 ms label:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 ycrn & # 39;+ & # 39;a=ssrc:1741155232标签:fb34f 344-FBE 3-45e 9-969d-af 4d 9 FB 5 BDC 4 rn & # 39;+ & # 39;a = ssrc:1898443615 cname:p 0 kgpa 3 ohyfih 1 hwrn & # 39;+ & # 39;a = ssrc:1898443615 msid:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 YC FB 34 f 344-FBE 3-45e 9-969d-af 4d 9 FB 5 BDC 4 rn & # 39;+ & # 39;a = ssrc:1898443615 ms label:ehsxxpkpbffwyglbtifht 4 exe 6 smvehn9 ycrn & # 39;+ & # 39;a=ssrc:1898443615标签:fb34f 344-FBE 3-45e 9-969d-af 4d 9 FB 5 BDC 4 rn & # 39; } setlocaldescription方法是什么?setRemoteDescription是做什么的?setLocalDescription是为了让你的webrtc实例清楚自己的媒体信息和编解码器信息。 setRemoteDescription是为了让对方的webrtc实例清楚我的媒体信息和编解码信息。【/h/】一个提议,一个回答,我们都知道对方的媒体信息和编解码信息,这样才能好好谈判,还有你的视音频流我该怎么解码渲染?
向 sturn/turn 中继服务器发送请求收集 candidate 是什么鬼?
流程有些复杂,具体流程伙伴可以看这篇文章,WebRTC Turnaround protocol和turnserver practice。
目的了解webrtc的音视频采集和桌面采集;
了解websocket和webrtc的整个链路建立过程;
实现1V1文字传输、视频通话、语音通话、屏幕共享;
实现视频通话、语音通话、屏幕共享时截图、录音、录屏和截图、录音和录屏的在线播放和下载;
在线部署上述功能;
流程构建这里我们要画一个音视频建立过程的基本流程图。
基本流程图
对于这些信号,我们使用websocket进行转发。在这里,人们会问,为什么不用http?
首先,我们要实现的demo已经具备了发送普通短信的功能,也就是使用websocket。(长短轮询太老,性能太差)
其次,第一点可以忽略。http请求会回到原来的方式,A请求来自服务器,但永远不会传输到b。
但是,如果我们想使用websocket转发信令,我们需要清楚地知道,同一管道的所有端点都会接收到这条消息。所以对于上面的流程图,其实所有的小箭头都是双向的。
这时候我们可以在节点服务中控制推送消息的方向,也可以在客户端控制。这里我选择控制在AB端。
其次,我们在本地开发。如果我们用一台电脑,两个浏览器,websocket短信是可以的。但是音视频通话是不行的,因为无论是传输通道还是音视频设备(麦克风、扬声器等。)有冲突。所以我们可以用两台电脑通过同一个局域网来解决这个问题。而且由于webrtc的安全限制,必须使用https(无论是在线还是本地)和域名。我们可以通过在线配置https和域名,本地设置浏览器忽略https和主机文件的映射来解决这个问题。
接下来,我们可以使用vue和nodejs来尽可能快速简单地实现demo。
废话少说,我们撕了它!
开撕展示一些代码
socket-io在这里,我使用第三方包socket.io来快速发送消息和转发信令。(建议使用vue-socket.io)可以在组件中发送和接收消息和信令。
与socket-io的websocket建立连接并接收消息,将收到的信令放入vuex。
Async connectsocket ({commit,state,dispatch }){ /LAN let socket = io . connect(& # 34;http://172 . 28 . 74 . 16:3004 & # 34;); //在线 //let socket = io . connect(& # 34;https://www . codeting . top:3004 & # 34;); socket . on(& # 34;连接& # 34;,()= & gt{ 提交(类型。SET_SOCKET,SOCKET); 派遣(& # 34;handleChatData & # 34,state . friends); }); //监控好友发送的消息 socket . on(& # 34;friendMessage & # 34,(RES)= & gt;{ if (res) { 提交(类型。SET_MESSAGE,RES); } else { console . log(& # 34;有问题& # 34;); } }); socket . on(& # 34;应用& # 34;,(RES)= & gt;{ if(!state . is calling){ let text = & # 34;"; if(RES . webrtctype = = = & # 34;视频& # 34;){ text = & # 34;视频通话& # 34;; } MessageBox( `您的朋友${res.userName}向您索要${text}。你同意吗?`, & # 34;提示& # 34;, { confirm button text:& # 34;同意& # 34;, cancel button text:& # 34;拒绝& # 34;, 类型:& # 34;警告& # 34;, } ) 。然后(()= & gt{ const friend info = { account:RES . username, _id: res.Id, }; 提交(类型。SET_FRIENDINFO,friend info); 提交(类型。SET_ISCALLING,true); 提交(类型。SET_WEBRTCSTATE,& # 34;回复& # 34;); }) 。catch(()= & gt;{ console . log(& # 34;9.拒绝回答& # 34;); }); } else { } }); socket . on(& # 34;回复& # 34;,(RES)= & gt;{ if(RES){ local storage . setitem(& # 34;nowRoomId & # 34,RES . roomid); 提交(类型。SET _ REPLYISAGREE,true); } }); socket . on(& # 34;1v1ICEA & # 34,(RES)= & gt;{ if(RES & & state . role = = = & # 34;接收者& # 34;){ 提交(类型。SET_ECE,RES); } }); socket . on(& # 34;1v1ICEB & # 34,(RES)= & gt;{ if(RES & & state . role = = = & # 34;来电者& # 34;){ 提交(类型。SET_ECE,RES); } }); socket . on(& # 34;1v1OFFER & # 34,(RES)= & gt;{ if (res) { 提交(类型。SET_ISOFFER,true); 提交(类型。SET_OFFER,RES); } }); socket . on(& # 34;1 v1答案& # 34;,(RES)= & gt;{ if (res) { 提交(类型。SET_ISANSWER,true); 提交(类型。SET_ANSWER,RES); } }); }, 同样,我们也在节点服务中使用socket-io包。
io . on(& # 34;连接& # 34;,函数(socket){ socket . on(& # 39;friendMessage & # 39,异步函数(RES){ const roomId = RES . userid & gt;res.friendId?RES . userid+RES . friendid:RES . friendid+RES . userid io . to(roomId)。发出(& # 39;friendMessage & # 39,RES) }); socket . on(& # 39;申请& # 39;,异步函数(res) { io.to(res.roomId)。发出(& # 39;申请& # 39;,RES) }); socket . on(& # 39;回复& # 39;,data = & gt{ io.to(data.roomId)。发出(& # 39;回复& # 39;,data) }) socket . on(& # 39;1v1ICEA & # 39,data = & gt{ console . log(& # 39;1v1ICEA & # 39,data) io.to(data.roomId)。发出(& # 39;1v1ICEA & # 39,data) }) socket . on(& # 39;1v1ICEB & # 39,data = & gt{ io.to(data.roomId)。发出(& # 39;1v1ICEB & # 39,data) }) socket . on(& # 39;1v1OFFER & # 39,data = & gt{//远期报价 io.to (data.roomid)。发出(& # 39;1v1offer & # 39,data) }) socket . on(& # 39;1 v1答案& # 39;,data = & gt{//转发回答 io.to (data.roomid)。发出(& # 39;1 v1答案& # 39;,数据) }) }); 音视频采集对于视频、音频和屏幕共享,代码都差不多。举个例子,视频捕捉。
const constraints = { 音频:{ 噪声抑制:true, 回声消除:true, }, 视频:true, }; this . local stream = navigator . media devices . getuser media(constraints); let video = document . query selector(& # 34;# rtcA & # 34); video . src object = this . local stream; 通过使用getUserMedia,我们可以捕获音频和视频双声道的媒体流,我们传入一个参数约束,可以配置(控制是捕获音频还是视频)。
将采集到的动态媒体流赋给视频标签,我们自己的图片就会显示在网页上。
同样,如果是音频采集,只需要将参数约束中的音频配置为假即可。
对于电脑截屏,只需用getDisplayMedia替换API getUserMedia即可。
传输过程此时,视频发起方需要在收集到媒体流后向接收方发送应用信令。这个信令是询问接收者是否接听视频呼叫。
如果被拾取,接收端将收集自己的音视频双轨媒体流,初始化peerconnection,将媒体流流式传输到此管道,如果收集到监听ICE的候选信息,则发送给对方,并向视频发起方回复自己的同意信令。
如果你拒绝接听,接收者将回复一个拒绝信号给视频发起者。
当接收方此时收到拒绝时,将关闭视频和音频流的收集。

此时,当接收方接收并接听时,它将初始化peerconnection,并将自己的媒体流式传输到此管道中。如果采集到监控ICE的候选信息,会发送给对方。并创建一个offer(这个offer包含sdp),将offer放在本地并发送给视频接收方。
视频接收器接收提议,将其放在自己的远端,并创建一个答案。答案放在本地后,发送给视频发起者。
视频发起者接收答案,并将答案放到远端。
props: { visible: Boolean, friendInfo: Object, webRtcType: String, }, data(){ showDialog:false, localstream: {}, peer: null, isToPeer: false, ice servers:{ ice servers: { 转:www . codeting . top:3478 & # 34;,// xxxxx是域名 凭据:& # 34;xxxxx & # 34,//密码 用户名:& # 34;xx & # 34,//账号 }, ], SDP语义:& # 34;b计划& # 34;, }, bufferFriend: [], bufferMy: [], mediaRecorder: {}, startRecordVideo: true, }; }, watch:{ visible(val){ this . handle visible(val); 如果(!val) { 这个。$ store . commit(& # 34;chat/SET _ is calling & # 34;,假); } }, & # 34;$ store . state . chat . replys agree & # 34;(v) { if (v && this。$ store . state . chat . role = = = & # 34;来电者& # 34;){ let roomId = getLocalStorage(& # 34;nowRoomId & # 34).nowRoomId this.initPeer({ roomId,webRtcType:& # 34;视频& # 34;}); this.createOffer({ roomId,webRtcType:& # 34;视频& # 34;}); } }, & # 34;$ store . state . chat . isece & # 34;(v) { if (v && this。$ store . state . chat . role = = = & # 34;接收者& # 34;){ if(这个。$ store . state . chat . ECE){ this . on ice(this。$ store . state . chat . ECE); } } if (v && this。$ store . state . chat . role = = = & # 34;来电者& # 34;){ if(这个。$ store . state . chat . ECE){ this . on ice(this。$ store . state . chat . ECE); } } }, & # 34;$ store . state . chat . isoffer & # 34;(v) { if (v && this。$ store . state . chat . role = = = & # 34;接收者& # 34;){ if(这个。$ store . state . chat . offer){ this . on offer(this。$ store . state . chat . offer); } } }, & # 34;$ store . state . chat . is answer & # 34;(v) { if (v && this。$ store . state . chat . role = = = & # 34;来电者& # 34;){ if(这个。$ store . state . chat . answer){ this . on answer(this。$ store . state . chat . answer); } } }, }, 方法:{ handle visible(val){ this . showdialog = val; 这个。$ emit(& # 34;更新:可见& # 34;,val); }, async apply(){ let constraints = null; if(this . webrtctype = = = & # 34;视频& # 34;){ constraints = { 音频:{ 噪声抑制:true, 回声消除:true, }, 视频:true, }; } else { constraints = { audio:true, video: false, }; } this . local stream = await navigator . media devices . getuser media(约束); let video = document . query selector(& # 34;# rtcA & # 34); video . src object = this . local stream; const userId = get cookie(); const friendId = this . friend info . _ id; let roomId = userId & gt;friendId?用户标识+朋友标识:朋友标识+用户标识; 这个。$ store . state . chat . socket . emit(& # 34;应用& # 34;,{ webRtcType:this . webRtcType, roomId: roomId, userName:getLocalStorage(& # 34;账户& # 34;).账号, id: userId, }); }, 回复(roomId) { 这个。$ store . state . chat . socket . emit(& # 34;回复& # 34;,{ roomId, webRtcType:this . webRtcType, }); }, async create P2P(data){ wait this . create media(data); }, async create media(data){ try { let constraints = null; if(this . webrtctype = = = & # 34;视频& # 34;){ constraints = { 音频:{ 噪声抑制:true, 回声消除:true, }, 视频:true, }; } else { constraints = { audio:true, video: false, }; } this . local stream = await navigator . media devices . getuser media( constraints ); let video = document . query selector(& # 34;# rtcA & # 34); video . src object = this . local stream; } catch(e){ console . log(& # 34;getuser media:& # 34;,e); } wait this . init peer(data); }, init peer(data){ let peer connection = window。RTCPeerConnection | | window . mozrtpeerconnection | | window . webkitrtcpeerconnection; this . peer = new peer connection(this . ice servers); this . peer . addstream(this . local stream); window . stream my = this . local stream; this . peer . onicecandidate =(event)= & gt;{ if (event.candidate && this。$ store . state . chat . role = = = & # 34;来电者& # 34;){ 这个。$ store . state . chat . socket . emit(& # 34;1v1ICEA & # 34,{ ...data, sdp: event.candidate, }); } if(event . candidate & & this。$ store . state . chat . role = = = & # 34;接收者& # 34;){ 这个。$ store . state . chat . socket . emit(& # 34;1v1ICEB & # 34,{ ...data, sdp: event.candidate, }); } }; this . peer . on addstream =(event)= & gt;{ window . stream friend = event . stream; this . istopeer = true; let video = document . query selector(& # 34;# rtcB & # 34); video . src object = event . stream; }; }, async create offer(data){ try { let offer = wait this . peer . create offer({ offerToReceiveAudio:1, offerToReceiveVideo: 1, }); wait this . peer . setlocaldescription(offer); 这个。$ store . state . chat . socket . emit(& # 34;1v1offer & # 34,{ ...data, sdp: offer, }); } catch(e){ console . log(& # 34;create offer:& # 34;,e); } }, async onIce(data){ try { wait this . peer . addicecandidate(data . SDP); } catch(e){ console . log(& # 34;on answer:& # 34;,e); } }, async onOffer(data){ try { wait this . peer . set remote description(data . SDP); let answer = wait this . peer . create answer(); wait this . peer . setlocaldescription(答案); 这个。$ store . state . chat . socket . emit(& # 34;1 v1答案& # 34;,{ ...数据, sdp:答案, }); } catch(e){ console . log(& # 34;onOffer:& # 34;,e); } }, async on answer(data){ try { wait this . peer . set remote description(data . SDP); } catch(e){ console . log(& # 34;on answer:& # 34;,e); } }, } 播放对方的屏幕此时,接收方和发起方都在监听ICE候选信息,如果收集到,就会发送给对方。但是一监听到,就把对方的动态媒体流分配给B,播放出去。
if (event.candidate && this。$ store . state . chat . role = = = & # 34;接收者& # 34;){ 这个。$ store . state . chat . socket . emit(& # 34;1v1ICEB & # 34,{ ...data, sdp: event.candidate, }); } 截图和音视频录制画面截图:我们可以用canvas来获取context(& # 34;2d & # 34).drawImage,实现网页级别的图片抓取。
let picture = document . query selector(& # 34;#图片& # 34;); let rtcA = document . query selector(& # 34;# rtcA & # 34); picture . get context(& # 34;2d & # 34).drawImage(rtcA,0,0,200,120); 录音/录像/录屏:使用MediaRecorder将我们的媒体流或者对方的媒体流保存到一个数组中。
让那个=这个; let options = { mineType:& # 34;视频/webm;code = vp8 & # 34, }; 如果(!media recorder . istype supported(options . minetype)){ console . error(` ${ options . minetype }不受支持`); } try { this . media recorder = new media recorder(window . stream friend,options); } catch(error){ console . error(error,& # 34;失败& # 34;); 返回; } /数据有效时触发的事件 this . media recorder . on data available = function(e){ If(e & & e . data & . e . data . size > 0){ that . buffer friend . push(e . data); } }; this . media recorder . start(10); 播放音视频录制画面只需要将保存的静态媒体流赋给视频标签即可。
let rec player = document . query selector(& # 34;# recplayer & # 34); let Blob = new Blob(this . buffer friend,{ type:& # 34;视频/网络媒体& # 34;}); recplayer.src = window。URL . createobjecturl(blob); rec player . src object = null; rec player . controls = true; rec player . 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) }, 本地效果桌面分享
download(video name){
var Blob = new Blob(this . buffer friend,{ type:& # 34;视频/网络媒体& # 34;});
var url = window。URL . createobjecturl(blob);
var download link = document . createelement(& # 34;一& # 34;);
download link . href = URL;
download link . style . display = & # 34;无& # 34;;
download link . download = ` $ { video name } . webm `;//可以任意命名格式,比如mp4/webm等。,其中webm格式可以使用浏览器
downloadLink.click()播放视频;
document . body . remove child(download link)
},
本地效果桌面共享
部署webrtc有两个重要条件:域名和https,我们需要配置。
我们的节点服务不仅仅是https+域名,websocket还需要更安全的wss协议。我们需要为websocket配置wss。
前面说过,本地开发之所以能成功有效,是因为内网直接通信,没有公网,所以没有内网渗透。
如果要在线实现这个功能,就必须配置中转服务器。centos内核的配置文章可以参考这篇文章,ubuntu内核的配置可以参考这篇文章。
缺陷经过开发和上线,可以发现以下问题。
环境、设备、信号溢出、算法不兼容导致的噪声、声学和线路导致的回声、数据包传输速率不稳定导致的网络拥塞和延迟。
我们可以通过接入一些算法和提高硬件设备的质量来减少噪声回声和延迟。
对于噪音,可以在采集音频时设置noiseSuppression: true,这样可以降低一些环境和设备的噪音。
对于回声,回声消除:采集音频时可以设置true,可以去除回声。
剩下的就交给算法、设备和网络了。
这方面我就不探讨了。然后可以研究
WebRTC传输如何保证音视频服务质量以及成熟的应用如何解决这三个难点。特效在视频通话过程中,每个人都可以使用各种特效。美颜,贴纸等等。
但是在webrtc的web领域,视频特效领域是非常有潜力的。原因是js的性能问题。
一个比较简单的方法就是用canvas给我们的视频图像加一层滤镜,但本质上并没有改变媒体流。传输到远端还是没有特别效果。当然,我们可以通过websocket控制远程视频特效,但是由于视频流没有发生变化,如果对方下载了视频流,播放时仍然没有特效。
另一个方案如下,这里不再赘述。可以想想是怎么实现的(以下是简单的特效和贴纸)。
我们需要创建n-1个PeerConnection连接,因为我们要和n-1个人分享视频,每个人都是这样。但是会有一个谁主动提供的问题。我们可以让新加入的成员向其他n-1个成员发送要约,或者让n-1个成员向新加入的成员发送要约。在这里,我们可以通过遍历生成对等连接和要约。当然多人通话要看你的服务器能不能支持。
这里我们无意中使用了众所周知的多终端通信的通信方案——Mesh,也就是两两通信形成网状结构。除了Mesh的通信方案,还有MCU。MCU方案主要是把同一个房间的所有终端的音视频流混合起来发送到各个终端,所以服务器的压力其实是非常大的。此外,还有SFU交流计划。在从一个终端接收到音频和视频流之后,中继服务器将其转发给其他终端。
总结[/s2/]经过一系列的了解、思考、构建、开发、部署等,我们对webrtc有了一些初步的了解和认识。我们应该继续深化这方面的研究和探索。因为满足我们的好奇心和求知欲,提高我们在这方面的技术,丰富我们整体的知识体系,何乐而不为呢?
最后,以上内容均来自资料、个人实验和个人总结。我希望你能及时改正文中的错误。








