git地址
https://gitee.com/chr_demo/web-rtc.git
这里不详细介绍websocket,只针对websocket整合rtc。如果不会websokcet的,可以转到 springboot整合websocket(一)简单聊天室
1、简单说下webrtc的流程
webrtc是P2P通信,也就是实际交流的只有两个人,而要建立通信,这两个人需要交换一些信息来保证通信安全。而且,webrtc必须通过ssh加密,也就是使用https协议、wss协议。
借用一幅图
1.1 创建端点的解析
以下解析不包括websockt,只针对stun做解析。与上图略有不同
-
首先,Client A创建端点(Create PeerConnection),并添加音视频流(Add Streams)。接下来通知Client B,让Client B也创建一个端点。
-
Client B收到通知后,Client B创建端点(Create PeerConnection),并添加音视频流(Add Streams),
-
接下来,Client B创建一个用于answer的SDP对象(Create Answer),保存并发送给Client A。
-
Client A收到用于answer的SDP后,保存下来。
-
然后, Client A创建一个用于offer的SDP对象(Create Office),保存并发送给Client B。
-
最后,Client B保存收到的用于offer的SDP对象
以上步骤完成之后:
1、rtc会自动收集Candidate信息,并通过回调函数通知你,用于交换Candidate信息。
2、交换完Candidate信息后,P2P连接就建立好了。并通过回调函数,将远程视频流给你
1.2 交换Candidate信息
Candidate信息是交换完SDP对象之后,自动收集的信息。在创建端点(PeerConnection)的时候,添加回调函数(onIceCandidate)
创建回调函数(onIceCandidate)
将Candidate信息发送给另一端(a发给b,b发给a)
保存发过来的 Candidate信息(addIceCandidate)。注意是保存发过来的,不是保存自己的!!!
交换完Candidate信息后,P2P连接就建立好了。
2、新建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。
3、编写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); } }
4、页面
4.1 html
这部分不太重要,就直接放上来了
<!DOCTYPE html> <html xmlns:th="Thymeleaf" 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
全球定位:
原文链接:webrtc音视频通话(二)简单音视频通话QQ两个人的博客-CSDN博客webrtc语音通话
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓