一、信令系统

  • 信令系统主要用来进行信令的交换

  • 在通信双方彼此连接、传输媒体数据之前,它们要通过信令服务器交换一些信息,如规范协商

  • 若 A 与 B 要进行音视频通信,那么 A 要知道 B 已经上线了,同样,B 也要知道 A 在等着与它通信呢

  • 只有双方都知道彼此存在,才能由一方向另一方发起音视频通信请求,并最终实现音视频通话

  • 客户端代码如下:

  • 第一步:首先弹出一个输入框,要求用户写入要加入的房间

  • 第二步:通过 io.connect() 建立与服务端的连接

  • 第三步:再根据 socket 返回的消息做不同的处理

<!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">
<title>信令系统</title>
</head> <body> </body>
<script src="/socket.io/socket.io.js"></script>
<script>
var isInitiator; // 弹出一个输入窗口
room = prompt('Enter room name:'); // 与服务端建立 socket 连接
const socket = io.connect(); // 如果房间不空,则发送 "create or join" 消息
if (room !== '') {
console.log('Joining room ' + room);
socket.emit('create or join', room);
} // 如果从服务端收到 "full" 消息
socket.on('full', (room) => {
console.log('Room ' + room + ' is full');
}); // 如果从服务端收到 "empty" 消息
socket.on('empty', (room) => {
isInitiator = true;
console.log('Room ' + room + ' is empty');
}); // 如果从服务端收到 “join" 消息
socket.on('join', (room) => {
console.log('Making request to join room ' + room);
console.log('You are the initiator!');
}); // 如果从服务端收到 “log" 消息
socket.on('log', (array) => {
console.log.apply(console, array);
});
</script> </html>
  • 服务端代码如下:

  • 需要通过 npm install socket.io 安装socket模块

  • 需要通过 npm install node-static 安装socket模块,使服务器具有发布静态文件的功能

  • 服务端侦听 2022 这个端口,对不同的消息做相应的处理

const static = require('node-static');
const http = require('http');
const file = new (static.Server)(); const app = http.createServer(function (req, res) {
file.serve(req, res);
}).listen(2022); // 侦听 2022
const io = require('socket.io').listen(app); io.sockets.on('connection', (socket) => {
// convenience function to log server messages to the client
function log() {
const array = ['>>> Message from server: '];
for (var i = 0; i < arguments.length; i++) {
array.push(arguments[i]);
}
socket.emit('log', array);
} socket.on('message', (message) => {
// 收到 message 时,进行广播
log('Got message:', message);
// for a real app, would be room only (not broadcast)
socket.broadcast.emit('message', message); // 在真实的应用中,应该只在房间内广播
}); socket.on('create or join', (room) => {
// 收到 “create or join” 消息
var clientsInRoom = io.sockets.adapter.rooms[room];
var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room); if (numClients === 0) {
// 如果房间里没人
socket.join(room);
// 发送 "created" 消息
socket.emit('created', room);
} else if (numClients === 1) {
// 如果房间里有一个人
io.sockets.in(room).emit('join', room);
socket.join(room);
// 发送 “joined”消息
socket.emit('joined', room);
} else {
// max two clients
// 发送 "full" 消息
socket.emit('full', room);
} socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);
});
});

二、RTCPeerConnection

  • RTCPeerConnection 类是在浏览器下使用 WebRTC 实现 1 对 1 实时互动音视频系统最核心的类

  • 它是WebRTC传输音视频和交换数据的API

  • RTCPeerConnection 就与普通的 socket 一样,在通话的每一端都至少有一个RTCPeerConnection 对象。在 WebRTC 中它负责与各端建立连接,接收、发送音视频数据,并保障音视频的服务质量

三、实现视频通话

  • 为连接的每个端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加一个本地流,该流是从 getUserMedia() 获取的

  • 获取本地媒体描述信息,即 SDP 信息,并与对端进行交换

  • 获得网络信息,即 Candidate(IP 地址和端口),并与远端进行交换

<!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">
<title>实现视频通话</title>
</head> <body>
<video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video> <div class="box">
<button onclick="start()">Start</button>
<button onclick="call()">Call</button>
<button onclick="hangup()">Hang Up</button>
</div>
</body>
<script>
// 获取元素
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo'); // 定义全局变量
var localStream;
var pc1;
var pc2; function start() {
console.log('Requesting local stream'); // 开始采集音视频
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then(function (stream) {
// 这个全局localStream是为了后面我们去添加流用的
localStream = stream; // 兼容性监测
if (window.URL) {
// 挂在数据在本地播放
localVideo.src = window.URL.createObjectURL(stream)
} else {
localVideo.srcObject = stream
}
})
.catch(function (e) {
// 如果获取视频失败,在这里进行错误处理
console.dir(e);
alert(`getUserMedia() error: ${e.message}`);
});
} function call() {
// 创建offerOption, 指定创建本地的媒体的时候,都包括哪些信息
// 可以有视频流和音频流,因为我们这里没有采集音频所以offerToReceiveAudio是0
var offerOptions = {
offerToReceiveAudio: 0,
offerToReceiveVideo: 1
} // 这里的 RTCPeerConnection 可以有可选参数, 进行一些网络传输的配置
// 由于是我们在本机内进行传输,所以在这里我们就不需要设置参数, 所以它这里就会使用本机host类型的candidate
pc1 = new RTCPeerConnection();
pc1.onicecandidate = (e) => {
console.log('pc1 ICE candidate:', e.candidate); // 我们A调用者收到candidate之后,它会将这个candidate发送给这个信令服务器
// 那么信令服务器会中转到这个B端,那么这个B端会调用这个AddIceCandidate这个方法,将它存到对端的candidate List里去
// 所以整个过程就是A拿到它所有的可行的通路然后都交给B,B形成一个列表
// 那么B所以可行的通路又交给A,A拿到它的可行列表,然后双方进行这个连通性检测
// 那么如果通过之后那就可以传数据了,就是这样一个过程
// 所以我们收到这个candidate之后就要交给对方去处理,所以pc1要调用pc2的这个
// 因为是本机这里就没有信令了,假设信令被传回来了,这时候就给了pc2
// pc2收到这个candidate之后就调用addIceCandidate方法,传入的参数就是e.candidate
pc2.addIceCandidate(e.candidate)
.catch(function (e) {
console.log("Failed to call getUserMedia", e);
});
} pc1.iceconnectionstatechange = (e) => {
console.log(`pc1 ICE state: ${pc.iceConnectionState}`);
console.log('ICE state change event: ', e);
} // 创建一个pc2这样我们就创建了两个连接
pc2 = new RTCPeerConnection(); // 对于pc2也是同样道理,那它就交给p1
pc2.onicecandidate = (e) => {
console.log('pc2 ICE candidate:', e.candidate); // 所以它就调用pc1.addIceCandidate
pc1.addIceCandidate(e.candidate)
.catch(function (e) {
console.log("Failed to call getUserMedia", e);
});
} pc2.iceconnectionstatechange = (e) => {
console.log(`pc2 ICE state: ${pc.iceConnectionState}`);
console.log('ICE state change event: ', e);
} // pc2是被调用方,被调用方是接收数据的,所以对于pc2它还有个ontrack事件
// 当双方通讯连接之后,当有流从对端过来的时候,会触发这个onTrack事件
pc2.ontrack = gotRemoteStream; // 将本地采集的数据添加到第一添加到第一个pc1 = new RTCPeerConnection()中去
// 这样在创建媒体协商的时候才知道我们有哪些媒体数据,这个顺序不能乱,必须要先添加媒体数据再做后面的逻辑
// 另外不能先做媒体协商然后在添加数据,因为你先做媒体协商的时候它知道你这里没有数据那么在媒体协商的时候它就没有媒体流
// 就是说在协商的时候它知道你是没有的,那么它在底层就不设置这些接收信息发收器,那么这个时候即使你后面设置了媒体流传给这个PeerConnection,它也不会进行传输的,所以我们要先添加流
// 添加流也比较简单,通过localStream调用getTracks就能调用到所有的轨道(音频轨/视频轨)
// 那对于每个轨道我们添加进去就完了,也就是forEach遍历进去,每次循环都能拿到一个track
// 当我们拿到这个track之后直接调用pc1.addTrack添加就好了,第一个参数就是track,第二个参数就是这个track所在的流localStream
// 这样就将本地所采集的音视频流添加到了pc1 这个PeerConnection
localStream.getTracks().forEach((track) => {
pc1.addTrack(track, localStream);
}); // 那么这个时候我们就可以去创建这个pc1去媒体协商了
// 媒体协商第一步就是创建createOffer
pc1.createOffer(offerOptions)
.then(function (desc) {
// 当我们拿到这个描述信息之后呢,还是回到我们当时协商的逻辑
// 对于A来说它首先创建Offer,创建Offer之后它会调用setLocalDescription
// 将它设置到这个PeerConnection当中去,那么这个时候它会触发底层的ICE的收集candidate的这个动作
// 所以这里要调用pc1.setLocalDescription这个时候处理完了它就会收集candidate
// 这个处理完了之后按照正常逻辑它应该send desc to signal到信令服务器
pc1.setLocalDescription(desc); // 到了信令服务器之后,信令服务器会发给第二个人
// 所以第二个人就会receive
// 所以第二个人收到desc之后呢首先pc2要调用setRemoteDescription,这时候将desc设置成它的远端
pc2.setRemoteDescription(desc); // 设成远端之后, pc2就要调用createAnswer
pc2.createAnswer().then(function (desc) {
// 当远端它得到这个Answer之后,它也要设置它的setLocalDescription
// 当它调用了setLocalDescription之后它也开始收集candidate了
pc2.setLocalDescription(desc); // 完了之后它去进行send desc to signal与pc1进行交换,pc1会接收recieve desc from signal
// 那么收到之后他就会设置这个pc1的setRemoteDescription
// 那么经过这样一个步骤整个协商就完成了
// 当所有协商完成之后,这些底层对candidate就收集完成了
// 收集完了进行交换形成对方的列表然后进行连接检测
// 连接检测完了之后就开始真正的数据发送过来了
pc1.setRemoteDescription(desc);
})
.catch(function (e) {
console.log("Failed to call getUserMedia", e);
});
})
.catch(function (e) {
console.log("Failed to call getUserMedia", e);
}); } // 当发送ontrack的时候也就是数据通过的时候, 将远端的音视频流传给了remoteVideo
function gotRemoteStream(e) {
if (remoteVideo.srcObject !== e.streams[0]) {
remoteVideo.srcObject = e.streams[0];
}
} // 挂断,将pc1和pc2分别关闭
function hangup() {
console.log('Ending call');
pc1.close();
pc2.close();
pc1 = null;
pc2 = null;
}
</script> </html>

四、视频通话流程详解

  • 视频通话本是不同的端与端连接,上面的代码在同一个浏览器中模拟多端连接的情况,可以通过开两个标签页,来模拟pc1端和pc2端

  • 所以大家会看到两个视频是一摸一样的,但是它的整个底层都是从本机自己IO的那个逻辑网卡转过来的

  • 当调用 call 的时候就会调用双方的 RTCPeerConnection

  • 当这个两个 PeerConnection 创建完成之后,它们会作协商处理

  • 协商处理完成之后进行 Candidate 采集,也就是说有效地址的采集

  • 采集完了之后进行交换,然后形成这个Candidate pair再进行排序

  • 然后再进行连接性检测,最终找到最有效的那个链路

  • 之后就将 localVideo 展示的这个数据通过 PeerConnection 传送到另一端

  • 另一端收集到数据之后会触发 onAddStream 或者 onTrack 就是说明我收到数据了,那当收到这个事件之后

  • 我们再将它设置到这个 remoteVideo 里面去

  • 这样远端的这个 video 就展示出来了,显示出我们本地采集的数据了

8┃音视频直播系统之 WebRTC 信令系统实现以及通讯核心并实现视频通话的更多相关文章

  1. Vue + WebRTC 实现音视频直播(附自定义播放器样式)

    1. 什么是WebRTC 1.1 WebRTC简介 WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频 ...

  2. 12┃音视频直播系统之 WebRTC 实现1对1直播系统实战

    一.搭建 Web 服务器 前面我们已经实现过,但是没有详细说HTTPS服务 首先需要引入了 express 库,它的功能非常强大,用它来实现 Web 服务器非常方便 同时还需要引入 HTTPS 服务, ...

  3. 3┃音视频直播系统之浏览器中通过 WebRTC 直播视频实时录制回放下载

    一.录制分类 在音视频会议.在线教育等系统中,录制是一个特别重要的功能 录制一般分为服务端录制和客户端录制 服务端录制:优点是不用担心客户因自身电脑问题造成录制失败(如磁盘空间不足),也不会因录制时抢 ...

  4. 5┃音视频直播系统之 WebRTC 中的协议UDP、TCP、RTP、RTCP详解

    一.UDP/TCP 如果让你自己开发一套实时互动直播系统,在选择网络传输协议时,你会选择使用UDP协议还是TCP协议 假如使用 TCP 会怎样呢?在极端网络情况下,TCP 为了传输的可靠性,将会进行反 ...

  5. 1┃音视频直播系统之浏览器中通过WebRTC访问摄像头

    一.WebRTC的由来 对于前端开发小伙伴而言,如果用 JavaScript 做音视频处理 在以前是不可想象的,因为首先就要考虑浏览器的性能是否跟得上音视频的采集 但是 Google 作为国际顶尖科技 ...

  6. 10┃音视频直播系统之 WebRTC 中的数据统计和绘制统计图形

    一.数据统计 在视频直播中,还有一项比较重要,那就是数据监控 比如开发人员需要知道收了多少包.发了多少包.丢了多少包,以及每路流的流量是多少,才能评估出目前用户使用的音视频产品的服务质量是好还是坏 如 ...

  7. 11┃音视频直播系统之 WebRTC 进行文本聊天并实时传输文件

    一.RTCDataChannel WebRTC 不但可以让你进行音视频通话,而且还可以用它传输普通的二进制数据,比如说可以利用它实现文本聊天.文件的传输等 WebRTC 的数据通道(RTCDataCh ...

  8. 2┃音视频直播系统之浏览器中通过 WebRTC 拍照片加滤镜并保存

    一.拍照原理 好多人小时候应该都学过,在几张空白的纸上画同一个物体,并让物体之间稍有一些变化,然后连续快速地翻动这几张纸,它就形成了一个小动画,音视频播放器就是利用这样的原理来播放音视频文件的 播放器 ...

  9. 6┃音视频直播系统之 WebRTC 核心驱动SDP规范协商

    一.什么是SDP SDP(Session Description Protocal)其实就是当数据过来时候,告诉数据自己这里支持的解码方式.传输协议等等,这样数据才能根据正确的方式进行解码使用 SDP ...

随机推荐

  1. python办公自动化系列之金蝶K3(三)

    小爬在之前的两篇文章 [python办公自动化系列之金蝶K3自动登录(一)].[python办公自动化系列之金蝶K3自动登录(二)]带大家系统搞定了K3客户端的自动登录难题,但是搞定[自动登录]只是我 ...

  2. 惯性传感器(IMU)

    近两年来,车联网.自动驾驶.无人驾驶.汽车智能化.网联化等成为了汽车行业的热点话题,未来汽车一定是朝着安全.可靠及舒适的方向发展.而这一切背后的发展都离不开传感器的作用,今天我们就来聊聊用途越来越广的 ...

  3. C++类中隐藏的六个默认函数

    Test类中隐藏的六个默认的函数 class Test { public: //默认的构造函数 Test(): //析构函数 ~Test(): //拷贝构造函数 Test(const Test &am ...

  4. PCB产业链、材料、工艺流程详解(1)

    PCB知识大全 1.什么是pcb,用来干什么? PCB( Printed Circuit Board),中文名称为印制电路板,又称印刷线路板,是重要的电子部件,是电子元器件的支撑体,是电子元器件电气连 ...

  5. 小小标签,强大功能——深藏不露的 <input>

    <input> 虽只是一个看似简单的 HTML 表单元素,但它这么一个单一的元素,就有多达 30 多个属性(attribute),相信无论你是个小菜鸟还是像我一样写了 15 年 HTML ...

  6. video元素和audio元素相关事件

    前言 在利用video元素或audio元素读取或播放媒体数据时,会触发一系列事件,如果用js脚本来捕抓这些事件,就可以对着这些事件进行处理了. 捕抓的方式有两种: 第一种是监听的方式.使用vedio或 ...

  7. java中封装encapsulate的概念

    封装encapsulate的概念:就是把一部分属性和方法非公有化,从而控制谁可以访问他们. https://blog.csdn.net/qq_44639795/article/details/1018 ...

  8. 使用 IDEA 创建 SpringBoot 项目(详细介绍)+ 源码案例实现

    使用 IDEA 创建 SpringBoot 项目 一.SpringBoot 案例实现源码 二.SpringBoot 相关配置 1. 快速创建 SpringBoot 项目 1.1 新建项目 1.2 填写 ...

  9. css实现半圆效果

    效果图: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...

  10. BootstrapBlazor实战-Tree树形控件使用(1)

    实战BootstrapBlazor树型控件Tree的使用, 以及整合Freesql orm快速制作数据库后台维护页面 demo演示的是Sqlite驱动,FreeSql支持多种数据库,MySql/Sql ...