文章目录

  • 前言
  • P2P 通话实现
    • 媒体设备
    • 通讯连接
  • 在线监考
  • 后记

前言

在上一篇博文 【复】基于 WebRTC 的音视频在线监考模块的设计与实现(上) 中,主要介绍了关于 WebRTC 的基本理论,那么这篇文章我们将进入实战阶段,通过 WebRTC 框架,去实现 P2P 通话,以及延伸到一对多的音视频通话,从而实现在线监考功能;

P2P 通话实现

媒体设备

在开发 Web 时,WebRTC 标准提供了 API,用于访问连接到计算机或智能手机的相机和麦克风,这些设备通常称为媒体设备,可以通过实现 MediaDevices 接口的 navigator.mediaDevices 对象使用 JavaScript 进行访问。通过该对象,我们可以枚举所有已连接的设备,侦听设备更改(连接或断开设备时),并打开设备以检索媒体流。

调用 getUserMedia() 将触发权限请求。如果用户接受许可,则通过包含一个视频和一个音轨的 MediaStream 来解决承诺。如果权限被拒绝,则抛出 PermissionDeniedError。如果没有连接匹配的设备,则会抛出 NotFoundError

  • 创建媒体流
 async createMedia() {let streamTep = null;// 保存本地流到全局streamTep = await navigator.mediaDevices.getUserMedia({audio: true, video: true})console.log("streamTep",streamTep)return streamTep;},
  • 播放媒体流
<div style="float: left"><video id="sucA" autoplay></video>
</div>
// 打开本地摄像头
async nativeMedia(){const that = this;that.localStream = await this.createMedia()let video = document.querySelector('#sucA');// 旧的浏览器可能没有srcObjectif ("srcObject" in video) {video.srcObject = that.localStream;} else {video.src = window.URL.createObjectURL(that.localStream);}that.initPeer(); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
},
  • 媒体设备约束条件
// 设置视频窗口的范围
{"video": {"width": {"min": 640,"max": 1024},"height": {"min": 480,"max": 768}}
}
// 获取手机端前置摄像头
{ audio: true, video: { facingMode: "user" } }// 后置摄像头
{ audio: true, video: { facingMode: { exact: "environment" } } }// 具有带宽限制的WebRTC传输,可能需要较低的帧速率
{ video: { frameRate: { ideal: 10, max: 15 } } }

通讯连接

RTCPeerConnection 接口表示本地计算机和远程对等方之间的 WebRTC 连接。它提供了连接到远程对等方,维护和监视连接以及在不再需要连接时关闭连接的方法。

RTCPeerConnection 建立

  • 本地流获取(上述内容)
  • 全局参数初始化
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;var iceServers = {iceServers: [{ url: "stun:stun.l.google.com:19302"},// 谷歌的公共服务{url: 'turn:numb.viagenie.ca',credential: 'muazkh',username: 'webrtc@live.com'}]
};
  • 初始化两个模拟客户端
// 以下 pc1 和 pc2 分别代表两个模拟客户端的链接服务简写
// pc1: 代表 pc1->pc2 链接  pc2: 代表 pc2->pc1 链接const that = this;
that.pc1 = new PeerConnection(iceServers);
that.pc2 = new PeerConnection(iceServers);// 将全局视频流赋给 pc1 链接服务
that.pc1.addStream(this.localStream);// 监听 ice 候选信息
that.pc1.onicecandidate = function(event) {console.log("pc1 onicecandidate", event)if (event.candidate) {// 一般来说这个地方是通过第三方 (socket 后面会将网络端点对点) 发送给另一个客户端,但是现在本地演示直接将候选信息发送到 pc2 链接服务that.pc2.addIceCandidate(event.candidate.toJSON());}
};// 监听远程视频 pc1 充当呼叫端,所以只要监听 pc2 有无视频流信息进来
that.pc2.onaddstream = (event) => {console.log("pc2 onaddstream",event)// 监听到流后将视频流赋给另一个 video 标签let video = document.querySelector('#sucB');video.srcObject = event.stream;video.onloadedmetadata = function(e) {console.log(e)video.play();};
};

onicecandidate: 候选 ICE 描述了 WebRTC 能够与远程设备进行通信所需的协议和路由。在启动 WebRTC 对等连接时,通常在连接的每一端都建议多个候选对象,直到他们相互同意描述他们认为最好的连接的候选对象为止。

  • 呼叫端模拟呼叫(pc1)和应答端模拟应答(pc2)
 async createOffer() {const that = this;// 创建 offerlet offer_tep = await that.pc1.createOffer(this.offerOption);console.log("offer_tep", offer_tep)// 设置本地描述await that.pc1.setLocalDescription(offer_tep)//接收端设置远程 offer 描述await that.pc2.setRemoteDescription(offer_tep)// 接收端创建 answerlet answer = await that.pc2.createAnswer();// 接收端设置本地 answer 描述await that.pc2.setLocalDescription(answer);// 发送端设置远程 answer 描述await that.pc1.setRemoteDescription(answer);
},// 呼叫
async call() {const that = this;//创建 offer 并保存本地描述await that.createOffer()
},

为何呼叫会有这么麻烦的步骤呢?这就又涉及到 WebRTC 的会话了,具体看下面一条:

“当用户 (上述pc1) 向另一个用户(上述pc2)发起 WebRTC 呼叫时,会创建一个特殊的描述,称为 offer。此描述包括有关呼叫者为呼叫建议的配置的所有信息。然后,接收者用一个答案来回应,这是他们通话结束的描述。以此方式,两个设备彼此共享为了交换媒体数据所需的信息。这种交换是使用交互式连接建立(ICE)处理的,该协议允许两个设备使用中介程序交换要约和答复,即使两个设备之间都被网络地址转换(NAT)隔开。然后,每个对等方都保留两个描述:本地描述(描述自己)和远程描述(描述呼叫的另一端)”

上面的话简单来说就是 A 呼叫 B,A 创建 offer,在本地保留 offer,然后发送给 B,B 创建 answer,之后本地保留 answer,再将 answer 发送给 A,A 拿到后将 B 的 answer 设置为本地的远程描述。

在线监考

通过刚才的 P2P 学习,想必已经了解了双方之间是如何建立通讯的,那么基于 WebRTC 的在线监考原理也是如此,老师与同学们建立通讯即可,即一对多的关系,这样就能实现在线监考了;

这里使用的是 vue + node 的实现形式,可以根据自己的需要进行改进;

<script>
import * as config from '../../configure';navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
window.RTCSessionDescription =window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;const socket = io.connect(config.API_ROOT);
const configuration = {iceServers: [config.DEFAULT_ICE_SERVER],
};let localStream, peerConn;
let connectedUser = null;export default {data() {return {user_name: '',show: true,users: '',call_username: '',remote_video: '',accept_video: false,peersList: [],};},mounted() {socket.on('message',function(data) {console.log(data);switch (data.event) {case 'show':this.users = data.allUsers;break;case 'join':this.handleLogin(data);break;case 'call':this.handleCall(data);break;case 'accept':this.handleAccept(data);break;case 'offer':this.handleOffer(data);break;case 'candidate':this.handleCandidate(data);break;case 'msg':this.handleMsg(data);break;case 'answer':this.handleAnswer(data);break;case 'leave':this.handleLeave();break;default:break;}}.bind(this));},methods: {submit() {if (this.user_name !== '') {this.send({event: 'join',name: this.user_name,});}},send(message) {if (connectedUser !== null) {message.connectedUser = connectedUser;}socket.send(JSON.stringify(message));},handleLogin(data) {if (data.success === false) {alert('Ooops...please try a different username');} else {this.show = false;this.users = data.allUsers;this.initCreate();}},addVideoURL(elementId, stream) {var video = document.getElementById(elementId);// Older brower may have no srcObjectif ('srcObject' in video) {video.srcObject = stream;} else {// 防止在新的浏览器里使用它,应为它已经不再支持了video.src = window.URL.createObjectURL(stream);}},initCreate() {const self = this;navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function(stream) {var video = document.getElementById('localVideo');self.addVideoURL('localVideo', stream);video.muted = true;localStream = stream;}).catch(function(err) {console.log(err.name + ': ' + err.message);});},call() {if (this.call_username.length > 0) {if (this.users[this.call_username] === true) {connectedUser = this.call_username;this.createConnection(connectedUser);this.send({event: 'call',});} else {alert('The current user is calling, try another');}} else {alert('Ooops...this username cannot be empty, please try again');}},createConnection(username) {peerConn = new RTCPeerConnection(configuration);peerConn.addStream(localStream);peerConn.onaddstream = e => {this.addVideoURL('remoteVideo'+username, e.stream);};peerConn.onicecandidate = event => {setTimeout(() => {if (event.candidate) {this.send({event: 'candidate',candidate: event.candidate,});}});};},handleCall(data) {this.accept_video = true;connectedUser = data.name;},reject() {this.send({event: 'accept',accept: false,});this.accept_video = false;},accept() {this.send({event: 'accept',accept: true,});this.accept_video = false;},handleAccept(data) {if (data.accept) {// Create an offerpeerConn.createOffer(offer => {this.send({event: 'offer',offer: offer,});peerConn.setLocalDescription(offer);},error => {alert('Error when creating an offer');});} else {alert('对方已拒绝');}},handleOffer(data) {connectedUser = data.name;this.createConnection(connectedUser);peerConn.setRemoteDescription(new RTCSessionDescription(data.offer));// Create an answer to an offerpeerConn.createAnswer(answer => {peerConn.setLocalDescription(answer);this.send({event: 'answer',answer: answer,});},error => {alert('Error when creating an answer');});this.peersList.push(connectedUser)},handleMsg(data) {console.log(data.message);},handleAnswer(data) {peerConn.setRemoteDescription(new RTCSessionDescription(data.answer));},handleCandidate(data) {// ClientB 通过 PeerConnection 的 AddIceCandidate 方法保存起来peerConn.addIceCandidate(new RTCIceCandidate(data.candidate));},hangUp() {this.send({event: 'leave',});this.handleLeave();},handleLeave() {alert('通话已结束');connectedUser = null;this.remote_video = '';peerConn.close();peerConn.onicecandidate = null;peerConn.onaddstream = null;if (peerConn.signalingState === 'closed') {this.initCreate();}},closePreview() {this.accept_video = false;},},
};
</script>
var express = require('express');
var app = express();
var http = require('http');
var fs = require('fs');
var IO = require('socket.io');
var { APT_HOST, API_PORT } = require('./configure');app.use(express.static('dist'));var server = http.createServer(app);
console.log('The HTTPS server is up and running');var io = IO(server);
console.log('Socket Secure server is up and running.');server.listen(API_PORT, APT_HOST, function(){console.log('Access Address: http://%s:%s', APT_HOST, API_PORT);
});// All joined users
var allUsers = {};
// All joined sockets
var allSockets = {};io.on('connect', function(socket) {var user = ''; // current joined usersocket.on('message', function(data) {var data = JSON.parse(data);switch (data.event) {// When has new user join incase 'join':user = data.name;console.log('User joined', data.name);// Save users infoallUsers[user] = true; // 'true' means has not call, 'false' means callingallSockets[user] = socket;socket.name = user;showUserInfo(allUsers);sendTo(socket, {event: 'join',allUsers: allUsers,success: true,});break;case 'call':var conn = allSockets[data.connectedUser];sendTo(conn, {event: 'call',name: socket.name,});break;case 'offer':// i.e. UserA wants to call UserBconsole.log('Sending offer to: ', data.connectedUser);//if UserB exists then send him offer detailsvar conn = allSockets[data.connectedUser];// allUsers[user] = false;allUsers[user] = true;if (conn != null) {showUserInfo(allUsers);// Setting that UserA connected with UserBsocket.otherName = data.connectedUser;sendTo(conn, {event: 'offer',offer: data.offer,name: socket.name,});} else {sendTo(socket, {event: 'msg',message: 'Not found this name',});}break;case 'accept':var conn = allSockets[data.connectedUser];if (conn != null) {if (data.accept) {sendTo(conn, {event: 'accept',accept: true,});} else {allUsers[data.connectedUser] = true;sendTo(conn, {event: 'accept',accept: false,});}}break;case 'answer':console.log('Sending answer to: ', data.connectedUser);// i.e. UserB answers UserAvar conn = allSockets[data.connectedUser];// allUsers[user] = false;allUsers[user] = true;if (conn != null) {showUserInfo(allUsers);socket.otherName = data.connectedUser;sendTo(conn, {event: 'answer',answer: data.answer,});}break;case 'candidate':console.log('Sending candidate to:', data.connectedUser);var conn1 = allSockets[data.connectedUser];var conn2 = allSockets[socket.otherName];if (conn1 != null) {sendTo(conn1, {event: 'candidate',candidate: data.candidate,});} else {sendTo(conn2, {event: 'candidate',candidate: data.candidate,});}break;case 'leave':console.log('Disconnecting from', data.connectedUser);var conn = allSockets[data.connectedUser];allUsers[socket.name] = true;allUsers[data.connectedUser] = true;socket.otherName = null;// Notify the other user so he can disconnect his peer connectionif (conn != null) {showUserInfo(allUsers);sendTo(conn, {event: 'leave',});}break;}});socket.on('disconnect', function() {if (socket.name) {delete allUsers[socket.name];delete allSockets[socket.name];showUserInfo(allUsers);if (socket.otherName) {console.log('Disconnecting from ', socket.otherName);var conn = allSockets[socket.otherName];allUsers[socket.otherName] = true;socket.otherName = null;if (conn != null) {sendTo(conn, {type: 'leave',});}}}});
});function showUserInfo(allUsers) {sendTo(io, {event: 'show',allUsers: allUsers,});
}function sendTo(connection, message) {connection.send(message);
}

界面自己调整,这里就是为了方便展示;

注意,如果浏览器无法获取到摄像头,并报错Cannot read properties of undefined (reading 'getUserMedia'),是因为浏览器有安全设置,只需要进行如下操作即可开放摄像头权限:

chrome://flags/#unsafely-treat-insecure-origin-as-secure

不是用谷歌的小伙伴可以自行替换前缀,比如 Edge 浏览器:

edge://flags/#unsafely-treat-insecure-origin-as-secure

最后在旁边的空白处点一下,底部就会出现如下图所示:

点一下 Relauch 就可以了;

后记

总的来说,WebRTC 还是超赞的,node.js 也是,记录每一个脚印!

参考:

  • webrtc实现群聊系列文章(一)本地模拟视频通话
  • Introduction to WebRTC protocols
  • vue+node(socket.io)+webRTC实现一对一通话测试
  • 使用浏览器访问远程服务,调用本地摄像头录制音视频报错

【复】基于 WebRTC 的音视频在线监考模块的设计与实现(下)相关推荐

  1. 【复】基于 WebRTC 的音视频在线监考模块的设计与实现(上)

    文章目录 前言 什么是 WebRTC? WebRTC 架构 WebRTC 通讯内容 WebRTC 通讯协议 WebRTC 连接建立过程 后记 前言 最近在做关于考试系统的项目,其中有一项需求分析是要做 ...

  2. 基于WebRTC实现音视频及数据通信

    文章目录 前言 一.WebRTC的组成? 二.信令交换的方式 三.会话描述 四.客户端应用 1.HTML 2.JavaScript 五.效果演示 六.项目地址 总结 前言 刚写了篇基于WebRTC使用 ...

  3. 技术分享| 如何快速实现音视频在线通话

    请问咱们支持像微信一样的音视频呼叫功能吗? 请问呼叫邀请怎么实现? 如果客户端离线了,怎么呼叫到客户? 怎么添加呼叫铃声?以及接收铃声? 经常能听到有用户问上述的问题,今天借此机会向大家讲解下音视频呼 ...

  4. html5音频剪辑,一种基于HTML5Canvas画布音视频分段剪辑方法与流程

    技术特征: 1.一种基于html5canvas画布音视频分段剪辑方法,其特征在于:包括如下步骤: 步骤一:首先使用者预先获取源音视频文件,然后使用者对音视频文件分段剪辑时,进入音视频文件分段剪辑主单元 ...

  5. 基于FFMPEG的音视频截取(C++Qt 版)

    基于FFMPEG的音视频截取(C++Qt 版) 这篇博客是基于上篇博客的: https://blog.csdn.net/liyuanbhu/article/details/121744275 上篇博客 ...

  6. 基于springboot的短视频网站的开发与设计

    通知:代本科论文"润色"<基于视频监控的智慧消防系统>,可私聊. [注]该项目<基于springboot的短视频网站的开发与设计>为本人毕业设计.使用的开发 ...

  7. 基于B/S架构的在线考试系统的设计与实现

    前言 这个是我的Web课程设计,用到的主要是JSP技术并使用了大量JSTL标签,所有代码已经上传到了我的Github仓库里,地址:https://github.com/quanbisen/online ...

  8. mysql意见反馈表设计_一个基于PHP和MySQL的意见反馈模块的设计和实现

    一个基于PHP和MySQL的意见反馈模块的设计和实现杜大刚 [期刊名称]<计算机与现代化> [年(卷),期]2005(000)003 [摘要]本文涉及的意见反馈模块是一个基于PHP和MyS ...

  9. 网站在线客服系统实时语音视频聊天实战开发,利用peerjs vue.js实现webRTC网页音视频客服系统...

    webRTC机制和peerjs库的介绍在其他博客中已经有了很多介绍,这里我直接搬运过来 一.webrtc回顾 WebRTC(Web Real-Time Communication)即:网页即时通信. ...

  10. WebRTC实时音视频技术基础:基本架构和协议栈

    概述 本文主要介绍WebRTC的架构和协议栈. 最基本的三角形WebRTC架构 为了便于理解,我们来看一个最基本的三角形WebRTC架构(见下图): 在这个架构中,移动电话用"浏览器M&qu ...

最新文章

  1. django-后台sms管理系统的css框架
  2. 从“诺奖级”成果到“非主观造假”,时隔6年,韩春雨带着原一作,再发高分文章!...
  3. IBM 3650 M3阵列卡配置
  4. Tomcat学习总结(6)——Tomca常用配置详解
  5. 常用的java虚拟机_带你了解 JAVA虚拟机 面试必备
  6. MySQL为表添加外键约束
  7. 在html里面动画变颜色,html – 在悬停时填充文本颜色动画,带有动画颜色
  8. Linux内核LED子系统、请务必看
  9. 字符串匹配之PabinKarp(模式匹配)
  10. 迅捷CAD编辑器剪切框架工具具体使用方法
  11. RS485芯片UN485E的特点及其应用
  12. Microsoft office 2016在win10上的安装
  13. 多因子选股模型 —— 因子历史收益率(因子与股票收益率回归后的收益率)加权法
  14. 【AI语音】华为EC6110M、Q21AQ、Q21C部分EC6110T、EC6110U_海思3798MV310_通刷_卡刷固件
  15. 基于电流型磁链观测器的异步电机矢量控制学习
  16. 大数据窥探微信表情背后的含义,结论可能就是,你老了……
  17. python玩转android_Python Xplorer
  18. C语言实现STL静态链表,先进后出的数据结构-栈 一
  19. Java 线程安全问题及解决
  20. 湖北专升本-湖师计科

热门文章

  1. 未来教育计算机题库三合一,未来教育-全国计算机等级考试真考题库、高频考点、模拟考场三合一(二级MS Office高级应用)...
  2. 传送带计数器c语言程序,脉搏计数器的程序(用C语言编写程序)
  3. thinkphp5实战系列(二)前台模板的引入
  4. python3 pdf转图片_Python 将pdf转成图片的方法
  5. python解析xml文件为pdf_用Python解析XML文件的软件实现
  6. 弘辽科技:电商壹周大事
  7. Maven:repositories、distributionManagement、pluginRepositories中repository的区别(轻松搞明白)
  8. 聊聊最近的几件小事儿
  9. 戴尔电脑装ubuntu报ACPI错误解决过程
  10. seo与sem的区别