美文网首页
01-WebRTC一对一通话

01-WebRTC一对一通话

作者: 一亩三分甜 | 来源:发表于2022-01-24 07:48 被阅读0次

    音视频文章汇总,上一篇文章《00-WebRTC入门》介绍了nodejs作为信令服务器,客户端和服务器端的交互选择websocket作为通信协议。本文从代码层面实现一对一视频通话。

    1.一对一通话原理

    主要分为四大块:
    I.信令设计:进入房间,离开房间等
    II.媒体协商:交换彼此客户端的媒体信息sdp
    III.加入Stream/Track
    IV.网络协商:Candidate,网络地址,端口号等
    先看一张图


    image

    1.1信令协议设计

    采用json封装格式

    1. join 加入房间
    2. respjoin
      当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回
    3. leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开
    4. newpeer
      服务器通知客户端有新人加入,收到newpeer
      则发起连接请求
    5. peerleave
      服务器通知客户端有人离开
    6. offer 转发offer sdp
    7. answer 转发answer sdp
    8. candidate 转发candidate sdp

    Join

    var jsonMsg = {
    'cmd': 'join',
    'roomId': roomId,
    'uid': localUserId,
    };
    

    respjoin

    jsonMsg = {
    'cmd': 'resp‐join',
    'remoteUid': remoteUid
    };
    

    leave

    var jsonMsg = {
    'cmd': 'leave',
    'roomId': roomId,
    'uid': localUserId,
    };
    

    newpeer

    var jsonMsg = {
    'cmd': 'new‐peer',
    'remoteUid': uid
    };
    

    peerleave

    var jsonMsg = {
    'cmd': 'peer‐leave',
    'remoteUid': uid
    };
    

    offer

    var jsonMsg = {
    'cmd': 'offer',
    'roomId': roomId,
    'uid': localUserId,
    'remoteUid':remoteUserId,
    'msg': JSON.stringify(sessionDescription)
    };
    

    answer

    var jsonMsg = {
    'cmd': 'answer',
    'roomId': roomId,
    'uid': localUserId,
    'remoteUid':remoteUserId,
    'msg': JSON.stringify(sessionDescription)
    };
    

    candidate

    var jsonMsg = {
    'cmd': 'candidate',
    'roomId': roomId,
    'uid': localUserId,
    'remoteUid':remoteUserId,
    'msg': JSON.stringify(candidateJson)
    };
    

    1.2媒体协商

    image

    createOffer
    基本格式
    aPromise = myPeerConnection.createOffer([options]);
    [options]

    var options = {
    offerToReceiveAudio: true, // 告诉另一端,你是否想接收音频,默认true
    offerToReceiveVideo: true, // 告诉另一端,你是否想接收视频,默认true
    iceRestart: false, // 是否在活跃状态重启ICE网络协商
    };
    

    iceRestart:只有在处于活跃的时候,iceRestart=false才有作用。
    createAnswer
    基本格式
    aPromise = RTCPeerConnection .createAnswer([ options ]); 目前createAnswer的options是
    无效的。
    setLocalDescription
    基本格式
    aPromise = RTCPeerConnection .setLocalDescription(sessionDescription);
    setRemoteDescription
    基本格式
    aPromise = pc.setRemoteDescription(sessionDescription);

    1.3加入Stream/Track

    addTrack
    基本格式
    rtpSender = rtcPeerConnection .addTrack(track,stream ...);
    track:添加到RTCPeerConnection中的媒体轨(音频track/视频track)
    stream:getUserMedia中拿到的流,指定track所在的stream

    1.4网络协商

    addIceCandidate
    基本格式
    aPromise = pc.addIceCandidate(候选人);

    candidate

    属性 说明
    candidate 候选者描述信息
    sdpMid 与候选者相关的媒体流的识别标签
    sdpMLineIndex 在SDP中 m=的索引值
    usernameFragment 包括了远端的唯一标识

    Android和Web端不同。

    1.5RTCPeerConnection补充

    1.5.1构造函数

    configuration可选
    属性说明
    candidate 候选者描述信息
    sdpMid 与候选者相关的媒体流的识别标签
    sdpMLineIndex 在SDP中 m=的索引值
    usernameFragment 包括了远端的唯一标识
    bundlePolicy 一般用maxbundle
    banlanced:音频与视频轨使用各自的传输通道
    maxcompat:
    每个轨使用自己的传输通道
    maxbundle:
    都绑定到同一个传输通道
    iceTransportPolicy 一般用all
    指定ICE的传输策略
    relay:只使用中继候选者
    all:可以使用任何类型的候选者
    iceServers
    其由RTCIceServer组成,每个RTCIceServer都是一个ICE代理的服务器

    属性 含义
    credential 凭据,只有TURN服务使用
    credentialType 凭据类型,可以password或oauth
    urls 用于连接服中的ur数组
    username 用户名,只有TURN服务使用

    rtcpMuxPolicy 一般用require
    rtcp的复用策略,该选项在收集ICE候选者时使用

    属性 含义
    negotiate 收集RTCP与RTP复用的ICE候选者,如果RTCP能复用就与RTP复用,如果不能复用,就将他们单独使用
    require 只能收集RTCP与RTP复用的ICE候选者,如果RTCP不能复用,则失败

    1.5.2重要事件

    onicecandidate 收到候选者时触发的事件
    ontrack 获取远端流
    onconnectionstatechange PeerConnection的连接状态,参考: https://developer.mozilla.org/enUS/
    docs/Web/API/RTCPeerConnection/connectionState

    pc.onconnectionstatechange = function(event) {
        switch(pc.connectionState) {
        case "connected":
        // The connection has become fully   connected
              break;
        case "disconnected":
        case "failed":
    // One or more transports has terminated       unexpectedly or in an error
        break;
        case "closed":
        // The connection has been closed
        break;
      }
    }
    

    oniceconnectionstatechange ice连接事件 具体参考:https://developer.mozilla.org/enUS/docs/Web/API/RTCPeerConnection/iceConnectionState

    1.6实现WebRTC音视频通话

    开发步骤

    1. 客户端显示界面
    2. 打开摄像头并显示到页面
    3. websocket连接
    4. join、newpeer
      、respjoin
      信令实现
    5. leave、peerleave
      信令实现
    6. offer、answer、candidate信令实现
    7. 综合调试和完善

    1.6.1客户端显示界面

    image
    <!DOCTYPE html>
    <link rel="shortcut icon" href="#">
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <title>WebRTC Demo</title>
        </head>
    
        <h1>WebRTC Demo</h1>
    
        <div id="buttons">
            <input id="zero-RoomId" type="text" placeholder="请输入房间ID" maxlength="40"/>
            <button id="joinBtn" type="button">加入</button>
            <button id="leaveBtn" type="button">离开</button>
        </div>
        <div id="videos">
            <video id="localVideo" autoplay muted playsinline>本地窗口</video>
            <video id="remoteVideo" autoplay playsinline>远端窗口</video>
        </div>
        <script src="js/main.js"></script>
        <script src="js/adapter-latest.js"></script>
    </html>
    

    1.6.2打开摄像头并显示到界面

    image
    0

    1.6.3WebSocket连接

    fishRTCEngine = new FishRTCEngine("wss://192.168.1.102:8098/ws");
    fishRTCEngine.createWebSocket();
    FishRTCEngine.prototype.createWebSocket = function () {
      fishRTCEngine = this;
      fishRTCEngine.signaling = new WebSocket(this.wsUrl);
      fishRTCEngine.signaling.onopen = function () {
        fishRTCEngine.onOpen();
      };
    
      fishRTCEngine.signaling.onmessage = function (ev) {
        fishRTCEngine.onMessage(ev);
      };
    
      fishRTCEngine.signaling.onerror = function (ev) {
        fishRTCEngine.onError(ev);
      };
    
      fishRTCEngine.signaling.onclose = function (ev) {
        fishRTCEngine.onClose(ev);
      };
    };
    

    1.6.4 join、newpeer、respjoin信令实现

    思路:(1)点击加入开妞;
    (2)响应加入按钮事件;
    (3)将join发送给服务器;
    (4)服务器 根据当前房间的人数
    做处理,如果房间已经有人则通知房间里面的人有新人加入(newpeer),并通知自己房间里面是什么人(respjoin)。

    1.6.5 leave、peerleave信令实现

    思路:(1)点击离开按钮;
    (2)响应离开按钮事件;
    (3)将leave发送给服务器;
    (4)服务器处理leave,将发送者删除并通知房间(peerleave)的其他人;
    (5)房间的其他人在客户端响应peerleave事件。
    // One or more transports has terminated unexpectedly or in an error
    break;
    case "closed":
    // The connection has been closed
    break;
    }
    }

    1.6.6 offer、answer、candidate信令实现

    思路:
    (1)收到newpeer
    (handleRemoteNewPeer处理),作为发起者创建RTCPeerConnection,绑定事件响应函数,
    加入本地流;
    (2)创建offer sdp,设置本地sdp,并将offer sdp发送到服务器;
    (3)服务器收到offer sdp 转发给指定的remoteClient;
    (4)接收者收到offer,也创建RTCPeerConnection,绑定事件响应函数,加入本地流;
    (5)接收者设置远程sdp,并创建answer sdp,然后设置本地sdp并将answer sdp发送到服务器;
    (6)服务器收到answer sdp 转发给指定的remoteClient;
    (7)发起者收到answer sdp,则设置远程sdp;
    (8)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄;
    (9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方
    (10)如果P2P能成功则进行P2P通话,如果P2P不成功则进行中继转发通话。

    1.6.7 综合调试和完善

    思路:
    (1)点击离开时,要将RTCPeerConnection关闭(close);
    (2)点击离开时,要将本地摄像头和麦克风关闭;
    (3)检测到客户端退出时,服务器再次检测该客户端是否已经退出房间。
    (4)RTCPeerConnection时传入ICE server的参数,以便当在公网环境下可以进行正常通话。
    客户端代码

    "use strict";
    
    // join 主动加入房间
    // leave 主动离开房间
    // new-peer 有人加入房间,通知已经在房间的人
    // peer-leave 有人离开房间,通知已经在房间的人
    // offer 发送offer给对端peer
    // answer发送offer给对端peer
    // candidate 发送candidate给对端peer
    const SIGNAL_TYPE_JOIN = "join";
    const SIGNAL_TYPE_RESP_JOIN = "resp-join"; // 告知加入者对方是谁
    const SIGNAL_TYPE_LEAVE = "leave";
    const SIGNAL_TYPE_NEW_PEER = "new-peer";
    const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
    const SIGNAL_TYPE_OFFER = "offer";
    const SIGNAL_TYPE_ANSWER = "answer";
    const SIGNAL_TYPE_CANDIDATE = "candidate";
    
    var localUserId = Math.random().toString(36).substr(2); //本地uid
    var remoteUserId = -1; //对端uid
    var roomId = 0;
    
    var localVideo = document.querySelector("#localVideo");
    var remoteVideo = document.querySelector("#remoteVideo");
    var localStream = null;
    var remoteStream = null;
    var pc = null; //RTCPeerConnection
    
    var fishRTCEngine;
    
    function handleIceCandidate(event) {
      console.info("handleIceCandidate");
      if (event.candidate) {
        //不为空才发送candidate
        var jsonMsg = {
          cmd: "candidate",
          roomId: roomId,
          uid: localUserId,
          remoteUid: remoteUserId,
          msg: JSON.stringify(event.candidate),
        };
        var message = JSON.stringify(jsonMsg);
        fishRTCEngine.sendMessage(message);
        // console.info("handleIceCandidate message: "+message);
        console.info("send Candidate message:");
      } else {
        //不再去请求打洞了
        console.warn("End of candidates");
      }
    }
    
    function handleRemoteStreamAdd(event) {
      console.info("handleRemoteStreamAdd");
      remoteStream = event.streams[0];
      remoteVideo.srcObject = remoteStream;
    }
    function handleConnectionStateChange(){
        if(pc != null){
            console.info("handleConnectionStateChange: " + pc.connectionState);
        }
    }
    function handleIceConnectionStateChange(){
        if(pc != null){
            console.info("handleIceConnectionStateChange: " + pc.iceConnectionState);
        }
    }
    
    function createPeerConnection() {
      var defaultConfiguration = {
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy: "require",
        iceTransportPolicy: "relay", //relay or all
        // 修改ice数组测试效果,需要进行封装
        iceServers: [
          {
            urls: [
              "turn:192.168.1.102:3478?transport=udp",
              "turn:192.168.1.102:3478?transport=tcp", // 可以插入多个进行备选
            ],
            username: "ydy",
            credential: "123456",
          },
          {
            urls: ["stun:192.168.1.102:3478"],
          },
        ],
      };
      pc = new RTCPeerConnection(defaultConfiguration);
      pc.onicecandidate = handleIceCandidate;
      pc.ontrack = handleRemoteStreamAdd;
      pc.oniceconnectionstatechange = handleIceConnectionStateChange;
      pc.onconnectionstatechange = handleConnectionStateChange;
      localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));
    }
    
    function createOfferAndSendMessage(session) {
      pc.setLocalDescription(session)
        .then(function () {
          var jsonMsg = {
            cmd: "offer",
            roomId: roomId,
            uid: localUserId,
            remoteUid: remoteUserId,
            msg: JSON.stringify(session),
          };
          var message = JSON.stringify(jsonMsg);
          fishRTCEngine.sendMessage(message);
          // console.info("send offer message: "+message);
          console.info("send offer message: ");
        })
        .catch(function (error) {
          console.error("offer setLocalDescription failed: " + error);
        });
    }
    function handleCreateOfferError(error) {
      console.error("handleCreateOfferError failed: " + error);
    }
    
    function createAnswerAndSendMessage(session) {
      console.info("doAnswer createAnswerAndSendMessage");
      pc.setLocalDescription(session)
        .then(function () {
          var jsonMsg = {
            cmd: "answer",
            roomId: roomId,
            uid: localUserId,
            remoteUid: remoteUserId,
            msg: JSON.stringify(session),
          };
          var message = JSON.stringify(jsonMsg);
          fishRTCEngine.sendMessage(message);
          console.info("send answer message: ");
          // console.info("send answer message: "+message);
        })
        .catch(function (error) {
          console.error("answer setLocalDescription failed: " + error);
        });
    }
    function handleCreateAnswerError(error) {
      console.error("handleCreateAnswerError failed: " + error);
    }
    var FishRTCEngine = function (wsUrl) {
      this.init(wsUrl);
      fishRTCEngine = this;
      return this;
    };
    
    FishRTCEngine.prototype.init = function (wsUrl) {
      //设置wbsocket url
      this.wsUrl = wsUrl;
      //websocket对象
      this.signaling = null;
    };
    
    FishRTCEngine.prototype.createWebSocket = function () {
      fishRTCEngine = this;
      fishRTCEngine.signaling = new WebSocket(this.wsUrl);
      fishRTCEngine.signaling.onopen = function () {
        fishRTCEngine.onOpen();
      };
    
      fishRTCEngine.signaling.onmessage = function (ev) {
        fishRTCEngine.onMessage(ev);
      };
    
      fishRTCEngine.signaling.onerror = function (ev) {
        fishRTCEngine.onError(ev);
      };
    
      fishRTCEngine.signaling.onclose = function (ev) {
        fishRTCEngine.onClose(ev);
      };
    };
    
    FishRTCEngine.prototype.onOpen = function () {
      console.log("websocket open");
    };
    
    FishRTCEngine.prototype.onMessage = function (event) {
      // console.info("websocket onMessage:");
      console.log("websocket onMessage:" + event.data);
      var jsonMsg = null;
      try {
        jsonMsg = JSON.parse(event.data);
      } catch (e) {
        console.warn("onMessage parse Json failed: " + e);
        return;
      }
      switch (jsonMsg.cmd) {
        case SIGNAL_TYPE_NEW_PEER:
          handleRemoteNewPeer(jsonMsg);
          break;
        case SIGNAL_TYPE_RESP_JOIN:
          handleResponseJoin(jsonMsg);
          break;
        case SIGNAL_TYPE_PEER_LEAVE:
          handleRemotePeerLeave(jsonMsg);
          break;
        case SIGNAL_TYPE_OFFER:
          handleRemoteOffer(jsonMsg);
          break;
        case SIGNAL_TYPE_ANSWER:
          handleRemoteAnswer(jsonMsg);
          break;
        case SIGNAL_TYPE_CANDIDATE:
          handleRemoteCandidate(jsonMsg);
          break;
      }
    };
    
    FishRTCEngine.prototype.onError = function (event) {
      console.log("websocket onError" + event.data);
    };
    
    FishRTCEngine.prototype.onClose = function (event) {
      console.log(
        "websocket onClose code:" + event.code + ",reason:" + EventTarget.reason
      );
    };
    
    FishRTCEngine.prototype.sendMessage = function (message) {
      this.signaling.send(message);
    };
    function handleResponseJoin(message) {
      console.info("handleResponseJoin, remoteUid: " + message.remoteUid);
      remoteUserId = message.remoteUid;
      //doOffer();
    }
    function handleRemotePeerLeave(message) {
      console.info("handleRemotePeerLeave, remoteUid: " + message.remoteUid);
      remoteVideo.srcObject = null; //远程对象置空
      if (pc != null) {
        pc.close();
        pc = null;
      }
    }
    //新人加入房间保存userId
    function handleRemoteNewPeer(message) {
      console.info("handleRemoteNewPeer, remoteUid: " + message.remoteUid);
      remoteUserId = message.remoteUid;
      doOffer();
    }
    function handleRemoteOffer(message) {
      console.info("handleRemoteOffer");
      if (pc == null) {
        createPeerConnection();
      }
      var desc = JSON.parse(message.msg);
      pc.setRemoteDescription(desc);
      doAnswer();
    }
    function handleRemoteAnswer(message) {
      console.info("handleRemoteAnswer");
      var desc = JSON.parse(message.msg);
      // console.info("desc: " + desc);
      pc.setRemoteDescription(desc);
    }
    function handleRemoteCandidate(message) {
      console.info("handleRemoteCandidate");
      var candidate = JSON.parse(message.msg);
      pc.addIceCandidate(candidate).catch((e) => {
        console.error("addIceCandidate failed: " + e.name);
      });
    }
    function doOffer() {
      //创建RCTPeerConnection
      if (pc == null) {
        createPeerConnection();
      }
      pc.createOffer()
        .then(createOfferAndSendMessage)
        .catch(handleCreateOfferError);
    }
    
    function doAnswer() {
      console.info("doAnswer");
      pc.createAnswer()
        .then(createAnswerAndSendMessage)
        .catch(handleCreateAnswerError);
    }
    
    function doJoin(roomId) {
      console.info("doJoin roomId:" + roomId);
      var jsonMsg = {
        cmd: "join",
        roomId: roomId,
        uid: localUserId,
      };
      var message = JSON.stringify(jsonMsg);
      fishRTCEngine.sendMessage(message);
      console.info("doJoin message: " + message);
    }
    function doLeave() {
      var jsonMsg = {
        cmd: "leave",
        roomId: roomId,
        uid: localUserId,
      };
      var message = JSON.stringify(jsonMsg);
      fishRTCEngine.sendMessage(message); //发信令给服务器离开
      console.info("doLeave message: " + message);
      hangup(); //挂断
    }
    function hangup() {
      localVideo.srcObject = null; //0.关闭自己的本地显示
      remoteVideo.srcObject = null; //1.关闭远端的流
      closeLocalStream(); //2.关闭本地流,摄像头关闭,麦克风关闭
      if (pc != null) {
        //3.关闭RTCPeerConnection
        pc.close();
        pc = null;
      }
    }
    function closeLocalStream() {
      if (localStream != null) {
        localStream.getTracks().forEach((track) => {
          track.stop();
        });
      }
    }
    
    function openLocalStream(stream) {
      console.log("Open Local stream");
      doJoin(roomId);
      localVideo.srcObject = stream;
      localStream = stream;
    }
    
    function initLocalStream() {
      navigator.mediaDevices
        .getUserMedia({
          audio: true,
        //   video: true,
          video:{
              width:640,
              height:480
          }
        })
        .then(openLocalStream)
        .catch(function (e) {
          alert("getUserMedia() error" + e.name);
        });
    }
    
    fishRTCEngine = new FishRTCEngine("wss://192.168.1.102:8098/ws");
    fishRTCEngine.createWebSocket();
    document.getElementById("joinBtn").onclick = function () {
      roomId = document.getElementById("zero-RoomId").value;
      if (roomId == "" || roomId == "请输入房间ID") {
        alert("请输入房间ID");
        return;
      }
      console.log("加入按钮被点击,roomId:" + roomId);
      //初始化本地码流
      initLocalStream();
    };
    
    document.getElementById("leaveBtn").onclick = function () {
      console.log("离开按钮被点击");
      doLeave();
    };
    

    服务端代码

    var ws = require("nodejs-websocket")
    var port = 8099;
    
    // join 主动加入房间
    // leave 主动离开房间
    // new-peer 有人加入房间,通知已经在房间的人
    // peer-leave 有人离开房间,通知已经在房间的人
    // offer 发送offer给对端peer
    // answer发送offer给对端peer
    // candidate 发送candidate给对端peer
    const SIGNAL_TYPE_JOIN = "join";
    const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
    const SIGNAL_TYPE_LEAVE = "leave";
    const SIGNAL_TYPE_NEW_PEER = "new-peer";
    const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
    const SIGNAL_TYPE_OFFER = "offer";
    const SIGNAL_TYPE_ANSWER = "answer";
    const SIGNAL_TYPE_CANDIDATE = "candidate";
    
    /** ----- ZeroRTCMap ----- */
    var ZeroRTCMap = function () {
        this._entrys = new Array();
        // 插入
        this.put = function (key, value) {
            if (key == null || key == undefined) {
                return;
            }
            var index = this._getIndex(key);
            if (index == -1) {
                var entry = new Object();
                entry.key = key;
                entry.value = value;
                this._entrys[this._entrys.length] = entry;
            } else {
                this._entrys[index].value = value;
            }
        };
        // 根据key获取value
        this.get = function (key) {
            var index = this._getIndex(key);
            return (index != -1) ? this._entrys[index].value : null;
        };
        // 移除key-value
        this.remove = function (key) {
            var index = this._getIndex(key);
            if (index != -1) {
                this._entrys.splice(index, 1);
            }
        };
        // 清空map
        this.clear = function () {
            this._entrys.length = 0;
        };
        // 判断是否包含key
        this.contains = function (key) {
            var index = this._getIndex(key);
            return (index != -1) ? true : false;
        };
        // map内key-value的数量
        this.size = function () {
            return this._entrys.length;
        };
        // 获取所有的key
        this.getEntrys = function () {
            return this._entrys;
        };
        // 内部函数
        this._getIndex = function (key) {
            if (key == null || key == undefined) {
                return -1;
            }
            var _length = this._entrys.length;
            for (var i = 0; i < _length; i++) {
                var entry = this._entrys[i];
                if (entry == null || entry == undefined) {
                    continue;
                }
                if (entry.key === key) {// equal
                    return i;
                }
            }
            return -1;
        };
    }
    
    //总的房间号
    var roomTableMap = new ZeroRTCMap();
    
    function Client(uid,conn,roomId){
        this.uid = uid;//用户所属的id
        this.conn = conn;//uid对应的websocket连接
        this.roomId = roomId;//用户所在的房间
    }
    
    function handleJoin(message,conn){
         var roomId = message.roomId;
         var uid = message.uid;
         console.info("uid" + uid + " try to join roomId: " + roomId);
         //查找房间目前是否已经存在了
         var roomMap = roomTableMap.get(roomId);
         if(roomMap == null){//房间不存在
            roomMap = new ZeroRTCMap();
            roomTableMap.put(roomId,roomMap);
         }
    
         //房间已经有两个人了
         if(roomMap.size() >= 2){
           console.error("roomId:" + roomId + "已经有两个人存在,请使用其他房间");
           //加信令通知客户端,房间已满
           return null;
         }
        var client = new Client(uid,conn,roomId);
        roomMap.put(uid,client);
        if(roomMap.size() > 1){
            //房间里面已经有人了,加上新进来的人,那就是>=2了,所以要通知对方
            var clients = roomMap.getEntrys();
            for(var i in clients){
                var remoteUid = clients[i].key;
                if(remoteUid != uid){
                    var jsonMsg = {
                       'cmd':SIGNAL_TYPE_NEW_PEER,
                       'remoteUid':uid
                    };
                    var msg = JSON.stringify(jsonMsg);
                    var remoteClient = roomMap.get(remoteUid);
                    console.info("new-peer: " + msg);
                    //新加入人之后,重新通知远程的对方
                    remoteClient.conn.sendText(msg);
    
                    jsonMsg = {
                        'cmd':SIGNAL_TYPE_RESP_JOIN,
                        'remoteUid':remoteUid
                    };
                    msg = JSON.stringify(jsonMsg);
                    console.info("resp-join: " + msg);
                    //新加入人之后,通知自己,有人加入了
                    conn.sendText(msg);
                }
            }
        }
        return client;
    }
    
    function handleLeave(message){
        var roomId = message.roomId;
        var uid = message.uid;
        console.info("handleLeave uid:" + uid + " leave roomId: " + roomId);
        //查找房间目前是否已经存在了
        var roomMap = roomTableMap.get(roomId);
        if(roomMap == null){//房间不存在
           console.warn("can't find the roomId: " + roomId);
           return;
        }
        roomMap.remove(uid);//删除发送者
        //退出房间通知其他人
        if(roomMap.size() >= 1){
            var clients = roomMap.getEntrys();
            for(var i in clients){
                var jsonMsg = {
                    'cmd': SIGNAL_TYPE_PEER_LEAVE,
                    'remoteUid': uid//谁离开就填写谁
                    };
                var msg = JSON.stringify(jsonMsg);
                var remoteUid = clients[i].key;
                var remoteClient = roomMap.get(remoteUid);
                if(remoteClient){
                    //通知此uid离开了房间
                    console.info("notify peer:" + remoteClient.uid + " , uid: " + uid + " leave");
                    remoteClient.conn.sendText(msg);
                }
            }
        }
    }
    function handleForceLeave(client){
        var roomId = client.roomId;
        var uid = client.uid;
        //1.先查找房间号
        var roomMap = roomTableMap.get(roomId);
        if(roomMap == null){//房间不存在
           console.warn("handleForceLeave can't find the roomId: " + roomId);
           return;
        }
        //2.判断uid是否在房间
        if(!roomMap.contains(uid)){
            console.info("uid: " + uid + " have leave roomId: " + roomId);
            return;
        }
        //3.走到这一步,客户端没有正常离开,我们要执行离开程序
        console.info("handleForceLeave uid:" + uid + " force leave roomId: " + roomId);
        roomMap.remove(uid);//删除发送者
        //退出房间通知其他人
        if(roomMap.size() >= 1){
            var clients = roomMap.getEntrys();
            for(var i in clients){
                var jsonMsg = {
                    'cmd': SIGNAL_TYPE_PEER_LEAVE,
                    'remoteUid': uid//谁离开就填写谁
                    };
                var msg = JSON.stringify(jsonMsg);
                var remoteUid = clients[i].key;
                var remoteClient = roomMap.get(remoteUid);
                if(remoteClient){
                    //通知此uid离开了房间
                    console.info("notify peer:" + remoteClient.uid + " , uid: " + uid + " leave");
                    remoteClient.conn.sendText(msg);
                }
            }
        }
    }
    function handleOffer(message){
        var roomId = message.roomId;
        var uid = message.uid;
        var remoteUid = message.remoteUid;
    
        console.info("handleOffer uid: " + uid + " transfer offer to remoteUid: " + remoteUid);
        var roomMap = roomTableMap.get(roomId);
        if(roomMap == null){//房间不存在
           console.error("handleOffer can't find the roomId: " + roomId);
           return;
        }
        if(roomMap.get(uid) == null){//人不存在
            console.error("handleOffer can't find the uid: " + uid);
            return;
        }
    
        var remoteClient = roomMap.get(remoteUid);
        if(remoteClient){
            var msg = JSON.stringify(message);
            remoteClient.conn.sendText(msg);
        }else{
            console.error("handleOffer can't find remoteUid: " + remoteUid);
        }
    }
    
    function handleAnswer(message){
        var roomId = message.roomId;
        var uid = message.uid;
        var remoteUid = message.remoteUid;
    
        console.info("handleAnswer uid: " + uid + " transfer answer to remoteUid: " + remoteUid);
        var roomMap = roomTableMap.get(roomId);
        if(roomMap == null){//房间不存在
           console.error("handleAnswer can't find the roomId: " + roomId);
           return;
        }
        if(roomMap.get(uid) == null){//人不存在
            console.error("handleAnswer can't find the uid: " + uid);
            return;
        }
    
        var remoteClient = roomMap.get(remoteUid);
        if(remoteClient){
            var msg = JSON.stringify(message);
            remoteClient.conn.sendText(msg);
        }else{
            console.error("handleAnswer can't find remoteUid: " + remoteUid);
        }
    }
    
    function handleCandidate(message){
        var roomId = message.roomId;
        var uid = message.uid;
        var remoteUid = message.remoteUid;
    
        console.info("handleCandidate uid: " + uid + " transfer candidate to remoteUid: " + remoteUid);
        var roomMap = roomTableMap.get(roomId);
        if(roomMap == null){//房间不存在
           console.error("handleCandidate can't find the roomId: " + roomId);
           return;
        }
        if(roomMap.get(uid) == null){//人不存在
            console.error("handleCandidate can't find the uid: " + uid);
            return;
        }
    
        var remoteClient = roomMap.get(remoteUid);
        if(remoteClient){
            var msg = JSON.stringify(message);
            remoteClient.conn.sendText(msg);
        }else{
            console.error("handleCandidate can't find remoteUid: " + remoteUid);
        }
    }
    
    var server = ws.createServer(function(conn){
        console.log("创建一个新的连接---------")
        conn.client = null;//对应的客户端信息
        // conn.sendText("我收到你的连接了......");
        conn.on("text",function(str){
        // console.info("recv msg:" + str);
           var jsonMsg = JSON.parse(str);
           switch(jsonMsg.cmd){
               case SIGNAL_TYPE_JOIN:
                   conn.client = handleJoin(jsonMsg,conn);
                   break;
               case SIGNAL_TYPE_LEAVE:
                   handleLeave(jsonMsg);
                   break;
               case SIGNAL_TYPE_OFFER:
                    handleOffer(jsonMsg);
                    break;
               case SIGNAL_TYPE_ANSWER:
                    handleAnswer(jsonMsg);
                    break;
               case SIGNAL_TYPE_CANDIDATE:
                    handleCandidate(jsonMsg);
                    break;
           }
        });
    
        conn.on("close",function(code,reason){
            console.info("连接关闭 code: " + code + ", reason: " + reason);
            if(conn.client != null){
                //强制让客户端从房间退出
                handleForceLeave(conn.client);
            }
        });
    
        conn.on("error",function(err){
            console.info("监听到错误:" + err);
        });
    }).listen(port);
    
    图片.png

    启动coturn

    # nohup是重定向命令,输出都将附加到当前目录的 nohup.out 文件中; 命令后加 & ,后台执行起来后按
    ctr+c,不会停止
    sudo nohup turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov &
    # 前台启动
    sudo turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov
    #然后查看相应的端口号3478是否存在进程
    sudo lsof ‐i:3478
    

    设置configuration,先设置为relay中继模式,只有relay中继模式可用的时候,才能部署到公网去(部署到公网后也先测试relay)。

    
    function createPeerConnection() {
      var defaultConfiguration = {
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy: "require",
        iceTransportPolicy: "relay", //relay or all
        // 修改ice数组测试效果,需要进行封装
        iceServers: [
          {
            urls: [
              "turn:192.168.1.102:3478?transport=udp",
              "turn:192.168.1.102:3478?transport=tcp", // 可以插入多个进行备选
            ],
            username: "ydy",
            credential: "123456",
          },
          {
            urls: ["stun:192.168.1.102:3478"],
          },
        ],
      };
      pc = new RTCPeerConnection(defaultConfiguration);
      pc.onicecandidate = handleIceCandidate;
      pc.ontrack = handleRemoteStreamAdd;
      pc.oniceconnectionstatechange = handleIceConnectionStateChange;
      pc.onconnectionstatechange = handleConnectionStateChange;
      localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));
    }
    

    all模式:局域网可用优先走局域网,通过命令sar -n DEV 1查看一秒钟的网络传输量发现为0

    image
    图片.png
    relay模式:走中继服务器模式,不会走局域网,网络传输量不为0
    图片.png
    image
    image
    21297F0B-100C-439D-B73D-975EA0E73821

    编译和启动nginx

    sudo apt‐get update
    #安装依赖:gcc、g++依赖库
    sudo apt‐get install build‐essential libtool
    #安装 pcre依赖库(http://www.pcre.org/)
    sudo apt‐get install libpcre3 libpcre3‐dev
    #安装 zlib依赖库(http://www.zlib.net)
    sudo apt‐get install zlib1g‐dev
    #安装ssl依赖库
    sudo apt‐get install openssl
    #下载nginx 1.20.1版本
    wget http://nginx.org/download/nginx‐1.15.8.tar.gz
    tar xvzf nginx‐1.20.1.tar.gz
    cd nginx‐1.15.8/
    # 配置,一定要支持https
    ./configure ‐‐with‐http_ssl_module
    # 编译
    make
    #安装
    sudo make install
    

    默认安装目录:/usr/local/nginx
    启动:sudo /usr/local/nginx/sbin/nginx
    停止:sudo /usr/local/nginx/sbin/nginx s
    stop
    重新加载配置文件:sudo /usr/local/nginx/sbin/nginx s
    reload

    生成证书

    mkdir ‐p ~/cert
    cd ~/cert
    # CA私钥
    openssl genrsa ‐out key.pem 2048
    # 自签名证书
    openssl req ‐new ‐x509 ‐key key.pem ‐out cert.pem ‐days 1095
    
    image

    配置Web服务器

    (1)配置自己的证书
    ssl_certificate /root/cert/cert.pem; // 注意证书所在的路径
    ssl_certificate_key /root/cert/key.pem;
    (2)配置主机域名或者主机IP server_name 192.168.1.103;
    (3)web页面所在目录root /mnt/WebRTC/src/04/6.4/client;
    完整配置文件:/usr/local/nginx/conf/conf.d/webrtchttps.conf

    server {
      listen 443 ssl;
      ssl_certificate /root/cert/cert.pem;
      ssl_certificate_key /root/cert/key.pem;
      charset utf‐8;
      # ip地址或者域名
      server_name 192.168.1.103;
        location / {
         add_header 'Access‐Control‐Allow‐Origin' '*';
         add_header 'Access‐Control‐Allow‐Credentials' 'true';
         add_header 'Access‐Control‐Allow‐Methods' '*';
         add_header 'Access‐Control‐Allow‐Headers' 'Origin, X‐Requested‐With, Content‐Type,
         Accept';
         # web页面所在目录
         root /mnt/WebRTC/src/04/6.4/client;
         index index.php index.html index.htm;
       }
    }
    

    编辑nginx.conf文件,在末尾}之前添加包含文件include /usr/local/nginx/conf/conf.d/*.conf;

    image

    配置websocket代理

    ws 不安全的连接 类似http
    wss是安全的连接,类似https
    https不能访问ws,本身是安全的访问,不能降级做不安全的访问。


    image

    ws协议和wss协议两个均是WebSocket协议的SCHEM,两者一个是非安全的,一个是安全的。也是统一的资源标志
    符。就好比HTTP协议和HTTPS协议的差别。
    Nginx主要是提供wss连接的支持,https必须调用wss的连接。
    完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.
    conf

    map $http_upgrade $connection_upgrade {
    default upgrade;
      '' close;
    }
    upstream websocket {
      server 192.168.1.103:8099;
    }
    server {
    listen 8098 ssl;
      #ssl on;
      ssl_certificate /root/cert/cert.pem;
      ssl_certificate_key /root/cert/key.pem;
      server_name 192.168.1.103;
      location /ws {
         proxy_pass http://websocket;
         proxy_http_version 1.1;
         keepalive_timeout 6000000000s;
         proxy_connect_timeout 400000000s; #配置点1
         proxy_read_timeout 60000000s; #配置点2,如果没效,可以考虑这个时间配置长一点
         proxy_send_timeout 60000000s; #配置点3
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
      }
    }
    

    wss://192.168.221.134:8098/ws 端口是跟着IP后面
    信令服务器后台执行nohup node ./signal_server.js &如果退出终端信令服务器会停止,需exit退出终端或安装forever和pm2,才能保持服务器在后台执行

    解决websocket自动断开(这是重点!!!!!设置超时时间无效。。。)

    我们在通话时,出现60秒后客户端自动断开的问题,是因为经过nginx代理时,如果websocket长时间没有收发消息
    则该websocket将会被断开。我们这里可以修改收发消息的时间间隔。
    proxy_connect_timeout :后端服务器连接的超时时间发起握手等候响应超时时间
    proxy_read_timeout:连接成功后
    等候后端服务器响应时间其实已经进入后端的排队之中等候处理(也可以说是
    后端服务器处理请求的时间)
    proxy_send_timeout :后端服务器数据回传时间
    就是在规定时间之内后端服务器必须传完所有的数据
    nginx使用proxy模块时,默认的读取超时时间是60s
    完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.conf

    心跳(待补充)维持心跳才能保证WebSocket连接不会被断开,前面设置超时时间都无效,90秒后WebSocket连接还是会断开

    客户端 服务器 信令:心跳包
    keeplive 间隔5秒发送一次给信令服务器,说明客户端一直处于活跃的状态。

    相关文章

      网友评论

          本文标题:01-WebRTC一对一通话

          本文链接:https://www.haomeiwen.com/subject/fogvhrtx.html