WebRTC 概述
(部分摘自百度百科)
WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写, 是一个支持网页浏览器进行实时语音对话或视频对话的API。 它于2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。
2021年1月26日,万维网联盟(W3C)和互联网工程任务组(IETF)宣布, Web实时支持多种服务的通信(WebRTC)现在已成为官方标准, 可将音频和视频通信带到Web上的任何位置。 www.w3.org/TR/webrtc/
2010年5月,Google以6820万美元收购VoIP软件开发商Global IP Solutions的GIPS引擎, 并改为名为“WebRTC”。WebRTC使用GIPS引擎,实现了基于网页的视频会议,并支持722,PCM,ILBC,ISAC等编码, 同时使用谷歌自家的VP8视频解码器;同时支持RTP/SRTP传输等。
2012年1月,谷歌已经把这款软件集成到Chrome浏览器中。 同时FreeSWITCH项目宣称支持iSAC audio codec。
QQ语音技术也来自Global IP Solutions公司
WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能, 并且还支持跨平台:windows,linux,mac,android。
WebRTC 名字带Web但不仅作用于Web平台,其核心代码使用C++编写, Web平台只是WebRTC的一种实现,其核心代码仍然是WebRTC的C++版本
WebRTC架构图(转载于网络)
从架构图中我们看到,WebRTC帮我实现了音频引擎
,视频引擎
,网络传输引擎
,封装了上层API,让开发者能够基于浏览器(Chrome\FireFox...)轻易快捷开发出丰富的实时多媒体应用,
而无需下载安装任何插件,也无需关注多媒体的数字信号处理过程,只需编写简单的**Javascript
**程序即可实现
WebRTC 原理
浏览器点对点P2P的信道(peer-to-peer),信道可发送任何数据并无需经过服务器。
WebRTC的实现是建立客户端之间的直接连接而无需服务器中转的,即P2P。所以要求彼此知道对方外网地址, 而计算机大多位于NAT之后,只有少数主机拥有外网地址。这就需要一种可以穿透NAT技术,比如:STUN、TRUN。
WebRTC使用默认STUN服务器获取当前主机的外网地址和端口,Chrome默认的是Google域名下的一个STUN服务器。
NAT穿越(打洞)
在处于使用NAT设备的私有TCP/IP网络中的主机之间建立连接时需使用NAT穿越。
NAT的行为是非标准化的,穿越技术大多使用公共服务器,使全球任何地方都能访问得到IP地址, 在RTCPeerConnection
中实用ICE框架来保证RTCPeerConnection
实现NAT穿越。
ICE 综合性NAT穿越技术
ICE(Interactive Connectivity Establishment)综合性NAT穿越技术
ICE框架整合各种NAT穿越技术如STUN、TURN(Traversal Using Relay NAT,中继NAT实现的穿透), ICE先使用STUN尝试建立一个基于UDP的连接,失败后使用TCP, 先尝试HTTP,失败后尝试HTTPS, 如果还是失败,ICE就会使用中继的TURN服务器。
在Web平台做WebRTC
创建WebRTC的流程
- ClientA 创建
Offer
- ClientB 绑定
Offer
- ClientB 创建
Answer
- ClientA 绑定
Answer
,建立P2P
连接
浏览器端代码实现
MediaStream
获取本地音视频流RTCPeerConnection
用于建立P2P连接,传输音视频流等,可理解为RTC对象RTCDataChannel
用于传输自定义数据。
1.ClientA 创建WebRTC
对象和RTC数据通道,并绑定一些事件
名词解释
SDP:会话描述协议 Session Description Protocol
// 创建WebRTC对象
const pc = new RTCPeerConnection();
// 创建WebRTC数据通道,可以传输自定义数据
const dc = pc.createDataChannel("channel");
// 数据通道开启
dc.onopen = () => console.log("Data Channel Opened");
// 数据通道收到数据的事件
dc.onmessage = (e) => console.log("Get Message:", e.data);
// SDP变化的事件
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") { // 在SPD创建完成的时候打印他
console.log(JSON.stringify(pc.localDescription));
}
}
2.ClientA 创建Offer
使用pc.createOffer();
来创建Offer
,并且调用setLocalDescription
将Offer绑定在本地SDP
执行完成后可以看到在控制台中打印了你的Offer
复制这端Offer,我们可以来写ClientB的代码了
/**
* 创建Offer
* @return {Promise<void>}
*/
async function createOffer() {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
}
3.ClientB 绑定Offer
先编写ClientB的代码
使用类似的代码来创建ClientB
由于ClientA已经有数据通道了,所以ClientB不需要自己创建
const pc = new RTCPeerConnection();
let dc = null;
// 同样的监听SDP变化的事件,在SPD创建完成的时候打印他
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") {
console.log(JSON.stringify(pc.localDescription));
}
}
// 监听数据通道的事件,并添加上事件,保存一下这条数据通道
pc.ondatachannel = (e) => {
console.log("data channel");
dc = e.channel;
dc.onmessage = (e) => console.log("get message", e.data);
dc.onopen = async (e) => console.log("channel opened");
}
接下来,我们来绑定之前创建的Offer
async function setOffer() {
await pc.setRemoteDescription(JSON.parse(需要绑定的offer));
console.log("Offer Set");
}
调用这个setOffer去设置之前创建的Offer
其中setRemoteDescription
表示设置另一个客户端的SDP
4.ClientB 创建Answer
修改ClientB代码,使用createAnswer
来创建一个Answer
并且用setLocalDescription
设置到本地SDP
我们在设置完Offer后立刻创建Answer
经过上面的过程,可以在控制台中看到打印的本地Answer
,复制这个Answer
,我们去ClientA绑定它
async function setOffer() {
await pc.setRemoteDescription(JSON.parse(需要绑定的offer));
console.log("Offer Set");
await createAnswer();
}
async function createAnswer() {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
}
5.ClientA 绑定Answer
,建立P2P
连接
回到ClientA,通过下面的代码来绑定我们刚才创建的Answer
async function setAnswer() {
await pc.setRemoteDescription(JSON.parse(需要绑定的Answer));
console.log("Answer Set");
}
执行完后我们看到在ClientA中控制台打印了数据通道开启提示
在ClientB中打印了收到数据通道和数据通道开启提示
6.使用RTCDataChannel
发送自定义数据
已经开启了数据通道,我们可以直接使用数据通道发送任何数据
dc.send("我是ClientA,你是谁?");
以上数据传输不依赖任何服务器,这就是点对点传输P2P(peer-to-peer)
7.开启本地摄像头
在两个客户端的Html中都写入两个video,并用id取出备用
<style>
.video-div div {
width: 50%;
}
.video-div div video {
width: 100%;
background-color: #6D7587;
}
.video-div {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
</style>
<div class="video-div">
<div>
<label>Ta的视频</label>
<video id="video" autoplay="autoplay"></video>
</div>
<div>
<label>你的视频</label>
<video id="myVideo" autoplay="autoplay"></video>
</div>
</div>
<script>
const video = document.getElementById("video");
const myVideo = document.getElementById("myVideo");
</script>
在两个客户端中使用navigator.mediaDevices.getUserMedia
开启摄像头,并将流推送给video标签
async function openVideo() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true, audio: true
});
myVideo.srcObject = stream;
}
执行项目代码可以看到右边的视频里有你帅气的脸
8.将音视频流推送给WebRTC
,并接收对方传来的音视频流
修改openVideo
函数,加入推流给WebRTC
的代码
async function openVideo() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true, audio: true
});
myVideo.srcObject = stream;
stream.getTracks().forEach((track) => {
pc.addTrack(track, stream);
});
}
在创建本地SPD之前完成开启摄像头和推流操作
利用异步变成更方便的控制调用顺序
async function init() {
await openVideo();
await createOffer();
}
init();
然后给RTCPeerConnection
监听ontrack
事件,表示有音视频轨道被推送进来
将音视频流推给另一个video标签
pc.ontrack = (e) => {
console.log("track");
video.srcObject = e.streams[0];
}
加上了这些操作,你已经可以在两个video标签上都看到自己帅气的脸了, 其中一个视频来自另一个一个客户端的推送
更好的WebRTC架构
1.将ClientA和ClientB统一为一个客户端软件
市面上所有的音视频,会议产品,恐怕不存在呼叫者和被呼叫者是需要区分两个APP的
编写一个网页,让他即具备呼叫者功能,又具备被呼叫者功能
2.完成屏幕共享
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always"
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
3.引入另一台服务器
使用一台服务器来转发SDP,SDP是会根据网络状态变化的,而且SPD非常长,用户交换SDP是非常不方便的
如果你有自己的用户系统,可以以唯一的用户ID来做身份标识,
用户只需要呼叫某个身份ID如QQ号,而SDP则使用信令服务器进行转发,同时承担一些其他内容,
如:转发被呼用户是否接听电话,转发用户挂断等
4.做个牛客网在线面试?
做个牛客网在线面试?
贴上代码
以下代码已整合双客户端
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>WebRTC视频通话</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.video-div div {
width: 50%;
}
.video-div div video {
width: 100%;
background-color: #6D7587;
}
.video-div {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.input-div {
width: 100%;
margin-top: 50px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
}
.input-div > div {
flex-direction: column;
}
.input-div div {
margin-top: 30px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#SDP {
width: 50%;
height: 300px;
}
</style>
</head>
<body>
<div class="video-div">
<div>
<label>Ta的视频</label>
<video id="video" autoplay="autoplay"></video>
</div>
<div>
<label>你的视频</label>
<video id="myVideo" autoplay="autoplay"></video>
</div>
</div>
<div class="input-div">
<div>
<!--创建一个Offer-->
<button onclick="createOffer()">创建Offer</button>
<!--设置offer的区域-->
<div>
<label for="offerInput">输入offer:</label>
<input type="text" id="offerInput"/>
<button onclick="setOffer()">设置offer</button>
</div>
<!--设置answer的区域-->
<div>
<label for="answerInput">输入answer:</label><input type="text"
id="answerInput"/>
<button onclick="setAnswer()">设置answer</button>
</div>
</div>
<!--本地的SDP-->
<div>
<label for="SDP">你的SDP:</label>
<textarea id="SDP"></textarea>
</div>
</div>
<script>
const SDP = document.getElementById("SDP");
const answerInput = document.getElementById("answerInput");
const offerInput = document.getElementById("offerInput");
const video = document.getElementById("video");
const myVideo = document.getElementById("myVideo");
/**
* 打开摄像头并且添加Track到RTCPeerConnection
* @return {Promise<void>}
*/
async function openVideo() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true, audio: true
});
myVideo.srcObject = stream;
stream.getTracks().forEach((track) => {
pc.addTrack(track, stream);
});
}
// 创建 RTCPeerConnection
const pc = new RTCPeerConnection();
// 监听ontrack,表示有视频流或音频流被推送过来
pc.ontrack = (e) => {
console.log("ontrack");
video.srcObject = e.streams[0];
}
// 这个事件表示本地SDP有变化
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") { // 如果SDP创建完成了就给他显示出来
SDP.value = JSON.stringify(pc.localDescription);
}
}
// 创建自定义数据信道
let dc = null;
dc = pc.createDataChannel("channel");
// onmessage表示有消息接收到
dc.onmessage = (e) => console.log("Get Message:", e.data);
// 表示自定义数据信道开启
dc.onopen = () => console.log("Data Channel Opened");
// 这个事件表示有一条自定义数据信道传入,即PeerA创建了一条自定义数据信道,PeerB不需要自己创建
pc.ondatachannel = (e) => {
console.log("Data Channel");
dc = e.channel;
// 同样的给他监听一些事件
dc.onmessage = (e) => console.log("Get Message", e.data);
dc.onopen = () => console.log("Channel Opened");
}
openVideo(); // 打开视频添加音视频轨道到 RTCPeerConnection
/**
* 设置Answer
* @return {Promise<void>}
*/
async function setAnswer() {
await pc.setRemoteDescription(JSON.parse(answerInput.value));
console.log("Answer Set");
}
/**
* 设置Offer
* @return {Promise<void>}
*/
async function setOffer() {
await pc.setRemoteDescription(JSON.parse(offerInput.value));
console.log("Offer Set");
await createAnswer();
}
/**
* 创建Answer
* @return {Promise<void>}
*/
async function createAnswer() {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
}
/**
* 创建Offer
* @return {Promise<void>}
*/
async function createOffer() {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
}
</script>
</body>
</html>