一、前端页面
前端页面的设计如下,有一个房间输入文本框,一个加入房间按钮和离开房间按钮,然后下面有两个视频窗口,分别是显示本地的窗口和远端的窗口
对应的html
代码也很简单,就是需要注意将本地视频的属性设置上静音,否则有回声,使用两个<div>
标签分别包裹本地视频和远端视频,完整的html
代码如下:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC一对一通话</title>
</head>
<body>
<h1>WebRTC一对一通话</h1>
<div id="buttonsDiv">
<input id="roomIdInput" type="text" placeholder="请输入房间ID">
<button id="joinBtn">加入房间</button>
<button id="leaveBtn">离开房间</button>
</div>
<div id="localVideosDiv">
<video id="localVideo" autoplay muted playsinline>本地窗口</video>
</div>
<div id="remoteVideoDiv">
<video id="remoteVideo" autoplay playsinline>远端窗口</video>
</div>
</body>
</html>
二、RTCEngine 类
我们使用WebSocket
的方式与服务器保持长连接,我们下面实现一个RTCEngine
类,这个类用于与服务器的连接,并且发送信令
2.1 构造函数
该函数的的构造函数需要传入服务器的url
,然后使用这个url
初始化本地的WebSocket
连接:
var rtcEngine = null;
var RTCEngine = function (wsUrl) {
//传入的函数形参
this.init(wsUrl); //init相当于成员函数
rtcEngine = this;
return this; //返回自身实例
};
2.2 init 函数
- 构造函数内部使用了
init
成员函数,我们可以使用prototype
的方式来定义这个成员函数 init
函数内部实现的就是对应的赋值操作,signaling
是我们定义类的WebSocket
成员对象
//初始化RTCEngine
RTCEngine.prototype.init = function (wsUrl) {
this.wsUrl = wsUrl; //wsUrl是成员变量
this.signaling = null; //websocket对象
};
2.3 createWebSocket 函数
内层封装
我们在类内添加一个连接函数
createWebSocket
,用于创建一个WebSocket
对象,并且根据构造函数得到的url
向服务器发送连接请求:在函数内部需要实现对应的几个
WebSocket
回调函数,比如onopen
、onmessage
等等
//创建WebSocket连接
RTCEngine.prototype.createWebSocket = function () {
rtcEngine = this;
rtcEngine.signaling = new WebSocket(this.wsUrl);
//绑定WebSocket回调函数
rtcEngine.signaling.onopen = function () {
rtcEngine.onOpen();
};
rtcEngine.signaling.onmessage = function (event) {
rtcEngine.onMessage(event);
};
rtcEngine.signaling.onerror = function (event) {
rtcEngine.onError(event);
};
rtcEngine.signaling.onclose = function (event) {
rtcEngine.onClose(event);
};
};
外层封装
类内的回调函数调用的都是我们的成员函数,比如onOpen
、onMessage
,这些我们需要在外部定义:
//WebSocket连接成功回调函数,RTCEngine的成员函数
RTCEngine.prototype.onOpen = function () {
console.log("WebSocket连接成功");
};
//WebSocket连接失败回调函数,RTCEngine的成员函数
RTCEngine.prototype.onError = function (event) {
console.log("WebSocket连接失败:" + event.data);
};
//WebSocket连接关闭回调函数,RTCEngine的成员函数
RTCEngine.prototype.onClose = function (event) {
console.log("WebSocket连接关闭:" + event.data);
};
//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
console.log("WebSocket接收消息:" + event.data);
};
2.4 sendMessageh函数
这个函数使用我们的sigaling
对象发送消息:
//向WebSocket服务器发送消息
RTCEngine.prototype.sendMessage = function (message) {
rtcEngine.signaling.send(message);
};
三、信令处理
3.1 信令定义
我们需要实现下面的信令,以实现本地端和远端的交互,期间通过服务器转发信令,我们的信令都是使用JSON
格式序列化的:
- join 加入房间
- resp_join 当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回
- leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开
- new_peer 服务器通知客户端有新人加入,收到newpeer则发起连接请求
- peer_leave 服务器通知客户端有人离开
- offer 转发offer sdp
- answer 转发answer sdp
- candidate 转发candidate sdp
信令的定义如下:
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"; //发送offer
const SIGNAL_TYPE_ANSWER = "answer"; //对端回复
const SIGNAL_TYPE_CANDIDATE = "candidate"; //发送candidate
3.2 信令响应
在onMessage
函数我们需要处理来自服务端的消息,解析后可以的到信令的类型,根据不同的类型我们做不同的处理:
//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
console.log("WebSocket接收消息:" + event.data);
//解析消息:JSON格式
var msg = JSON.parse(event.data);
console.log("解析后的JSON消息:", msg);
switch (msg.cmd) {
case SIGNAL_TYPE_NEW_PEER: //新加入者
handleRemoteNewPeer(msg);
break;
case SIGNAL_TYPE_RESP_JOIN: //加入者回复
handleResponseJoin(msg);
break;
case SIGNAL_TYPE_PEER_LEAVE: //离开者
handleRemotePeerLeave(msg);
break;
case SIGNAL_TYPE_OFFER: //收到offer
handleRemoteOffer(msg);
break;
case SIGNAL_TYPE_ANSWER: //收到answer
handleRemoteAnswer(msg);
break;
case SIGNAL_TYPE_CANDIDATE: //收到candidate
handleRemoteCandidate(msg);
break;
}
};
3.3 join
点击加入按钮的时候,首先需要查看按钮的信息是否合法,然后再进行后续的操作:
document.querySelector("#joinBtn").onclick = function () {
if (isInRoom) {
alert("请先离开当前房间");
return;
}
roomId = document.getElementById("roomIdInput").value;
if (roomId == "" || roomId == "请输入房间ID") {
alert("请输入房间ID");
return;
}
console.log("触发加入按钮点击事件,roomId = " + roomId);
//初始化本地流
initLocalStream();
};
信息合法,那么我们就可以打开本地摄像头,将摄像头画面添加到<video>
标签内,对应的initLocalStream
就是实现这样的功能,这里的可以选择麦克风和摄像头:
//初始化本地流
function initLocalStream() {
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
.then(openLocalStream)
.catch(function (e) {
alert("getUserMedia() error: " + e.name);
});
}
如果正常执行,那么就会异步执行openLocalStream
,如果过程中发生了异常就执行捕捉异常的函数
var localVideo = document.querySelector("#localVideo");
var remoteVideo = document.querySelector("#remoteVideo");
//打开本地流并设置
function openLocalStream(stream) {
console.log("打开本地流成功");
//加入房间
doJoin(roomId);
//设置本地视频
localVideo.srcObject = stream;
localStream = stream;
}
然后我们开始封装join
信令,使用rtcEngine
这个对象内部的sendMessage
函数发送,告诉服务器我们的uid
,以及需要加入的房间号roomId
//加入房间,向服务器发送JSON消息
function doJoin(roomId) {
var jsonMsg = {
cmd: SIGNAL_TYPE_JOIN,
roomId: roomId,
uid: localUserId,
};
//发送JSON消息
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
isInRoom = true;
}
注意这里的uid
实际只是我们随机生成的,实际需要修改这部分逻辑:
var localUserId = Math.random().toString(36).substring(2); //生成本地id
3.4 resp_join
这个信令触发代表我们当前加入了这个房间,并且服务器告诉我们房间里面的人是谁,返回的是远端的uid
,这里我们需要做的就是将这个uid
存储起来:
var remoteUserId = -1;
//有人加入,发送给加入者
function handleResponseJoin(msg) {
console.log("加入者:" + msg.remoteUid);
remoteUserId = msg.remoteUid;
}
3.5 new_peer
这个信令是当我们处在房间中,另一个人加入的时候,服务器告诉我们有人加入房间了,并且告诉我们这个人的uid
,我们也是需要将它存储起来:
//有人加入,发送给房间的人
function handleRemoteNewPeer(msg) {
console.log("房间里面的人:" + msg.remoteUid);
remoteUserId = msg.remoteUid;
doOffer();
}
当然,这里还需要就是给他发offer
,也就是房间里面的人齐了,我们可以开始媒体协商了:
//加入房间,给房间里面的人发送offer
function doOffer() {
//创建RTCPeerConnection对象
if (pc == null) {
createPeerConnection();
}
pc.createOffer().then(createOfferAndSendMessage).catch(function (error) {
console.log("创建offer失败");
});
}
- 因此我们就需要使用到
WebRTC
的接口了,首先我们需要先创建一个WebRTC
的RTCPeerConnection
对象 - 我们先不配置
stun
和turn
服务器,使用浏览器默认的配置,传入null
到构造函数 - 然后还需要绑定两个回调函数,分别是当网络协商
ICECandidate
收集完成的时候触发、远端流到达本地时触发 - 最后,我们遍历自己本地的所有流,加入到
RTCPeerConnection
的类中,帮助媒体协商
//创建和远端的连接
function createPeerConnection() {
//创建RTCPeerConnection对象
pc = new RTCPeerConnection(null);
// pc = new RTCPeerConnection(null);
//设置回调函数
pc.onicecandidate = handleIceCandidate; //设置IceCandidate回调函数
pc.ontrack = handleRemoteStreamAdd; //设置远端流回调函数
//遍历本地流,加入流中
localStream.getTracks().forEach(function (track) {
pc.addTrack(track, localStream);
});
console.log("创建RTCPeerConnection对象成功");
}
使用
createOffer
函数创建好的offer
之后,会调用then
里面的函数,会传入一个session
到该函数,表示本地的媒体信息sdp
,我们需要将它存储在本地,然后发送一份到对端:存储本地使用的
setLocalDescription
接口,存储成功后我们调用then
里面的函数发出去到服务器
//发送offer并且发送sdp对象session,创建sdp
function createOfferAndSendMessage(session) {
//设置本地sdp对象
pc.setLocalDescription(session)
.then(function () {
//发送offer
var jsonMsg = {
"cmd": SIGNAL_TYPE_OFFER,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
})
.catch(function (error) {
console.log("设置本地sdp对象失败");
});
}
接收到对端流的时候,我们需要将对端流存储起来,然后将其渲染在<video>
标签内:
//设置ontrack回调函数,接收到对端流时触发
function handleRemoteStreamAdd(event) {
console.log("接收对端流");
//设置对端流
remoteStream = event.streams[0];
remoteVideo.srcObject = remoteStream;
}
3.6 candidate
- 收集到媒体协商后,触发回调函数,我们这里要将它发送到服务器,然后转发给对端,用于网络协商
ICECandidate
的信息存储在回调函数的event
的candidate
成员里面,它通常是一个JSON
类型,我们将其序列化为字符串,存储到我们的JSON
消息中,然后发给服务器
//设置onicecandidate回调函数,收集到Candidate信息时触发
function handleIceCandidate(event) {
console.log("handleIceCandidate");
if (event.candidate) {
//发送candidate
var jsonMsg = {
"cmd": SIGNAL_TYPE_CANDIDATE,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(event.candidate),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送candidate:" + message);
}
else {
console.warn("结束candidate");
}
}
同样,对端接收到candidate
信令的响应,需要将candidate
信息存储在本地,这里调用的是addIceCandidate
接口:
//处理candidate
function handleRemoteCandidate(msg) {
console.log("收到candidate");
var candidate = JSON.parse(msg.message);
//添加IceCandidate
pc.addIceCandidate(candidate)
.then(function () {
console.log("添加IceCandidate成功");
})
.catch(function (error) {
console.log("添加IceCandidate失败:" + error);
});
}
当两端都设置好candidate
的时候,可以认为连接已经建立了,前提是candidate
至少有一条有效
3.7 offer
房间第二个人加入的时候,会给另一个人发
offer
,我们收到offer
的时候会响应,offer
中包含着对端的媒体信息sdp
,我们需要查看本地媒体信息,然后进行媒体协商,将协商好的sdp
存储在自己的本地,然后回复一个answer
到对端,answer
中包含协商好的sdp
信息,以及自己的对端的uid
存储
sdp
信息的时候,调用RTCPeerConnection
对象的setRemoteDescription
接口实现
//收到offer,创建PeerConnection对象,保存sdp,并且回复answer
function handleRemoteOffer(msg) {
console.log("收到offer");
if (pc == null) {
createPeerConnection();
}
var desc = JSON.parse(msg.message);
pc.setRemoteDescription(desc); //设置对端sdp
doAnswer(); //回复answer
}
3.8 answer
回复answer
调用RTCPeerConnection
对象的createAnswer
接口实现,创建好answer
之后,使用then
里面的函数异步发送到服务器中:
//加入房间,房间里面的人回复answer
function doAnswer() {
pc.createAnswer().then(createAnswerAndSendMessage).catch(function (error) {
console.log("创建answer失败");
});
}
该回调函数中会传入一个session
,是协商好的媒体信息,使用setLocalDescription
接口存储sdp
,然后在通过then
存储之后发送协商好的媒体信息到对端
//发送answer并且发送sdp对象session,对比对端sdp后设置本地sdp
function createAnswerAndSendMessage(session) {
pc.setLocalDescription(session)
.then(function () {
//发送answer
var jsonMsg = {
"cmd": SIGNAL_TYPE_ANSWER,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
})
.catch(function (error) {
console.log("设置本地sdp对象失败");
});
}
3.10 leave
点击离开按钮,我们需要处理状态是否合法:
//设置离开按钮点击事件
document.querySelector("#leaveBtn").onclick = function () {
if (roomId == -1) {
alert("请先加入房间");
return;
}
console.log("触发离开按钮点击事件,roomId = " + roomId);
if(rtcEngine != null){
doLeave();
}
}
如果状态合法,那么我们就可以处理离开事件了,doLeave
函数内部向服务器发送leave
信令,告诉服务器自己离开了,同样封装JSON
:
//离开房间,向服务器发送JSON消息
function doLeave() {
isInRoom = false;
console.log("is in Room " + isInRoom);
var jsonMsg = {
cmd: SIGNAL_TYPE_LEAVE,
roomId: roomId,
uid: localUserId,
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
hangup();
}
离开房间后,就可以将本地视频和远端的视频都关掉了,也就是在hangup
函数内部实现的,这里还需要关闭RTCPeerConnection
对象,后续会介绍这个类
//挂断视频
function hangup() {
//关闭本地流
if (localStream != null) {
localStream.getTracks().forEach(function (track) {
track.stop();
});
localVideo.srcObject = null;
localStream = null;
}
//关闭远端流
if (remoteStream != null) {
remoteStream.srcObject = null;
remoteStream = null;
}
if (remoteVideo != null) {
remoteVideo.srcObject = null;
}
if (pc != null) {
pc.close(); //关闭RTCPeerConnection对象
pc = null;
}
}
3.11 peer_leave
当有人离开的时候,服务器会告知房间里面的另一个人,已经有人离开了,那么此时就可以把远端的窗口给关闭了,并且关闭RTCPeerConnection
对象、清空远端的用户信息:
//有人离开,发送给房间的人
function handleRemotePeerLeave(msg) {
console.log("离开者:" + msg.remoteUid);
remoteVideo.srcObject = null;
remoteUserId = -1;
if (pc != null) {
pc.close();
pc = null;
}
}
四、完整代码
完整的Web
端一对一视频通话代码如下:
4.1 index.html
页面显示代码:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC一对一通话</title>
</head>
<body>
<h1>WebRTC一对一通话</h1>
<div id="buttonsDiv">
<input id="roomIdInput" type="text" placeholder="请输入房间ID">
<button id="joinBtn">加入房间</button>
<button id="leaveBtn">离开房间</button>
</div>
<div id="localVideosDiv">
<video id="localVideo" autoplay muted playsinline>本地窗口</video>
</div>
<div id="remoteVideoDiv">
<video id="remoteVideo" autoplay playsinline>远端窗口</video>
</div>
</body>
<script src="js/main.js"></script>
<script> src = "js/adapter-latest.js" </script>
</html>
4.2 main.js
完整的信令实现代码
"use strict";
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"; //发送offer
const SIGNAL_TYPE_ANSWER = "answer"; //对端回复
const SIGNAL_TYPE_CANDIDATE = "candidate"; //发送candidate
var localVideo = document.querySelector("#localVideo");
var remoteVideo = document.querySelector("#remoteVideo");
var localStream = null;
var remoteStream = null;
var rtcEngine = null;
var localUserId = Math.random().toString(36).substring(2); //生成本地id
var remoteUserId = -1; //对端id
var roomId = 0; //房间号
var pc = null; //RTCPeerConnection对象
var isInRoom = false;
//相当于构造函数
var RTCEngine = function (wsUrl) {
//传入的函数形参
this.init(wsUrl); //init相当于成员函数
rtcEngine = this;
return this; //返回自身实例
};
//初始化RTCEngine
RTCEngine.prototype.init = function (wsUrl) {
this.wsUrl = wsUrl; //wsUrl是成员变量
this.signaling = null; //websocket对象
};
//创建WebSocket连接
RTCEngine.prototype.createWebSocket = function () {
rtcEngine = this;
rtcEngine.signaling = new WebSocket(this.wsUrl);
//绑定WebSocket回调函数
rtcEngine.signaling.onopen = function () {
rtcEngine.onOpen();
};
rtcEngine.signaling.onmessage = function (event) {
rtcEngine.onMessage(event);
};
rtcEngine.signaling.onerror = function (event) {
rtcEngine.onError(event);
};
rtcEngine.signaling.onclose = function (event) {
rtcEngine.onClose(event);
};
};
//向WebSocket服务器发送消息
RTCEngine.prototype.sendMessage = function (message) {
rtcEngine.signaling.send(message);
};
//WebSocket连接成功回调函数,RTCEngine的成员函数
RTCEngine.prototype.onOpen = function () {
console.log("WebSocket连接成功");
};
//WebSocket连接失败回调函数,RTCEngine的成员函数
RTCEngine.prototype.onError = function (event) {
console.log("WebSocket连接失败:" + event.data);
};
//WebSocket连接关闭回调函数,RTCEngine的成员函数
RTCEngine.prototype.onClose = function (event) {
console.log("WebSocket连接关闭:" + event.data);
isInRoom = false;
};
//WebSocket接收消息回调函数,RTCEngine的成员函数
RTCEngine.prototype.onMessage = function (event) {
console.log("WebSocket接收消息:" + event.data);
//解析消息:JSON格式
var msg = JSON.parse(event.data);
console.log("解析后的JSON消息:", msg);
switch (msg.cmd) {
case SIGNAL_TYPE_NEW_PEER: //新加入者
handleRemoteNewPeer(msg);
break;
case SIGNAL_TYPE_RESP_JOIN: //加入者回复
handleResponseJoin(msg);
break;
case SIGNAL_TYPE_PEER_LEAVE: //离开者
handleRemotePeerLeave(msg);
break;
case SIGNAL_TYPE_OFFER: //收到offer
handleRemoteOffer(msg);
break;
case SIGNAL_TYPE_ANSWER: //收到answer
handleRemoteAnswer(msg);
break;
case SIGNAL_TYPE_CANDIDATE: //收到candidate
handleRemoteCandidate(msg);
break;
}
};
//有人加入,发送给房间的人
function handleRemoteNewPeer(msg) {
console.log("房间里面的人:" + msg.remoteUid);
remoteUserId = msg.remoteUid;
doOffer();
}
//有人加入,发送给加入者
function handleResponseJoin(msg) {
console.log("加入者:" + msg.remoteUid);
remoteUserId = msg.remoteUid;
}
//有人离开,发送给房间的人
function handleRemotePeerLeave(msg) {
console.log("离开者:" + msg.remoteUid);
remoteVideo.srcObject = null;
remoteUserId = -1;
if (pc != null) {
pc.close();
pc = null;
}
}
//收到offer,创建PeerConnection对象,保存sdp,并且回复answer
function handleRemoteOffer(msg) {
console.log("收到offer");
if (pc == null) {
createPeerConnection();
}
var desc = JSON.parse(msg.message);
pc.setRemoteDescription(desc); //设置对端sdp
doAnswer(); //回复answer
}
//收到answer,保存sdp
function handleRemoteAnswer(msg) {
console.log("收到answer");
var desc = JSON.parse(msg.message);
pc.setRemoteDescription(desc); //设置对端sdp
}
//处理candidate
function handleRemoteCandidate(msg) {
console.log("收到candidate");
var candidate = JSON.parse(msg.message);
//添加IceCandidate
pc.addIceCandidate(candidate)
.then(function () {
console.log("添加IceCandidate成功");
})
.catch(function (error) {
console.log("添加IceCandidate失败:" + error);
});
}
//设置onicecandidate回调函数,收集到Candidate信息时触发
function handleIceCandidate(event) {
console.log("handleIceCandidate");
if (event.candidate) {
//发送candidate
var jsonMsg = {
"cmd": SIGNAL_TYPE_CANDIDATE,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(event.candidate),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送candidate:" + message);
}
else {
console.warn("结束candidate");
}
}
//设置ontrack回调函数,接收到对端流时触发
function handleRemoteStreamAdd(event) {
console.log("接收对端流");
//设置对端流
remoteStream = event.streams[0];
remoteVideo.srcObject = remoteStream;
}
//加入房间,向服务器发送JSON消息
function doJoin(roomId) {
var jsonMsg = {
cmd: SIGNAL_TYPE_JOIN,
roomId: roomId,
uid: localUserId,
};
//发送JSON消息
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
isInRoom = true;
}
//离开房间,向服务器发送JSON消息
function doLeave() {
isInRoom = false;
console.log("is in Room " + isInRoom);
var jsonMsg = {
cmd: SIGNAL_TYPE_LEAVE,
roomId: roomId,
uid: localUserId,
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
hangup();
}
//加入房间,给房间里面的人发送offer
function doOffer() {
//创建RTCPeerConnection对象
if (pc == null) {
createPeerConnection();
}
pc.createOffer().then(createOfferAndSendMessage).catch(function (error) {
console.log("创建offer失败");
});
}
//加入房间,房间里面的人回复answer
function doAnswer() {
pc.createAnswer().then(createAnswerAndSendMessage).catch(function (error) {
console.log("创建answer失败");
});
}
//创建和远端的连接
function createPeerConnection() {
//stun服务器配置信息
var defaultConfiguration = {
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "relay",
iceServers: [
{
"urls": [
"turn:192.168.10.251:3478?transport=udp",
"turn:192.168.10.251:3478?transport=tcp" // 可以插入多个进行备选
],
"username": "lh",
"credential": "123456"
},
{
"urls": [
"stun:192.168.10.251:3478"
]
}
]
};
//创建RTCPeerConnection对象
pc = new RTCPeerConnection(defaultConfiguration);
// pc = new RTCPeerConnection(null);
//设置回调函数
pc.onicecandidate = handleIceCandidate; //设置IceCandidate回调函数
pc.ontrack = handleRemoteStreamAdd; //设置远端流回调函数
//遍历本地流,加入流中
localStream.getTracks().forEach(function (track) {
pc.addTrack(track, localStream);
});
console.log("创建RTCPeerConnection对象成功");
}
//发送offer并且发送sdp对象session,创建sdp
function createOfferAndSendMessage(session) {
//设置本地sdp对象
pc.setLocalDescription(session)
.then(function () {
//发送offer
var jsonMsg = {
"cmd": SIGNAL_TYPE_OFFER,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
})
.catch(function (error) {
console.log("设置本地sdp对象失败");
});
}
//发送answer并且发送sdp对象session,对比对端sdp后设置本地sdp
function createAnswerAndSendMessage(session) {
pc.setLocalDescription(session)
.then(function () {
//发送answer
var jsonMsg = {
"cmd": SIGNAL_TYPE_ANSWER,
"roomId": roomId,
"uid": localUserId,
"remoteUid": remoteUserId,
"message": JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
rtcEngine.sendMessage(message);
console.log("发送JSON消息:" + message);
})
.catch(function (error) {
console.log("设置本地sdp对象失败");
});
}
//挂断视频
function hangup() {
//关闭本地流
if (localStream != null) {
localStream.getTracks().forEach(function (track) {
track.stop();
});
localVideo.srcObject = null;
localStream = null;
}
//关闭远端流
if (remoteStream != null) {
remoteStream.srcObject = null;
remoteStream = null;
}
if (remoteVideo != null) {
remoteVideo.srcObject = null;
}
if (pc != null) {
pc.close(); //关闭RTCPeerConnection对象
pc = null;
}
}
//打开本地流并设置
function openLocalStream(stream) {
console.log("打开本地流成功");
//加入房间
doJoin(roomId);
//设置本地视频
localVideo.srcObject = stream;
localStream = stream;
}
//初始化本地流
function initLocalStream() {
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
.then(openLocalStream)
.catch(function (e) {
alert("getUserMedia() error: " + e.name);
});
}
/*相当于main函数开始*/
//设置加入按钮点击事件
document.querySelector("#joinBtn").onclick = function () {
if (isInRoom) {
alert("请先离开当前房间");
return;
}
roomId = document.getElementById("roomIdInput").value;
if (roomId == "" || roomId == "请输入房间ID") {
alert("请输入房间ID");
return;
}
console.log("触发加入按钮点击事件,roomId = " + roomId);
//初始化本地流
initLocalStream();
};
//设置离开按钮点击事件
document.querySelector("#leaveBtn").onclick = function () {
if (roomId == -1) {
alert("请先加入房间");
return;
}
console.log("触发离开按钮点击事件,roomId = " + roomId);
if(rtcEngine != null){
doLeave();
}
}
//创建WebSocket连接
// rtcEngine = new RTCEngine("ws://192.168.217.128:9002");
rtcEngine = new RTCEngine("ws://192.168.10.251:9002");
rtcEngine.createWebSocket();
4.3 adpater-latest.js
这个文件是 WebRTC 项目中的一个适配器文件,其主要作用是为不同浏览器提供统一的 WebRTC API 接口,解决不同浏览器对 WebRTC 标准实现的差异问题
代码链接:
https://link.csdn.net/?from_id=79704635&target=https%3A%2F%2Fgithub.com%2Fwebrtc%2Fadapter