WebRTC音视频通话(二)简单音视频通话

本篇不详细介绍websocket,只针对websocket整合rtc。

一、简单说下webrtc的流程

webrtc是P2P通信,也就是实际交流的只有两个人,而要建立通信,这两个人需要交换一些信息来保证通信安全。而且,webrtc必须通过ssh加密,也就是使用https协议、wss协议。

借用一幅图

1.1 创建端点的解析

以下解析不包括websockt,只针对stun做解析。与上图略有不同

  1. 首先,Client A创建端点(Create PeerConnection),并添加音视频流(Add Streams)。接下来通知Client B,让Client B也创建一个端点。

  2. Client B收到通知后,Client B创建端点(Create PeerConnection),并添加音视频流(Add Streams),

  3. 接下来,Client B创建一个用于answer的SDP对象(Create Answer),保存并发送给Client A。

  4. Client A收到用于answer的SDP后,保存下来。

  5. 然后, Client A创建一个用于offer的SDP对象(Create Office),保存并发送给Client B。

  6. 最后,Client B保存收到的用于offer的SDP对象

以上步骤完成之后:

1、rtc会自动收集Candidate信息,并通过回调函数通知你,用于交换Candidate信息。

2、交换完Candidate信息后,P2P连接就建立好了。并通过回调函数,将远程视频流给你

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

1.2 交换Candidate信息

Candidate信息是交换完SDP对象之后自动收集的信息。在创建端点(PeerConnection)的时候,添加回调函数(onIceCandidate

创建回调函数(onIceCandidate)

将Candidate信息发送给另一端(a发给b,b发给a)

保存发过来的 Candidate信息(addIceCandidate)。注意是保存发过来的,不是保存自己的!!!

交换完Candidate信息后,P2P连接就建立好了。

二、新建springboot项目,并开启ssh

因为rtc必须使用ssh,所以springboot需要使用https协议才可以

2.1 生成ssh自签名文件

在终端中执行

keytool -genkey -alias webrtc -dname "CN=Andy,OU=kfit,O=kfit,L=HaiDian,ST=BeiJing,C=CN" -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore webrtc.keystore -validity 36500

执行时,会要求输入密码;
执行后,会在根目录下生成一个webrtc.keystore的文件

 

2.2 配置ssh信息

webrtc.keystore文件放在resource目录下

application.yaml中填写配置信息

server:
  ssl:
    #证书的路径
    key-store: classpath:webrtc.keystore
    #证书密码
    key-store-password: 123456
    #秘钥库类型
    key-store-type: JKS

2.3 检测一下能不能跑起来

运行就行,能跑起来就OK。

三、编写websocket服务类

这个简单的demo只考虑一个房间,没有房间控制,所以websocket代码很少,主要代码都在js里面。

3.1 先放一下Message实体类

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message
{
    private String operation;
    private Object msg;

    public Message setOperation(String operation)
    {
        this.operation = operation;
        return this;
    }

    public Message setMsg(Object msg)
    {
        this.msg = msg;
        return this;
    }

    public String getMsgStr(){
        return msg == null ? "" : msg.toString();
    }
}

3.2 服务类

主要有以下信息:

  • 加入房间(into)
  • 发送 sdp 对象(send-sdp)
  • 交换 candidate 信息(send-candidate)
package com.websocket.controller;

import com.alibaba.fastjson.JSONObject;
import com.websocket.pojo.Message;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@Log4j2
@Controller
@ServerEndpoint("/webrtc")
public class WebrtcController
{
    /**
     * 这里只做一个最简单的, 只有一个房间, 后面有需要自己可以改
     */
    private static Session offer;
    private static Session answer;

    @OnMessage
    public void onMessage(Session session, String message)
    {
        final Message data = JSONObject.parseObject(message, Message.class);
        final Message response = Message.builder()
                .operation(data.getOperation())
                .build();
        switch (data.getOperation()) {
            //加入房间
            case "into": {
                if (offer == null) {
                    offer = session;
                    response.setMsg("offer");
                }
                else if (answer == null) {
                    answer = session;
                    response.setMsg("answer");
                }
                else {
                    response.setMsg("none");
                }
                sendMessage(session, response);
                break;
            }
            case "start":
                sendMessage(offer, response);
                break;
            //发送 offer 的 SDP 对象
            case "send-offer":
                //发送 answer 的 SDP 对象
            case "send-answer":
                //交换 candidate 信息
            case "send-candidate": {
                sendOther(session, response.setMsg(data.getMsg()));
                break;
            }
        }
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason)
    {
        if (offer != null && session.getId().equals(offer.getId())) {
            offer = null;
        }
        else if (answer != null && session.getId().equals(answer.getId())) {
            answer = null;
        }
    }

    public static void sendOther(Session session, Object msg)
    {
        if (offer != null && session.getId().equals(offer.getId())) {
            sendMessage(answer, msg);
        }
        else if (answer != null && session.getId().equals(answer.getId())) {
            sendMessage(offer, msg);
        }
    }

    public static void sendMessage(Session session, Object msg)
    {
        sendMessage(session, JSONObject.toJSONString(msg));
    }


    @SneakyThrows
    private static void sendMessage(Session session, String msg)
    {
        final RemoteEndpoint.Basic basic = session.getBasicRemote();
        basic.sendText(msg);
    }
}


本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

四、页面

4.1 html

这部分不太重要,就直接放上来了

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
  <meta charset="UTF-8">
  <title>websocket-demo</title>
  
  <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.2.1/css/bootstrap.min.css">
</head>
<body>
  <div class="container py-3">
    <div class="row">
      
      <div class="col-12">
        <div id="addRoom" class="btn btn-primary">加入房间</div>
      </div>
      
      <div class="col-12 col-lg-6">
        <p>本地视频:</p>
        <video id="localVideo" width="500px" height="300px" autoplay style="border: 1px solid black;"></video>
      </div>
      
      <div class="col-12 col-lg-6">
        <p>远程视频:</p>
        <video id="remoteVideo" width="500px" height="300px" autoplay style="border: 1px solid black;"></video>
      </div>
    
    </div>
  
  </div>
  
  <script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>

</body>
</html>

4.2 webrtc工具类 webrtc-util.js

包括以下方法:

打开本地音视频流

创建PeerConnection对象

创建用于office的SDP对象

创建用于answer的SDP对象

保存SDP对象

保存Candidate信息

收集 candidate 的回调

获得远程视频流的回调

需要注意的是:最后的两个回调,需要在创建PeerConnection对象之后,打开本地音视频流之前执行。

4.2.1 本地变量

其中的 ice对象,根据上一篇测试通过的stun服务器信息填写。

//端点对象
let rtcPeerConnection;

//本地视频流
let localMediaStream = null;

//ice服务器信息, 用于创建 SDP 对象
let iceServers = {
  "iceServers": [
    // {"url": "stun:stun.l.google.com:19302"},
    {"urls": ["stun:159.75.239.36:3478"]},
    {"urls": ["turn:159.75.239.36:3478"], "username": "chr", "credential": "123456"},
  ]
};

// 本地音视频信息, 用于 打开本地音视频流
const mediaConstraints = {
  video: {width: 500, height: 300},
  audio: true //由于没有麦克风,所有如果请求音频,会报错,不过不会影响视频流播放
};

// 创建 offer 的信息
const offerOptions = {
  iceRestart: true,
  offerToReceiveAudio: true, //由于没有麦克风,所有如果请求音频,会报错,不过不会影响视频流播放
};

4.2.2 打开本地音视频流 

// 1、打开本地音视频流
const openLocalMedia = (callback) => {
  console.log('打开本地视频流');
  navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then(stream => {
      localMediaStream = stream;
      //将 音视频流 添加到 端点 中
      for (const track of localMediaStream.getTracks()) {
        rtcPeerConnection.addTrack(track, localMediaStream);
      }
      callback(localMediaStream);
    })
}

4.2.3 创建 PeerConnection 对象 

// 2、创建 PeerConnection 对象
const createPeerConnection = () => {
  rtcPeerConnection = new RTCPeerConnection(iceServers);
}

4.2.4 创建用于 offer 的 SDP 对象 

// 3、创建用于 offer 的 SDP 对象
const createOffer = (callback) => {
  // 调用PeerConnection的 CreateOffer 方法创建一个用于 offer的SDP对象,SDP对象中保存当前音视频的相关参数。
  rtcPeerConnection.createOffer(offerOptions)
    .then(sdp => {
      // 保存自己的 SDP 对象
      rtcPeerConnection.setLocalDescription(sdp)
        .then(() => callback(sdp));
    })
    .catch(() => console.log('createOffer 失败'));
}

 4.2.5 创建用于 answer 的 SDP 对象

// 4、创建用于 answer 的 SDP 对象
const createAnswer = (callback) => {
  // 调用PeerConnection的 CreateAnswer 方法创建一个 answer的SDP对象
  rtcPeerConnection.createAnswer(offerOptions)
    .then(sdp => {
      // 保存自己的 SDP 对象
      rtcPeerConnection.setLocalDescription(sdp)
        .then(() => callback(sdp));
    })
    .catch(() => console.log('createAnswer 失败'))
}

4.2.6 保存远程的 SDP 对象 

// 5、保存远程的 SDP 对象
const saveSdp = (answerSdp, callback) => {
  rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(answerSdp))
    .then(callback);
}

4.2.7 保存 candidate 信息 

// 6、保存 candidate 信息
const saveIceCandidate = (candidate) => {
  let iceCandidate = new RTCIceCandidate(candidate);
  rtcPeerConnection.addIceCandidate(iceCandidate)
    .then(() => console.log('addIceCandidate 成功'));
}

4.2.8 收集 candidate 的回调

// 7、收集 candidate 的回调
const bindOnIceCandidate = (callback) => {
  // 绑定 收集 candidate 的回调
  rtcPeerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      callback(event.candidate);
    }
  };
};

4.2.9 获得 远程视频流 的回调 

// 8、获得 远程视频流 的回调
const bindOnTrack = (callback) => {
  rtcPeerConnection.ontrack = (event) => callback(event.streams[0]);
};

以上代码都写在 webrtc-util.js 中,是可以复用滴

接下来,就是在html中引入这个js,然后和websocket整合一下,把通知、candidate 信息等等,通过websocket发送给另一端

End

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区讨论!

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/127230338