一对一WebRTC视频通话系列(二)——websocket和join信令实现

发布于:2024-05-07 ⋅ 阅读:(22) ⋅ 点赞:(0)

本系列博客主要记录WebRtc实现过程中的一些重点,代码全部进行了注释,便于理解WebRTC整体实现。


一对一WebRTC视频通话系列往期博客:

一对一WebRTC视频通话系列(一)—— 创建页面并显示摄像头画面


一、websocket实现

1.1客户端

main.js文件中,定义了一个名为ZeroRTCEngine的类,该类使用WebSocket进行通信。ZeroRTCEngine类的实例化过程如下:

  1. 首先,声明并定义一个名为zeroRTCEngine的类,接受一个wsUrl参数。
  2. 在ZeroRTCEngine类中,定义一个init方法,用于初始化WebSocket地址。
  3. 定义ZeroRTCEngine类的方法createWebsocket,用于创建WebSocket对象。
  4. createWebsocket方法中,定义多种WebSocket事件处理函数.
  5. 定义onopenonmessageoncloseonerror方法,用于处理WebSocket连接状态和接收到的消息。

在实际使用中,通过调用ZeroRTCEngine类的createWebsocket方法来创建一个WebSocket实例,并监听各种事件(如打开、消息传递、关闭和错误)。

	//定义 ZeroRTCEngine 的 init 方法
	//参数 wsUrl:WebSocket 的地址
	ZeroRTCEngine.prototype.init = function(wsUrl) {
	    //设置 WebSocket url
	    this.wsUrl = wsUrl;
	    this.signaling =null;
	}
	
	//定义 ZeroRTCEngine 的 createWebsocket 方法
	ZeroRTCEngine.prototype.createWebsocket = function() {
	    //创建 WebSocket 对象
	    zeroRTCEngine = this;
	    zeroRTCEngine.signaling = new WebSocket(this.wsUrl);
	
	    //设置打开函数
	    zeroRTCEngine.signaling.onopen = function () {
	        zeroRTCEngine.onopen();
	    }
	
	    //设置关闭函数
	    zeroRTCEngine.signaling.onclose = function (event) {
	        zeroRTCEngine.onclose(event);
	    }
	    //设置错误函数
	    zeroRTCEngine.signaling.onerror = function (event) {
	        zeroRTCEngine.onerror(event);
	    }
	    //设置消息传递函数
	    zeroRTCEngine.signaling.onmessage = function (event) {
	        zeroRTCEngine.onmessage(event);
	    }
	}
	
	
	ZeroRTCEngine.prototype.onopen = function() {
	    console.log("WebSocket connection established.");
	}
	
	ZeroRTCEngine.prototype.onmessage = function(event) {
	    console.log("Received message:" + event.data);
	    //解析收到的消息
	    var message = JSON.parse(event.data);
	}
	
	ZeroRTCEngine.prototype.onclose = function(event) {
	    console.log("WebSocket connection closed." + event.data + ", reason" + EventCounts.reason);
	}
	
	ZeroRTCEngine.prototype.onerror = function(event) {
	    console.log("WebSocket connection error:" + event.data);  
	}
	
	zeroRTCEngine = new ZeroRTCEngine("ws://192.168.3.181:8099");
	zeroRTCEngine.createWebsocket();

1.2服务端

创建一个名为signal_server.jsjs新文件,搭建简单的WebSocket服务器,用于处理客户端连接、消息发送和接收、连接关闭等基本操作。在实际应用中,可能需要根据需求对代码进行修改和扩展。

  1. 使用ws.createServer()方法创建一个WebSocket服务器。这个方法接受一个回调函数作为参数,该回调函数在客户端连接到服务器时被调用。
  2. 在回调函数中,使用console.log()输出一条connection established的消息,表示连接已建立。然后,向客户端发送一条消息,例如"我收到你的连接了"。
  3. 使用conn.on("text", function (str) {...})监听客户端发送的消息。
    当客户端发送消息时,会触发这个回调函数。使用console.info()输出一条消息,表示收到的消息。
  4. 在连接关闭时,使用conn.on("close", function (code, reason) {...})监听连接关闭。
    当客户端关闭连接时,会触发这个回调函数。使用console.log()输出一条消息,表示连接已关闭。
  5. 使用了conn.on("error", function (err) {...})来监听连接错误。当发生错误时,会触发这个回调函数,输出错误信息。
  6. 使用server.listen()方法将服务器绑定到一个指定的端口。在实际应用中,通常需要根据不同的需求来修改这个端口。
	// 引入ws模块
	var ws = require("nodejs-websocket");
	// 定义端口号
	var port =8099;
	
	// 创建一个WebSocket服务器
	var server = ws.createServer(function (conn) {
	    // 连接建立时的输出
	    console.log("New connection");
	    // 向客户端发送消息
	    conn.sendText("我收到你的连接了")
	    // 监听客户端发送的消息
	    conn.on("text", function (str) {
	        console.info("Received msg:"+str);
	    });
	
	    // 连接关闭时的输出
	    conn.on("close", function (code, reason) {
	        console.log("Connection closed,code:" + code + " reason:" + reason);
	    });
	    // 监听连接错误
	    conn.on("error", function (error) {
	        console.error("发生错误:", error);
	    })
	}).listen(port);

在ubuntu中使用下列指令开启服务端:

	node signal_server.js

在这之前切记先用以下两行命令初始化:

	sudo npm init -y
	sudo npm install nodejs-websocket

在这里插入图片描述
在这里插入图片描述

二、join信令实现

2.1 客户端

修改按钮点击事件

document.getElementById('joinBtn').onclick = function () {
    roomId = document.getElementById('roomId').value;
    if(roomId == '' || roomId == "Please enter the room ID"){
        alert('Please enter the room ID');
        return;
    }

    console.log("joinBtn clicked,roomId: " + roomId);
    //初始化本地码流
    initLocalStream();
}

首先创建一个JSON对象,包含命令(cmd)、房间ID(roomId)和用户ID(uid),然后将这个JSON对象转换为字符串。接着,调用zeroRTCEngine.sendMessage方法将这个字符串发送给服务器,表示用户要加入房间。最后,在控制台输出一条消息,表明用户已经成功加入房间。

// 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);//本地ID   
var remoteUserId = -1; //对方用户ID
var roomId = -1; //房间ID

ZeroRTCEngine.prototype.sendMessage = function(message) {
    this.signaling.send(message);
}

function doJoin(roomId) {
    var jsonMsg = {
        'cmd': 'join',
        'roomId': roomId,
        'uid': localUserId,
    }; 
    var message = JSON.stringify(jsonMsg); //将json对象转换为字符串
    zeroRTCEngine.sendMessage(message);   //设计方法:用实现方法而不是直接用变量    
    console.info("doJoin message: " + message);
}

运行效果:
在这里插入图片描述

2.2 服务端

修改signal_server.js
1.添加相关宏定义,以及WebRTC所需要用到的Map类。

// 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;
        }
    };
    this.get = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? this._entrys[index].value : null;
    };
    this.remove = function (key) {
        var index = this._getIndex(key);
        if (index != -1) {
            this._entrys.splice(index, 1);
        }
    };
    this.clear = function () {
        this._entrys.length = 0;
    };
    this.contains = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? true : false;
    };
    this.size = function () {
        return this._entrys.length;
    };
    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;
    };
}

2.定义Client函数,用于初始化用户信息

function Client(uid, conn, roomId) {
    this.uid = uid;     // 用户所属的id
    this.conn = conn;   // uid对应的websocket连接
    this.roomId = roomId;
}

3.处理加入房间请求
监听客户端发送的消息,如果时加入房间的,利用handleJoin()函数进行处理

// 监听客户端发送的消息
    conn.on("text", function (str) {
        console.info("Received msg:"+str);
        var jsonMsg = JSON.parse(str);

        switch(jsonMsg.cmd){
            case SIGNAL_TYPE_JOIN:
                handleJoin(jsonMsg, conn); 
            break;
            
        }
    });

定义handleJoin()函数,处理加入房间请求。它首先获取房间ID用户ID,然后获取房间Map。如果房间不存在,则创建一个新的房间。如果房间已满,给出提示并拒绝加入请求。如果房间未满,创建一个新的客户端并将其添加到房间。如果房间中用户数大于1,则通知房间中的其他用户有新人加入。

实现原理:
1.从传入的messageconn参数中获取房间ID用户ID
2.尝试获取roomId对应的房间Map。如果房间Map不存在,则创建一个新的房间Map。
3.检查房间是否已满。如果已满,给出提示并返回;否则,创建一个新的客户端并将其添加到房间。
4.如果房间中用户数大于1,则遍历房间中的所有用户,通知他们有新人加入。同时,通知自己房间中有新人加入。

function handleJoin(message, conn){
    // 获取房间ID和用户ID
    var roomId = message.roomId;
    var uid = message.uid;
    console.log(" uid:"+uid + "try to join room: "+roomId);

    // 获取房间Map
    var roomMap = roomTableMap.get(roomId);
    // 如果房间不存在,则创建一个新的房间
    if(roomMap == null){
        roomMap = new ZeroRTCMap();
        roomTableMap.put(roomId, roomMap);
    }

    // 如果房间已满,给出提示
    if(roomMap.size() >= 1){
        console.error("roomId:" + roomId + " is full");

        conn.sendText('roomId:' + roomId + ' is full');
        return;
    }

    // 创建一个新的客户端
    var client = new Client(uid, conn, roomId);
    // 将用户添加到房间
    roomMap.put(uid, client);
    // 如果房间中的用户数大于1,则通知房间中的其他用户
    if(roomMap.size() > 1){
        
        // 获取房间中的所有用户
        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);

                // 通知自己,房间里有的人
                jsonMsg = {
                    'cmd':SIGNAL_TYPE_RESP_JOIN,
                    'remoteUid':remoteUid
                };
                msg = JSON.stringify(jsonMsg);
                console.info("resp-join"+msg);
                conn.sendText(msg);
            }
        }
    }
}

整体代码:

// 引入ws模块
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;
        }
    };
    this.get = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? this._entrys[index].value : null;
    };
    this.remove = function (key) {
        var index = this._getIndex(key);
        if (index != -1) {
            this._entrys.splice(index, 1);
        }
    };
    this.clear = function () {
        this._entrys.length = 0;
    };
    this.contains = function (key) {
        var index = this._getIndex(key);
        return (index != -1) ? true : false;
    };
    this.size = function () {
        return this._entrys.length;
    };
    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){
    // 获取房间ID和用户ID
    var roomId = message.roomId;
    var uid = message.uid;
    console.log(" uid:"+uid + "try to join room: "+roomId);

    // 获取房间Map
    var roomMap = roomTableMap.get(roomId);
    // 如果房间不存在,则创建一个新的房间
    if(roomMap == null){
        roomMap = new ZeroRTCMap();
        roomTableMap.put(roomId, roomMap);
    }

    // 如果房间已满,给出提示
    if(roomMap.size() >= 1){
        console.error("roomId:" + roomId + " is full");

        conn.sendText('roomId:' + roomId + ' is full');
        return;
    }

    // 创建一个新的客户端
    var client = new Client(uid, conn, roomId);
    // 将用户添加到房间
    roomMap.put(uid, client);
    // 如果房间中的用户数大于1,则通知房间中的其他用户
    if(roomMap.size() > 1){
        
        // 获取房间中的所有用户
        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);

                // 通知自己,房间里有的人
                jsonMsg = {
                    'cmd':SIGNAL_TYPE_RESP_JOIN,
                    'remoteUid':remoteUid
                };
                msg = JSON.stringify(jsonMsg);
                console.info("resp-join"+msg);
                conn.sendText(msg);
            }
        }
    }
}

// 创建一个WebSocket服务器
var server = ws.createServer(function (conn) {
    // 连接建立时的输出
    console.log("New connection");
    // 向客户端发送消息
    conn.sendText('我收到你的连接了')
    // 监听客户端发送的消息
    conn.on("text", function (str) {
        console.info("Received msg:"+str);
        var jsonMsg = JSON.parse(str);

        switch(jsonMsg.cmd){
            case SIGNAL_TYPE_JOIN:
                handleJoin(jsonMsg, conn); 
            break;
            
        }
    });
    
    // 连接关闭时的输出
    conn.on("close", function (code, reason) {
        console.log("Connection closed,code:" + code + " reason:" + reason); 
    });
    // 监听连接错误
    conn.on("error", function (error) {
        console.error("发生错误:", error);
    })
}).listen(port);

运行效果:

客户1:
第一个创建,刚开始没有红框的内容。在客户2新加入后,收到服务端发来的信令。
在这里插入图片描述

客户2:
创建后,会收到服务端发来的,房间内已有的客户1的信息。
在这里插入图片描述
客户3:
房间内已经有2个客户了,会提示房间已满,无法加入房间。
在这里插入图片描述
服务端:
在这里插入图片描述