李超老师的项目
先肯定分为两个两个端,一个是服务器端一个是客户端。客户端用于UI界面的显示,服务器端用于处理客户端发来的消息。
我们先搭建stun和turn服务器
首先介绍一下什么是stun协议,
它是用来干什么的?
stun协议存在的目的就是进行NAT穿越。stun是典型的客户端、服务器模式。客户端发送请求,服务器进行响应。
那么是什么NAT穿越呢?
首先我们先了解一下为什么要进行NAT穿越。下面举个例子,在两个浏览器之间进行实时的音视频互动,对于底层来说,这就是两个端点之间进行高效的网络传输。
为了解决音视频网络传输的问题,webrtc引入了一些网络传输协议。
1.NAT:那么此时我们介绍NAT,简单理解为将内网的地址转换为公网的地址,内网地址无法通讯,通过NAT转换为公网之后,才有通信的可能。
2.说到这里那么顺便介绍一下stun,这个stun充当的是中介的作用,在NAT的基础上,交换两个公网的信息,使得;两个公网之间可以建立连接。
3.turn,stun是有一定几率是不成功的,因此turn会在云端架设一个服务器,在p2p连接不成功的情况下,保证音视频的互通,它就相当于一个中转站。
4.ICE, 罗列所有通信可能性,选择最优解。
NAT又分为四种类型:
完全锥型
地址限制锥型
端口限制锥型
对称型
根据图中所示很好理解,caller与信令服务器连接,同时callee也跟信令服务器连接,第二个callee也跟信令服务器连接;caller向信令服务器发出join请求,信令服务器响应返回joined信息,第一个callee同理,但是第二个callee发送完join之后服务器发现成员已满,因此返回一个full信息,该callee不能join。然后caller和callee进行媒体协商,协商成功之后进行媒体流的数据传输。之后callee主动发出leave请求,服务器响应跟caller发出bye信息并返回callee leaved信息。
我们来看一下服务器端的代码。主要就是处理客户端发来的信令消息。也就是以上的流程转换为代码。
- io.sockets.on('connection', (socket)=> {
-
- socket.on('message', (room, data)=>{
- socket.to(room).emit('message',room, data);
- });
-
- socket.on('join', (room)=>{
- socket.join(room);
- var myRoom = io.sockets.adapter.rooms[room];
- var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
- logger.debug('the user number of room is: ' + users);
-
- if(users < USERCOUNT){
- socket.emit('joined', room, socket.id); //发给自己
- if(users > 1){
- socket.to(room).emit('otherjoin', room, socket.id);//发给除自己之外的房间内的所有人
- }
-
- }else{
- socket.leave(room);
- socket.emit('full', room, socket.id);
- }
- //socket.emit('joined', room, socket.id); //发给自己
- //socket.broadcast.emit('joined', room, socket.id); //发给除自己之外的这个节点上的所有人
- //io.in(room).emit('joined', room, socket.id); //发给房间内的所有人
- });
-
- socket.on('leave', (room)=>{
- var myRoom = io.sockets.adapter.rooms[room];
- var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
- logger.debug('the user number of room is: ' + (users-1));
- //socket.emit('leaved', room, socket.id);
- //socket.broadcast.emit('leaved', room, socket.id);
- socket.to(room).emit('bye', room, socket.id);
- socket.emit('leaved', room, socket.id);
- //io.in(room).emit('leaved', room, socket.id);
- });
-
- });
iceRestart是一个很好的方案,能够帮助我们重新选择数据传输的线路
下面了解一下状态机是什么这点蛮重要
下面介绍一下客户端代码编写的流程图
首先写函数start
start函数用于采集音视频数据
采集成功则与信令服务器连接,并注册信令函数
注册以上这些消息的处理函数
如果是joined则设置状态为joined,创建PC并绑定媒体流。
如果是otherjoin,则判断自身的状态是否为joined_unbind,如果是则需要重新创建PC并绑定媒体流并将状态设置为joined_conn,如果一开始状态为joined则直接将状态转换为joined_conn,接着开始媒体协商。
如果是full则状态设置为full并关闭PC,并关闭本地媒体流
客户端的实现需要注意的几点是
1.网络连接要在音视频数据获取到之后,否则可能导致绑定音视频流失败
2.当一端退出房间之后,另一端的PeerConnection要关闭重建,否则与新用户互通时媒体协商会失败。
3.所有的处理流程为异步处理
这里要了解一下什么是异步处理:
异步事件处理:要等待收到一个消息或事件后,才能做下一步的操作
同步处理:做完一步,直接做下一步
接下来我们介绍一下客户端的代码
首先先了解一个api
其中较为关键的是iceServers
第二个api
根据流程写以下代码:
首先start函数获取音视频数据
- function start(){
-
- if(!navigator.mediaDevices ||
- !navigator.mediaDevices.getUserMedia){
- console.error('the getUserMedia is not supported!');
- return;
- }else {
-
- var constraints;
-
- if( shareDeskBox.checked && shareDesk()){
-
- constraints = {
- video: false,
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true
- }
- }
-
- }else{
- constraints = {
- video: true,
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true
- }
- }
- }
-
- navigator.mediaDevices.getUserMedia(constraints)
- .then(getMediaStream)
- .catch(handleError);
- }
-
- }
与信令服务器连接,注册处理函数
- function conn(){
-
- socket = io.connect();
-
- socket.on('joined', (roomid, id) => {
- console.log('receive joined message!', roomid, id);
- state = 'joined'
-
- //如果是多人的话,第一个人不该在这里创建peerConnection
- //都等到收到一个otherjoin时再创建
- //所以,在这个消息里应该带当前房间的用户数
- //
- //create conn and bind media track
- createPeerConnection();
- bindTracks();
-
- btnConn.disabled = true;
- btnLeave.disabled = false;
- console.log('receive joined message, state=', state);
- });
-
- socket.on('otherjoin', (roomid) => {
- console.log('receive joined message:', roomid, state);
-
- //如果是多人的话,每上来一个人都要创建一个新的 peerConnection
- //
- if(state === 'joined_unbind'){
- createPeerConnection();
- bindTracks();
- }
-
- state = 'joined_conn';
- call();
-
- console.log('receive other_join message, state=', state);
- });
-
- socket.on('full', (roomid, id) => {
- console.log('receive full message', roomid, id);
- hangup();
- closeLocalMedia();
- state = 'leaved';
- console.log('receive full message, state=', state);
- alert('the room is full!');
- });
-
- socket.on('leaved', (roomid, id) => {
- console.log('receive leaved message', roomid, id);
- state='leaved'
- socket.disconnect();
- console.log('receive leaved message, state=', state);
-
- btnConn.disabled = false;
- btnLeave.disabled = true;
- });
-
- socket.on('bye', (room, id) => {
- console.log('receive bye message', roomid, id);
- //state = 'created';
- //当是多人通话时,应该带上当前房间的用户数
- //如果当前房间用户不小于 2, 则不用修改状态
- //并且,关闭的应该是对应用户的peerconnection
- //在客户端应该维护一张peerconnection表,它是
- //一个key:value的格式,key=userid, value=peerconnection
- state = 'joined_unbind';
- hangup();
- offer.value = '';
- answer.value = '';
- console.log('receive bye message, state=', state);
- });
-
- socket.on('disconnect', (socket) => {
- console.log('receive disconnect message!', roomid);
- if(!(state === 'leaved')){
- hangup();
- closeLocalMedia();
-
- }
- state = 'leaved';
-
- });
-
- socket.on('message', (roomid, data) => {
- console.log('receive message!', roomid, data);
-
- if(data === null || data === undefined){
- console.error('the message is invalid!');
- return;
- }
-
- if(data.hasOwnProperty('type') && data.type === 'offer') {
-
- offer.value = data.sdp;
-
- pc.setRemoteDescription(new RTCSessionDescription(data));
-
- //create answer
- pc.createAnswer()
- .then(getAnswer)
- .catch(handleAnswerError);
-
- }else if(data.hasOwnProperty('type') && data.type == 'answer'){
- answer.value = data.sdp;
- pc.setRemoteDescription(new RTCSessionDescription(data));
-
- }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
- var candidate = new RTCIceCandidate({
- sdpMLineIndex: data.label,
- candidate: data.candidate
- });
- pc.addIceCandidate(candidate);
-
- }else{
- console.log('the message is invalid!', data);
-
- }
-
- });
-
-
- roomid = getQueryVariable('room');
- socket.emit('join', roomid);
-
- return true;
- }
媒体协商call函数
- function call(){
-
- if(state === 'joined_conn'){
-
- var offerOptions = {
- offerToRecieveAudio: 1,
- offerToRecieveVideo: 1
- }
-
- pc.createOffer(offerOptions)
- .then(getOffer)
- .catch(handleOfferError);
- }
- }
- function getOffer(desc){
- pc.setLocalDescription(desc);
- offer.value = desc.sdp;
- offerdesc = desc;
-
- //send offer sdp
- sendMessage(roomid, offerdesc);
-
- }
- function getAnswer(desc){
- pc.setLocalDescription(desc);
- answer.value = desc.sdp;
-
- //send answer sdp
- sendMessage(roomid, desc);
- }
每个端都维护一个自己的peerconnection
- var pcConfig = {
- 'iceServers': [{
- 'urls': 'turn:stun.al.learningrtc.cn:3478',
- 'credential': "mypasswd",
- 'username': "garrylea"
- }]
- };
- function createPeerConnection(){
-
- //如果是多人的话,在这里要创建一个新的连接.
- //新创建好的要放到一个map表中。
- //key=userid, value=peerconnection
- console.log('create RTCPeerConnection!');
- if(!pc){
- pc = new RTCPeerConnection(pcConfig);
-
- pc.onicecandidate = (e)=>{
-
- if(e.candidate) {
- sendMessage(roomid, {
- type: 'candidate',
- label:event.candidate.sdpMLineIndex,
- id:event.candidate.sdpMid,
- candidate: event.candidate.candidate
- });
- }else{
- console.log('this is the end candidate');
- }
- }
-
- pc.ontrack = getRemoteStream;
- }else {
- console.warning('the pc have be created!');
- }
-
- return;
- }