痴痴网站源码分享 痴痴迷迷打一个生肖

大家好,关于痴痴网站源码分享很多朋友都还不太明白,不过没关系,因为今天小编就来为大家分享关于痴痴迷迷打一个生肖的知识点,相信应该可以解决大家的一些困惑和问题,如果碰巧可以解决您的问题,还望关注下本站哦,希望对各位有所帮助!

前言

【从头到脚】会作为一个系列文章来发布,它包括但不限于WebRTC多人视频,预计会有:

WebRTC实战(一):也就是本期,主要是基础讲解以及一对一的本地对等连接,网络对等连接。

WebRTC实战(二):主要讲解数据传输以及多端本地对等连接、网络对等连接。

WebRTC实战(三):实现一个一对一的视频聊天项目,包括但不限于截图、录制等。

WebRTC+Canvas实现一个共享画板项目。

作者开源作品???Vchat—一个社交聊天系统(vue+node+mongodb)的系列文章

什么是WebRTC?

WebRTC是由一家名为GobalIPSolutions,简称GIPS的瑞典公司开发的。Google在2011年收购了GIPS,并将其源代码开源。然后又与IETF和W3C的相关标准机构合作,以确保行业达成共识。其中:

WebReal-TimeCommunications(WEBRTC)W3C组织:定义浏览器API。

Real-TimeCommunicationinWeb-browsers(RTCWEB)IETF标准组织:定义其所需的协议,数据,安全性等手段。

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

WebRTC的音视频处理引擎:

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

getUserMedia:获取音频和视频流(MediaStream)

RTCPeerConnection:点对点通信

RTCDataChannel:数据通信

不过,虽然浏览器给我们解决了大部分音视频处理问题,但是从浏览器请求音频和视频时,我们还是需要特别注意流的大小和质量。因为即便硬件能够捕获高品质流,CPU和带宽也不一定可以跟上,这也是我们在建立多个对等连接时,不得不考虑的问题。

实现

接下来,我们通过分析上文提到的API,来逐步弄懂WebRTC实时通信实现的流程。

getUserMedia

MediaStream

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

navigator.mediaDevices.getUserMedia({audio:true,video:true})\t\n//参数表示需要同时获取到音频和视频\t\n.then(stream=>{\t\n//获取到优化后的媒体流\t\nletvideo=document.querySelector(&rtc&39;getUserMediaisnotimplementedinthisbrowser&39;39;);\t\nif(&39;invideo){//判断是否支持srcObject属性\t\nvideo.srcObject=stream;\t\n}else{\t\nvideo.src=window.URL.createObjectURL(stream);\t\n}\t\nvideo.onloadedmetadata=function(e){\t\nvideo.play();\t\n};\t\n})\t\n.catch((err)=>{//捕获错误\t\nconsole.error(err.name+&39;+err.message);\t\n});

constraints

对于constraints约束对象,我们可以用来指定一些和媒体流有关的属性。比如指定是否获取某种流:

navigator.mediaDevices.getUserMedia({audio:false,video:true});\t\n//只需要视频流,不要音频

指定视频流的宽高、帧率以及理想值:

//获取指定宽高,这里需要注意:在改变视频流的宽高时,\t\n//如果宽高比和采集到的不一样,会直接截掉某部分\t\n{audio:false,\t\nvideo:{width:1280,height:720}\t\n}\t\n//设定理想值、最大值、最小值\t\n{\t\naudio:true,\t\nvideo:{\t\nwidth:{min:1024,ideal:1280,max:1920},\t\nheight:{min:776,ideal:720,max:1080}\t\n}\t\n}

对于移动设备来说,还可以指定获取前摄像头,或者后置摄像头:

{audio:true,video:{facingMode:&34;}}//前置\t\n{audio:true,video:{facingMode:{exact:&34;}}}//后置\t\n//也可以指定设备id,\t\n//通过navigator.mediaDevices.enumerateDevices()可以获取到支持的设备\t\n{video:{deviceId:myCameraDeviceId}}\n还有一个比较有意思的就是设置视频源为屏幕,但是目前只有火狐支持了这个属性。\n{audio:true,video:{mediaSource:&39;}}

这里就不接着做搬运工了,更多精彩尽在MDN,^_^!

RTCPeerConnection

概述

RTCPeerConnection作为创建点对点连接的API,是我们实现音视频实时通信的关键。在点对点通信的过程中,需要交换一系列信息,通常这一过程叫做—信令(signaling)。在信令阶段需要完成的任务:

我们虽然把WebRTC称之为点对点的连接,但并不意味着,实现过程中不需要服务器的参与。因为在点对点的信道建立起来之前,二者之间是没有办法通信的。这也就意味着,在信令阶段,我们需要一个通信服务来帮助我们建立起这个连接。WebRTC本身没有指定信令服务,所以,我们可以但不限于使用XMPP、XHR、Socket等来做信令交换所需的服务。我在工作中采用的方案是基于XMPP协议的Strophe.js来做双向通信,但是在本例中则会使用Socket以及Koa来做项目演示。

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

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

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

NAT穿越技术

我们先看连接任务的第一条:为每个连接端创建一个RTCPeerConnection,并添加本地媒体流。事实上,如果是一般直播模式,则只需要播放端添加本地流进行输出,其他参与者只需要接受流进行观看即可。

因为各浏览器差异,RTCPeerConnection一样需要加上前缀。

letPeerConnection=window.RTCPeerConnection||\t\nwindow.mozRTCPeerConnection||\t\nwindow.webkitRTCPeerConnection;\t\nletpeer=newPeerConnection(iceServers);

我们看见RTCPeerConnection也同样接收一个参数—iceServers,先来看看它长什么样:

{\t\niceServers:[\t\n{url:&34;},//谷歌的公共服务\t\n{\t\nurl:&34;,\t\nusername:***,//用户名\t\ncredential:***//密码\t\n}\t\n]\t\n}

参数配置了两个url,分别是STUN和TURN,这便是WebRTC实现点对点通信的关键,也是一般P2P连接都需要解决的问题:NAT穿越。

NAT(NetworkAddressTranslation,网络地址转换)简单来说就是为了解决IPV4下的IP地址匮乏而出现的一种技术,也就是一个公网IP地址一般都对应n个内网IP。这样也就会导致不是同一局域网下的浏览器在尝试WebRTC连接时,无法直接拿到对方的公网IP也就不能进行通信,所以就需要用到NAT穿越(也叫打洞)。以下为NAT穿越基本流程:

一般情况下会采用ICE协议框架进行NAT穿越,ICE的全称为InteractiveConnectivityEstablishment,即交互式连接建立。它使用STUN协议以及TURN协议来进行穿越。关于NAT穿越的更多信息可以参考ICE协议下NAT穿越的实现(STUN&TURN)、P2P通信标准协议(三)之ICE。

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

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

STUN、TURN辅助实现NAT穿越。

建立点对点连接

WebRTC的点对点连接到底是什么样的过程呢,我们通过结合图例来分析连接。

显而易见,在上述连接的过程中:

*呼叫端(在这里都是指代浏览器)需要给接收端发送一条名为offer的信息。*接收端在接收到请求后,则返回一条answer信息给呼叫端。

这便是上述任务之一,SDP格式的本地媒体元数据的交换。sdp信息一般长这样:

v=0\t\no=-18379335896860187262INIP4127.0.0.1\t\ns=-\t\nt=00\t\na=group:BUNDLEaudiovideo\t\na=msid-semantic:WMSyvKeJMUSZzvJlAJHn4unfj6q9DMqmb6CrCOT\t\nm=audio9UDP/TLS/RTP/SAVPF11110310490810610513110112113126\t\n…\t\n…

但是任务不仅仅是交换,还需要分别保存自己和对方的信息,所以我们再加点料:

***呼叫端**创建offer信息后,先调用setLocalDescription存储本地offer描述,再将其发送给**接收端**。

***接收端**收到offer后,先调用setRemoteDescription存储远端offer描述;然后又创建answer信息,同样需要调用setLocalDescription存储本地answer描述,再返回给**接收端**

***呼叫端**拿到answer后,再次调用setRemoteDescription设置远端answer描述。

到这里点对点连接还缺一步,也就是网络信息ICE候选交换。不过这一步和offer、answer信息的交换并没有先后顺序,流程也是一样的。即:在呼叫端和接收端的ICE候选信息准备完成后,进行交换,并互相保存对方的信息,这样就完成了一次连接。

这张图是我认为比较完善的了,详细的描述了整个连接的过程。正好我们再来小结一下:

*基础设施:必要的信令服务和NAT穿越服务

*clientA和clientB分别创建RTCPeerConnection并为输出端添加本地媒体流。如果是视频通话类型,则意味着,两端都需要添加媒体流进行输出。

*本地ICE候选信息采集完成后,通过信令服务进行交换。

*呼叫端(好比A给B打视频电话,A为呼叫端)发起offer信息,接收端接收并返回一个answer信息,呼叫端保存,完成连接。

本地1v1对等连接

基础流程讲完了,那么是骡子是马拉出来溜溜。我们先来实现一个本地的对等连接,借此熟悉一下流程和部分API。本地连接,意思就是不经过服务,在本地页面的两个video之间进行连接。算了,还是上图吧,一看就懂。

明确一下目标,A作为输出端,需要获取到本地流并添加到自己的RTCPeerConnection;B作为呼叫端,并没有输出的需求,因此只需要接收流。

创建媒体流

页面布局很简单,就是两个video标签,分别代表A和B。所以我们直接看代码,虽然源码是用Vue构建的,但是并没有用到特别的API,整体上和es6的class语法相差不大,而且都有详细的注释,所以建议没有Vue基础的同学可以直接当成es6来阅读。

asynccreateMedia(){\t\n//保存本地流到全局\t\nthis.localstream=awaitnavigator.mediaDevices.getUserMedia({audio:true,video:true})\t\nletvideo=document.querySelector(&rtcA&39;39;);\t\nvideo.srcObject=event.stream;\t\n};\t\nthis.peerB.onicecandidate=(event)=>{连接状态\t\n//监听B的ICE候选信息如果收集到,就添加给A\t\nif(event.candidate){\t\nthis.peerA.addIceCandidate(event.candidate);\t\n}\t\n};\t\n}

这部分主要就是分别创建peer实例,并互相交换ICE。不过有一个属性需要在这里提一下,就是iceConnectionState。

peer.oniceconnectionstatechange=(evt)=>{\t\nconsole.log(&39;+evt.target.iceConnectionState);\t\n};

我们可以通过oniceconnectionstatechange方法来监测ICE连接的状态,它一共有七种状态:

newICE代理正在收集候选人或等待提供远程候选人。

checkingICE代理已经在至少一个组件上接收了远程候选者,并且正在检查候选但尚未找到连接。除了检查,它可能还在收集。

connectedICE代理已找到所有组件的可用连接,但仍在检查其他候选对以查看是否存在更好的连接。它可能还在收集。

completedICE代理已完成收集和检查,并找到所有组件的连接。

failedICE代理已完成检查所有候选对,但未能找到至少一个组件的连接。可能已找到某些组件的连接。

disconnectedICE连接断开

closedICE代理已关闭,不再响应STUN请求。

我们需要注意的是completed和disconnected,一个是完成连接时触发,一个在断开连接时触发。

创建连接

asynccall(){\t\nif(!this.peerA||!this.peerB){//判断是否有对应实例,没有就重新创建\t\nthis.initPeer();\t\n}\t\ntry{\t\nletoffer=awaitthis.peerA.createOffer(this.offerOption);//创建offer\t\nawaitthis.onCreateOffer(offer);\t\n}catch(e){\t\nconsole.log(&39;,e);\t\n}\t\n}

这基本就是之前重复过好几次的流程用代码写出来而已,看到这里,思路应该比较清晰了。不过有一点需要说明一下,就是现在这种情况,A作为呼叫端,B一样是可以拿到A的媒体流的。因为连接一旦建立了,就是双向的,只不过B初始化peer的时候没有添加本地流,所以A不会有B的媒体流。

网络1v1对等连接

想必基本流程大家都已经熟悉了,通过图解、实例来来回回讲了好几遍。所以趁热打铁,我们这次把服务加上,做一个真正的点对点连接。在看下面的文章之前,我希望你有一点点Koa和Scoket.io的基础,了解一些基本API即可。不熟悉的同学也不要紧,现在看也来得及,Koa、Socke.io,或者可以参考我之前的文章Vchat-一个社交聊天系统(vue+node+mongodb)。

需求

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

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

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

选择任一成员进行通话,也就是呼叫动作。这时候就有一些细节问题要处理:不能呼叫自己、同一时刻只允许呼叫一个人且需要判断对方是否是通话中、呼叫后回复需要有相应判断(同意、拒绝以及通话中)

拒绝或通话中,都没有后续动作,可以换个人再呼叫。同意之后,就要开始建立点对点连接。

加入房间

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

//前端\t\njoin(){\t\nif(!this.account)return;\t\nthis.isJoin=true;//输入框弹层逻辑\t\nwindow.sessionStorage.account=this.account;//刷新判断是否登录过\t\nsocket.emit(&39;,{roomid:this.roomid,account:this.account});//发送加入房间请求\t\n}\t\n//后端\t\nconstsockS={};//不同客户端对应的sock实例\t\nconstusers={};//成员列表\t\nsock.on(&39;,data=>{\t\nsock.join(data.roomid,()=>{\t\nif(!users[data.roomid]){\t\nusers[data.roomid]=[];\t\n}\t\nletobj={\t\naccount:data.account,\t\nid:sock.id\t\n};\t\nletarr=users[data.roomid].filter(v=>v.account===data.account);\t\nif(!arr.length){\t\nusers[data.roomid].push(obj);\t\n}\t\nsockS[data.account]=sock;//保存不同客户端对应的sock实例\t\n//将房间内成员列表发给房间内所有人\t\napp._io.in(data.roomid).emit(&39;,users[data.roomid],data.account,sock.id);\t\n});\t\n});

后端成员列表的处理,是因为做了多房间的逻辑,按每个房间的成员表返回的。你们如果做的时候没有多房间,则不需要这么考虑。sockS的处理,是为了发送私聊消息。

呼叫

前面已经说了呼叫的注意事项,所以这里就一起来讲。需要注意的就是消息中需要带有自己和对方的account,因为这是判断成员sock的标识,也就是之前存储在socks中的用来发私聊消息的。然后是前面说的三种状态,在这里用type值1,2,3来区分,然后给出不同的回复。

//前端\t\napply(account){//发送请求\t\n//account对方accountself是自己的account\t\nthis.loading=true;\t\nthis.loadingText=&39;;//呼叫中loading\t\nsocket.emit(&39;,{account:account,self:this.account});\t\n},\t\nreply(account,type){//处理回复\t\nsocket.emit(&39;,{account:account,self:this.account,type:type});\t\n}\t\n//收到请求\t\nsocket.on(&39;,data=>{\t\nif(this.isCall){//判断是否在通话中\t\nthis.reply(data.self,&39;);\t\nreturn;\t\n}\t\nthis.$confirm(data.self+&39;,&39;,{\t\nconfirmButtonText:&39;,\t\ncancelButtonText:&39;,\t\ntype:&39;\t\n}).then(async()=>{\t\nthis.isCall=data.self;\t\nthis.reply(data.self,&39;);\t\n}).catch(()=>{\t\nthis.reply(data.self,&39;);\t\n});\t\n});\t\n//后端\t\nsock.on(&39;,data=>{//转发申请\t\nsockS[data.account].emit(&39;,data);\t\n});

后端比较简单,仅仅是转发一下请求,给对应的客户端。其实我们这个例子的后端,基本都是这个操作,所以后面的后端代码就不贴了,可以去源码直接看。

回复

回复和和呼叫是一样的逻辑,分别处理不同的回复就好了。

//前端\t\nsocket.on(&39;,asyncdata=>{//收到回复\t\nthis.loading=false;\t\nswitch(data.type){\t\ncase&39;://同意\t\nthis.isCall=data.self;//存储通话对象\t\nbreak;\t\ncase&39;://拒绝\t\nthis.$message({\t\nmessage:&39;,\t\ntype:&39;\t\n});\t\nbreak;\t\ncase&39;://正在通话中\t\nthis.$message({\t\nmessage:&39;,\t\ntype:&39;\t\n});\t\nbreak;\t\n}\t\n});

创建连接

呼叫和回复的逻辑基本清楚了,那我们继续思考,应该在什么时机创建P2P连接呢?我们之前说的,拒绝和通话中都不需要处理,只有同意需要,那就应该在同意请求的位置开始创建。需要注意的是,同意请求有两个地方:一个是你点了同意,另一个是对方知道你点了同意之后。

本例采取的是呼叫方发送Offer,这个地方一定得注意,只要有一方创建Offer就可以了,因为一旦连接就是双向的。

socket.on(&39;,data=>{//你点同意的地方\t\n…\t\nthis.$confirm(data.self+&39;,&39;,{\t\nconfirmButtonText:&39;,\t\ncancelButtonText:&39;,\t\ntype:&39;\t\n}).then(async()=>{\t\nawaitthis.createP2P(data);//同意之后创建自己的peer等待对方的offer\t\n…//这里不发offer\t\n})\t\n…\t\n});\t\nsocket.on(&39;,asyncdata=>{//对方知道你点了同意的地方\t\nswitch(data.type){\t\ncase&39;://只有这里发offer\t\nawaitthis.createP2P(data);//对方同意之后创建自己的peer\t\nthis.createOffer(data);//并给对方发送offer\t\nbreak;\t\n…\t\n}\t\n});

和微信等视频通话一样,双方都需要进行媒体流输出,因为你们都要看见对方。所以这里和之前本地对等连接的区别就是都需要给自己的RTCPeerConnection实例添加媒体流,然后连接后各自都能拿到对方的视频流。在初始化RTCPeerConnection时,记得加上onicecandidate函数,用以给对方发送ICE候选。

asynccreateP2P(data){\t\nthis.loading=true;//loading动画\t\nthis.loadingText=&39;;\t\nawaitthis.createMedia(data);\t\n},\t\nasynccreateMedia(data){\t\n…//获取并将本地流赋值给video同之前\t\nthis.initPeer(data);//获取到媒体流后,调用函数初始化RTCPeerConnection\t\n},\t\ninitPeer(data){\t\n//创建输出端PeerConnection\t\n…\t\nthis.peer.addStream(this.localstream);//都需要添加本地流\t\nthis.peer.onicecandidate=(event)=>{\t\n//监听ICE候选信息如果收集到,就发送给对方\t\nif(event.candidate){//发送ICE候选\t\nsocket.emit(&39;,\t\n{account:data.self,self:this.account,sdp:event.candidate});\t\n}\t\n};\t\nthis.peer.onaddstream=(event)=>{\t\n//监听是否有媒体流接入,如果有就赋值给rtcB的src,改变相应loading状态,赋值省略\t\nthis.isToPeer=true;\t\nthis.loading=false;\t\n…\t\n};\t\n}

createOffer等信息交换和之前一样,只是需要通过Socket转发给对应的客户端。然后各自接收到消息后分别采取对应的措施。

socket.on(&39;,(data)=>{//接收到answer\t\nthis.onAnswer(data);\t\n});\t\nsocket.on(&39;,(data)=>{//接收到ICE\t\nthis.onIce(data);\t\n});\t\nsocket.on(&39;,(data)=>{//接收到offer\t\nthis.onOffer(data);\t\n});\t\n//这里只贴一个createOffer的代码,因为和之前的思路都一样,只是写法有些区别\t\n//建议大家都自己敲一遍,有问题可以交流,也可以去源码查看。\t\nasynccreateOffer(data){//创建并发送offer\t\ntry{\t\n//创建offer\t\nletoffer=awaitthis.peer.createOffer(this.offerOption);\t\n//呼叫端设置本地offer描述\t\nawaitthis.peer.setLocalDescription(offer);\t\n//给对方发送offer\t\nsocket.emit(&39;,{account:data.self,self:this.account,sdp:offer});\t\n}catch(e){\t\nconsole.log(&39;,e);\t\n}\t\n}

挂断

挂断的思路依然是将各自的peer关闭,但是这里挂断方还需要借助Socket告诉对方,你已经挂电话了,不然对方还在痴痴地等。

hangup(){//挂断通话并做相应处理对方收到消息后一样需要关闭连接\t\nsocket.emit(&39;,{account:this.isCall,self:this.account});\t\nthis.peer.close();\t\nthis.peer=null;\t\nthis.isToPeer=false;\t\nthis.isCall=false;\t\n}

下期预告

到这里,这期的内容已经讲完了。这篇文章花了不少时间,周末两天都在写这了,希望你们看完能有所收获。

系列的下一期(ps:系列下一期不一定是文章下一期,写作不易,多多包涵?)主要是讲数据传输以及多端本地对等连接、网络对等连接,因为还有一个重要的API没有提到,就是RTCDataChannel-数据传输,然后多人视频需要注意的地方也不少。

后记

如果你看到了这里,且本文对你有一点帮助的话,希望你可以动动小手支持一下作者,感谢?。文中如有不对之处,也欢迎大家指出,共勉。

OK,关于痴痴网站源码分享和痴痴迷迷打一个生肖的内容到此结束了,希望对大家有所帮助。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平