环境需求
-
服务器
-
阿里云服务器(后台)
-
腾讯云服务器(前端)
-
信令服务器(通信)
-
stun/turn 打洞服务器(p2p)
-
-
操作系统
- Ubuntu(服务器)
- Windows(开发)
- Android (测试)
-
语言
- Java
- JavaScript
- Html(不太算语言)
- Linux(应该算命令)
-
软件支持
- VS Code
- IntelliJ IDEA 2020.1.2
- Win SCP
- Chrome
- MicroSoft Edge
环境搭建
信令服务器
本例中是基于spring-boot实现的信令服务器,依托websocket,大致创建过程如下
-
添加依赖
<!-- 添加对websocket的支持 --> <dependency> <groupId>com.github.lazyboyl</groupId> <artifactId>websocket-spring-boot-starter</artifactId> <version>1.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> xml -
编写实现
-
创建SocketServerController.java,写入
package cool.hyz.blog.services.impl.socket; import com.google.gson.Gson; import cool.hyz.blog.pojo.SocketMessage; import cool.hyz.blog.utils.TextUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @Slf4j @Component @ServerEndpoint("/websocket/{server-id}") public class SocketServerController { private String WELCOME = new Gson().toJson(new SocketMessage("welcome", "欢迎访问米瑶的家", "text", new Date())); private String JOIN_IN = new Gson().toJson(new SocketMessage("join", "加入了房间", "text", new Date())); private String FULL = new Gson().toJson(new SocketMessage("full", "房间已满", "text", new Date())); private String LEAVE = new Gson().toJson(new SocketMessage("leave", "离开了房间", "text", new Date())); /** * server-id: * 23:全局链接 */ //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<SocketServerController> webSocketSet = new CopyOnWriteArraySet<SocketServerController>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //接收sid private String sid = ""; //接收用户信息 private String userInfo = ""; //接收用户Id private String userId = ""; private int maxCount = -1; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("server-id") String sid) { this.session = session; addOnlineCount(); //在线数加1 log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCountAll()); this.sid = sid; } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCountAll()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session, @PathParam("server-id") String sid) throws IOException { if (TextUtils.isEmpty(sid)) { return; } SocketMessage socketMessage = new Gson().fromJson(message, SocketMessage.class); String type = socketMessage.getType(); if (type.equals("connect")) { this.maxCount = socketMessage.getMaxCount(); this.userId = socketMessage.getId(); this.userInfo = socketMessage.getUserInfo(); webSocketSet.add(this); //加入set中 sendText(this, WELCOME); return; } log.info("收到来自窗口" + sid + "的信息:" + message); //群发消息 if (type.equals("join")) { int onlineCount = getOnlineCount(this.sid); int max = getMaxCount(this.sid); if (max >= onlineCount || max == -1) { sendTextInfo(JOIN_IN, sid, this.userId); } else { sendText(this,FULL); } return; } if (type.equals("leave")) { sendTextInfo(LEAVE, sid, this.userId); String text = new Gson().toJson(new SocketMessage("bye", "再见!", "text", new Date())); sendText(this, text); return; } if (type.equals("secret")) { String text = new Gson().toJson(new SocketMessage("secret", socketMessage.getMessage(), "text", new Date())); sendTextInfo(text, sid, socketMessage.getTargetUser(), true); return; } String text = new Gson().toJson(new SocketMessage("normal", socketMessage.getMessage(), socketMessage.getMessageType(), new Date())); sendTextInfo(text, sid, this.userId); } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */ public static void sendText(SocketServerController controller, String message) throws IOException { if (controller == null) { return; } controller.session.getBasicRemote().sendText(message); } /** * 实现服务器主动推送 */ public static void sendImage(SocketServerController controller, ByteBuffer image) throws IOException { if (controller == null) { return; } controller.session.getBasicRemote().sendBinary(image); } /** * 群发自定义消息 */ public static void sendTextInfo(String message, String sid) { log.info("推送消息到窗口" + sid + ",推送内容:" + message); for (SocketServerController item : webSocketSet) { try { if (TextUtils.isEmpty(sid)) { //这里可以设定只推送给这个sid的,为null则全部推送 sendText(item, message); } else if (item.sid.equals(sid)) { sendText(item, message); } } catch (IOException e) { continue; } } } /** * 群发自定义消息 */ public static void sendTextInfo(String message, String sid, String userId) { log.info("推送消息到窗口" + sid + ",推送内容:" + message); for (SocketServerController item : webSocketSet) { try { if (TextUtils.isEmpty(sid)) { //这里可以设定只推送给这个sid的,为null则全部推送 if (TextUtils.isEmpty(userId)) { sendText(item, message); } else if (!item.userId.equals(userId)) { sendText(item, message); } } else { if (TextUtils.isEmpty(userId)) { if (item.sid.equals(sid)) { sendText(item, message); } } else if (!item.userId.equals(userId)) { if (item.sid.equals(sid)) { sendText(item, message); } } } } catch (IOException e) { continue; } } } /** * 群发自定义消息 */ public void sendTextInfo(String message, String sid, String userId, boolean secret) { log.info("推送消息到窗口" + sid + ",推送内容:" + message); if (secret) { for (SocketServerController item : webSocketSet) { try { if (TextUtils.isEmpty(sid)) { //这里可以设定只推送给这个sid的,为null则全部推送 String text = new Gson().toJson(new SocketMessage("normal", "找不到房间!", "text", new Date())); sendText(this, text); return; } else if (TextUtils.isEmpty(userId) || !item.sid.equals(sid)) { String text = new Gson().toJson(new SocketMessage("normal", "找不到用户!", "text", new Date())); sendText(this, text); return; } else if (item.userId.equals(userId)) { sendText(item, message); return; } } catch (IOException e) { continue; } } } } /** * 群发自定义消息 */ public static void sendImageInfo(ByteBuffer image, String sid) throws IOException { for (SocketServerController item : webSocketSet) { try { //这里可以设定只推送给这个sid的,为null则全部推送 if (TextUtils.isEmpty(sid)) { sendImage(item, image); } else if (item.sid.equals(sid)) { sendImage(item, image); } } catch (IOException e) { continue; } } } public static synchronized int getOnlineCountAll() { return onlineCount; } public static synchronized int getMaxCount(String sid) { int count = -1; if (!TextUtils.isEmpty(sid)) { for (SocketServerController item : webSocketSet) { if (item.sid.equals(sid) && item.maxCount > count) { count = item.maxCount; } } } return count; } public static synchronized int getOnlineCount(String sid) { int count = 0; for (SocketServerController item : webSocketSet) { if (!TextUtils.isEmpty(sid)) { if (item.sid.equals(sid)) { count++; } } else { count++; } } return count; } public static synchronized List<String> getOnlineUserInfo(String sid) { List<String> list = new ArrayList<>(); for (SocketServerController item : webSocketSet) { if (!TextUtils.isEmpty(sid)) { if (item.sid.equals(sid)) { list.add(item.userInfo); } } else { list.add(item.userInfo); } } return list; } public static synchronized List<String> getOnlineUserId(String sid) { List<String> list = new ArrayList<>(); for (SocketServerController item : webSocketSet) { if (!TextUtils.isEmpty(sid)) { if (item.sid.equals(sid)) { list.add(item.userId); } } else { list.add(item.userId); } } return list; } public static synchronized void addOnlineCount() { SocketServerController.onlineCount++; } public static synchronized void subOnlineCount() { SocketServerController.onlineCount--; } } java具体的实现可根据自己的需求,本例中写的比较冗杂,为了能在一个类里面展示,就没有抽取出来,大概逻辑较为清晰,读者可根据自己需求开发
-
编译运行
服务端连接地址:
ws://localhost:2020/websocket/+cid -
配置wss
-
需求原因
浏览器端发起视频通话需要获取摄像头和音频权限,在http协议下无法实现,只能依托https协议,而在https协议中连接信令服务器需要同样配置wss协议
-
实现方式(nginx)
location /websocket { proxy_pass http://lib-blog-server;#这里是你的服务端服务器 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } kotlin -
重启nginx
-
-
搭建stun/turn 打洞服务器
本例中采用较为广泛采用的coturn,搭建p2p打洞服务器
-
获取资源文件
- 去
https://github.com/coturn/coturn打包下载(推荐) - 采用
git clone或者apt install(暂不推荐,国内较慢)
- 去
-
上传资源文件
- 解压后的文件到服务器某个目录下,如
/etc/coturn
- 解压后的文件到服务器某个目录下,如
-
安装一些依赖
apt-get install libssl-deapt-get install pkg-config
-
安装
-
依次执行一下四条命令
cd /etc/coturn ./configure make make install
-
-
配置:
vim /etc/turnserver.conf-
配置端口Portlistening-port=3478 -
配置
公网IPexternal-ip=你的服务器公网IP -
配置用户
user=username:password -
配置域名
realm: realm=跟上述公网服务器解析了的域名 -
配置服务器安全组
- 开放端口
3478/tcp和3478/udp
- 开放端口
-
-
启动服务:
sudo turnserver -c /etc/turnserver.conf --daemon kotlin -
测试服务
-
访问网站
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ -
输入
-
STUN or TURN URI:输入turn:你的域名:3478(或者turn:IP:3478,或者turn:IP,即可以不用端口) -
TURN username:用户名 -
TURN password:密码
-
-
单击
Add -
单击
Gather Candidate -
成功标志
- 如果出现
Component Type一行有relay字段,并且address对应你的公网地址,则成功 - 域名解析可能需要一段时间,耐心等待一会儿就行
- 如果出现
-
第一弹补充
stun/turn服务器配置turnserver
在上一篇文章中,由于文章是用markdown写的,结果格式出现了问题,所以这里更正一下出现的错误
下面针对turnserver.conf 配置重新介绍一下:
-
配置turnserver
-
找到文件位置
vim /etc/turnserver.conf` -
配置端口
Portlistening-port=3478 -
配置
公网IPexternal-ip=你的服务器公网IP -
配置用户
user=username:password -
配置域名
realm: realm=跟上述公网服务器解析了的域名
-
-
配置服务器安全组
在购买服务器的控制台找到安全组,开放端口
3478/tcp和3478/udp或者通过iptables等也可
后端补充
规范socket message的格式有利于通信识别,故定义一个message类
package cool.hyz.blog.pojo;
import java.util.Date;
public class SocketMessage {
public SocketMessage() {
}
public SocketMessage(String type, String message, String messageType, Date time) {
this.type = type;
this.message = message;
this.messageType = messageType;
this.time = time;
}
public SocketMessage(int maxCount, String type, String id, String message, String messageType, Date time, String location, String browser, String userInfo, String targetUser, String duration) {
this.maxCount = maxCount;
this.type = type;
this.id = id;
this.message = message;
this.messageType = messageType;
this.time = time;
this.location = location;
this.browser = browser;
this.userInfo = userInfo;
this.targetUser = targetUser;
this.duration = duration;
}
private int maxCount=-1;
// 消息类型
private String type="connect";
// 用户Id
private String id;
// 消息内容
private String message;
// 消息类型
private String messageType;
// 消息时间
private Date time;
// 地点
private String location;
// 浏览器
private String browser;
// 用户信息
private String userInfo;
// 用户信息
private String targetUser;
// 显示时长
private String duration="normal";// "normal"||"long"
public int getMaxCount() {
return maxCount;
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getBrowser() {
return browser;
}
public void setBrowser(String browser) {
this.browser = browser;
}
public String getTargetUser() {
return targetUser;
}
public void setTargetUser(String targetUser) {
this.targetUser = targetUser;
}
public String getUserInfo() {
return userInfo;
}
public void setUserInfo(String userInfo) {
this.userInfo = userInfo;
}
public String getDuration() {
return duration;
}
public void setDuration(String duration) {
this.duration = duration;
}
}
java
============
前端实现
Html UI部分
UI这里面比较简单,只有两个video组件和三个按钮
css部分写的比较简单,能够实现效果就行,读者可根据自己的需求合理设计
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频通话</title>
<script src="./index.js"></script>
</head>
<style>
.video-box {
display: flex;
justify-content: center;
padding: 50px 0;
}
video {
height: 360px;
width: 450px;
margin: 20px;
box-shadow: 0 0 15px 3px grey;
}
.control-box {
display: flex;
justify-content: center;
}
.control-box div {
cursor: pointer;
padding: 5px;
box-shadow: 0 0 5px 1px #00AAAA;
border-radius: 3px;
margin: 15px;
}
.control-box div:hover{
transform: scale(.9);
}
</style>
<body>
<div class="video-box">
<video id="local" autoplay controls></video>
<video id="remote" autoplay controls></video>
</div>
<div class="control-box">
<div id="start">开启音视频</div>
<div id="call">呼叫/接通</div>
<div id="hang-up">挂断</div>
</div>
</body>
</html>
html
JavaScript 部分
初始化组件
-
预先定义需要用到的全局变量
-
在窗口加载完毕之后,通过
document.getElementById()方法找到各个组件 -
设置各个按钮的点击事件监听方法
var local, remote,//本地、远程的video组件
startBtn, callBtn, hangUpBtn,//三个按钮
localStream,//定义全局变量存储本地媒体流
peer,//RTCPeerConnection对象
socket//定义全局变量socket连接
window.onload = function () {
local = document.getElementById("local")//本地的video组件
remote = document.getElementById("remote")//远程的video组件
startBtn = document.getElementById("start")//start按钮
startBtn.onclick = start//start按钮点击事件
callBtn = document.getElementById("call")//call按钮
callBtn.onclick = call//call按钮点击事件
hangUpBtn = document.getElementById("hang-up")//hang up按钮
hangUpBtn.onclick = hangUp//hang up 按钮点击事件
}
function start(){}//start按钮点击事件
function call(){}//call按钮点击事件
function hangUp(){}//hang up 按钮点击事件
javascript
编写开启视频实现
实现开启视频的点击方法start(),针对多个平台兼容
- 配置媒体设备获取参数
- 判断媒体设备是否支持
- 发起调用媒体设备申请
- 处理媒体设备申请结果
function start() {//start按钮点击事件
let constrains = {//连接调用媒体的配置,这里表示视频和音频都获取,且参数配置均默认
video: true,//获取视频,参数默认
audio: true//获取音频,参数默认
}
if (navigator.mediaDevices.getUserMedia) {//最新标准API
navigator.mediaDevices.getUserMedia(constrains)
.then(getStream)//成功结果getStream()方法处理
.catch(handlerGetStreamError)//handlerGetStreamError()方法处理
} else if (navigator.webkitGetUserMedia) {//webkit内核浏览器
navigator.webkitGetUserMedia(constrains)
.then(getStream)
.catch(handlerGetStreamError)
} else if (navigator.mozGetUserMedia) {//Firefox浏览器
navigator.mozGetUserMedia(constrains)
.then(getStream)
.catch(handlerGetStreamError)
} else if (navigator.msGetUserMedia) {//微软内核
navigator.msGetUserMedia(constrains)
.then(getStream)
.catch(handlerGetStreamError)
} else if (navigator.getUserMedia) {//旧版API
navigator.getUserMedia(constrains)
.then(getStream)
.catch(handlerGetStreamError)
} else {
alert("Media Devices Is Not Supported !")
}
}
javascript
实现处理获取视频结果方法
这里时对于开启视频时获取媒体信息结果的处理
获取成功
- 设置本地video的媒体流数据
- 保存媒体流数据到全局变量localMedia
- 开启webSocket服务
- 创建RtcPeerConnection
function getStream(stream) {//成功结果getStream()方法处理
local.srcObject = stream//本地视频设置获取到的媒体流
localStream = stream//存储本地媒体流
startConnWebSocket()//开启webSocket服务
createPeerConnection()//创建RTCPeerConnection
}
javascript
此时便可以获取到自己的视频媒体流并显示在第一个窗口啦
获取出错
- 打印TAG标签和错误原因
function handlerGetStreamError(e) {//handlerGetStreamError()方法处理
console.log("GetStreamError", e)//打印失败原因
}
javascript
-
检查是否是由于浏览器或是设备自身的原因
-
建议使用Chrome、Edge、FireFox浏览器使用
-
读者可自行查询针对各浏览器的兼容方式
实现开启webSocket服务
-
判断是否支持websocket服务
-
目标信令服务器
-
连接信令服务器
-
设置连接监听方法回调
function startConnWebSocket() {//开启websocket服务
if (!isSupportsWebSocket) {//判断是否支持websocket
alert("WebSocket Is Not Supported !")
return
}
let url = "ws://localhost:2020/websocket/1"//这里是你的websocket服务器地址和房间号
socket = new WebSocket(url)//创建新的连接
socket.onopen=onOpen//连接成功回调
socket.onmessage=onMessage//收到信息回调
socket.onerror=onError//连接出错回调
socket.onclose=onClose//连接关闭回调
}
JavaScript
- 连接成功即触发onOpen()
- 收到消息即触发onMessage()
- 连接出错即触发onError()
- 连接关闭即触发onClose()
各个方法的具体实现,请读者继续阅读
这里用到了一个工具方法
function isSupportsWebSocket() {//判断是否支持WebSocket
var n = window.WebSocket
return !!n;
}
javascript
实现创建RTCPeerConnection
- 判断是否支持RTCPeerConnection
- 配置ice协议
- 创建RTCPeerConnection
- 添加本地媒体流至RTCPeerConnection
- 监听对方传输过来的媒体流信息
- 设置对方媒体流至remote并渲染
function createPeerConnection() {
if (!isSupportsRtcPeer()) {
alert("Rtc Is Not Supported !")
return
}
let iceConfig = {//ice穿透协议配置
"iceService": [{//stun/turn穿透服务器
"url": "turn:你的stun / turn服务器地址: 端口",//服务器地址,格式为turn:xxxx.xxx.xxx:xxxx
"credential": 你的密码,
"username": 你的账号
}]
}
if (!peer) {//判断peer是否已经创建
peer = new RTCPeerConnection(iceConfig)//未创建则创建新的peer
}
if (localStream) {//判断本地媒体流是否存在
peer.addStream(localStream)//peer添加本地媒体流,用于双方交互
}
peer.ontrack = function (e) {//监听对方传来的媒体流信息
console.log(e)
remote.srcObject = e.streams[0]//将对方媒体流信息显示在remote窗口
}
}
javascript
这里用到了一个工具方法
function isSupportsRtcPeer() {//判断是否支持RTC
var e = window.RTCPeerConnection
|| window.mozRTCPeerConnection
|| window.webkitRTCPeerConnection
return !!e;
}
javascript
编写呼叫/接听实现
实现呼叫/接听的点击方法call()
- 监听候选者信息
- 将监听到的候选者信息通过信令服务器发送给对方
- 设置用于创建识别连接的offer配置
- 创建识别连接的offer
- 实现创建成功/失败回调
function call() {//call按钮点击事件
peer.onicecandidate = function (e) {//获取候选者信息
if (e.candidate) {//存在候选者
sendMsg("normal", JSON.stringify({//向对方发送候选者信息用于交换
type: "candidate",
data: {
sdpMLineIndex: e.candidate.sdpMLineIndex,
sdpMid: e.candidate.sdpMid,
candidate: e.candidate.candidate
}
}), "object")//此处的message格式可以根据自己的信令服务器相适应
}
}
let options = {//配置createOffer的属性值
offerToReceiveAudio: 1,//接受一个音频流
offerToReceiveVideo: 1//接受一个视频流
}
peer.createOffer(options)//创建offer信息
.then(setOffer)//创建成功方法回调
.catch(handlerCreateOfferError)//创建失败方法回调
}
javascript
上文中用到了一个方法sendMsg(),其实现将在下文中进行介绍
实现挂断
function hangUp() {//hang up按钮点击事件
if (peer) {
peer.close()
peer = null//peer置空
}
socket = null//socket置空
remote.srcObject = null//remote媒体信息置空
}
javascript
实现处理创建offer信息结果方法
这里时对于创建offer结果的处理
创建成功
-
接受offer作为结果参数
-
设置localDescription
-
发送offer信息给对方
function setOffer(offer) {//创建offer成功回调
if (peer) {//判断peer是否存在
peer.setLocalDescription(offer)//设置localDescription
sendMsg("normal", JSON.stringify({//向对方发送offer信息
type: "offer",
data: offer
}), "object")
}
}
javascript
上文中用到了一个方法sendMsg(),其实现将在下文中进行介绍
创建失败
- 打印TAG标签和错误原因
function handlerCreateOfferError(e) {
console.log("CreateOfferError", e)//打印失败原因
}
javascript
socket信息格式
为了便于快速有效识别传输的信息内容,对于传输信息的格式做了相应的限定,以便于解析和阅读
{
maxCount :-1,
type : "normal",
id : "string",
message : "string",
messageType : "text",
time : new Date(),
location : "string",
browser : "string",
userInfo : "string",
targetUser : "string",
duration : "normal"
}
json
实现sendMsg()方法
封装了一个发送信令信息的方法sendMsg()
- type:信息属性,normal表示用户之间沟通的信息,join、connect等表示向服务器发送的识别信息
- message:信息内容
- messageType:信息内容类型,如text、object、image等
function sendMsg(type, message, messageType) {//封装发送信息方法
if (socket) {//判断socket是否为空
let msg = JSON.stringify({//Json 转 String
type: type,
message: message,
messageType: messageType,
time: new Date()
})
socket.send(msg)//向服务器发送信息
} else {
console.log("socket is Null!")
}
}
javascript
实现socket监听事件
这里实现socket的几个监听事件内容,即onopen()、onmessage()、onerror()、onclose()
连接成功
这里实现onOpen()
function onOpen() {//连接成功回调
console.log("连接成功!")
socket.send(JSON.stringify({//向服务器发送连接socket通知
type: "connect",
id: createUniqueId(),//用户唯一标识
userInfo: JSON.stringify({ name: "嘿嘿" + getRandom() }),
message: "我连接上你啦",
messageType: "text",
maxCount: 2,//最大连接数
time: new Date()
}))
sendMsg("join", "我加入啦", "text")//向服务器发送加入房间(视频聊天)通知
}
javascript
这里用到了两个工具方法
function createUniqueId() {//创建唯一Id
return getRandom() + new Date().getTime().toString() + getRandom()
}
function getRandom() {//生成3位随机数
let num = (Math.random() * 1000).toFixed(0).toString()
return num.length == 3 ? num : num.length == 2 ? "1" + num : "12" + num
}
javascript
收到消息
这里实现onMessage()
function onMessage(data) {//收到信息回调
//接收到的message数据类型为String 需要转为json格式
let message = JSON.parse(data.data)
if (message.type == "normal") {//normal表示用户之间的普通消息
//用户之间的消息分为text、object、image三种,根据不同类型分别解析
if (message.messageType == "object") { //object表示二者在连接时互相通讯的对象信息,如offer、answer、candidate等
if (peer) {
dealMessage(message.message)//定义新方法处理message
} else {
console.log("peer is null")
}
} else {
console.log("text", message.message)
}
} else {//其他类型如“join”、“welcome”表示系统发送的消息
console.log(message.message)
}
}
javascript
实现处理object类型的message的方法dealMessage()
- 将String类型的message转为Json格式用于解析
- type=offer
- 重新创建新的offer对象:使用收到的字符串形式的offer,依靠RTCSessionDescription类
- 创建Answer作为回应
- 实现处理创建Answer的结果
- type=answer
- 重新创建新的answer对象:使用收到的字符串形式的answer,依靠RTCSessionDescription类
- 设置answer对象到RemoteDescription
- type=candidate
- 重新创建新的candidate对象:使用收到的对象形式的candidate,依靠RTCIceCandidate类
- 添加candidate到peer连接当中
function dealMessage(message) {
let desc = JSON.parse(message)//接收到的message数据类型为String 需要转为json格式
if (desc.type == "offer") {
let offer = new RTCSessionDescription(desc.data)//offer已经被转为字符串,需要重新创建offer
peer.setRemoteDescription(offer)//设置offer到RemoteDescription
peer.createAnswer()//创建answer
.then(setAnswer)//成功回调
.catch(handlerCreateAnswerError)//失败回调
} else if (desc.type == "answer") {
let answer = new RTCSessionDescription(desc.data)//answer已经被转为字符串,需要重新创建answer
peer.setRemoteDescription(answer)//设置answer到RemoteDescription
} else if (desc.type == "candidate") {
let candidate = new RTCIceCandidate({
sdpMLineIndex: desc.data.sdpMLineIndex,
sdpMid: desc.data.sdpMid,
candidate: desc.data.candidate
})//candidate已经被转为字符串,需要重新创建candidate
peer.addIceCandidate(candidate)//添加候选者candidate
} else {
console.log("no match for this type", desc)
}
}
javascript
连接出错
function onError(err) {//连接出错回调
console.log("socket error:", error)
}
javascript
连接关闭
function onClose() {//连接关闭回调
console.log("socket conn closed !")
}
javascript
实现处理创建Answer信息结果方法
这里时对于创建Answer结果的处理
创建成功
- 接受到answer
- 判断peer状态
- 设置answer到LocalDescription
- 封装answer 信息并发送给对方
function setAnswer(answer) {
if (peer) {//判断peer是否为空
peer.setLocalDescription(answer)//设置LocalDescription
sendMsg("normal", JSON.stringify({//向对方发送answer信息
type: "answer",
data: answer
}), "object")
} else {
console.log("peer is null")
}
}
javascript
创建失败
处理创建失败的结果
function handlerCreateAnswerError(e) {
console.log("createAnswerError", e)
}
javascript
觉得有用的话辛苦点个关注鸭嘻嘻
往期原创推荐传送门:
2021-03-23 Web Socket +Rtc 实现视频通话(二)
2021-03-21 Web Socket +Rtc 实现视频通话(一)
2020.11.1 JavaScript网络爬虫之——英文文章
欢迎关注我的微信公众号






