环境需求

  • 服务器

    • 阿里云服务器(后台)

    • 腾讯云服务器(前端)

    • 信令服务器(通信)

    • 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-de
    • apt-get install pkg-config
  • 安装

    • 依次执行一下四条命令

      cd /etc/coturn
      ./configure
      make
      make install
      
  • 配置:vim /etc/turnserver.conf

    • 配置端口Port

      listening-port=3478
      
    • 配置公网IP

      external-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`
      
    • 配置端口Port

      listening-port=3478
      
    • 配置公网IP

      external-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-22 对于实现视频通话(一)的补充

2021-03-21 Web Socket +Rtc 实现视频通话(一)

2021-02-03 Vuex实现组件间通讯

2021-01-27 Ansys 计算实例

2020.12.19 今日份日记

2020.12.13 docker 介绍及常见命令

2020.12.12 Swagger安全配置——用户名密码

2020.12.12 solr安全配置——用户名密码

2020.11.15 Spring Boot项目创建

2020.11.1 JavaScript网络爬虫之——英文文章

2020.9.15 今日份日记

2020.9.01 今日份日记

2020.8.07 今日份日记

2020.8.02 晚安

2020.7.31 今日份日记

2020.7.31 你还要我怎样

2020.7.24 乐谱收藏夹(一)

2020.7.24 下雨了

2020.7.21 米瑶留言小程序上线啦~

2020.7.21 微信小程序开发之——米瑶云音乐

2020.7.14 网站分享

2020.7.08 技术分享——批量修改文件名

2020.6.11 访寻

2020.5.21 来世

欢迎关注我的微信公众号

续加仪

打赏
  • 微信
  • 支付宝
评论
来发评论吧~
···

歌手: