后端篇在这里
参考文档:WebRTC API
首先了解一下什么是WebRTC
WebRTC(Web 实时通信)是一种使 Web 应用程序和站点能够捕获和选择性地流式传输音频或视频媒体,以及在浏览器之间交换任意数据的而无需中间件的技术。WebRTC 的一系列标准使得在不需要用户安装插件或任何其他第三方软件的情况下,可以实现点对点数据共享和电话会议。
其WebRTC的用途
WebRTC 有多种用途;与媒体捕捉与媒体流 API 一起使用时,它们为 Web 提供了强大的多媒体功能,包括支持音频和视频会议、文件交换、屏幕共享、身份管理以及与传统电话系统的接口,包括发送 DTMF(按键拨号)信号。两个对等方之间的连接可以在不需要任何特殊驱动程序或插件的情况下建立,并且通常可以在没有任何中间服务器的情况下建立连接。
本文主要通过WebRTC来实现web页面的视频通信
1、首先需要先来做一个前端页面(必须要的,后面会讲为什么)
可以在网上自己找一个前端登录页面的html的Demo,然后把js的登录逻辑改一下即可,这个不是重点。
例如我自己在网上找的这个:可以直接复制使用
有两个页面:登录页面就是login.html,而进行视频通话的是main.html
html代码:(login.html)
注意:在login页面,需要把用户登录后的用户名保存后携带到main.html页面,这个参数需要在main.html页面会用到,很重要!!!
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录页面</title> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script> <style> * { box-sizing: border-box; padding: 0; margin: 0; } html { height: 100%; } body { background-image: linear-gradient(to top, #37ecba 0%, #72afd3 100%); background-repeat: no-repeat; background-position: center center; background-size: cover; height: 100%; } .nav { width: 100%; height: 50px; background-color: rgba(91, 99, 120, 0.8); display: flex; align-items: center; justify-content: space-between; } .flag { display: flex; align-items: center; } .nav img { width: 40px; height: 40px; border-radius: 20px; margin-left: 30px; margin-right: 10px; } .title { /* margin-right: 100px; */ line-height: 50px; color: white; } .nav a { padding: 0 10px; color: white; text-decoration: none; } .page { margin-right: 30px; } .container { width: 1000px; margin: 0 auto; height: calc(100% - 50px); display: flex; justify-content: space-between; } .left { width: 200px; height: 100%; } .card { background-color: rgba(224, 227, 233, 0.8); border-radius: 10px; padding: 30px; } .card img { width: 140px; height: 140px; border-radius: 70px; } .card h3 { text-align: center; padding: 10px 0; } .card a { display: block; text-align: center; color: gray; text-decoration: none; margin-bottom: 10px; } .conter { display: flex; justify-content: space-around; padding: 5px; } .right { width: 795px; height: 100%; background-color: rgba(224, 227, 233, 0.8); border-radius: 10px; padding: 20px; overflow: auto; } .login_container { width: 100%; height: calc(100% - 50px); display: flex; justify-content: center; align-items: center; } .login_dialog { width: 500px; height: 350px; background-color: rgba(222, 220, 220, 0.8); border-radius: 10px; display: flex; justify-content: center; align-items: center; } table { margin-bottom: 50px; } th { font-size: 22px; height: 90px; } td { text-align: center; height: 50px; width: 100px; } #username, #password, #password2 { height: 40px; font-size: 18px; text-indent: 10px; border-radius: 8px; border-color: #848688; } #submit { width: 330px; height: 40px; background-color: orange; color: white; font-size: 18px; border: none; border-radius: 5px; } #submit:hover { background-color: gray; } </style></head><body> <div class="nav"> <div class="flag"> <div class="title">WebRTC</div> </div> <div class="page"> </div> </div> <div class="login_container"> <div class="login_dialog"> <table> <tr> <th colspan="2">登 录</th> </tr> <tr> <td class="t1">用户名</td> <td><input type="text" id="username"></td> </tr> <tr> <td class="t1">密码</td> <td><input type="password" id="password"></td> </tr> <tr> <td colspan="2"><input type="submit" value="提交" id="submit" onclick="login()"></td> </tr> </table> </div> </div> <script> function login() { // 1.参数校验 var username = jQuery("#username"); var password = jQuery("#password"); if (username.val().trim() == "") { username.focus(); alert("请输入用户名!") return false; } if (password.val().trim() == "") { username.focus(); alert("请输入密码!"); return false; } // 2.将参数提交给后端 jQuery.ajax({ //注意你是不是前后端分离启动了,url要写对 url: "/user/login", type: "POST", data: { "username": username.val().trim(), "password": password.val().trim() }, success: function (res) { console.log(res); // 3.将结果返回给用户 if (res.flag) { // 跳转到主页 //注意你是不是前后端分离启动了,url要写对 location.href = "main.html?localUser=" + username.val().trim(); } else { alert("登录失败,用户名或密码错误!"); } } }); } </script></body></html>
1.1我们需要知道当前用户是谁
在main.html页面,一进去我们就检查URL携带的参数(我这里带的参数名为localUser)并且解析出来保存到变量localUser上,这样我们就知道当前用户是谁了:
const queryString = window.location.search; // 使用URLSearchParams API来解析查询字符串 const urlParams = new URLSearchParams(queryString); // 获取名为 "localUser" 的参数值 const localUser = urlParams.get('localUser'); if(localUser==null){ location.href="login.html" }
1.2我们需要知道要给谁发起视频通话
这里我就弄一个很简单粗暴的方法:直接输入对方的用户名,然后通过按钮(call)来获取输入的用户名并且发起视频通话。
callBtn.addEventListener("click", function () { var callToUsername = inputUser.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // 创建一个offer,自己A存一份,发给对方B存一份 yourConn.createOffer().then( async function (offer){ await yourConn.setLocalDescription(offer) await send({ type:"offer", sdp:offer }) console.log("发送offer") }).catch(err=>{ console.log(err) }) } });
只需要看这行即可:其他的等下再看
var callToUsername = inputUser.value;
1.3我们怎么连接到对方?
在这里就需要来介绍一下后端要实现的一个东西----信令服务器以及WebSocket
信令服务器在WebRTC中是一个关键组件,它负责在两个端点(如浏览器或应用程序)之间交换必要的连接信息(媒体协商信息,网络连接信息等等),以建立和维护实时通信会话。
WebSocket是一种双向通信协议使得服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
我们可以通过WebSocket来实现两个端点的链接,从而进行信息交换。比如用户A和用户B打开了一个页要进行实时的信息传递,如聊天室。A用户进入到页面会发送一个websocket协议的请求给服务器请求连接,服务器处理后连接A用户;B用户同样操作,进入页面后也会发送一个websocket协议的请求给同一个服务器,服务器同样处理后连接用户B。这样用户A就可以通过连接的服务器发消息给B用户,B用户也可以通过这个服务器发送消息给用户A。效果可以类比我们经常使用的QQ或者微信。
所以我们可以借助WebSocket技术来连接到对方
在进入main.html页面时,我们就可以发送连接请求去连接后端的服务器处理WebSocket连接的路径(我这里是ws://localhost:8080/video)后面会讲如何建立信令服务器:
/* 进入页面就连接至信令服务器 */ var conn = new WebSocket("ws://localhost:8080/video");
然后接下来可以使用一下WebSocket的API,来监听服务器传给我们的信息:
其中有三个重要的事件:onopen、onmessage、onerror
使用如下:
/* 2、进入页面就连接至信令服务器 */ var conn = new WebSocket("ws://localhost:8080/video"); //监听onopen事件,在连接成功时调用 conn.onopen = function () { console.log("Connected to the signaling server"); }; //监听onmessage事件,服务器发送消息过来会调用这个方法 conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch (data.type) { case "offer": handleOffer(data.sdp, data.from); break; case "answer": handleAnswer(data.sdp); break; case "candidate": handleCandidate(data.sdp); break; case "leave": handleLeave(); break; default: break; } }; //连接发生错误的时候调用 conn.onerror = function (err) { console.log("Got error", err); };
到这里我们我们前端与服务器建立连接的工作就完成了!!!
1.4那我们怎么实现与对方的视频通话?
要实现视频通话,需要来了解一下WebRTC需要怎么样才可以建立
先看一下整一个建立连接的流程图:
(1)先创建PeerConnection对象,然后打开本地音视频设备,将音视频数据封装成MediaStream添加到PeerConnection中,具体实现如下:
var inputUser = document.querySelector(".inputUser"); var callBtn = document.querySelector(".callBtn"); var hangUpBtn = document.querySelector(".hangUpBtn"); var localVideo = document.querySelector(".localVideo"); var remoteVideo = document.querySelector(".remoteVideo"); var yourConn; var stream; //获取PeerConnection var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined); navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); //获取本地媒体 navigator.getUserMedia({ video: true, audio: true }, function (myStream) { stream = myStream; //将获取到的本地媒体流设置到页面展示 localVideo.srcObject = stream; //需要一个给PeerConnection配置一个stun服务器,可以直接用google的,以便获取双方的网络信息 var configuration = { "iceServers": [ { "urls": "stun:stun.l.google.com:19302" } ] }; // 创建一个自己A 的连接 yourConn = new PeerConnection(configuration); // 把自己A的视频流加入到自己A的连接中 yourConn.addStream(stream); //监听对方B的视频流,成功连接对方后将会触发,将其加入到页面展示 yourConn.onaddstream = function (e) { remoteVideo.srcObject = e.stream; }; //为了方便测试,写了一个这个,监听连接信号状态的改变 yourConn.onsignalingstatechange = function () { console.log('信号状态变为:', yourConn.signalingState); } /*当 RTCPeerConnection 通过 RTCPeerConnection.setLocalDescription() 方法更改本地描述之后, 该 RTCPeerConnection 会抛出 icecandidate 事件。 该事件的监听器需要将更改后的描述信息传送给远端 RTCPeerConnection, 以更新远端的备选源*/ yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", sdp: event.candidate, }); } }; }, function (error) { //处理错误 console.log(error); }); //与后端约定一下传送数据的格式 //我这里采用这种 /* { type:"",消息的类型 sdp:"",消息传输的sdp信息 name:"",发给谁 from:"" 谁发的 } */ //因此封装一个send函数: function send(message) { message.name = connectedUser message.from = localUser conn.send(JSON.stringify(message)); }
(2)用户A调用PeerConnection的CreateOffer方法创建一个含offer的SDP对象,SDP对象中保存当前音视频的相关参数。并且用户A需要通过PeerConnection的SetLocalDescription方法将该SDP对象保存起来,并通过信令服务器发送给用户B,实现如下:
//监听一下呼叫按钮,并且实现回调函数 callBtn.addEventListener("click", function () { // 获取一下用户要发给谁 var callToUsername = inputUser.value; //检查合法 if (callToUsername.length > 0) { //合法就设置一下当前用户要连接对象,后面会用到 connectedUser = callToUsername; // 创建一个offer,自己A存一份,发给对方B存一份 yourConn.createOffer().then(async function (offer) { await yourConn.setLocalDescription(offer) await send({ type: "offer", sdp: offer }) console.log("发送offer") }).catch(err => { console.log(err) }) } });
(3)用户B接收到用户A发送过的offer SDP对象,通过PeerConnection的SetRemoteDescription方法将offer SDP对象保存起来,并调用PeerConnection的CreateAnswer方法创建一个answer SDP对象,给用户A一个回复。用户B也需要通过PeerConnection的SetLocalDescription的方法自己保存一份该answer SDP对象并将它通过信令服务器发送给用户A。实现如下:
//当对方B接送到A发来的offer时,调用这个函数 async function handleOffer(offer, from) { //设置当前用户要连接的用户名,即用户B要与谁连接,前面是设置用户A要与谁连接 //这也是为什么我们与后端约定的JSon要有一个from属性 connectedUser = from; //接收A传来的offer,B存一份 await yourConn.setRemoteDescription(new RTCSessionDescription(offer)); console.log("处理offer") //回复一下A,即B发送一个answer给A yourConn.createAnswer().then(async (answer)=>{ await yourConn.setLocalDescription(answer) await send({ type: "answer", sdp: answer }); console.log("发送answer") }).catch(err=>console.log(err)) }
(4)用户A接收到用户B发送过来的answer SDP对象,将其通过PeerConnection的SetRemoteDescription方法保存起来。实现如下:
// 当A接收到B发来的answer时调用这个函数 async function handleAnswer(answer) { console.log(answer) console.log("处理answer") //A本地存一份 console.log(yourConn.iceConnectionState); await yourConn.setRemoteDescription(new RTCSessionDescription(answer)).catch(err=>console.log(err)); console.log(yourConn.iceConnectionState); }
(5)最后,还要有一个就是在完成上面的交换信息过程中,同时也在进行Candidate信息的交换。还记得我们刚在getUserMedia()中实现的一个onicecandidate回调函数吗?
yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", sdp: event.candidate, }); } };
这个是用来收集Candidate信息的,当用户A收集到Candidate信息后,PeerConnection会通过OnIceCandidate接口自动给用户A发送通知,用户A需要将收到的Candidate信息通过信令服务器发送给用户B,用户B通过PeerConnection的AddIceCandidate方法保存起来。同样,用户B也要对对用户A再来一次相同的操作。
再说明一下这里为什么要这样做:是为了更方便的处理信令服务器返回给我们客户端的数据!!!
//监听onmessage事件,服务器发送消息过来会调用这个方法 conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch (data.type) { case "offer": handleOffer(data.sdp, data.from); break; case "answer": handleAnswer(data.sdp); break; case "candidate": handleCandidate(data.sdp); break; case "leave": handleLeave(); break; default: break; } };
1.5我们应该怎么挂断对方呢?
实现如下:给对方发送一个挂断的信号,可以自己自定义!
//给对方发送一个挂断的信号,可以自己自定义! hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); });
再处理一下这个信号:
//接收到挂断的信号调用这个函数来处理 function handleLeave() { //取消当前的连接用户名 connectedUser = null; //关闭对方的视频展示 remoteVideo.src = null; //关闭PeerConnection yourConn.close(); yourConn.onicecandidate = null; //关闭自己的媒体流 yourConn.onaddstream = null; }
完成到这里,基本上就可以实现视频通话了!!!
附上前端完整代码:main.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebRTC</title> <style> *{ margin: 0 auto; padding: 0; } .container{ position: relative; height:550px; width: 400px; background-color: pink; border: 1px solid gray; } .remoteVideo{ width: 100%; height: 400px; background-color: rgb(58, 42, 165); } .localVideo{ position: absolute; height: 150px; width: 150px; margin: 0; top:10px; right: 10px; background-color: wheat; } .btnBox{ display: flex; align-items: center; width: 100%; height: 50px; margin-top: 20px; } .btn{ color: white; width: 100px; height: 50px; text-align: center; align-content: center; border-radius: 10px; } .btn:hover{ cursor:pointer; } .callBtn{ background-color: rgb(28, 68, 167); } .hangUpBtn{ background-color: red; } .inputUser{ font-size: 14px; height: 25px; } </style></head><body><div class="container"> <video class="localVideo" autoplay> </video> <video class="remoteVideo" autoplay></video> <div class="btnBox"> <input class="inputUser" type="text" placeholder="请输入呼叫的用户名"> <div class="callBtn btn">呼叫</div> <div class="hangUpBtn btn">挂断</div> </div></div> </body><script type="text/javascript"> /* 1、先获取login.html传来的参数 */ const queryString = window.location.search; // 使用URLSearchParams API来解析查询字符串 const urlParams = new URLSearchParams(queryString); // 获取名为 "localUser" 的参数值 const localUser = urlParams.get('localUser'); if (localUser == null) { location.href = "login.html" } /* 2、进入页面就连接至信令服务器 */ var conn = new WebSocket("ws://localhost:8080/video"); //监听onopen事件,在连接成功时调用 conn.onopen = function () { console.log("Connected to the signaling server"); }; //监听onmessage事件,服务器发送消息过来会调用这个方法 conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch (data.type) { case "offer": handleOffer(data.sdp, data.from); break; case "answer": handleAnswer(data.sdp); break; case "candidate": handleCandidate(data.sdp); break; case "leave": handleLeave(); break; default: break; } }; //连接发生错误的时候调用 conn.onerror = function (err) { console.log("Got error", err); }; //与后端约定一下传送数据的格式 //我这里采用这种 /* { type:"",消息的类型 sdp:"",消息传输的sdp信息 name:"",发给谁 from:"" 谁发的 } */ //因此封装一个send函数: function send(message) { message.name = connectedUser message.from = localUser conn.send(JSON.stringify(message)); } var inputUser = document.querySelector(".inputUser"); var callBtn = document.querySelector(".callBtn"); var hangUpBtn = document.querySelector(".hangUpBtn"); var localVideo = document.querySelector(".localVideo"); var remoteVideo = document.querySelector(".remoteVideo"); var yourConn; var stream; //获取PeerConnection var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined); navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); //获取本地媒体 navigator.getUserMedia({ video: true, audio: true }, function (myStream) { stream = myStream; //将获取到的本地媒体流设置到页面展示 localVideo.srcObject = stream; //需要一个给PeerConnection配置一个stun服务器,可以直接用google的,以便获取双方的网络信息 var configuration = { "iceServers": [ { "urls": "stun:stun.l.google.com:19302" } ] }; // 创建一个自己A 的连接 yourConn = new PeerConnection(configuration); // 把自己A的视频流加入到自己A的连接中 yourConn.addStream(stream); //监听对方B的视频流,成功连接对方后将会触发,将其加入到页面展示 yourConn.onaddstream = function (e) { remoteVideo.srcObject = e.stream; }; //为了方便测试,写了一个这个,监听连接信号状态的改变 yourConn.onsignalingstatechange = function () { console.log('信号状态变为:', yourConn.signalingState); } /*当 RTCPeerConnection 通过 RTCPeerConnection.setLocalDescription() 方法更改本地描述之后, 该 RTCPeerConnection 会抛出 icecandidate 事件。 该事件的监听器需要将更改后的描述信息传送给远端 RTCPeerConnection, 以更新远端的备选源*/ yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", sdp: event.candidate, }); } }; }, function (error) { //处理错误 console.log(error); }); //监听一下呼叫按钮,并且实现回调函数 callBtn.addEventListener("click", function () { // 获取一下用户要发给谁 var callToUsername = inputUser.value; //检查合法 if (callToUsername.length > 0) { //合法就设置一下当前用户要连接对象,后面会用到 connectedUser = callToUsername; // 创建一个offer,自己A存一份,发给对方B存一份 yourConn.createOffer().then(async function (offer) { await yourConn.setLocalDescription(offer) await send({ type: "offer", sdp: offer }) console.log("发送offer") }).catch(err => { console.log(err) }) } }); //当对方B接送到A发来的offer时,调用这个函数 async function handleOffer(offer, from) { //设置当前用户要连接的用户名,即用户B要与谁连接,前面是设置用户A要与谁连接 connectedUser = from; //接收A传来的offer,B存一份 await yourConn.setRemoteDescription(new RTCSessionDescription(offer)); console.log("处理offer") //回复一下A,即B发送一个answer给A yourConn.createAnswer().then(async (answer) => { await yourConn.setLocalDescription(answer) await send({ type: "answer", sdp: answer }); console.log("发送answer") }).catch(err => console.log(err)) } // 当A接收到B发来的answer时调用这个函数 async function handleAnswer(answer) { console.log(answer) console.log("处理answer") //A本地存一份 console.log(yourConn.iceConnectionState); await yourConn.setRemoteDescription(new RTCSessionDescription(answer)).catch(err => console.log(err)); console.log(yourConn.iceConnectionState); } //接收到A发来的candidate,调用这个函数 async function handleCandidate(candidate) { await yourConn.addIceCandidate(new RTCIceCandidate(candidate)); } //给对方发送一个挂断的信号,可以自己自定义! hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); //接收到挂断的信号调用这个函数来处理 function handleLeave() { //取消当前的连接用户名 connectedUser = null; //关闭对方的视频展示 remoteVideo.src = null; //关闭PeerConnection yourConn.close(); yourConn.onicecandidate = null; //关闭自己的媒体流 yourConn.onaddstream = null; }</script></html>
当然,这个Demo不完美:
1、一进页面就获取自己的媒体流;
2、需要输入对方的用户名;
3、视频挂断后再次请求视频通话会报错;
4、......
只是这里重点在于WebRTC的使用,所以这些细节就留个你们自己优化咯!!!