WebRTC教程

MDN(Mozilla Developer Network)对WebRTC的定义:WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。


注:引用自https://juejin.cn/post/7071994793710075911

架构

image-20221103232054752

可以看到,一个简单的点对点通讯系统主要由四部分组成:

  • WebRTC客户端:负责生产/消费音视频数据,位于NAT之内,属于内网
  • NAT:Network Address Translation (NAT),网络地址转换协议, 能够将设备的内网地址映射到一个公网的地址。
  • 信令服务器:用于传输SDP、candidate等信令数据。
  • STUN/TURN服务器(中继服务器):
    • STUN:用于为位于NAT内的设备找到自己的公网地址。WebRTC客户端通过给处于公网的STUN服务器发送请求来获得自己的公网地址信息,以及是否能够被(穿过路由器)访问。
    • TURN:对于无法通过STUN服务器进行内网穿越的“对称型NAT”,我们可以借助TURN服务器作为中继服务器,通过TURN服务器对数据进行转发。

点对点的通信原理:

  1. 首先客户端需要信令服务器连接,后续双方需要通过信令服务器来了解对方的一些必要的信息,比如告诉对方自己的支持的音视频格式、自己外网IP地址和端口是多少等(此时还无法知道自己的公网地址)。
  2. 与STUN建立连接,获得自己的外网IP地址和端口,以及是否能够进行内网穿越。不支持内网穿越的情况下还需要连接TURN服务器进行中继通信。
  3. WebRTC客户端拿到自己的外网IP地址和端口后,通过信令服务器将自己的信息(candidate信息)交换给对方。当双方都获取到对方的地址后,它们就可以尝试NAT穿越,进行P2P连接了。

预备知识

MediaStreamTrack

WebRTC中的基本媒体单位,一个MediaStreamTrack包含一种媒体源(媒体设备或录制内容)返回的单一类型的媒体(如音频,视频)。单个轨道可包含多个通道,如立体声源尽管由多个音频轨道构成,但也可以看作是一个轨道

MediaStream

MediaStream是MediaStreamTrack的合集,可以包含 >=0 个 MediaStreamTrack。MediaStream能够确保它所包含的所有轨道都是是同时播放的,以及轨道的单一性。

source 与 sink

​ 在MediaTrack的源码中,MediaTrack都是由对应的source和sink组成的。

​ 浏览器中存在从source到sink的媒体管道。其中source负责生产媒体资源,包括多媒体文件,web资源等静态资源以及麦克风采集的音频,摄像头采集的视频等动态资源。而sink则负责消费source生产媒体资源,也就是通过等媒体标签进行展示,或者是通过RTCPeerConnection将source通过网络传递到远端。RTCPeerConnection可同时扮演source与sink的角色,作为sink,可以将获取的source降低码率,缩放,调整帧率等,然后传递到远端,作为source,将获取的远端码流传递到本地渲染。

MediaTrackConstraints

MediaTrackConstraints描述MediaTrack的功能以及每个功能可以采用的一个或多个值,从而达到选择和控制源的目的。 MediaTrackConstraints 可作为参数传递给applyConstraints()以达到控制轨道属性的目的,同时可以通过调getConstraints()用来查看最近应用自定义约束。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const constraints = {
width: {min: 640, ideal: 1280},
height: {min: 480, ideal: 720},
advanced: [
{width: 1920, height: 1280},
{aspectRatio: 1.333}
]
};
//使用1280x720的摄像头分辨率
const constraints = {
audio: true,
video: { width: 1280, height: 720 }
};
const constraints = {
audio: true,
video: { facingMode: "user" }
};
const constraints = {
audio: true,
video: { facingMode: { exact: "environment" } }
};

//{ video: true }也是一个MediaTrackConstraints对象,用于指定请求的媒体类型和相对应的参数。
navigator.mediaDevices.getUserMedia({ video: true })
.then(mediaStream => {
const track = mediaStream.getVideoTracks()[0];
track.applyConstraints(constraints)
.then(() => {
// Do something with the track such as using the Image Capture API.
})
.catch(e => {
// The constraints could not be satisfied by the available devices.
});
});

如何播放MediaStream

​ 可将MediaStream对象直接赋值给HTMLMediaElement 接口的 srcObject属性。

1
video.srcObject = stream;

WebRTC实现点对点通信

  1. 检测本地音视频设备和进行采集音视频的采集;
  2. 通过信令服务器与对方建立连接;
  3. 创建RTCPeerConnection对象
    • 绑定音视频数据
    • 进行媒体协商
    • 交换candidate信息
  4. 音视频数据传输与渲染

检测本地音视频设备 and 进行采集音视频的采集

检测本地音视频设备

​ 通过MediaDevices.enumerateDevices()我们可以得到一个本机可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。

1
2
3
4
//获取媒体设备
navigator.mediaDevices.enumerateDevices().then(res => {
console.log(res);
});

​ 列表中的每个媒体输入都可作为MediaTrackConstraints中对应类型的值,如一个音频设备输入audioDeviceInput可设置为MediaTrackConstraints中audio属性的值。

1
const constraints = { audio : audioDeviceInput }

​ 将该constraint值作为参数传入到MediaDevices.getUserMedia(constraints)中,便可获得该设备的MediaStream。

采集本地音视频数据

​ 可通过调用MediaDevices.getUserMedia()来访问本地媒体,调用该方法后浏览器会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。

1
2
3
4
5
6
7
8
9
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
/* 使用这个stream*/
video.srcObject = stream;
})
.catch(function(err) {
/* 处理error */
});

通过信令服务器与对方建立连接

​ 信令服务器主要用于帮我们进行业务逻辑的处理(如加入房间、离开房间)以及进行媒体协商和交换candidate。

​ 信令服务器可以有很多种方案,在这里我们借助node.js和socket.io实现一个简单的信令服务器。

创建HTTP服务器

1
2
3
4
5
6
7
let http = require('http'); // 提供HTTP 服务
let express = require('express');

let app = express();

let http_server = http.createServer(app);
http_server.listen(8081, '127.0.0.1');

引入 socket.io 实现两端的实时通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let http = require('http'); // 提供HTTP 服务
const { Server } = require('socket.io');
let express = require('express');

let app = express();

//HTTP 服务
let http_server = http.createServer(app);
http_server.listen(8081, '127.0.0.1');

const io = new Server(http_server, {
// 处理跨域配置
cors: {
origin: ['http://127.0.0.1:3000', 'http://localhost:3000'],
credentials: true,
},
});

监听客户端的消息

1
socket.on('messageName', messageHandler)

客户端加入房间

1
socket.join(roomId);

向房间内的客户端发送消息

1
socket.to(roomId).emit('messageName', data);

转发消息

1
2
3
4
// 用于转发sdp、candidate等消息
socket.on('message', ({ roomId, data }) => {
socket.to(roomId).emit('message', data);
});

创建RTCPeerConnection对象

RTCPeerConnection 接口代表一个由本地计算机到远端的WebRTC连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。

1
const pc = new RTCPeerConnection()

绑定音视频数据

我们可以通过 addTrack 方法和 addStream 方法(已过时,不推荐)将音视频数据和 RTCPeerConnection 对象进行绑定。

1
2
3
mediaStream.getTracks().forEach(track => {
peerConnection.addTrack(track, mediaStream);
});

进行媒体协商

​ 所谓的媒体协商,就是交换双方SDP信息,SDP包含音视频的编解码(codec),源地址,和时间信息等信息。

​ 呼叫端获取本地sdp(offer),调用pc.setLocalDescription(offer)保存本地的sdp信息后,通过信令服务器发送本地sdp到远端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 呼叫端获取本地sdp(offer)
pc.createOffer().then(offer => {
console.log('------ 获取到了本地offer', offer);

// 绑定本地sdp信息
pc.setLocalDescription(offer);

// 通过信令服务器发送本地sdp到远端
signalServer.send({
type: 'offer',
value: offer,
});
});

​ 被叫端接收到来自远端的offer后,调用 pc.setRemoteDescription(offer) 绑定远端sdp,然后调用pc.createAnswer() 创建本地sdp并使用 pc.setLocalDescription(answer) 进行保存,最后利用信令服务器将 answer sdp 发送给远端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const onGetRemoteOffer = offer => {
console.log('------ 获取到了远端offer', offer);
// 远端发起呼叫,开始建立连接
// 绑定远端sdp
pc.setRemoteDescription(offer);
// 创建本地sdp
pc.createAnswer().then(answer => {
// 绑定本地sdp
pc.setLocalDescription(answer);
console.log('------ 获取到了本地answer', answer);
// 发送本地sdp到远端
signalServer.send({
type: 'answer',
value: answer,
});
});
};

​ 呼叫端接收到远端的answer后,调用 pc.setRemoteDescription(answer) 绑定远端sdp。

1
2
3
4
5
const onGetRemoteAnswer = answer => {
console.log('------ 获取到了远端answer', answer);
// 绑定远端sdp
pc.setRemoteDescription(answer);
};

ICE

​ 当媒体协商完成后,WebRTC就开始建立网络连接了。建立网络连接的前提是客户端需要知道对端的外网地址,这个获取并交换外网地址的过程,我们称为ICE

收集

​ WebRTC内部集成了收集Candidate的功能。收集到Candidate后,为了通知上层,WebRTC还提供onicecandidate事件。

1
2
3
4
5
6
7
8
// 监听 candidate 获取事件
pc.addEventListener('icecandidate', event => {
const candidate = event.candidate;
if (candidate) {
console.log('------ 获取到了本地 candidate:', candidate)
//...
}
});
交换

​ 收集到candidate后,可以通过信令系统将candidate信息发送给远端。

1
2
// 发送candidate到远端
signalServer.send({ type: 'candidate', value: candidate });

​ 远端接收到对端的candidate后,会与本地的candidate形成CandidatePair(即连接候选者对)。有了CandidatePair,WebRTC就可以开始尝试建立连接了。

1
2
3
4
5
// 获取到远端的candidate
const onGetRemoteCandidate = candidate => {
console.log('------ 获取到了远端candidate', candidate);
pc.addIceCandidate(candidate);
};

远端音视频数据接收与渲染

当双方都获取到对端的candidate信息后,WebRTC内部就开始尝试建立连接了。连接一旦建成,音视频数据就开始源源不断地由发送端发送给接收端。

通过RTCPeerConnection对象的track事件,我们能接收到远端的音视频数据并进行渲染。

1
2
3
4
5
6
7
// 监听到远端传过来的媒体数据
pc.addEventListener('track', e => {
console.log('------ 获取到了远端媒体数据:', e);
if (remoteVideo.srcObject !== e.streams[0]) {
remoteVideo.srcObject = e.streams[0];
}
});

注:引用自https://blog.csdn.net/qq_44938451/article/details/122158975

WebRTC 简单概念

image-20221105195123025

​ 上述序列中,WebRTC并不提供Stun服务器和Signal服务器,服务器端需要自己实现。Stun服务器可以用google提供的实现stun协议的测试服务器(stun:stun.l.google.com:19302),Signal服务器则完全需要自己实现了,它需要在ClientA和ClientB之间传送彼此的SDP信息和candidate信息,ClientA和ClientB通过这些信息建立P2P连接来传送音视频数据。由于网络环境的复杂性,并不是所有的客户端之间都能够建立P2P连接,这种情况下就需要有个relay服务器做音视频数据的中转。而Signal服务器其实就是我们的websocket,stun服务器其实就是webrtc。

上图中的流程如下:

  1. ClientA首先创建PeerConnection对象,然后打开本地音视频设备,将音视频数据封装成MediaStream添加到PeerConnection中。
  2. ClientA调用PeerConnection的CreateOffer方法创建一个用于offer的SDP对象,SDP对象中保存当前音视频的相关参数。ClientA通过PeerConnection的SetLocalDescription方法将该SDP对象保存起来,并通过Signal服务器发送给ClientB。
  3. ClientB接收到ClientA发送过的offer SDP对象,通过PeerConnection的SetRemoteDescription方法将其保存起来,并调用PeerConnection的CreateAnswer方法创建一个应答的SDP对象,通过PeerConnection的SetLocalDescription的方法保存该应答SDP对象并将它通过Signal服务器发送给ClientA。
  4. ClientA接收到ClientB发送过来的应答SDP对象,将其通过PeerConnection的SetRemoteDescription方法保存起来
  5. 在SDP信息的offer/answer流程中,ClientA和ClientB已经根据SDP信息创建好相应的音频Channel和视频Channel并开启Candidate数据的收集,Candidate数据可以简单地理解成Client端的IP地址信息(本地IP地址、公网IP地址、Relay服务端分配的地址)
  6. 当ClientA收集到Candidate信息后,PeerConnection会通过OnIceCandidate接口给ClientA发送通知,ClientA将收到的Candidate信息通过Signal服务器发送给ClientB,ClientB通过PeerConnection的AddIceCandidate方法保存起来。同样的操作ClientB对ClientA再来一次。
  7. 这样ClientA和ClientB就已经建立了音视频传输的P2P通道,ClientB接收到ClientA传送过来的音视频流,会通过PeerConnection的OnAddStream回调接口返回一个标识ClientA端音视频流的MediaStream对象,在ClientB端渲染出来即可。同样操作也适应ClientB到ClientA的音视频流的传输。

搭建 turn 服务器

0x00:下载各种东西。

1
2
3
4
5
6
7
8
9
apt -y install openssl-devel
openssl req -x509 -newkey rsa:2048 -keyout /etc/turn_server_pkey.pem -out /etc/turn_server_cert.pem -days 99999 -nodes
wget --no-check-certificate https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz
tar -zxvf libevent-2.1.12-stable.tar.gz
cd libevent-2.1.12-stable/
./configure
make
make install
apt-get install coturn

0x01:更改配置

1
2
3
4
5
6
7
8
9
10
11
#与前ifconfig查到的网卡名称一致
relay-device=eth0
listening-ip=10.0.16.7
external-ip=159.75.239.36
user=chr:123456
realm=chr.com
listening-port=3478
cli-password=qwerty

cert=/etc/turn_server_cert.pem
pkey=/etc/turn_server_pkey.pem

0x02:开端口&启动

1
turnserver -a -o -f -r xxx.com

留言

2022-11-03

© 2024 wd-z711

⬆︎TOP