一、webrtc版本接听视频电话-纯js版
先看效果
用户1--拨打
用户2–接听
前端代码
index.html
<!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"> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css"> <link rel="stylesheet" href="./assets/style.css"> <title>选择角色</title> </head> <body class="index"> <div class="card"> <div class="card-body"> <h5 class="card-title">选择角色</h5> <p class="card-text">就像微信视频一样,总有一方是发起,另一方是接受。</p> <a href="./a.html" class="btn btn-primary">我是发起方</a> <a href="./b.html" class="btn btn-secondary">我是接受方</a> </div> </div> </body> </html>
a.html

<!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"> <link rel="stylesheet" href="./assets/style.css"> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" > <title>发起方</title> </head> <body> <div class="a-wrapp"> <div class="flex-center-wrapp"> <div class="status"> <table class="table"> <thead> <tr> <th scope="col">设备</th> <th scope="col">信令服务器</th> <th scope="col">webrtc状态</th> </tr> </thead> <tbody> <tr class="pc1-info"> <td class="name">本机</td> <td class="websockt">断开</td> <td class="webrtc">断开</td> </tr> <tr class="pc2-info"> <td class="name">远程</td> <td class="websockt">断开</td> <td class="webrtc">断开</td> </tr> </tbody> </table> </div> <div class="videos"> <video class="local-video video" muted autoplay controls></video> <video class="remote-video video" autoplay controls></video> </div> <div class="btns"> <button type="button" class="btn btn-secondary" onclick="start()">开始</button> <button type="button" class="btn btn-success" onclick="call()">呼叫</button> <button type="button" class="btn btn-danger" onclick="hungup()">挂断</button> </div> </div> </div> <script src="./assets/helper.js"></script> <script> // 初始化ws const myWs = initWs('pc1'); // 获取一些dom和定义变量 const pc1Info = document.querySelector('.pc1-info'); let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td'); const pc2Info = document.querySelector('.pc2-info'); let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td'); const remoteVideo = document.querySelector('.remote-video'); const localVideo = document.querySelector('.local-video'); let pc1 = null; let localStram = null; // 开始按钮点击事件 const start = async () => { localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = localStream; } // 呼叫按钮点击事件 const call = async () => { pc1 = new RTCPeerConnection(); // 核心:ice交换(ice即收集可用链路) pc1.onicecandidate = (e) => { e.candidate && myWs.sendIce('pc1',e.candidate); } localStream.getTracks().forEach(track => { pc1.addTrack(track, localStream); }); pc1.ontrack = async (event) => { console.log(898989); remoteVideo.srcObject = event.streams[0]; }; // 核心:sdp交换(spd即会话描述,如编码、stun、本机外网ip等基本信息) const offer = await pc1.createOffer(); pc1.setLocalDescription(offer); myWs.sendOffer(offer); } const hungup = ()=>{ } // ws的onmessage事件 myWs.onmessage = async ({event,data}) => { console.log(event, data); if(event === "onlineChange"){ document.querySelectorAll('.websockt').forEach((item)=>{ item.innerHTML = '断开'; }) data.forEach((item)=>{ eval(`${item}Ws`).innerHTML = '已连接'; }) } if (event === "answer") { await pc1.setRemoteDescription(data); pc1Ws.innerHTML = '收到对方回应anser类型的sdp'; }else if(event === "ice" && data.id ==='pc2'){ pc1.addIceCandidate(data.ice) pc1Ws.innerHTML = '收到对方回应ice'; } else if(event === "hello"){ eval(`${data.id}Ws`).innerHTML = '已连接' } } </script> </body> </html>
b.html

<!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"> <link rel="stylesheet" href="./assets/style.css"> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" > <title>接收方</title> </head> <body> <div class="a-wrapp"> <div class="flex-center-wrapp"> <div class="status"> <table class="table"> <thead> <tr> <th scope="col">设备</th> <th scope="col">信令服务器</th> <th scope="col">webrtc状态</th> </tr> </thead> <tbody> <tr class="pc1-info"> <td class="name">本机</td> <td class="websockt">断开</td> <td class="webrtc">已就绪</td> </tr> <tr class="pc2-info"> <td class="name">远程</td> <td class="websockt">断开</td> <td class="webrtc">已就绪</td> </tr> </tbody> </table> </div> <div class="videos"> <video class="local-video video" muted autoplay controls></video> <video class="remote-video video" autoplay controls></video> </div> <div class="btns"> <div>接收方禁用以下功能,是给发送方用的</div> <button type="button" disabled class="btn btn-secondary" onclick="start()">开始</button> <button type="button" disabled class="btn btn-success" onclick="call()">呼叫</button> <button type="button" disabled class="btn btn-danger" onclick="hungup()">挂断</button> </div> </div> </div> <div class="modal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">呼入</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <p>您有新的来电.</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary">取消</button> <button type="button" class="btn btn-primary">接听</button> </div> </div> </div> </div> <script src="./assets/helper.js"></script> <script> // 初始化ws const myWs = initWs('pc2'); // 获取一些dom和定义变量 const modal = document.querySelector('.modal'); const pc1Info = document.querySelector('.pc1-info'); let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td'); const pc2Info = document.querySelector('.pc2-info'); let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td'); const remoteVideo = document.querySelector('.remote-video'); const localVideo = document.querySelector('.local-video'); let pc2 = new RTCPeerConnection(); // 本地媒体展示和peer加入流监听 navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((localStream)=>{ localVideo.srcObject = localStream; localStream.getTracks().forEach(track => { pc2.addTrack(track, localStream); }); }); // 核心:ice交换(ice即收集可用链路) pc2.onicecandidate = (e) => { if (e.candidate) { myWs.sendIce('pc2', e.candidate) } } pc2.ontrack = async (event) => { remoteVideo.srcObject = event.streams[0]; // remoteVideo.play() }; let offerSdp = null; // ws的onmessage事件 myWs.onmessage = async ({event,data}) => { if(event === "onlineChange"){ document.querySelectorAll('.websockt').forEach((item)=>{ item.innerHTML = '断开'; }) data.forEach((item)=>{ eval(`${item}Ws`).innerHTML = '已连接'; }) } if (event === "offer") { pc1Ws.innerHTML = '收到对方回应的offer类型的sdp'; offerSdp = data; modal.style.display="block"; } else if (event === "ice" && data.id === 'pc1') { pc1Ws.innerHTML = '收到对方回应ice'; pc2.addIceCandidate(data.ice) } else if(event === "hello"){ eval(`${data.id}Ws`).innerHTML = '已连接' } } // 有来电弹窗,点击接听的时候按钮开始交换sdp document.querySelector('.btn-primary').onclick = async ()=>{ await pc2.setRemoteDescription(offerSdp); const answer = await pc2.createAnswer(); pc2.setLocalDescription(answer); myWs.sendAnswer(answer); modal.style.display="none"; } document.querySelector('.btn-secondary').onclick = ()=>{ modal.style.display="none"; } </script> </body> </html>
helper.js

// 判断是不是json字符串 const isJsonStr = (str) => { if (typeof str == 'string') { try { var obj = JSON.parse(str); if (typeof obj == 'object' && obj) { return true; } else { return false; } } catch (e) { console.log('error:' + str + '!!!' + e); return false; } } }; // 判断是不是json const isJson = (data) => { const typeofRes = typeof (data) == "object"; const toStringRes = Object.prototype.toString.call(data).toLowerCase() == "[object object]"; const isLen = !data?.length; return typeofRes && toStringRes && isLen; } const initWs = (id) => { const ws = new WebSocket(`wss://dshvv.com:8888/my_ws/${id}`); // 重写ws,便于传参和接参数--主要是json序列化和反序列化 const myWs = new Proxy(ws, { get(obj, prop) { const value = obj[prop]; if (!typeof value === "function") { return obj[prop]; } //如果不这么做会出现this指向问题:https://juejin.cn/post/6844903730987401230 return (...args) => { //处理ws上传消息的json格式转换成字符串 if (isJson(args[0]) && prop === 'send') { args[0] = JSON.stringify(args[0]); } return value.apply(obj, args) } }, set(obj, prop, value) { if (prop !== 'onmessage') { obj[prop] = value } else { obj[prop] = function (e) { const res = null; if (isJsonStr(e.data)) { value({ ...e, ...JSON.parse(e.data) }) } else { value(e) } } } return true; } }); myWs.sendSdp = function (event, data) { myWs.send({ event, data }) } myWs.sendOffer = function (sdp) { myWs.sendSdp('offer', sdp) } myWs.sendAnswer = function (sdp) { myWs.sendSdp('answer', sdp) } myWs.sendIce = function (id, ice) { myWs.send({ event: 'ice', data:{ ice, id } }) } return myWs; }
style.css

.index{ display: flex; justify-content: center; align-items: center; height: 100vh; } .index>.card{ width:90% ; max-width:600px ; } html,body{ height:100%; width: 100%; padding: 0; margin: 0; width: 100%; } .a-wrapp { display: flex; justify-content: center; align-items: center; height: 100%; } .flex-center-wrapp{ width: 90%; max-width: 400px; } .btns{ margin-top: 10px; text-align: center; } .videos{ border: 1px solid gainsboro; padding: 10px; border-radius: 10px; box-sizing: border-box; display: flex; justify-content: space-between; } .video{ width: 160px; height: 140px; background-color: gainsboro; } .local-video{ width: 80px; height: 70px; }
后端代码
主要是ws服务

package com.dshvv.myblogserver.websocket; import com.alibaba.fastjson.JSONObject; 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.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.concurrent.CopyOnWriteArraySet; /** * 前后端交互的类实现消息的接收推送(自己发送给自己) * 参考:https://www.cnblogs.com/xuwenjin/p/12664650.html * @ServerEndpoint(value = "/my_ws") 前端通过此URI和后端交互,建立连接 */ @Slf4j @ServerEndpoint(value = "/my_ws/{id}") @Component public class MyWebSocket { // 当前组测的用户id private static HashSet onlineIds = new HashSet<>(); //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法 * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(@PathParam("id") String id, Session session){ this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在线数加1 this.onlineChange(id, "onOpen"); System.out.println(session.getId()+"有新连接加入!当前在线人数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(@PathParam("id") String id){ webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 this.onlineChange(id, "onClose"); System.out.println(session.getId()+"有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { System.out.println(session.getId()+"来自客户端的消息:" + message); //群发消息 for(MyWebSocket item: webSocketSet){ try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); continue; } } } /** * 发生错误时调用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error){ System.out.println("发生错误"); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * @param message * @throws IOException */ public void sendMessage(String message) throws IOException{ this.session.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { MyWebSocket.onlineCount++; } public static synchronized void subOnlineCount() { MyWebSocket.onlineCount--; } public void onlineChange(String id, String type) { System.out.println("898989898989"); if(type.equals("onOpen")){ onlineIds.add(id); }else { onlineIds.remove(id); } Map<String, Object> initMsg = new HashMap<>(); initMsg.put("event","onlineChange"); initMsg.put("data",onlineIds); //群发消息 for(MyWebSocket item: webSocketSet){ try { item.sendMessage(JSONObject.toJSONString(initMsg)); } catch (IOException e) { e.printStackTrace(); continue; } } } }
注意里边用到了两个mvn包
<!-- websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- map转json的包 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>
后话
接下来我将用框架编写webrtc-demo,比如vue或react。原生的操作dom有点麻烦,相同代码不能抽离成公共组件复用