webrtc 搭建直播平台

不念不忘少年蓝@ 2022-04-24 15:52 388阅读 0赞

设计思路

需求: 一个直播页面,可以输入直播名。一个观看页面输入客户名个要看的直播名建立直播视频传输

思路:

  1. 直播页面输入直播名建立websocket连接,创建PeerConnection对象组存放连接本直播端的PeerConnection对象。
  2. 观看页面输入客户名与直播名建立websocket连接,通知直播端发送__offer给观看端
  3. 观看接收到__offer指令,将__offer中携带的ice会话描述信息加入PeerConnection中并发起一个_answer和n个__ice_candidate指令给直播端
  4. 直播端收到__answer 和 __ice_candidate指定 追加至 对应的PeerConnection对象中

引入的maven依赖

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>1.5.10.RELEASE</version>
  5. </parent>
  6. <dependencies>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. </dependency>
  11. <!--测试类包-->
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter-test</artifactId>
  15. <scope>test</scope>
  16. </dependency>
  17. <!-- freemarker模板引擎-->
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-freemarker</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>com.alibaba</groupId>
  24. <artifactId>fastjson</artifactId>
  25. <version>1.2.31</version>
  26. </dependency>
  27. <!--websocket-->
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-websocket</artifactId>
  31. </dependency>
  32. </dependencies>

直播页面

  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>播放页</title>
  6. </head>
  7. <body>
  8. <video id="video" autoplay></video>
  9. <span>主播名</span>
  10. <input id="zhubo" type="text"/>
  11. <button onclick="connect()">建立视频连接</button>
  12. </body>
  13. <script type="text/javascript"> /** * socket.send 数据描述 * event: 指令类型 * data: 数据 * name: 发送人 * receiver: 接收人 * * */ //使用Google的stun服务器 const iceServer = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }, { "url": "turn:numb.viagenie.ca", "username": "webrtc@live.com", "credential": "muazkh" }] }; //兼容浏览器的getUserMedia写法 const getUserMedia = (navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia); //兼容浏览器的PeerConnection写法 const PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.RTCPeerConnection || window.mozRTCPeerConnection); /** * 信令websocket * @type {WebSocket} */ var socket; /** * 视频信息 * */ var stream_two; /** * 播放视频video组件 * */ const video = document.getElementById('video'); /** * 连接的浏览器PeerConnection对象组 * { * 'id':PeerConnection * } * @type { {}} */ var pc = { }; // 建立scoket连接 function connect() { // 获取主播名称 const zhubo = document.getElementById('zhubo').value; /** * 信令websocket * @type {WebSocket} */ socket = new WebSocket("ws://192.168.31.13:6533/websocket?name=" + zhubo); //获取本地的媒体流,并绑定到一个video标签上输出,并且发送这个媒体流给其他客户端 getUserMedia.call(navigator, { "audio": true, "video": true }, function (stream) { //发送offer和answer的函数,发送本地session描述 stream_two = stream; video.srcObject = stream //向PeerConnection中加入需要发送的流 //如果是发送方则发送一个offer信令,否则发送一个answer信令 }, function (error) { //处理媒体流创建失败错误 alert("处理媒体流创建失败错误"); }); socket.close = function () { console.log("连接关闭") } //有浏览器建立视频连接 socket.onmessage = function (event) { var json = JSON.parse(event.data); if (json.name && json.name != null && !json.event) { pc[json.name] = new PeerConnection(iceServer); pc[json.name].addStream(stream_two); // 浏览器兼容 var agent = navigator.userAgent.toLowerCase(); if (agent.indexOf("firefox") > 0) { pc[json.name].createOffer().then(function (desc) { pc[json.name].setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__offer", "data": { "sdp": desc }, name: zhubo, receiver: json.name })); }); } else if (agent.indexOf("chrome") > 0) { pc[json.name].createOffer(function (desc) { pc[json.name].setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__offer", "data": { "sdp": desc }, name: zhubo, receiver: json.name })); }); } else { pc[json.name].createOffer(function (desc) { pc[json.name].setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__offer", "data": { "sdp": desc }, name: zhubo, receiver: json.name })); }); } } else { if (json.event === "__ice_candidate") { //如果是一个ICE的候选,则将其加入到PeerConnection中 pc[json.name].addIceCandidate(new RTCIceCandidate(json.data.candidate)); } else if (json.event === "__answer") { // 将远程session描述添加到PeerConnection中 pc[json.name].setRemoteDescription(new RTCSessionDescription(json.data.sdp)); } } }; } </script>
  14. </html>

观看页面

  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>观看页</title>
  6. </head>
  7. <body>
  8. <div id="eee">
  9. <video id="video" autoplay></video>
  10. </div>
  11. <span>用户名</span><input id="userName"/>
  12. <span>主播名</span><input id="receiver"/>
  13. <button onclick="communication()">建立视频通信</button>
  14. </body>
  15. <script type="text/javascript"> /** * socket.send 数据描述 * event: 指令类型 * data: 数据 * name: 发送人 * receiver: 接收人 * * */ //使用Google的stun服务器 const iceServer = { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }, { "url": "turn:numb.viagenie.ca", "username": "webrtc@live.com", "credential": "muazkh" }] }; //兼容浏览器的getUserMedia写法 const getUserMedia = (navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia); //兼容浏览器的PeerConnection写法 const PeerConnection = (window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection); /** * 信令websocket * @type {WebSocket} */ var socket; function communication() { //创建PeerConnection实例 var pc = new PeerConnection(iceServer); const video = document.getElementById('video'); //如果检测到媒体流连接到本地,将其绑定到一个video标签上输出 pc.ontrack = function async(event) { video.srcObject = event.streams[0] }; const userName = document.getElementById('userName').value; const receiver = document.getElementById('receiver').value; /** * 信令websocket * @type {WebSocket} */ socket = new WebSocket("ws://192.168.31.13:6533/websocket?name=" + userName + "&receiver=" + receiver); socket.close = function () { console.log("连接关闭") } //处理到来的信令 socket.onmessage = function (event) { var json = JSON.parse(event.data); //如果是一个ICE的候选,则将其加入到PeerConnection中 if (json.event === "__offer") { pc.setRemoteDescription(new RTCSessionDescription(json.data.sdp)); pc.onicecandidate = function (event) { if (event.candidate !== null && event.candidate !== undefined && event.candidate !== '') { socket.send(JSON.stringify({ "event": "__ice_candidate", "data": { "candidate": event.candidate }, name: userName, receiver: receiver, })); } }; var agent = navigator.userAgent.toLowerCase(); if (agent.indexOf("firefox") > 0) { pc.createAnswer().then(function (desc) { pc.setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__answer", "data": { "sdp": desc }, name: userName, receiver: receiver, })); }); } else { pc.createAnswer(function (desc) { pc.setLocalDescription(desc); socket.send(JSON.stringify({ "event": "__answer", "data": { "sdp": desc }, name: userName, receiver: receiver, })); }, function (eorr) { alert(eorr); }); } } }; } </script>
  16. </html>

WebSocket处理类

  1. package com.webrtc.config;
  2. import com.alibaba.fastjson.JSONObject;
  3. import org.springframework.stereotype.Component;
  4. import javax.websocket.*;
  5. import javax.websocket.server.ServerEndpoint;
  6. import java.io.IOException;
  7. import java.util.HashMap;
  8. import java.util.List;
  9. import java.util.Map;
  10. import java.util.concurrent.CopyOnWriteArraySet;
  11. /** * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 */
  12. @ServerEndpoint(value = "/websocket")
  13. @Component
  14. public class WebSocket {
  15. //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
  16. private static int onlineCount = 0;
  17. //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
  18. private static CopyOnWriteArraySet<Map<String,WebSocket>> webSocketSet = new CopyOnWriteArraySet<Map<String,WebSocket>>();
  19. //与某个客户端的连接会话,需要通过它来给客户端发送数据
  20. private Session session;
  21. /** * 连接建立成功调用的方法 * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 * @throws EncodeException * @throws IOException */
  22. @OnOpen
  23. public void onOpen(Session session) throws EncodeException, IOException{
  24. this.session = session;
  25. Map<String,WebSocket> map = new HashMap<String,WebSocket>();
  26. String name = "";
  27. Map<String, List<String>> listMap = session.getRequestParameterMap();
  28. // 非主播建立连接
  29. if (listMap.get("name") != null && listMap.get("receiver") != null) {
  30. name = listMap.get("name").get(0);
  31. String receiver = listMap.get("receiver").get(0);
  32. map.put(name,this);
  33. // 通知主播发送__offer指令
  34. this.onMessage("{\"name\": \"" + name + "\",\"receiver\": \"" + receiver + "\"}", session);
  35. } else {
  36. // 主播建立连接
  37. name = listMap.get("name").get(0);
  38. map.put(name,this);
  39. }
  40. addSocket(map, name);
  41. }
  42. // 添加map 到 webSocketSet,
  43. public void addSocket(Map<String,WebSocket> map, String name) {
  44. // 删除重复的连接
  45. for(Map<String,WebSocket> item: webSocketSet){
  46. for(String key : item.keySet()){
  47. if (key.toString().equals(name)) {
  48. webSocketSet.remove(item);
  49. subOnlineCount(); //在线数减1
  50. System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
  51. }
  52. }
  53. }
  54. webSocketSet.add(map); //加入set中
  55. addOnlineCount(); //在线数加1
  56. System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
  57. }
  58. /** * 连接关闭调用的方法 */
  59. @OnClose
  60. public void onClose(){
  61. for (Map<String,WebSocket> item : webSocketSet) {
  62. for(String key : item.keySet()){
  63. if(item.get(key) == this){
  64. // 删除关闭的连接
  65. webSocketSet.remove(item);
  66. subOnlineCount(); //在线数减1
  67. System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
  68. }
  69. }
  70. }
  71. }
  72. /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息 * @param session 可选的参数 * @throws EncodeException */
  73. @OnMessage
  74. public void onMessage(String message, Session session) throws EncodeException {
  75. System.out.println("来自客户端的消息:" + message);
  76. Map<String,Object> map = (Map<String, Object>) JSONObject.parse(message);
  77. // 接收人
  78. String receiver = (String) map.get("receiver");
  79. for(Map<String,WebSocket> item: webSocketSet){
  80. for(String key : item.keySet()){
  81. if (key.toString().equals(receiver.toString())) {
  82. WebSocket webSocket = item.get(key);
  83. try {
  84. webSocket.sendMessage(message);
  85. } catch (IOException e) {
  86. e.printStackTrace();
  87. continue;
  88. }
  89. }
  90. }
  91. }
  92. }
  93. /** * 发生错误时调用 * @param session * @param error */
  94. @OnError
  95. public void onError(Session session, Throwable error){
  96. System.out.println("发生错误");
  97. error.printStackTrace();
  98. }
  99. /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * @param message * @throws IOException */
  100. public void sendMessage(String message) throws IOException{
  101. synchronized (this.session) {
  102. this.session.getBasicRemote().sendText(message);
  103. }
  104. }
  105. public static synchronized int getOnlineCount() {
  106. return onlineCount;
  107. }
  108. public static synchronized void addOnlineCount() {
  109. WebSocket.onlineCount++;
  110. }
  111. public static synchronized void subOnlineCount() {
  112. WebSocket.onlineCount--;
  113. }
  114. }

源码地址:https://github.com/2425358736/webrtc\_live.git

截图

直播页
image
观看页
[外链图片转存失败(img-4CC7hmLt-1565920868044)(https://raw.githubusercontent.com/2425358736/webrtc\_live/master/guankan.png)\]

发表评论

表情:
评论列表 (有 0 条评论,388人围观)

还没有评论,来说两句吧...

相关阅读

    相关 直播平台

    前言:       相信很多小伙伴在日常开发中,都有遇到开发直播的需求,是不是感觉无从下手,如果你刚好看到这篇博客,那么你真的来对地方,本篇文章将详细的讲解,如果手把手的

    相关 WebRtc环境

    0.前言 这次的需求,准备做的是一个类似与QQ视频一样的点对点视频聊天。这几天了解了一些知识后,决定使用HTML5新支持的WebRtc来作为视频通讯。客户端使用