后端篇在这里
参考文档: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的使用,所以这些细节就留个你们自己优化咯!!!
反面会出一篇后端信令服务器的简单实现,感爱好的可以关注一下哦!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |