多人视频聊天(最新聊天软件排行榜)

什么是 WebRTC ?

WebRTC(Web Real-Time Communication)是一个API,可用于视频聊天、音频聊天或P2P文件共享等Web App中。- MDN

WebRTC有三个主要的API

收集本地音频和视频流。

Web实时通信(WebRTC) W3C组织:定义浏览器API。

网络浏览器中的实时通信(RTCWeb) IETF标准组织:定义其所需的协议、数据、安全和其他手段。

简单来说,WebRTC是一个开源项目,可以在Web应用中实现音频、视频和数据的实时通信。在实时通信中,音视频的获取和处理是一个非常复杂的过程。例如,音频和视频流的编码和解码、降噪和回声消除等。,但是在WebRTC中,这些都是由浏览器的底层包来完成的。我们可以直接得到优化后的媒体流,然后输出到本地的屏幕和扬声器,或者转发给它的对等端。

WebRTC的音频和视频处理引擎

因此,我们可以在没有任何第三方插件的情况下实现浏览器到浏览器的对等(P2P)连接,从而进行实时的音视频通信。当然,WebRTC提供了一些API供我们使用。在实时音视频通信过程中,我们主要使用以下三种:

获取音频和视频流(媒体流)

RTCPeerConnection:对等通信

RTCDataChannel:数据通信

但是,虽然浏览器已经为我们解决了大部分的音视频处理问题,但是在向浏览器请求音视频时,我们仍然需要特别注意码流的大小和质量。即使硬件能捕捉到高清质量的码流,CPU和带宽也不一定能跟得上,这也是我们在建立多个对等连接时要考虑的。

实现

接下来,我们将通过分析上面提到的API来逐步了解WebRTC实时通信的过程。

getUserMedia

GetUserMedia是一个大家可能并不陌生的API,因为常见的H5录制等功能都需要用到它,主要用于获取设备的MediaStream(即媒体流)。它可以接受约束对象constraints作为参数来指定需要获取哪种媒体流。

navigator . media devices . getuser media({ audio:true,video:true })//参数表示需要同时获取音频和视频。然后(stream = >;{//获取优化后的媒体流let video = document . query selector(& # 39;# rtc & # 39);video.srcObject = stream}) .catch(err = & gt;{//捕获错误});让我们简单看一下被收购的MediaStream。

可见它有很多属性,我们只需要了解一下就可以了。有关更多信息,请查看MDN。

* id [String]:唯一标识当前ms,因此每次刷新浏览器或再次获取MS时,id都会发生变化。* active [boolean]:表示当前MS是否处于活动状态(即是否可以播放)。* onactive:当active为true时触发此事件。结合上图,我们来回顾一下上次提到的原型和原型链。MediaStream的__proto__指向其构造函数对应的prototype对象,prototype对象中还有一个构造函数属性指向其对应的构造函数。也就是说MediaStream的构造函数是一个名为MediaStream的函数。可能会有点绕路。不熟悉原型的同学可以看一下上一篇文章Javascript原型和原型链与画布验证码练习1。

在这里,还可以通过getAudioTracks()和getVideoTracks()查看采集到的流的一些信息,查看MDN了解更多信息。

* kind:是当前获取的媒体流类型(音频/视频)。*标签:这是一个媒体设备。我在这里使用虚拟摄像机。*静音:表示媒体轨道是否静音。我们继续看兼容性。getUserMedia,navigator . media devices . getuser media是新的API,旧的是navigator.getUserMedia为了避免兼容性问题,我们可以稍微处理一下(其实毕竟现在WebRTC的支持率不高,有需要的话可以选择一些适配器,比如adapter.js)。

//判断是否有navigator.mediaDevices,该值没有赋给空对象if(navigator . media devices = = undefined){ navigator . media devices = { };}//继续判断是否有navigator . media devices . getuser media,未使用navigator . getuser media if(navigator . media devices . getuser media){ navigator . media devices . getuser media = function(prams)。{ let getuser media = navigator . WebKit getuser media | | navigator . mozgetuser media;//兼容获取if(!getuser media){ return promise . reject(新错误(& # 39;getUserMedia没有在这个浏览器中实现& # 39;));} return new Promise(function(resolve,reject){ getuser media . call(navigator,prams,resolve,reject);});};} navigator . media devices . get user media(约束条件)。然后(stream = & gt{ let video = document . query selector(& # 39;# Rtc & # 39);如果(& # 39;srcObject & # 39In video) {//确定是否支持srcObject属性video.srcObject = stream} else { video.src = window。URL.createObjectURL(流);} video . onloadedmetadata = function(e){ video . play();};}) .catch((err)= & gt;{//捕获错误console . error(err . name+& # 39;: '+err . message);});限制

对于约束约束对象,我们可以指定一些与媒体流相关的属性。例如,指定是否获取某种类型的流:

navigator . media devices . getuser media({ 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:{ facing mode:& # 34;用户& # 34;}}//pre {audio: true,video:{ facing mode:{ exact:& # 34;环境& # 34;} } }//发布//还可以指定设备id。//可以通过navigator . media devices . enumerate devices()获取支持的设备{ video:{ device id:mycameradeviceid } }。另一个有趣的事情是将视频源设置为屏幕,但是目前只有Firefox支持这个属性。

{音频:真实,视频:{ media source:& # 39;屏幕& # 39;}}我就不在这里当搬运工了,更多精彩在MDN,_

RTCPeerConnection

RTCPeerConnection接口表示从本地计算机到远程端的WebRTC连接。该接口提供了创建、维护、监控和关闭连接的方法的实现。—— MDN

概述RTCPeerConnection作为创建点对点连接的API,是实现实时音视频通信的关键。在点对点通信的过程中,需要交换一系列的信息,这个过程通常称为信令。信号阶段要完成的任务:

为每个连接创建一个RTCPeerConnection,并添加一个本地媒体流。

并交换本地和远程描述:SDP格式的本地媒体元数据。

获取和交换网络信息:潜在的连接端点被称为ICE候选。

虽然我们称WebRTC为点对点连接,但并不意味着服务器不需要参与实现过程。因为在点对点通道建立之前,它们之间是没有办法沟通的。这意味着在信令阶段,我们需要一个通信服务来帮助我们建立这个连接。WebRTC本身没有指定信令服务,所以我们可以使用XMPP、XHR、Socket等。来完成信令交换所需的服务。在我的工作中,基于XMPP协议的Strophe.js用于双向通信,但在这种情况下,将使用Socket.io和Koa进行项目演示。

NAT穿越技术我们先来看连接任务的第一项:为每个连接端创建一个RTCPeerConnection,并添加一个本地媒体流。其实如果是一般的直播模式,只需要玩家添加一个本地流进行输出,其他参与者只需要接受流就可以观看了。由于浏览器之间的差异,RTCPeerConnection也需要加上前缀。

让PeerConnection = window。RTCPeerConnection | | window . mozrtcpeerconnection | | window . webkitrtcpeerconnection;let peer = new peer connection(ice servers);我们看到RTCPeerConnection也接收了一个参数—iceServers。我们先来看看它是什么样子的:

{ iceServers:[{ URL:& # 34;stun:stun . l . Google . com:19302 & # 34;},//谷歌的公共服务{ URL:& # 34;转:* * * & # 34;,用户名:***,//用户名凭据:*//密码}]}参数配置了STUN和TURN两个URL,这是WebRTC实现点对点通信的关键,也是一般P2P连接需要解决的问题:NAT穿越。

NAT(网络地址转换)简单来说就是解决IPV4中IP地址不足的技术,即一个公有IP地址一般对应N个内部IP地址。这也会导致不在同一个局域网的浏览器试图连接WebRTC,无法直接获取对方的公共IP,无法通信,需要使用NAT穿越(也叫打洞)。以下是NAT穿越的基本过程:

正常情况下,会使用ICE协议框架进行NAT穿越。ICE的全称是interactive connectivity establishment,即交互连接建立。它使用STUN协议和TURN协议进行遍历。关于NAT穿越的更多信息,请参考ICE协议下NAT穿越的实现(STUN&TURN)和P2P通信标准协议的ICE(III)。

在这里,我们可以发现WebRTC的通信至少需要两种服务:

信令阶段需要双向通信服务来辅助信息交换。

STUN和TURN有助于实现NAT穿越。

WebRTC建立点对点连接的过程到底是怎样的?我们结合图例分析其中的联系。

显然,在上述连接过程中:

调用方(这里都指浏览器)需要向接收方发送一个名为offer的消息。

接收端收到请求后,向主叫端返回应答消息。

这是上述任务之一,以SDP格式交换本地媒体元数据。Sdp信息通常如下所示:

v = 0 o =-1837933589686018726 2 IN IP4 127 . 0 . 0 . 1s =-t = 0 0 a = group:BUNDLE audio video a = msid-semantic:WMS yvkejmuszzvjlajhn 4 unfj 6 q 9 dmqmb 6 crcot m = audio 9 UDP/TLS/RTP/savpf 111 103 104 9 8 106 105 13 110 112 113 123...................................................................................................................................

调用者创建要约信息后,调用setLocalDescription存储本地要约描述,然后发送给接收者。

接收方收到要约后,首先调用setRemoteDescription存储远程要约描述;然后创建答案信息,也需要调用setLocalDescription来存储本地答案描述,然后返回给接收端。

调用者得到答案后,再次调用setRemoteDescription来设置远程答案描述。

这里的点对点连接还缺少一步,就是网络信息的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-流5

Async createMedia() {//将本地流保存到全局this . local stream = await navigator . media devices . getuser media({ audio:true,video:true })let video = document . query selector(& # 39;# rtcA & # 39);video . src object = this . local stream;this . init peer();//获取媒体流后,调用函数初始化RTCPeerConnection}初始化RTCPeerConnection。

initPeer() {...this . peera . addstream(this . local stream);//添加本地流this . peera . onececandidate =(event)= >;{//如果采集到监控A的ICE候选信息,则将其添加到B的连接状态If(event . candidate){ this . peerb . additioncecandidate(event . candidate);} };...//监听是否有媒体流访问,如果有,赋给srcthis . peerb . onaddstream =(event)= >;{ let video = document . query selector(& # 39;# rtcB & # 39);video . src object = event . stream;};this . peerb . onicecandidate =(event)= & gt;{连接状态//如果监视器B的ICE候选信息被收集,它将被添加到If(事件中。考生){这个。皮拉。其他候选人(事件。候选人);} };}这部分主要是分别创建对等实例,互相交换ICE信息。但是,这里需要提到一个属性,即iceConnectionState。

peer . oniceconnectionstatechange =(evt)= & gt;{ console . log(& # 39;ICE连接状态更改:& # 39;+evt . target . iceconnectionstate);};我们可以通过oniceconnectionstatechange方法监视ICE连接的状态,该方法有七种状态:

新ICE代理正在收集候选人或等待提供远程候选人。检查ICE代理已在至少一个组件上接收到远程候选项,并且正在检查候选项,但尚未找到连接。除了查,可能还会收。已连接的ICE代理已找到所有组件的可用连接,但仍在检查其他候选对,以查看是否有更好的连接。它可能还在收集。完成的ICE代理已经完成收集和检查,并找到了所有组件的连接。失败的ICE代理已检查完所有候选对,但未能找到至少一个组件的连接。可能已经找到了与某些组件的连接。断开的ICE连接断开的closed ICE代理被关闭,不再响应STUN请求。我们需要注意完成和断开,一个是连接完成时触发,一个是连接断开时触发。

创建连接

异步调用(){ if(!this.peerA ||!This.peerB) {//判断是否有对应的实例,如果没有,重新创建this . init peer();} try { let offer = wait this . peera . create offer(this . offer option);//创建offer away this . oncreate offer(offer);} catch(e){ console . log(& # 39;create offer:& # 39;,e);}}这里需要判断是否有对应的实例,就是挂机后再打电话所做的处理。

async oncreate offer(desc){ try { await this . peerb . setlocaldescription(desc);//调用者设置本地提供描述} catch(e){ console . log(& # 39;offer-setLocalDescription:& # 39;,e);} try { await this . peera . setremotedescription(desc);//接收方设置远程提供描述} catch(e){ console . log(& # 39;offer-set remote description:& # 39;,e);} try { let answer = await this . peera . create answer();//接收方创建answer await this . oncreate answer(answer);} catch(e){ console . log(& # 39;create answer:& # 39;,e);} },async oncreate answer(desc){ try { await this . peera . setlocaldescription(desc);//接收方设置本地答案描述} catch(e){ console . log(& # 39;answer-setLocalDescription:& # 39;,e);} try { await this . peerb . setremotedescription(desc);//设置呼叫结束时的远程应答描述} catch(e){ console . log(& # 39;answer-setRemoteDescription:& # 39;,e);}}这个基本上就是之前重复过几次的过程用代码写的。看到这里,思路应该更清晰了。但是有一点需要说明,就是在现在的情况下,A是调用者,B同样可以得到A的媒体流。因为连接一旦建立,就是双向的,只不过B在初始化peer的时候没有添加本地流,所以A不会有B的媒体流。

1对1对等网络连接

想必大家对基本流程都很熟悉,也通过插图和例子来来回回讲过几次。所以趁热打铁,让我们这次把服务加进去,做一个真正的点对点连接。在看下面这篇文章之前,希望你有一点Koa和Scoket.io的基础,只知道一些基本的API。不熟悉也没关系,来不及看了,Koa,Socke.io,也可以参考我之前的文章v chat——一个社交聊天系统(vue+node+mongodb) 1。

还是需求的老规矩。先了解一下需求。图片加载慢,可以直接看演示地址2。

连接过程涉及的环节很多,这里就不一一截图了。可以直接查看演示地址。简单分析一下我们要做的事情:

加入房间后,获取房间的所有在线成员。

选择任意成员进行呼叫,即呼叫操作。这时候就有一些细节要处理了:不能给自己打电话,同一时间只能给一个人打电话,需要判断对方是否在通话中,通话结束后回复时需要做出相应的判断(同意、拒绝、通话中)。

或者拒接电话,没有后续行动,可以打给别人。同意后,就开始建立点对点的连接。

加入房间。简单看一下加入房间的流程:

//前端join() {if(!this.account)返回;this.isJoin = true//输入框弹性层逻辑window . session storage . account = this . account;//刷新确定是否登录socket . emit(& # 39;加入& # 39;,{roomid: this.roomid,account:this . account });//发送请求加入房间}//后端const sockS = { };//sock实例const users = {}对应不同的客户端;//成员列表sock . on(& # 39;加入& # 39;,data = & gt{ sock.join(data.roomid,()= & gt{如果(!users[data . roomid]){ users[data . roomid]=[];} let obj = { account: data.account,id:sock . id };设arr = users[data.roomid]。过滤器(v = & gtv . account = = = data . account);如果(!arr.length) { users[data.roomid]。push(obj);} sockS[data . account]= sock;//保存不同客户端对应的sock实例//将房间内的成员列表发送给房间内的所有人,app。_ io.in (data.roomid)。发出(& # 39;加入& # 39;,users[data.roomid],data.account,sock . id);});});后端成员列表的处理是因为多房间的逻辑,根据每个房间的成员列表返回。如果做的时候没有多个房间,就不需要考虑这个。SockS被处理成发送私人聊天消息。

打电话的注意事项前面已经说了,这里一起说一下。需要注意的是,消息中需要包含自己和对方的账号,因为这是评审成员sock的标识,之前存储在socks中,用于发送私聊消息。然后就是前面提到的三种状态。这里用类型值1,2,3来区分,然后给出不同的响应。

//前端apply(account) {//发送请求// account对方账号自身是自己的账号this.loading = truethis.loadingText = & # 39打电话& # 39;;//调用中的loading socket . emit(& # 39;申请& # 39;,{account: account,self:this . account });}、reply(account,type) {//处理回复socket . emit(& # 39;回复& # 39;,{account: account,self: this.account,type:type });}//接收请求socket . on(& # 39;申请& # 39;,data = & gt{if (this.isCall) {//确定你是否在调用this.reply(data.self,& # 39;3');返回;}这个。$ confirm(data . self+& # 39;您同意您的视频通话请求吗?', '提示& # 39;,{ confirm button text:& # 39;同意& # 39;,取消按钮文本:& # 39;拒绝& # 39;,类型:& # 39;警告& # 39;}).then(async()= & gt;{ this . is call = data . self;this.reply(data.self,& # 39;1');}).catch(()= & gt;{ this.reply(data.self,& # 39;2');});});//后端sock . on(& # 39;申请& # 39;,data = & gt{//转发应用sockS[data.account]。发出(& # 39;申请& # 39;,数据);});后端比较简单,只是把请求转发给对应的客户端。其实我们例子的后端基本就是这个操作,后端代码就不贴了。可以去源码直接看。

回复和打电话是同一个逻辑。只需分别处理不同的回复即可。

//前端socket . on(& # 39;回复& # 39;,异步数据= & gt{//收到回复this.loading = falseswitch(data . type){ case & # 39;1'://同意this . is call = data . self;//存储调用对象break案例& # 39;2'://拒绝这个。$ message({ message:& # 39;对方拒绝了你的要求!',类型:& # 39;警告& # 39;});打破;案例& # 39;3'://这个。$ message正在通话({ message:& # 39;对方在打电话!',类型:& # 39;警告& # 39;});打破;} });创建连接调用和回复的逻辑基本清晰,那么我们继续思考,什么时候应该创建P2P连接?我们之前说过,不需要处理拒绝和调用,只需要同意,然后要在同意请求的位置创建。需要注意的是,同意请求有两个地方:一是你点击同意,二是对方知道你点击同意之后。在这个例子中,呼叫者发送一个提议。这个地方一定要注意,只要有一方创造了要约,因为一旦建立了联系,就是双向的。

socket . on(& # 39;申请& # 39;,data = & gt{//你同意的地方...这个。$ confirm(data . self+& # 39;您同意您的视频通话请求吗?', '提示& # 39;,{ confirm button text:& # 39;同意& # 39;,取消按钮文本:& # 39;拒绝& # 39;,类型:& # 39;警告& # 39;}).then(async()= & gt;{ wait this . create P2P(data);//同意后,创建自己的同行,等待对方的报价...//此处无报价})...});socket . on(& # 39;回复& # 39;,异步数据= & gt{//对方知道你点击了约定地点开关(data . type){ case & # 39;1'://献上这个。创建P2P(数据)只发送到这里;//对方同意后创建自己的同行this . Create offer(data);//并向对方发送offer break...} });像微信等视频通话,双方都需要输出媒体流,因为你要看到对方。所以这里的本地对等连接和前面的不同之处在于,两者都需要在自己的RTCPeerConnection实例中添加媒体流,连接后就可以得到对方的视频流。初始化RTCPeerConnection时,记得添加onicecandidate函数,将ICE候选发送给对方。

async create P2P(data){ this . loading = true;//加载动画this.loadingText = & # 39建立通话连接& # 39;;wait this . create media(data);},异步创建媒体(数据){...//获取本地流并将其分配给视频。this.initPeer(data)和以前一样;//获取媒体流后,调用函数初始化RTCPeerConnection},init peer(data){//并创建输出对等连接...this . peer . addstream(this . local stream);//本地流this . peer . oniccandidate =(event)= >;{//如果采集到监控ICE候选信息,发送给对方if (event.candidate) {//发送ICE候选socket . emit(& # 39;1v1ICE & # 39,{account: data.self,self: this.account,SDP:event . candidate });} };this . peer . onaddstream =(event)= & gt;{//监听是否有媒体流访问,如果有,赋给rtcB的src,改变对应的加载状态,赋值时省略this.isToPeer = truethis.loading = false...};}createOffer等信息交换和之前一样,只是需要通过Socket转发给对应的客户端。然后在收到消息后采取相应的措施。

socket . on(& # 39;1 v1答案& # 39;,(数据)= & gt{//answer this.onAnswer(数据)收到;});socket . on(& # 39;1v1ICE & # 39,(数据)= & gt{//收到ICE this.onIce(数据);});socket . on(& # 39;1v1offer & # 39,(数据)= & gt{//Received offer this . onoffer(data);});//这里只贴一段createOffer的代码,因为和之前的思路一样,只是写的不一样。//建议你们都自己打。有问题可以交流或者查看源代码。Async createOffer(data) {//创建并发送offer try {//创建offer let offer = awaitthis . peer . Create offer(this . offer option);//调用者设置本地offer描述awaiths . peer . setlocaldescription(offer);//发送offer socket . emit(& # 39;1v1offer & # 39,{account: data.self,self: this.account,SDP:offer });} catch(e){ console . log(& # 39;create offer:& # 39;,e);}}}挂机的想法还是关闭他们的对等,但是这里挂机方还是需要用Socket告诉对方你已经挂机了,否则对方还在等待。

Hangup() {//挂断呼叫并进行相应的处理。对方需要关闭连接socket . emit(& # 39;1 v1挂起& # 39;,{account: this.isCall,self:this . account });this . peer . close();this.peer = nullthis.isToPeer = falsethis.isCall = false}后记

如果你看到这个,并且这篇文章对你有些帮助,希望你能用你的小手支持作者。谢谢:啤酒:。如文中有错误,请指出并鼓励。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。系信息发布平台,仅提供信息存储空间服务。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。

本文来自网络,若有侵权,请联系删除,作者:金楠一,如若转载,请注明出处:

发表回复

登录后才能评论