设计思路
需求: 一个直播页面,可以输入直播名。一个观看页面输入客户名个要看的直播名建立直播视频传输
思路:
- 直播页面输入直播名建立websocket连接,创建PeerConnection对象组存放连接本直播端的PeerConnection对象。
- 观看页面输入客户名与直播名建立websocket连接,通知直播端发送__offer给观看端
- 观看接收到__offer指令,将__offer中携带的ice会话描述信息加入PeerConnection中并发起一个_answer和n个__ice_candidate指令给直播端
- 直播端收到__answer 和 __ice_candidate指定 追加至 对应的PeerConnection对象中
引入的maven依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.10.RELEASE</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--测试类包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- freemarker模板引擎--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.31</version></dependency><!--websocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency></dependencies>
直播页面
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>播放页</title>
</head>
<body>
<video id="video" autoplay></video><span>主播名</span>
<input id="zhubo" type="text"/>
<button onclick="connect()">建立视频连接</button></body><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>
</html>
观看页面
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>观看页</title>
</head>
<body>
<div id="eee"><video id="video" autoplay></video>
</div>
<span>用户名</span><input id="userName"/>
<span>主播名</span><input id="receiver"/>
<button onclick="communication()">建立视频通信</button></body>
<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></html>
WebSocket处理类
package com.webrtc.config;import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;/*** @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端*/
@ServerEndpoint(value = "/websocket")
@Component
public class WebSocket {//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。private static int onlineCount = 0;//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识private static CopyOnWriteArraySet<Map<String,WebSocket>> webSocketSet = new CopyOnWriteArraySet<Map<String,WebSocket>>();//与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;/*** 连接建立成功调用的方法* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据* @throws EncodeException* @throws IOException */@OnOpenpublic void onOpen(Session session) throws EncodeException, IOException{this.session = session;Map<String,WebSocket> map = new HashMap<String,WebSocket>();String name = "";Map<String, List<String>> listMap = session.getRequestParameterMap();// 非主播建立连接if (listMap.get("name") != null && listMap.get("receiver") != null) {name = listMap.get("name").get(0);String receiver = listMap.get("receiver").get(0);map.put(name,this);// 通知主播发送__offer指令this.onMessage("{\"name\": \"" + name + "\",\"receiver\": \"" + receiver + "\"}", session);} else {// 主播建立连接name = listMap.get("name").get(0);map.put(name,this);}addSocket(map, name);}// 添加map 到 webSocketSet,public void addSocket(Map<String,WebSocket> map, String name) {// 删除重复的连接for(Map<String,WebSocket> item: webSocketSet){for(String key : item.keySet()){if (key.toString().equals(name)) {webSocketSet.remove(item);subOnlineCount(); //在线数减1System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());}}}webSocketSet.add(map); //加入set中addOnlineCount(); //在线数加1System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(){for (Map<String,WebSocket> item : webSocketSet) {for(String key : item.keySet()){if(item.get(key) == this){// 删除关闭的连接webSocketSet.remove(item);subOnlineCount(); //在线数减1System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());}}}}/*** 收到客户端消息后调用的方法* @param message 客户端发送过来的消息* @param session 可选的参数* @throws EncodeException*/@OnMessagepublic void onMessage(String message, Session session) throws EncodeException {System.out.println("来自客户端的消息:" + message);Map<String,Object> map = (Map<String, Object>) JSONObject.parse(message);// 接收人String receiver = (String) map.get("receiver");for(Map<String,WebSocket> item: webSocketSet){for(String key : item.keySet()){if (key.toString().equals(receiver.toString())) {WebSocket webSocket = item.get(key);try {webSocket.sendMessage(message);} catch (IOException e) {e.printStackTrace();continue;}}}}}/*** 发生错误时调用* @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error){System.out.println("发生错误");error.printStackTrace();}/*** 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。* @param message* @throws IOException*/public void sendMessage(String message) throws IOException{synchronized (this.session) {this.session.getBasicRemote().sendText(message);}}public static synchronized int getOnlineCount() {return onlineCount;}public static synchronized void addOnlineCount() {WebSocket.onlineCount++;}public static synchronized void subOnlineCount() {WebSocket.onlineCount--;}
}
源码地址:https://github.com/2425358736/webrtc_live.git
截图
直播页
观看页
[外链图片转存失败(img-4CC7hmLt-1565920868044)(https://raw.githubusercontent.com/2425358736/webrtc_live/master/guankan.png)]