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

一、WebRTC 音视频采集 API:MediaDevices.getUserMedia()

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

1
2
3
4
5
6
7
8
9
10
11
const constraints = {
video: true,
audio: true,
};
// 非安全模式(非https/localhost)下 navigator.mediaDevices 会返回 undefined
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
document.querySelector("video").srcObject = stream;
} catch (error) {
console.error(error);
}

二、获取音视频设备输入输出列表:MediaDevices.enumerateDevices()

MediaDevices 的方法 enumerateDevices() 请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。 返回的 Promise (en-US) 完成时,会带有一个描述设备的 MediaDeviceInfo (en-US) 的数组。

1
2
3
4
5
6
7
8
try {
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoinputs = devices.filter((device) => device.kind === "videoinput");
this.audiooutputs = devices.filter((device) => device.kind === "audiooutput");
this.audioinputs = devices.filter((device) => device.kind === "audioinput");
} catch (error) {
console.error(error);
}

三、点对点媒体协商方法

媒体协商方法:

1、createOffer:

createOffer 方法会生成描述信息的一个 blob 对象,它会帮助连接到本地机器。当你已经找到一个远端的 PeerConnection 并且打算设置建立本地的 PeerConnection 时,你可以使用该方法。

1
2
3
4
5
6
7
var pc = new PeerConnection();
pc.addStream(video);
pc.createOffer(function(desc){
pc.setLocalDescription(desc, function() {
// send the offer to a server that can negotiate with a remote client
});
}

2、createAnswer:

对从远方收到的 offer 进行回答。

1
2
3
4
5
6
7
8
var pc = new PeerConnection();
pc.setRemoteDescription(new RTCSessionDescription(offer), function () {
pc.createAnswer(function (answer) {
pc.setLocalDescription(answer, function () {
// send the answer to the remote connection
});
});
});

四、服务端:Koa + socket.io

1、server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// server 端 server.js
const Koa = require("koa");
const socket = require("socket.io");
const http = require("http");
const app = new Koa();
const httpServer = http.createServer(app.callback()).listen(3000, () => {});
socket(httpServer).on("connection", (sock) => {
// ....
});

// client 端 socket.js
import io from "socket.io-client";
const socket = io.connect(window.location.origin);
export default socket;

2、点对点分别连接信令服务器,信令服务器记录房间信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
socket(httpServer).on("connection", (sock) => {
// 用户离开房间
sock.on("userLeave", () => {
// ...
});
// 检查房间是否可加入
sock.on("checkRoom", () => {
// ...
});
// ....
});
// client 端 Room.vue
import socket from "../utils/socket.js";

// 服务端告知用户是否可加入房间
socket.on("checkRoomSuccess", () => {
// ...
});
// 服务端告知用户成功加入房间
socket.on("joinRoomSuccess", () => {
// ...
});
//....

3、A 端作为发起方向接收方 B 端发起视频邀请

在得到 B 同意视频请求后,双方都会创建本地的 RTCPeerConnection,添加本地视频流,其中发送方会创建 offer 设置本地 sdp 信息描述,并通过信令服务器将自己的 SDP 信息发送给对端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
socket.on("answerVideo", async (user) => {
VIDEO_VIEW.showInvideoModal();
// 创建本地视频流信息
const localStream = await this.createLocalVideoStream();
this.localStream = localStream;
document.querySelector("#echat-local").srcObject = this.localStream;
this.peer = new RTCPeerConnection();
this.initPeerListen();
this.peer.addStream(this.localStream);
if (user.sockId === this.sockId) {
// 接收方
} else {
// 发送方 创建 offer
const offer = await this.peer.createOffer(this.offerOption);
await this.peer.setLocalDescription(offer);
socket.emit("receiveOffer", { user: this.user, offer });
}
});

4、收集自己的网络信息并发送给对方

1
2
3
4
5
6
7
initPeerListen () {
// 收集自己的网络信息并发送给对端
this.peer.onicecandidate = (event) => {
if (event.candidate) { socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user }); }
};
// ....
}

5、当接收方 B 端通过信令服务器拿到对端发送方 A 端的含有 SDP 的 offer 信息后则会调用 setRemoteDescription 存储对端的 SDP 信息,创建及设置本地的 SDP 信息,并通过信令服务器传送含有本地 SDP 信息的 answer

1
2
3
4
5
6
socket.on("receiveOffer", async (offer) => {
await this.peer.setRemoteDescription(offer);
const answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
socket.emit("receiveAnsewer", { answer, user: this.user });
});

6、当发起方 A 通过信令服务器接收到接收方 B 的 answer 信息后则也会调用 setRemoteDescription,这样双方就完成了 SDP 信息的交换

1
2
3
socket.on("receiveAnsewer", (answer) => {
this.peer.setRemoteDescription(answer);
});

7、当双方 SDP 信息交换完成并且监听 icecandidate 收集到网络候选者通过信令服务器交换后,则会拿到彼此的视频流。

1
2
3
4
5
6
7
socket.on("addIceCandidate", async (candidate) => {
await this.peer.addIceCandidate(candidate);
});
this.peer.onaddstream = (event) => {
// 拿到对方的视频流
document.querySelector("#remote-video").srcObject = event.stream;
};

8、GitHub:https://github.com/HeyJudeYQ/webrtc