从0搭建一个WebRTC,实现多房间多对多通话,并实现屏幕录
这篇文章开始会实现一个一对一WebRTC和多对多的WebRTC,以及基于屏幕共享的录制。本篇会实现信令和前端部分,信令使用fastity来搭建,前端部分使用Vue3来实现。为什么要使用WebRTC
WebRTC全称WebRealTimeCommunication,是一种实时音视频的技术,它的优势是低延时。本文章食用者要求了解音视频基础能搭建简单的node服务,docker配置vue框架的使用环境搭建及要求
废话不多说,现在开始搭建环境,首先是需要开启socket服务,采用的是fastify来进行搭建。详情可以见文档地址,本例使用的是3。x来启动的。接下来安装fastifysocket。io3。0。0插件,详细配置可以见文档,此处不做详细解释。接下来是搭建Vue3,使用vite脚手架搭建简单的demo。
要求:前端服务运行在localhost或者https下。node需要redis进行数据缓存
C音视频开发WebRTC学习资料:点击领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
获取音视频
要实现实时音视频第一步当然是要能获取到视频流,在这里我们使用浏览器提供的API,MediaDevices来进行摄像头流的捕获enumerateDevices
第一个要介绍的API是enumerateDevices,是请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。直接在控制台执行API,获取的设备如图
我们注意到里面返回的设备ID和label是空的,这是由于浏览器的安全策略限制,必须授权摄像头或麦克风才能允许返回设备ID和设备标签,接下来我们介绍如何请求摄像头和麦克风getUserMedia
这个API顾名思义,就是去获取用户的Meida的,那我们直接执行这个API来看看效果
ps:由于掘金的代码片段的iframe没有配置allowdisplaycapture;microphone;camera属性,需要手动打开详情查看效果
通过上述例子我们可以获取到本机的音视频画面,并且可以播放在video标签里,那么我们可以在获取了用户的流之后,重新再获取一次设备列表看看发生了什么变化
在获取了音视频之后,获取的设备列表的详细信息已经出现,我们就可以获取指定设备的音视频数据,
这里介绍一下getUserMedia的参数constraints,视频参数配置interfaceMediaTrackConstraintSet{画面比例aspectRatio?:ConstrainDouble;设备ID,可以从enumerateDevices中获取deviceId?:ConstrainDOMString;摄像头前后置模式,一般适用于手机facingMode?:ConstrainDOMString;帧率,采集视频的目标帧率frameRate?:ConstrainDouble;组ID,用一个设备的输入输出的组ID是同一个groupId?:ConstrainDOMString;视频高度height?:ConstrainULong视频宽度width?:ConstrainULong;}音频参数配置interfaceMediaTrackConstraintSet{是否开启AGC自动增益,可以在原有音量上增加额外的音量autoGainControl?:ConstrainBoolean;声道配置channelCount?:ConstrainULong;设备ID,可以从enumerateDevices中获取deviceId?:ConstrainDOMString;是否开启回声消除echoCancellation?:ConstrainBoolean;组ID,用一个设备的输入输出的组ID是同一个groupId?:ConstrainDOMString;延迟大小latency?:ConstrainDouble;是否开启降噪noiseSuppression?:ConstrainBoolean;采样率单位HzsampleRate?:ConstrainULong;采样大小,单位位sampleSize?:ConstrainULong;本地音频在本地扬声器播放suppressLocalAudioPlayback?:ConstrainBoolean;}
C音视频开发WebRTC学习资料:点击领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
一对一连接
当我们采集到了音视频数据,接下来就是要建立链接,在开始之前需要科普一下WebRTC的工作方式,我们常见有三种WebRTC的网络结构MeshMCUSFU关于这三种模式的区别可以查看文章来了解
在这里由于设备的限制,我们采用Mesh的方案来进行开发一对一的流程
我们建立一对一的链接需要知道后流程是怎么流转的,接下来上一张图,便可以清晰的了解
这里是由ClientA发起B来接受A的视频数据。上图总结可以为A创建本地视频流,把视频流添加到PeerConnection里面创建一个Offer给B,B收到Offer以后,保存这个offer,并响应这个Offer给A,A收到B的响应后保存A的远端响应,进行NAT穿透,完成链接建立。
话已经讲了这么多,我们该怎么建立呢,光说不做假把式,接下来,用我们的项目创建一个来试试初始化
首先启动fastify服务,接下来在Vue项目安装socket。ioclient4然后连接服务端的socketimport{v4asuuid}fromuuid;import{io,Socket}fromsocket。ioclient;constmyUserIdref(uuid());letsocket:Socket;socketio(http:127。0。0。1:7070,{query:{房间号,由输入框输入获得room:room。value,userId通过uuid获取userId:myUserId。value,昵称,由输入框输入获得nick:nick。value}});
可以查看chrome的控制台,检查ws的链接情况,如果出现跨域,请查看socket。io的server配置并开启cors配置。创建offer
开始创建RTCPeerConnection,这里采用google的公共stun服务constpeerConnectnewRTCPeerConnection({iceServers:〔{urls:stun:stun。l。google。com:19302}〕})
根据上面的流程图我们下一步要做的事情是用上面的方式获取视频流,并将获取到的流添加到RTCPeerConnection中,并创建offer,把这个offer设置到这个rtcPeer中,并把offer发送给socket服务letlocalStream:MediaStream;stream。getTracks()。forEach((track){peerConnect。addTrack(track,stream)})constofferawaitpeerConnect。createOffer();awaitpeerConnect。setLocalDescription(offer);socket。emit(offer,{creatorUserId:myUserId。value,sdp:offer},(res:any){console。log(res);});
socket服务收到了这份offer后需要给B发送A的offerfastify。io。on(connection,async(socket){socket。on(offer,async(offer,callback){socket。emit(offer,offer);callback({status:ok})})})处理offer
B需要监听socket里面的offer事件并创建RTCPeerConnection,将这个offer设置到远端,接下来来创建响应。并且将这个响应设置到本地,发送answer事件回复给Asocket。on(offer,async(offer:{sdp:RTCSessionDescriptionInit,creatorUserId:string}){constpeerConnectnewRTCPeerConnection({iceServers:〔{urls:stun:stun。l。google。com:19302}〕})awaitpeerConnect。setRemoteDescription(offer。sdp);constanswerawaitpeerConnect。createAnswer();awaitpeerConnect。setLocalDescription(answer);socket。emit(answer,{sdp:answer},(res:any){console。log(res);})})处理answer
服务端广播answersocket。on(offer,async(offer,callback){socket。emit(offer,offer);callback({status:ok})})
A监听到socket里面的answer事件,需要将刚才的自己的RTCpeer添加远端描述socket。on(answer,async(data:{sdp:RTCSessionDescriptionInit}){awaitpeerConnect。setRemoteDescription(data。sdp)})处理ICEcandidate
接下来A会获取到ICE候选信息,需要发送给BpeerConnect。onicecandidate(candidateInfo:RTCPeerConnectionIceEvent){if(candidateInfo。candidate){socket。emit(ICEcandidate,{sdp:candidateInfo。candidate},(res:any){console。log(res);})}}
广播消息是同理这里就不再赘述了,B获取到了A的ICE,需要设置候选socket。on(ICEcandidate,async(data:{sdp:RTCIceCandidate}){awaitpeerConnect。addIceCandidate(data。sdp)})
接下来B也会获取到ICE候选信息,同理需要发送给A,待A设置完成之后便可以建立链接,代码同上,B接下来会收到流添加的事件,这个事件会有两次,分别是音频和视频的数据
C音视频开发WebRTC学习资料:点击领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
处理音视频数据peerConnect。ontrack(track:RTCTrackEvent){if(track。track。kindvideo){constvideodocument。createElement(video);video。srcObjecttrack。streams〔0〕;video。autoplaytrue;video。style。setProperty(width,400px);video。style。setProperty(aspectratio,169);video。setAttribute(id,track。track。id)document。body。appendChild(video)}if(track。track。kindaudio){constaudiodocument。createElement(audio);audio。srcObjecttrack。streams〔0〕;audio。autoplaytrue;audio。setAttribute(id,track。track。id)document。body。appendChild(audio)}}
到这里你就可以见到两个视频建立的P2P链接了。到这里为止只是建立了视频的一对一链接,但是我们可以通过这些操作进行复制,就能进行多对多的连接了。多对多连接
在开始我们需要知道,一个人和另一个人建立连接双方都需要创建自己的peerConnection。对于多人的情况,首先我们需要知道进入的房间里面当前的人数,给每个人都创建一个RtcPeer,同时收到的人也回复这个offer给发起的人。对于后进入的人,需要让已经创建音视频的人给后进入的人创建新的offer。
基于上面的流程,我们现在先实现一个成员列表的接口成员列表的接口
在我们登录socket服务的时候我们在query参数里面有房间号,userId和昵称,我们可以通过redis记录对应的房间号的登录和登出,从而实现成员列表。
可以在某一个人登录的时候获取一下redis对应房间的成员列表,如果没有这个房间,就把这个人丢进新的房间,并且存储到redis中,方便其他人登录这个房间的时候知道现在有多少人。fastify。io。on(connection,async(socket){constroomsocket。handshake。query。room;constredisfastify。redis;letuserList;获取当前房间的数据awaitgetUserList()asyncfunctiongetUserList(){constroomUserawaitredis。get(room);if(roomUser){userListnewMap(JSON。parse(roomUser))}else{userListnewMap();}}asyncfunctionsetRedisRoom(){awaitredis。set(room,JSON。stringify(〔。。。userList〕))}functionrmUser(userId){userList。delete(userId);}if(room){将这人加入到对应的socket房间socket。join(room);awaitsetRedisRoom();广播有人加入了socket。to(room)。emit(join,userId);}这个人断开了链接需要将这个人从redis中删除socket。on(disconnect,async(socket){awaitgetUserList();rmUser(userId);awaitsetRedisRoom();})})
到上面为止,我们实现了成员的记录、广播和删除。接下来是需要实现一个成员列表的接口,提供给前端项目调用。fastify。get(userlist,asyncfunction(request,reply){constredisfastify。redis;returnawaitredis。get(request。query。room);})多对多初始化
由于需要给每个人发送offer,需要对上面的初始化函数进行封装。创建RTCPeerConnectionparamcreatorUserId创建者id,本人paramrecUserId接收者idconstinitPeerasync(creatorUserId:string,recUserId:string){constpeerConnectnewRTCPeerConnection({iceServers:〔{urls:stun:stun。l。google。com:19302}〕})returnpeerConnect;})
由于存在多份rtc的映射关系,我们这里可以用Map来实现映射的保存constpeerConnectListnewMap();constinitPeer(){ice,track,newPeer等其他代码。。。。。。peerConnectList。set({creatorUserId}{recUserId},peerConnect);}获取成员列表
上面实现了成员列表。接下来进入了对应的房间后需要轮询获取对应的成员列表letuserListref(〔〕);constintoRoom(){其他代码。。。。。。setInterval((){axios。get(userlist,{params:{room:room。value}})。then((res){userList。valueres。data})},1000)}创建多对多的Offer和Answer
在我们获取到视频流的时候,可以对在线列表里除了自己的人都创建一个RTCpeer,来进行一对一连接,从而达到多对多连接的效果。过滤自己constemitListuserList。value。filter((item)item〔0〕!myUserId。value);for(constitemofemitList){item〔0〕就是目标人的userIdconstpeerawaitinitPeer(myUserId。value,item〔0〕);awaitcreateOffer(item〔0〕,peer);}constcreateOfferasync(recUserId:string,peerConnect:RTCPeerConnection,stream:MediaStreamlocalStream){if(!localStream)return;stream。getTracks()。forEach((track){peerConnect。addTrack(track,stream)})constofferawaitpeerConnect。createOffer();awaitpeerConnect。setLocalDescription(offer);socket。emit(offer,{creatorUserId:myUserId。value,sdp:offer,recUserId},(res:any){console。log(res);});}
那么在socket服务中我们怎么只给对应的人进行事件广播,不对其他人进行广播,我们可以用找到这个人userId对应的socketId,进而只给这一个人广播事件。首先获取IO对应的nameSpaceconstIONameSpacefastify。io。of();发送Offer给对应的人socket。on(offer,async(offer,callback){重新从reids获取用户列表awaitgetUserList();找到目标的UserId的数据constuseruserList。get(offer。recUserId);if(user){找到对应的socketIdconstioIONameSpace。sockets。get(user。sockId);if(!io)return;io。emit(offer,offer);callback({status:ok})}})
其他人需要监听socket的事件,每个人都需要处理对应自己的offer。socket。on(offer,handleOffer);consthandleOfferasync(offer:{sdp:RTCSessionDescriptionInit,creatorUserId:string,recUserId:string}){constpeerawaitinitPeer(offer。creatorUserId,offer。recUserId);awaitpeer。setRemoteDescription(offer。sdp);constanswerawaitpeer。createAnswer();awaitpeer。setLocalDescription(answer);socket。emit(answer,{recUserId:myUserId。value,sdp:answer,creatorUserId:offer。creatorUserId},(res:any){console。log(res);})}
接下来的步骤其实就是和一对一是一样的了,后面还需要发起offer的人处理对应peer的offer、以及ICE候选,还有流进行挂载播放。socket。on(answer,handleAnswer)应答方回复consthandleAnswerasync(data:{sdp:RTCSessionDescriptionInit,recUserId:string,creatorUserId:string}){constpeerpeerConnectList。get({data。creatorUserId}{data。recUserId});if(!peer){console。warn(handleAnswerpeer获取失败)return;}awaitpeer。setRemoteDescription(data。sdp)}。。。。。。处理播放,处理ICE候选
到目前为止,就实现了一个基于mesh的WebRTC的多对多通信
C音视频开发WebRTC学习资料:点击领取音视频开发(资料文档视频教程面试题)(FFmpegWebRTCRTMPRTSPHLSRTP)
基于WebRTC的屏幕录制getDisplayMedia
这个API是在MediaDevices里面的一个方法,是用来获取屏幕共享的。
这个MediaDevices接口的getDisplayMedia()方法提示用户去选择和授权捕获展示的内容或部分内容(如一个窗口)在一个MediaStream里。然后,这个媒体流可以通过使用MediaStreamRecordingAPI被记录或者作为WebRTC会话的一部分被传输。awaitnavigator。mediaDevices。getDisplayMedia()
MediaRecorder
获取到屏幕共享流后,需要使用MediaRecorder这个api来对流进行录制,接下来我们先获取屏幕流,同时创建一个MeidaRecord类letscreenStream:MediaStream;letmediaRecord:MediaRecorder;letblobMedia:(Blob)〔〕〔〕;conststartLocalRecordasync(){blobMedia〔〕;try{screenStreamawaitnavigator。mediaDevices。getDisplayMedia();screenStream。getVideoTracks()〔0〕。addEventListener(ended,(){console。log(用户中断了屏幕共享);endLocalRecord()})mediaRecordnewMediaRecorder(screenStream,{mimeType:videowebm});mediaRecord。ondataavailable(e){if(e。datae。data。size0){blobMedia。push(e。data);}};500是每隔500ms进行一个保存数据mediaRecord。start(500)}catch(e){console。log(屏幕共享失败{e});}}
获取到了之后可以使用Blob进行处理constreplayLocalRecordasync(){if(blobMedia。length){constscVideodocument。querySelector(screenVideo)asHTMLVideoElement;constblobnewBlob(blobMedia,{type:videowebm})if(scVideo){scVideo。srcURL。createObjectURL(blob);}}else{console。log(没有录制文件);}}constdownloadLocalRecordasync(){if(!blobMedia。length){console。log(没有录制文件);return;}constblobnewBlob(blobMedia,{type:videowebm});consturlURL。createObjectURL(blob);constadocument。createElement(a);a。hrefurl;a。download录屏{Date。now()}。webm;a。click();}
这里有一个基于Vue2的完整例子ps:由于掘金的代码片段的iframe没有配置allowdisplaycapture;microphone;camera属性,需要手动打开详情查看效果
后续将会更新,WebRTC的自动化测试,视频画中画,视频截图等功能