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

这篇具有很好参考价值的文章主要介绍了【复】基于 WebRTC 的音视频在线监考模块的设计与实现(下)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

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

 

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');
        // 旧的浏览器可能没有srcObject
        if ("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;
	
	// 创建 offer
	let 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)
	
	// 接收端创建 answer
	let 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 设置为本地的远程描述。

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

 

在线监考

通过刚才的 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 srcObject
      if ('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 offer
        peerConn.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 offer
      peerConn.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 user

  socket.on('message', function(data) {
    var data = JSON.parse(data);
    switch (data.event) {
      // When has new user join in
      case 'join':
        user = data.name;
        console.log('User joined', data.name);
        // Save users info
        allUsers[user] = true; // 'true' means has not call, 'false' means calling
        allSockets[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 UserB
        console.log('Sending offer to: ', data.connectedUser);
        //if UserB exists then send him offer details
        var conn = allSockets[data.connectedUser];
        // allUsers[user] = false;
        allUsers[user] = true;
        if (conn != null) {
          showUserInfo(allUsers);
          // Setting that UserA connected with UserB
          socket.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 UserA
        var 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 connection
        if (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);
}

界面自己调整,这里就是为了方便展示;
【复】基于 WebRTC 的音视频在线监考模块的设计与实现(下)

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

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

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

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

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

最后在旁边的空白处点一下,底部就会出现如下图所示:
【复】基于 WebRTC 的音视频在线监考模块的设计与实现(下)

点一下 Relauch 就可以了;

 

后记

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

参考:文章来源地址https://www.toymoban.com/news/detail-430255.html

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

到了这里,关于【复】基于 WebRTC 的音视频在线监考模块的设计与实现(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [开源]企业级在线办公系统,基于实时音视频完成在线视频会议功能

    本项目使用了SpringBoot+Mybatis+SpringMVC框架,技术功能点应用了WebSocket、Redis、Activiti7工作流引擎, 基于TRTC腾讯实时音视频完成在线视频会议功能。 使用GPL-3.0开源协议                   办公系统应用了工作流引擎,基于RBAC的权限管理系统,工作流审批有请假流程审批、会议

    2024年02月05日
    浏览(59)
  • Hololens2远程音视频通话与AR远程空间标注,基于OpenXR+MRTK3+WebRTC实现

    下面展示一些 内联代码片 。

    2024年04月10日
    浏览(43)
  • WebRTC音视频通话(二)简单音视频通话

    本篇不详细介绍websocket,只针对websocket整合rtc。 webrtc是P2P通信,也就是实际交流的 只有两个人 ,而要建立通信,这两个人需要 交换一些信息来保证通信安全 。而且, webrtc必须通过ssh加密 ,也就是使用https协议、wss协议。 借用一幅图 1.1 创建端点的解析 以下解析不包括we

    2023年04月09日
    浏览(47)
  • WebRTC音视频通话-WebRTC视频自定义RTCVideoCapturer相机

    WebRTC音视频通话-WebRTC视频自定义RTCVideoCapturer相机 在之前已经实现了WebRTC调用ossrs服务,实现直播视频通话功能。但是在使用过程中,RTCCameraVideoCapturer类提供的方法不能修改及调节相机的灯光等设置,那就需要自定义RTCVideoCapturer自行采集画面了。 iOS端WebRTC调用ossrs相关,实现

    2024年02月12日
    浏览(40)
  • 【WebRTC】音视频通信

    WebRTC对等体还需要查找并交换本地和远程音频和视频媒体信息,例如分辨率和编解码器功能。 交换媒体配置信息的信令通过使用被称为SDP的会话描述协议格式来交换,被称为提议和应答的元数据块 一方发起调用 getUserMedia 打开本地摄像头 媒体协商(信令交换,媒体协商主要

    2024年02月07日
    浏览(40)
  • WebRTC音视频原理

    WebRTC,网页即时通讯(Web Real-Time Communication),是直接在 Web 浏览器内驱动实时通信(语音、视频和任意数据)方法的API。它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准,并于 2011 年标准化,是谷歌开源的一款产品。 WebRTC 实现了浏览器快速

    2023年04月08日
    浏览(56)
  • WebRTC音视频通话-WebRTC本地视频通话使用ossrs服务搭建

    iOS开发-ossrs服务WebRTC本地视频通话服务搭建 之前开发中使用到了ossrs,这里记录一下ossrs支持的WebRTC本地服务搭建。 ossrs是什么呢? SRS(Simple Realtime Server)是一个简单高效的实时视频服务器,支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议。 官网地址:https://ossrs.net/lt

    2024年02月12日
    浏览(30)
  • WebRTC音视频会议底层支撑技术

    WebRTC允许应用使用P2P通信。WebRTC是一个广泛的话题,在本文中,我们将重点讨以下问题。 为什么Web RTC 如此受欢迎? 在P2P连接过程中会发生什么 信号传递 NATs和ICE STUN TURN服务器 VP9视频编解码器 WebRTC APIs 安全 1.为什么Web RTC 如此受欢迎? 开放源代码 它为浏览器提供了端到端

    2024年02月11日
    浏览(43)
  • mediasoup webrtc音视频会议搭建

    拉下源码: https://github.com/versatica/mediasoup-demo 源码里有以下目录其中,app网页的界面终端,broadcasters是广播,也就是他支持我们用ffmpeg推流上去给所有的成员广播,server是流媒体服务器。 源码包含了,https服务器用于浏览器获取界面,信令服务器用于房间管理,和流媒体服务

    2024年02月05日
    浏览(32)
  • webRTC一对一音视频对话

    环境 阿里云操作系统: ubuntu 18.4 amd         注意:安全组一定添加对应的入口端口 nodejs -v 18.19.0 npm -v 10.2.3 需要安装的库 package.json 服务器端         webRTC一定要使用https服务器,如果没有ssl证书,可以使用自制证书         1.创建HTTPS服务器                 使用soc

    2024年01月19日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包